- Today
- Total
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 후기
- chrome80
- 스프링
- docker
- JWT
- Java
- spring
- 브랜치전략
- infra
- 젠킨스
- 캐싱전략
- Spring Security
- jenkins
- SPRING JWT
- 팀네이버 공채
- 책
- JPA
- SpringBoot
- 팀네이버
- 리뷰
- 프로젝트
- Kotlin
- websocket
- network
- container
- 만들면서 배우는 클린 아키텍처
- LazyInitialization
- EntityTransaction
- Project
- redis
PPAK
[JPA] Join 과 Fetch Join (N+1 문제) 본문
SQL 에서 서로 다른 테이블을 연관지어 불러오기 위해 사용되는 inner join 과 outer join 외에 JPA 의 JPQL 에서는 fetch join 을 제공합니다.
이전 포스팅 에서 확인했듯 JPA 는 영속화를 바탕으로 데이터베이스 테이블과 직접적으로 연결되는 엔티티 객체를 추적하고, 관리하는 방식을 채택하여 불필요한 쿼리 생성을 최소화 합니다.
대표적으로 Transaction 내의 쓰기 지연 방식과 지연 로딩을 예로 들 수 있는데 오늘은 지연 로딩 사용시 발생할 수 있는 N+1 문제와 이를 해결할 수 있는 방법인 Fetch Join(join fetch) 에 대해서 알아보려고 합니다.
지연 로딩(Lazy Loading) 은 실제 연관관계에 있는 엔티티들 중 하나를 조회하려고 했을 때, 데이터베이스에서 모든 연관관계에 있는 엔티티 데이터들을 조회하는 것이 아닌 실제로 필요한 때에 초기화 하여 사용하는 방식을 의미합니다.
쉽게 말해 내가 선택한 엔티티의 데이터만 불러오고, 연관된 엔티티는 또 그 엔티티의 데이터가 필요할 때 추가적으로 데이터베이스에서 꺼내오는 아주 합리적인 방식을 의미한다고 볼 수 있습니다.
N+1 문제
단순 테이블 조회 쿼리를 하나 생성하였는데, 연관관계에 있는 데이터를 모두 불러오기 위해서 select 쿼리가 테이블의 레코드 수(혹은 호출한 데이터 수) 만큼 더 생성되는 문제를 의미합니다.
즉시로딩은 최초 테이블 데이터를 모두 불러온 후 바로 연관관계가 있는 엔티티를 조회하는 방식을
지연로딩은 최초 테이블 데이터를 모두 불러온 후 필요한 시점에 연관관계가 있는 엔티티를 조회하는 방식을 채택합니다.
즉시로딩은 워딩 그대로 조회 시점에 연관관계에 있는 엔티티가 모두 조회되어 N+1 문제가 생긴다는 것을 직관적으로 알 수 있지만 지연로딩은 얼핏보면 로딩 시점에 쿼리가 더 나가지 않아 N+1 문제가 발생하지 않을 것으로 보입니다. 하지만 지연로딩 N+1 문제를 피해갈 수는 없는데, 그 이유는 연관관계에 있는 엔티티가 조회 시점에 영속화되지 않았기 때문입니다.
일반적으로 JPA는 JPQL 로 생성된 쿼리문을 데이터베이스와 connection 을 만든 후에 전송하고, 이에 대한 결과 데이터를 전달받아 실제 엔티티 객체를 생성하고 이를 영속화 합니다.
하지만 단순한 join 쿼리를 작성한다면 연관관계에 있는 데이터의 영속화는 수행되지 않습니다.
// SomeEntity 와 연관관계에 있는 OtherEntity 를 호출하는 쿼리
@Query("select t From SomeEntity t join t.otherEntity")
가령 위와 같이 연관관계에 있는 데이터를 조회하기 위해 JPQL 로 join 문을 작성한다면, OtherEntity 와 연관관계를 가지는 SomeEntity 데이터를 불러올지는 몰라도 OtherEntity 의 데이터는 얻을 수 없습니다.
따라서 SomeEntity 를 통해 OtherEntity 를 조회하려고 한다면 추가적인 쿼리가 생성될 것이고, 이는 N+1 문제를 발생시킵니다.
@Entity
@Getter @Setter
public class SomeEntity {
@Id @GeneratedValue
private Long id;
private String someField;
@ManyToOne(fetch = FetchType.LAZY)
private OtherEntity otherEntity;
}
@Entity
@Getter @Setter
public class OtherEntity {
@Id @GeneratedValue
private Long id;
private String otherField;
}
위와 같은 간단한 Entity 를 생성하여 확인할 수 있습니다.
일반 join 문을 통해서 테이블 데이터를 호출하면 위와같이 otherEntity 에 대한 데이터는 불러오지 않는다는 것을 확인할 수 있고, 실제로 참조하려고 하면
N+1 문제가 발생합니다.
// SomeEntity 와 연관관계에 있는 OtherEntity 를 호출하는 쿼리
@Query("select t From SomeEntity t join fetch t.otherEntity")
하지만 위와같이 fetch join 으로 코드를 수정하면
테이블 데이터를 호출할 때 연관관계에 있는 테이블 데이터까지 함께 불러오는 것을 확인할 수 있고, N+1 문제 역시 해결되는 것을 확인할 수 있습니다.
LazyInitializationException
N+1 문제와 마찬가지로 "일대다" 연관관계를 가지는 엔티티 객체 데이터를 테이블에서 불러올 때 "다" 에 해당하는 테이블 데이터가 불러와지지 않아 해당 데이터를 조회하려고 할 때 발생하는 예외를 의미합니다.
엄밀히 이야기하면 테이블에서 연관관계에 있는 테이블의 pk 를 가지고 있지도 않아 기존 N+1 에서 본 추가 쿼리 생성도 하지 않고 예외를 발생시키는 것이라고 생각할 수 있습니다.
그렇다면 항상 fetch join 을 수행하는 것이 유리해 보이는데 join 문은 어떤 상황에서 작성할 수 있을까 생각을 할 수 있습니다.
ORM 을 가장 적절하게 사용하는 방법중 하나의 문맥(Transaction) 에서 필요한 데이터만을 DB 에서 꺼내와서 쓰는 것이라고 말할 수 있는데, 그 점에서 fetch join 은 문맥에서 필요하지않은 데이터까지도 불러올 수 있다는 단점 아닌 단점을 가지고 있습니다.
따라서 검색조건과 같은 필터링은 필요하지만 실제 연관관계에 있는 테이블 데이터가 필요하지않은 문맥에서는 join 문을 통해서 쿼리를 생성하는 것이 바람직하다고 볼 수 있습니다.
잘못된 정보가 있다면 댓글로 알려주시면 감사하겠습니다!!
'spring > jpa' 카테고리의 다른 글
[Spring/JPA] JPA 란? (ORM/Persistence Context) (0) | 2022.07.02 |
---|