[JPA] N+1 문제 (feat. 즉시 로딩과 지연 로딩)

옹심이·2025년 1월 1일
0
post-thumbnail

패치 전략 개요

패치 전략 설정 옵션이 왜 있을까?

JPA는 메서드 이름을 기반으로 JPQL을 자동 생성해 데이터베이스에 접근한다. 예를 들어 findAll()이라는 메서드를 실행하면 해당 엔티티를 조회하는 쿼리만 실행되기 때문에, 특정 SQL에 종속하지 않고 조회하는 엔티티 기준으로 쿼리를 실행하는 JPQL 입장에서는 연관 관계 데이터를 무시한다. 그러므로 필요 시 개발자가 지정한 옵션으로 연관 엔티티를 로드할 수 있게 이러한 기능을 제공한다.

지연 로딩

@Entity 
  public class Member { 
 
    @Id 
    @GeneratedValue 
    private Long id; 
 
    @Column(name = "USERNAME") 
    private String name; 
 
    @ManyToOne(fetch = FetchType.LAZY)  
    @JoinColumn(name = "TEAM_ID") 
    private Team team; 
    .. 
  }

fetch 속성을 통해 지연 로딩을 설정하면 조회 연관 객체에 대해 프록시 객체를 생성해 반환한다. 그리고 추후에 코드에서 연관 객체에 대한 조회가 실행되는 시점에 실제 엔티티를 찾아 반환한다.

지연 로딩 사용 전략

  1. 연관 엔티티를 많이 사용하지 않는 경우에는 부모 객체 조회 시 자식 객체를 함께 조회할 필요가 없다. 그러므로 필요한 경우에만 로드 되도록한다
  2. 대용량 데이터 처리에 유용하다. 연관된 엔티티를 무조건 로드하지 않기 때문에 불필요한 메모리 사용량을 줄인다. 따라서 성능 최적화에 유리하다

즉시 로딩

 @Entity 
  public class Member { 
 
    @Id 
    @GeneratedValue 
    private Long id; 
 
    @Column(name = "USERNAME") 
    private String name; 
 
    @ManyToOne(fetch = FetchType.EAGER) //** 
    @JoinColumn(name = "TEAM_ID") 
    private Team team; 
    .. 
  }

조회 시 쿼리를 통해 멤버와 팀을 한번에 들고오기 때문에 프록시 객체가 필요가 없으며 실제 가져온 값을 사용할 수 있다.

즉시 로딩을 적용하면 멤버 엔티티에 대한 조회만 실행해도 즉시 로딩으로 연관되어 있는 객체의 테이블과 조인이 발생한다. 따라서 예상치 못한 SQL이 발생해 쿼리 성능에 문제가 생긴다.

따라서 실무에서는 지연 로딩을 사용하는 것이 권장된다.

즉시 로딩 사용 전략

  1. 자주 사용되는 연관 엔티티가 있을 때 사용하는 것이 좋다. 자주 사용하는 경우에는 부모 엔티티 로딩 시 자식을 미리 로딩해 쿼리 수행 시간을 감소시킬 수 있다
  2. 연관 엔티티의 크기가 작을 때 사용하는 것이 좋다.

N+1 문제란 무엇일까

이는 연관 엔티티가 실제로 조회 될 때 별도의 SQL이 발생해 성능 이슈를 초래하는 문제이다.

한 객체에 대한 조회 쿼리 1번과 연관 객체에 관한 조회 쿼리 N번이 추가로 발생하는 문제 이기 때문에 1+N으로 이해하는 것이 더 편했다.

예를 들어 Team이 여러 Member를 가질 수 있는 N:1 연관 관계라고 가정하고 다음 과 같은 조회 쿼리를 실행했다고 가정하자

SELECT * FROM team;

팀 목록이 N개가 있다면 이 쿼리를 통해 N개의 팀을 조회한다.

즉, 존재하는 N개의 팀목록을 조회하기 위한 쿼리 한 번이 실행된다.

SELECT * FROM member WHERE team.id = 1;
SELECT * FROM member WHERE team.id = 2;
SELECT * FROM member WHERE team.id = N;

각 팀에 연관 된 멤버마다 추가 쿼리를 한번 씩 날린다.

즉, 팀마다 존재하는 멤버 N명을 조회하기 위해 N개의 쿼리가 추가로 실행된다.

개발자 입장에서는 team에 대한 조회 쿼리 한 번을 실행하기를 원했지만, 실제 데이터베이스에서 실행되는 쿼리는 연관 객체에 대한 조회 N번이 추가로 일어나게 되는 문제이다.

어떠한 패치 전략을 사용해도 N+1 문제는 발생한다

즉시 로딩 사용시

즉시 로딩은 꼭 한번의 쿼리로 부모 엔티티와 자식 엔티티 모두를 가져올 것 같지만 아니다.

JPQL은 엔티티를 기준으로 쿼리를 실행한다고 하였다. 그렇기 때문에 처음에 부모 엔티티를 조회한 후 패치 전략을 확인한다. 패치 전략이 즉시 로딩임이 확인되면 그 때 자식 엔티티 조회에 대한 추가 쿼리가 실행된다.

따라서 즉시 로딩을 사용할 때도 N+1 문제가 발생한다.

지연 로딩 사용시

지연 로딩으로 설정하면 처음에는 부모 엔티티 조회를 위한 쿼리 하나만 실행된다. 하지만 이것이 N+1 문제를 해결하지는 못한다.

지연 로딩도 추후에 실제 엔티티를 조회하기 위한 코드가 실행되면 데이터를 로드하기 위한 추가 쿼리를 실행해야 하기 때문에 연관 엔티티를 로드하기 위한 추가 쿼리의 실행 시점이 늦춰질 뿐이다.

해결 방법에 대해서는 다음 포스트를 참고하자

0개의 댓글