본 예시는 제 github에 springboot의 h2를 사용한 형태로 레파지토리를 구성해놨습니다.
(https://github.com/cobiyu/querydsl)
처음 JPA(Hibernate)를 도입하는 과정 중 제일 난감한 부분은 OneToOne이라고 생각합니다.
아래 테이블 구조를 참고로 문제 영역에 대한 예시를 설정해 보겠습니다.
- member는 1개의 phone만 소유 가능하며 소유하지 않을 수도 있음
- member는 favorite이라는 N개의 찜한 상품을 소유할 수 있음
- member는 favortie의 목록을 기반으로 order 테이블에 주문서를 생성 할 수 있음
"찜한 상품" 목록을 이용한 주문서 만들기
(https://github.com/cobiyu/querydsl 의 before 브랜치 참고)
찜한 상품을 가져와서 주문서를 만드는 로직을 hibernate로 작성할 때 단편적으로 생각하면 아래와 같은 로직을 작성하게 됩니다.
- member와 favorite을 fetch join으로 가져옴
- favorite의 sum으로 order 생성 및 1번의 member와 연관 관계 설정
public class MemberRepository {
private final JPAQueryFactory query;
public Order createOrderByFavorite(Long memberId){
Member selectMember = query
.selectFrom(member)
.leftJoin(member.favoriteList, favorite).fetchJoin()
.where(member.id.eq(memberId))
.fetchOne();
Integer totalPrice = selectMember.getFavoriteList().stream().
mapToInt(Favorite::getPrice).sum();
return Order.builder()
.name("ordername")
.totalPrice(totalPrice)
.member(selectMember)
.build();
}
}
위 로직을 실행했을 때 실행되는 쿼리는 아래와 같습니다.
select
member0_."id" as id1_1_0_,
favoriteli1_."id" as id1_0_1_,
member0_."email" as email2_1_0_,
member0_."name" as name3_1_0_,
favoriteli1_."member_id" as member_i4_0_1_,
favoriteli1_."name" as name2_0_1_,
favoriteli1_."price" as price3_0_1_,
favoriteli1_."member_id" as member_i4_0_0__,
favoriteli1_."id" as id1_0_0__
from
"member" member0_
left outer join
"favorite" favoriteli1_
on member0_."id"=favoriteli1_."member_id"
where
member0_."id"=?
select
phone0_."id" as id1_3_1_,
phone0_."member_id" as member_i3_3_1_,
phone0_."number" as number2_3_1_,
member1_."id" as id1_1_0_,
member1_."email" as email2_1_0_,
member1_."name" as name3_1_0_
from
"phone" phone0_
left outer join
"member" member1_
on phone0_."member_id"=member1_."id"
where
phone0_."member_id"=?
insert
into
"order"
("member_id", "name", "total_price", "id")
values
(?, ?, ?, ?)
위 로그를 보면 member와 onetoone의 관계인 phone까지 가져오는 것을 볼 수 있습니다.
member와 phone이 onetoone 관계이기 때문에 Lazy로 설정한다고 해도 member엔티티를 사용하는 순간 불러와 지는 문제입니다.
OneToOne의 Lazy로딩 문제는 Hibernate를 사용하는 과정에서 제일 골칫거리 중 하나입니다.
OneToOne의 Lazy로딩 문제
Hibernate를 사용하면서 OneToOne 관계에서 Lazy Loading을 사용하기 위해서는 여러 조건이 있습니다.
- 서로가 무조건 존재해야 합니다. OneToOne관계의 옵션 중 optional=false 가 만족해야 합니다.
- 양방향 관계가 아닌 단방향 관계이어야 하며 부모 테이블이 자식의 PK를 FK로 가지는 형태로 연관 관계의 주인이 되어야 합니다.
위 내용대로 하면 된다고는 하지만, 위 이슈로 인하여 테이블 구조를 변경하는 것에 대해서는 타당성을 충분히 검토해봐야 합니다.
그리고 위의 구조는 member가 phone이 없을 수도 있다는 조건이 있기 때문에 적합하지 않습니다.
member가 문제이지만 member 조회가 필요 없는 문제일 수도?
(https://github.com/cobiyu/querydsl 의 after 브랜치 참고)
예제 로직을 다시 생각해보면 아래와 같이 생각할 수도 있습니다.
- 1번 회원의 찜한 상품 목록을 가져온다.
- 1번 회원의 주문서를 만들어준다.
사실상 1번 회원의 정보가 필요한 것이 아니고 "찜한 상품 목록"과 생성할 주문서의 연관 관계를 위한 회원 번호가 필요한 것입니다.
SQL을 직접 작성하는 식으로 로직을 개발했다면 당연히 memeber를 조회하는 일은 없었겠지만, Hibernate의 연관 관계 설정에 대해 신경을 쓰다 보니 자연스레 member를 조회했던 것이었습니다.
Hibernate를 이용해 member를 조회하지 않고 연관 관계를 설정하는 방법은 아래와 같습니다.
public class MemberRepository {
private final JPAQueryFactory query;
public Order createOrderByFavorite(Long memberId){
List<Tuple> tuples = query
.select(favorite.name,
favorite.price)
.from(favorite)
.where(favorite.member.id.eq(memberId))
.fetch();
Integer totalPrice = tuples.stream()
.mapToInt(value -> value.get(favorite.price))
.sum();
/// 연관관계 설정할 id만 배정된 member
Member member = Member.builder()
.id(memberId)
.build();
return Order.builder()
.name("ordername")
.totalPrice(totalPrice)
.member(member)
.build();
}
}
위 로직을 실행했을 때 실행되는 쿼리는 아래와 같습니다.
select
favorite0_."name" as col_0_0_,
favorite0_."price" as col_1_0_
from
"favorite" favorite0_
where
favorite0_."member_id"=?
insert
into
"order"
("member_id", "name", "total_price", "id")
values
(?, ?, ?, ?)
member에 대한 조회는 실행되지 않은 모습을 볼 수 있습니다.
무조건 이 방법이 옳지는 않습니다.
만일 member의 phone 소유 여부에 따른 가격 할인 로직이 포함된다면 당연히 member의 조회가 이루어져야 하고
다른 방법을 찾아야 할 것입니다.
하지만 단순 연관 관계를 맺기 위한 조회 쿼리는 OneToOne이 포함 되어있는 Entity에서는 주의해야 하며,
위에서 제시한 방법도 하나의 해결책 중 하나로 고려해볼 수 있을 것 같습니다.
'JPA' 카테고리의 다른 글
[JPA] 일반 Join과 Fetch Join의 차이 (8) | 2021.06.30 |
---|---|
Proxy형태로 동작하는 JPA @Transactional (8) | 2021.02.10 |
JPQL과 영속성 컨텍스트의 관계 (1) | 2020.07.17 |
MultipleBagFetchException과 default_batch_fetch_size (0) | 2020.07.13 |
LazyInitializationException- no session(준영속상태에서의 참조) (0) | 2020.07.11 |