1. 경로 표현식
JPA 에서 경로 표현식은 엔티티 객체에 점을 찍어 객체 그래프를 탐색하는 방식을 의미한다. 경로 표현식을 통해서 엔티티가 가지고 있는 값들, 필드에 접근할 수 있다.
경로 표현식을 통해서 접근하는 필드는 기본 값 데이터를 저장하는 상태 필드와 연관관계 매핑으로 연결된 객체를 저장하는 연관 필드로 구분된다.
- 상태 필드
단순히 값을 저장하기 위한 필드이다. 더이상 탐색을 할 수 없는 경로 탐색의 마지막 필드이다.
# JPQL
select m.username, m.age from Member m
# SQL
select m.username, m.age from Member m
- 연관 필드
연관 관계를 저장하는 필드. 연관관계의 매핑 형식에 따라 단일 값 연관 필드와 컬렉션 값 연관 필드로 구분된다.
단일 값 연관 필드는 @ManyToOne, @OneToOne 과 같이 대상이 엔티티인 연관관계의 필드를 의미한다. 단일 값 연관 경로에서는 묵시적 내부 조인이 발생한다. 해당 엔티티의 내부 구조로 추가적인 탐색이 가능하다.
# JPQL
select o.member from Orders
# SQL
select m.*
from Orders o
inner join Member m on o.member_id = m.id
컬렉션 값 연관 필드는 @ManyToMany, @OneToMany 와 같이 대상이 컬렉션인 연관관계의 필드를 의미한다. 컬렉션 값 연관 경로도 묵시적 내부 조인이 발생한다. 하지만 추가적인 탐색이 불가능하다. 이때 조회되는 값에 대하여 별칭을 지정하여 별칭에서 원하는 속성을 가져오는 방식으로 추가 탐색을 수행할 수 있다.
- 명시적 조인, 묵시적 조인
명시적 조인은 쿼리 스트링에서 join 키워드를 직접 사용하는 경우를 의미한다. 반대로 묵시적 조인은 쿼리 스트링에는 join 키워드가 없지만 경로 표현식으로 인해서 묵시적으로 SQL 조인이 발생하는 경우를 의미한다. 다른 엔티티들과 연관관계를 가지고 있는 엔티티를 조회할 때는 경로 표현식을 통해서 연관 필드를 조회할 때 묵시적 조인이 발생한다. 이때 조인은 내부 조인만 가능하다.
실무적인 관점에서 묵시적 조인은 쿼리 튜닝 하기에 까다롭다. 해당 쿼리에서 묵시적 조인이 일어나는 지를 명확하게 알 수 없기 때문이다. 그렇기 때문에 실무에서는 묵시적 조인보다는 명시적 조인으로 구현하는 것을 더 권장한다.
2. fetch join
fetch join 은 SQL 에서 제공해주는 조인의 종류가 아니라 JPQL 에서 성능 최적화를 위해서 제공하는 기능이다. 연관된 엔티티나 컬렉션을 SQL 로 한번에 함께 조회하는 기능이다.
join fetch 명령어를 사용하는데 기존 SQL 의 조인문처럼 left, outer, inner 등의 키워드를 함께 사용하여 join 을 수행할 수 있다.
- entity fetch join
엔티티를 조회할 때 연관관계에 있는 엔티티도 함께 한번에 조회한다. 회원 정보를 조회할 때 회원이 포함된 팀의 정보도 함께 조회하는 것과 같다.
아래의 SQL 을 확인하면 Member M 과 Team T 를 함께 조회한다.
# JPQL
select m from Member m join fetch m.team
# SQL
SELECT M.*, T.*
FROM MEMBER M,
INNER JOIN TEAM T
ON M.TEAM_ID = T.ID
fetch join 을 사용하지 않고 그냥 select 문으로 조회를 하는 경우 N + 1 문제가 발생할 수 있다. 일반 select 문으로 쿼리하는 경우에 Member 의 Team 정보는 DB 에서 가져온 실제 데이터가 아니라 proxy 로 가져온 데이터가 들어간다. 그렇기 때문에 실제 Team 속성에 접근하는 경우에 다시 Team 정보 조회의 쿼리가 발생한다.
반면에 fetch join 의 경우 proxy 가 아닌 실제 데이터를 조회하기 때문에 추가적인 조회 쿼리가 발생하지 않는다.
- collection fetch join
연관관계에 있는 엔티티가 일대다 관계로 컬렉션으로 저장되어 있는 경우, 해당 엔티티 컬렉션을 한번에 함께 조회하기 위해서 사용하는 fetch join 이다. 특정 팀을 조회할 때, 해당 팀의 회원 정보들도 함께 조회되는 것과 같다.
# JPQL
select t from Team t join fetch t.members where t.name = 'A'
# SQL
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M
ON T.ID = M.TEAM_ID
WHERE T.NAME = 'A'
이때 팀에 포함되어 있는 회원들이 여럿인 경우, Team 의 정보가 회원의 수만큼 조회된다. 이 결과에서 중복을 제거하기 위해서는 DISTINCT 를 사용하면 된다. JPA 에서 DISTINCT 는 2가지 기능을 제공해주는데, SQL 문에 DISTINCT 를 제공해주고 application 에서는 같은 식별자를 가지는 엔티티의 중복을 제거해준다.
위의 예제에서 DISTINCT 를 사용하는 경우 Team 정보를 회원의 수만큼 조회하는 것이 아니라 중복을 제거하여 하나만 출력하도록 한다.
# JPQL
select distinct t from Team t join fetch t.members where t.name = 'A'
- fetch join vs join
JPA 에서 fetch join 이 아니라 일반 조인문을 사용할 수 있다. 이 경우에는 조회한 엔티티와 연관되어있는 엔티티를 함께 조회하지 않는다. 일반 조인문을 사용한 JPQL 은 결과를 반환할 때 연관된 엔티티를 고려하지 않고 지정한 엔티티만 조회한다.
fetch join 은 즉시 로딩으로 객체 그래프에 연관된 엔티티들을 모두 SQL 쿼리로 한번에 조회하는 개념이다.
이러한 방식으로 N + 1 문제를 해결할 수 있다.
- fetch join 의 한계
fetch join 의 대상에는 별칭을 주지 않는 것을 권장한다.
fetch join 의 결과를 해당 엔티티의 모든 데이터를 가져오는 것이 아니라 조건에 맞는 몇건의 데이터만 가져온다. 이러한 경우에 정합성이 일치하지 않아서 fetch join 에서의 별칭을 전체 엔티티를 지칭하는 것으로 인식되어 오류를 유발할 수 있다.
하이버네이트에서는 별칭을 주는것이 가능하지만 가급적으로 사용하지 않는 것이 좋다.
둘 이상의 컬렉션은 fetch join 을 사용할 수 없다. 1:N:M 의 관계가 발생하여서 의도하지 않은 쿼리들이 발생할 수 있다.
컬렉션을 fetch join 하면 페이징 API 를 사용할 수 없다. 페이징 API 를 사용하면 컬렉션 데이터의 lazy loading 으로 컬렉션의 각 요소별로 조회 쿼리가 나가야하기 때문에 N + 1 문제가 발생한다.
이러한 문제는 batch size 를 컬렉션 타입 속성에 어노테이션으로 지정하여 해결할 수 있다. 어노테이션의 배치 크기만큼 한번에 조회를 해버린다.
'Tech > JPA' 카테고리의 다른 글
[JPA] LazyInitializationException 과 OSIV (1) | 2024.02.07 |
---|---|
[JPA] JPQL 기본 문법 (0) | 2022.06.18 |
[JPA] 값 타입 (0) | 2022.06.04 |
[JPA] 영속성 전이 (CASCADE) (0) | 2022.05.26 |
[JPA] 프록시와 지연로딩 (0) | 2022.05.23 |