티스토리

  • 전체보기 (32)
    • 개념 (2)
    • 네트워크 (14)
    • 프리코스 (1)
    • 스프링 (8)
      • 테스트 (1)
    • 프로젝트 (1)
    • 회고 (1)
    • 알고리즘 (4)
    • 자바 (1)
    • MySQL (0)
코딩_초보
코초의 학습일지
코딩_초보
전체 방문자
오늘
어제

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 프로그래머스
  • grpc
  • typescript와 javascript의 차이
  • spring grpc
  • 동시 처리
  • 우아한 테크코스5기
  • 컴포넌트 스캔
  • ripv2
  • 스프링
  • 소켓프로그래밍
  • 함께하는 효도
  • @Mock
  • 충돌위험 찾기
  • @MockBean
  • 빈 등록방법
  • 코테
  • 정적 라우팅
  • 알고리즘
  • 동적 라우팅
  • 쓰레드 풀
  • n+1
  • 현대 sw
  • 프리코스
  • 소프티어
  • Servlet Container
  • softeer
  • @BeforeAll
  • 퍼즐 게임 챌린지
  • 스프링 멀티쓰레드
  • 우아한 테크코스

최근 댓글

최근 글

hELLO · Designed By 정상우.
코딩_초보

코초의 학습일지

[스프링] 1 + N
스프링

[스프링] 1 + N

2024. 2. 2. 16:17

목차

1. 1+N이란?

2. 즉시로딩과 지연로딩

3. 1+N이 발생하는 케이스

4. 1+N의 해결방안


🤔 1 + N 이란?

  • 연관관계가 설정된 엔티티 사이에서 한 엔티티(1)를 조회하였을 때, 조회된 엔티티의 개수(N)만큼 연관된 엔티티를 조회하기 위해 추가적인 쿼리가 발생하는 문제이다
  • 예를 들어 게시글(1) 과 댓글(N) 이 있을 때, 게시글과 댓글이 조인된 형태로 함께 조회되기를 기대했지만, 게시글을 조회(1)하는 쿼리와 댓글을 조회(N)하는 쿼리가 따로 날아가며 총 1+N 번 쿼리가 발생
  • DBMS에서 직접 쿼리를 작성하면 발생하지 않을 문제지만, JPA와 같은 ORM이 등장하고, 쿼리가 자동화 되면서 발생되는 문제이다
  • 즉시로딩 과 지연로딩 에 대한 개념을 먼저 설명하고, 간단한 예제를 통해 1 + N 을 더 자세히 알아보자!

 


🤔 즉시로딩

  • 즉시로딩은 연관관계가 맺어진 엔티티가 있을 때, 한 엔티티를 조회하면 그 엔티티와 연관관계가 맺어져있는 엔티티를 즉시 같이 가져오는 것을 말한다.
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private Set<Article> articles = emptySet();

@ManyToOne(fetch = FetchType.EAGER)
private User user;
  • 사용 방법은 관계를 맺는 @OneToMany(), @ManyToOne()과 같은 어노테이션에서 fetch 옵션을 FetchType.EAGER 로 설정하여 사용할 수 있다
  • Fetch type의 default는 ~ToMany()에서는 Lazy(지연로딩), ~ToOne()에서는 Eager(즉시로딩) 이다
  • 즉시로딩은 연관관계가 복잡해지고 데이터가 많지 않은 곳에서는 일부 사용할 수 있으나, 거의 모든 관계에서 지연로딩이 권장된다

 


🤔지연로딩

  • 지연로딩은 연관관계가 맺어진 엔티티가 있을 때, 한 엔티티(유저)를 조회하면 그 엔티티와 연관관계가 맺어져있는 엔티티(게시글)는 사용하기 전까지 지연시키다 사용하는 시점에 가져오는 것 을 말한다.
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private Set<Article> articles = emptySet();

@ManyToOne(fetch = FetchType.LAZY)
private User user;
  • 사용 방법은 fetch 옵션을 FetchType.EAGER 로 설정하여 사용할 수 있다
  • 대부분의 관계에서 지연로딩이 권장, 사용 된다

 


😵 1+N이 발생하는 케이스

