[Spring/JPA] 연관관계 엔티티를 가져올 때 가장 좋은 방법은 무엇일까
![[Spring/JPA] 연관관계 엔티티를 가져올 때 가장 좋은 방법은 무엇일까](/backend/images/2023-08-21-jpa-fetch-mapping-entity/title.jpg)
연관관계 엔티티를 가져오는 방법으론 Eager, Lazy, fetch, Entity Graph, batch size등이 있다. 이 중 가장 좋은 방법은 무엇일까?
1. 서론
JPA를 사용하면서 가장 편리하다고 생각하는 기능이 연관관계 Mapping이다. 엔티티간 연관관계를 정의하면 자식 엔티티나 부모 엔티티를 쉽게 가져올 수 있다.
JPA에서는 연관관계에 있는 엔티티를 여러가지 방법으로 가져올 수 있다. 제목은 가장 좋은 방법은 무엇일까? 이지만, 사실 상황에 따라 다른 것 같다.
개인적인 경험으로 편의성과, 성능을 고려해서 어떤 상황에서 어떤 방법으로 연관관계의 엔티티를 가져오는게 좋을지 정리해보았다.
2. 연관관계 엔티티를 가져오는 방법
A. Fetch Type Lazy
연관관계가 설정된 필드를 사용하는 시점에, 쿼리를 날려 엔티티를 가져오는 방법이다.
ToMany인 경우에는 기본적으로 Lazy
를 사용하기 때문에 따로 설정하지 않아도 된다.
하지만 ToOne은 기본적으로 Eager
이기 떄문에 Lazy를 사용하려면 어래와 같이 설정해야 한다.
@OneToOne(fetch = FetchType.LAZY)
var entityA: EntityA
@ManyToOne(fetch = FetchType.LAZY)
var entityB: EntityB
위와 같이 설정한 연관관계 엔티티를 가져올 땐 단순히 getter를 호출하면 된다.
호출하는 순간 데이터베이스에 쿼리를 날려 엔티티를 조회한다.
LAZY를 사용하면서 주의해야 할 점은 처음 조회한 엔티티가 단건인 경우에는 쿼리 호출 횟수가 많지 않아 괜찮지만,
다건에 대해 연관관계 엔티티에 접근한다면, 엔티티의 개수 만큼 쿼리를 호출하게 된다.
기본적으로 데이터베이스는 네트워크를 통해 접근하기 때문에, 아무리 간단한 쿼리라도 지연이 발생 할 수 밖에 없다.
따라서 단건에 대해서는 간편하게 써도 좋지만, 다건에 대해서는 지양하는게 좋다.
B. Fetch Type Eager
엔티티를 조회 할 때, 즉시 연관관계 엔티티를 가져오는 방법이다.
연관관계 맵핑이 ToOne인 경우에는 Eager
를 기본값으로 사용하기 때문에 따로 설정하지 않아도 된다.
하지만 ToMany 맵핑의 경우 Lazy
가 기본값이기 떄문에 Eager를 사용하려면 어래와 같이 설정해야 한다.
@OneToMany(fetch = FetchType.EAGER)
var entityAs: List<EntityA>
@ManyToMany(fetch = FetchType.EAGER)
var entityBs: List<EntityB>
Eager는 사실 사용을 권장하지 않는다. 왜냐하면, 특정 비즈니스 로직에서 연관관계의 엔티티가 필요하지 않음에도 쿼리를 요청하여 엔티티를 조회하기 때문이고, N+1 문제가 발생 할 수 있어 API 성능이 크게 떨어질 수 있기 때문이다.
C. Fetch Join
가져올 연관관계에 대해서 fetch join해오도록 JPQL을 작성하는 방법이다.
한 번의 쿼리로 연관관계 엔티티를 가져 올 수 있기 때문에 쿼리를 여러번 날리지 않아도 된다.
@Query("select a from Entity e join fetch e.entityA")
fun findWithEntityA()
D. Entity Graph
가져올 연관관계에 대해서 @EntityGraph
에 지정하는 방법이다.
// Query와 함께 사용 가능
@Query("select a from Entity e")
@EntityGraph(attributePaths = ["entityA"], type = EntityGraph.EntityGraphType.LOAD)
fun findWithEntityA()
파라미터로 EntityGraphType
을 LOAD와 FETCH 두 가지 중 하나를 설정 할 수 있다.
Entity Graph에 지정한 연관관계 엔티티를 한 번의 쿼리로 함께 조회 한다는 점은 동일하지만 차이점은 아래와 같다.
- FETCH: Entity Graph에 표기되어있지 않은 fetch type이 EAGER 인 연관관계에 대해서 추가 쿼리 호출 안됨
- LOAD: Entity Graph에 표기되어있지 않은 fetch type이 EAGER 인 연관관계에 대해서 추가 쿼리 호출됨
Entity Graph는 fetch join과 유사한 효과를 보이지만 차이점은 Entity Graph는 지정한 연관관계에 대해서 outer join을 하지만, fetch join은 따로 지정하지 않으면 inner join을 한다.
E. BatchSize
@BatchSize
를 지정한 연관관계에 대해서 where in으로 한 번에 조회하는 방법이다.
연관관계 맵핑에 간단하게 @BatchSize 어노테이션을 사용하기만 하면 되므로 Entity Graph, Fetch Join에 비해 구성이 가장 간편하다.
@OneToOne
@BatchSize(size = 100)
var entityA: EntityA
엔티티를 조회하면 BatchSize를 지정한 연관관계에 대해서 쿼리가 호출된다. FetchType.EAGER
와 다른 점은 EAGER는 조회된 엔티티 수만큼 추가 쿼리가 호출되지만, BatchSize는 size
파라미터로 지정된 크기만큼 where in 절에 묶어서 쿼리를 호출한다.
예를 들어 size를 50으로 설정 하고, 조회된 엔티티가 100개라면 50개씩 묶어 2 번의 추가 쿼리가 호출된다.
쿼리 호출 횟수를 줄일 수 있어 EAGER보다 성능이 뛰어나다.
결론
실무에서는 상황을 봐야겠지만 연관관계 데이터를 가져올 때, EAGER는 사용하지 않는다.
조회하는 데이터가 단건이라면 리포지토리를 건드릴 필요 없이 LAZY 를 사용한다.
조회하는 데이터가 다건이거나 ToOne 관계라면 fetch join이나, Entity Graph를 사용한다.
ToMany에서는 fetch join이나 Entity Graph를 사용하지 않는데, 테이블을 Join하면 데이터의 수가 뻥튀기 되어 엔티티를 만드는데 오류가 발생 할 수 있기 때문이다.
그래서 ToMany로 설정된 연관관계에 대해서는 @BatchSize를 사용하거나 별도의 쿼리를 호출한다.
만약 연관계에 있는 데이터의 수가 너무 많거나, 자주 사용하지 않는다면 @BatchSize를 사용하지 않고 별도의 쿼리를 호출한다.
출처
타이틀 이미지: Unsplash의Joshua Hoehne