JPA를 사용하다 보면 의도치 않게 N+1 문제가 발생하는 경우가 많습니다.
N+1 문제란, 한 번의 쿼리로 데이터를 조회할 때 연관된 엔티티를 지연 로딩(Lazy Loading)하면서 추가적인 쿼리가 반복적으로 발생하는 현상입니다. 저도 실제 프로젝트에서 N+1 문제를 경험하면서, 왜 이러한 문제가 발생하는지 공부한 지식을 정리하고자 합니다.
지연 로딩(Lazy Loading)이란❓
지연 로딩(Lazy Loading)은 JPA에서 제공하는 성능 최적화 기술 중 하나로, 엔티티의 연관된 데이터를 즉시 로딩하는 대신 실제로 필요한 시점에 데이터베이스에서 로드하는 방식입니다.
이 방식은 데이터베이스의 성능을 개선할 수 있지만, 잘못 사용하면 예상치 못한 성능 문제를 초래할 수 있습니다. 대표적인 예가 바로 N+1 문제입니다.
JPA에서 @ManyToOne, @OneToOne 관계는 기본적으로 지연 로딩으로 설정됩니다.
Entity
@Entity
public class HubPath {
@Id
private UUID hubPathId;
@ManyToOne(fetch = FetchType.LAZY)
private Hub departureHub;
@ManyToOne(fetch = FetchType.LAZY)
private Hub arrivalHub;
// 다른 필드들...
}
HubPath 엔티티는 departureHub와 arrivalHub라는 두 개의 Hub 엔티티와 연관되어 있으며, 지연 로딩으로 설정되어 있습니다.
이 설정에 따르면, HubPath 엔티티가 로드될 때 departureHub와 arrivalHub는 데이터베이스에서 로드되지 않고 프록시 객체로 대체됩니다.
DTO 변환 과정에서 발생하는 문제
Entity를 그대로 반환하면 보안상 문제가 있기 떄문에 주로 DTO로 변환하여 반환합니다.
public class HubPathResponseDto {
private UUID hubPathId;
private String departureHubName;
private String arrivalHubName;
// 다른 필드들...
public static HubPathResponseDto of(HubPath hubPath) {
return new HubPathResponseDto(
hubPath.getHubPathId(),
hubPath.getDepartureHub().getName(), // 여기서 지연 로딩 초기화 발생
hubPath.getArrivalHub().getName(), // 여기서 지연 로딩 초기화 발생
// 다른 필드 설정...
);
}
}
위와 같은 응답 DTO에서 of메서드가 구현되어 있을 때 hubPaths.map(HubPathResponseDto::of)
호출 시, 각 HubPath 엔티티에 대해 of
메서드가 실행됩니다.
of
메서드 내에서hubPath.getDepartureHub().getName()
을 호출할 때hubPath.getDepartureHub()
는 지연 로딩된 프록시 객체를 반환합니다..getName()
을 호출하면 이 프록시 객체가 초기화되며, 이때 데이터베이스 쿼리가 실행됩니다.
- 마찬가지로
hubPath.getArrivalHub().getName()
호출 시에도 동일한 과정이 발생합니다. - 이 과정이
hubPaths
의 각 엔티티에 대해 반복되면서 N+1 문제가 발생합니다.
발생 이유
트랜잭션 범위
- 이 작업이 트랜잭션 내에서 실행되고 있어, 지연 로딩된 엔티티에 접근할 때 데이터베이스 쿼리가 가능합니다.
프록시 객체의 동작
- 지연 로딩된 엔티티는 프록시 객체로 대체되며, 이 객체의 메서드를 처음 호출할 때 실제 데이터를 로드합니다.
캐시되지 않은 데이터
- 첫 번째 쿼리에서 Hub 정보를 함께 가져오지 않았기 때문에, 각 Hub에 대해 개별 쿼리가 필요합니다.
해결 방안
- Fetch Join 사용
- 앞서 제안한 대로 Fetch Join을 사용하여 초기 쿼리에서 모든 필요한 데이터를 한 번에 가져옵니다.
- DTO 프로젝션 사용
- 쿼리 단계에서 직접 필요한 데이터만 선택하여 DTO로 변환합니다.
- @EntityGraph 사용
- 특정 메서드에 대해 연관 엔티티를 함께 로드하도록 지정합니다.
- BatchSize 설정
- 연관 엔티티를 일괄적으로 로드하여 쿼리 수를 줄입니다.
'TIL,일일 회고' 카테고리의 다른 글
[TIL, 일일 회고] 2024.09.16 - Kafka Unrecognized token 에러원인과 해결방법 (0) | 2024.09.16 |
---|---|
[TIL, 일일 회고] 2024.09.15 - Docker Compose에서 Prometheus 설정 파일 경로 오류 해결 방법 (0) | 2024.09.15 |
[TIL, 일일 회고] 2024.09.13 - Git push 오류 해결: 패킷 버퍼 크기 증가로 문제 해결 (0) | 2024.09.13 |
[TIL, 일일 회고] 2024.09.12 - 멀티 모듈 환경에서의 Q클래스 생성오류 해결 (0) | 2024.09.12 |
[TIL, 일일 회고] 2024.09.11 - Hibernate @SQLRestriction 어노테이션: 사용법과 장단점 분석 (1) | 2024.09.11 |