1+N이 발생하는 케이스는 대표적으로 2가지가 있다.

  • FetchType.EAGER를 사용했을 때
  • Fetch join 없이 FetchType.LAZY를 사용했을 때

 

예제로 사용할 연관관계 (User : 1, Article : N)

 

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 10, nullable = false)
    private String name;

    @OneToMany(mappedBy = "user")
    private Set<Article> articles = emptySet();
@Entity
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 50, nullable = false)
    private String title;

    @Lob
    private String content;

    @ManyToOne
    private User user;

 


1. FetchType.EAGER를 사용했을 때

 

 

  • 위와 같이 User 가 Article에 대해 FetchType을 Eager로 설정했을 때
  • User를 SELECT 하면서 User가 가지고 있는 Article을 추가로 검색하게 되고 유저 모두 조회(1), 각 유저의 article 조회(N) 쿼리가 날라가게 된다
  • 단순히 User만 조회하려 했어도 1 + N 가 발생하고, User와 Article을 함께 조회하려 했어도 1 + N 이 발생하게 된다
  • 우리의 생각처럼 조인을 통해 결과를 가져다주지 않는다


2. Fetch join 없이 FetchType.LAZY를 사용

  • User를 조회할 때는, 1 + N 이 발생하지 않는다
  • 하지만 User의 Article을 조회하는 시점에 쿼리가 나가므로 여전히 1 + N은 해결되지 않는다


(추가) Fetch join과 Pagination을 같이 사용했을 때

  • 이 경우는 직접적으로 1 + N 을 발생시키지는 않지만 그보다 더 심각한 문제를 일으킬 수 있음
@Test
@DisplayName("fetch join을 paging처리에서 사용하면 Out Of Memory가 발생할 수 있다.")
void pagingFetchJoinTest() {
    System.out.println("== start ==");
    PageRequest pageRequest = PageRequest.of(0, 2);
    Page<User> users = userRepository.findAllPage(pageRequest);
    System.out.println("== find all ==");
    for (User user : users) {
        System.out.println(user.articles().size());
    }
}

  • 위 코드를 실행하면 1 + N 문제는 발생하지 않지만, 페이징 할 때 사용하는 Limit과 Offset이 보이지 않고, 결과 값 자체는 잘나오는 것처럼 보이기도 한다
  • 이는 스프링에서 인메모리를 적용하여 조인을 한 것인데, 모든 데이터를 select (풀스캔)하고 이를 인메모리에 저장하여 application 단에서 필요한 페이지만큼 반환을 해준 것이다
  • 이렇게되면 페이징을 한 이유가 없어지고, Out of Memory가 발생할 확률이 매우 높아짐

😵 1+N의 해결방안

1. FetchType.EAGER을 FetchType.LAZY로 설정하고, Fetch join 을 통해 해결

@Query("select distinct u from User u left join fetch u.articles")
List<User> findAllJPQLFetch();

  • 조인을 사용하여 쿼리 하나로 연관되어 있는 데이터를 전부 가져옴
  • ManyToOne 관계인 엔티티는 여러 개 가져와도 문제가 발생하지 않는다
  • OneToMany 관계인 엔티티를 두 개 이상 가지고 오려고 하면  MultiBagFetchException 이 발생하는데 아래의 @BatchSize를 통해 어느정도 해결할 수 있다.

2. @BatchSize

  • BatchSize 어노테이션을 통해 해결
  • 엔티티 위의 @Batch Size() 어노테이션을 통해 각 엔티티 마다 설정해줄 수 있음
  • application.yml 파일에 전역으로 Batch Size (default_batch_size) 를 설정하는 방법도 있음

  • 해당하는 Article에 대해서 In 절을 통해 batch size개 만큼 데이터를 가져오게 되고, 1 + N 문제를 완전히는 아니지만 해결할 수 있게 된다.
    '스프링' 카테고리의 다른 글
    • 스프링 트랜잭션
    • [스프링] Spring AOP
    • [스프링] 동시 요청 처리(멀티쓰레드)
    • [스프링] 서블릿(Servlet)과 서블릿 컨테이너(Servlet Container)
    코딩_초보
    코딩_초보

    티스토리툴바