계기
JPA에서 일대일 연관관계에서는 지연로딩이 적용되지 않는 경우가 있다. 프로젝트 진행중에 지연로딩을 지정하였음에도 지연로딩이 적용되지 않고 즉시로딩이 강제로 진행되는 것을 확인하게 찾아보게 되었다. 먼저 아래의 코드를 보자
사용자 정보를 가지고 있는 Member 엔티티와 사용자 세부 정보를 가지고 있는 MemberDetail 엔티티가 서로 일대일 연관관계를 가지게 된다.
@Entity
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String summoner;
private int medal;
private String puuid;
private String profileIconId;
@OneToOne(mappedBy = "member", fetch = FetchType.LAZY)
private MemberDetail memberDetail;
//중략
}
@Entity
@Getter
public class MemberDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(name = "kills")
private int kill;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name="member")
private Member member;
//중략
}
일반적으로 외래키를 가지고 있는 쪽이 연관관계의 주인이기 때문에 MemberDetail엔티티가 외래키를 가지도록 설정하였다. 여기서 부터 문제가 발생하는데 아래 로직을 보자(애초에 비즈니스 로직을 생각하지 않고 연관관계를 짠 나의 잘못이 크다)
public String getPuuidBySummoner(String summoner){
try {
return em.createQuery("select m from Member m where " +
"m.summoner=:summoner", Member.class)
.setParameter("summoner", summoner)
.getSingleResult().getPuuid();
}
catch (Exception e) {
return null;
}
}
summoner이름과 동일한 Member를 가져오는 select 쿼리이다. 우리는 JPA에서 지연로딩을 사용할 경우, 프록시를 사용하여 조회할때까지 실제 db를 조회하지 않고 LAZY하게 프록시가 버티고 있다는 것을 알고 있다. 그러면 위 getPuiidBysummoner 함수가 실행되면 Member 테이블에 select 쿼리하나만 날라간다는 것을 추측할 수 있다.(Member안의 MemberDetail은 프록시 객체가 들어갈것이다.)
Hibernate:
select
m
from
Member m
where
m.summoner=:summoner */ select
m1_0.id,
m1_0.medal,
m1_0.profile_icon_id,
m1_0.puuid,
m1_0.summoner
from
member m1_0
where
m1_0.summoner=?
Hibernate:
select
m1_0.id,
m1_0.damage,
m1_0.damaged_taken,
m1_0.death,
m1_0.kills,
m1_0.member
from
member_detail m1_0
where
m1_0.member=?
그러나 select 쿼리가 2개 날라갔고, 나머지 하나의 쿼리는 member_detail을 조회하는 쿼리로 지연로딩이 아닌 즉시 로딩이 되었다는 것을 확인 할 수 있다. 왜그럴까?
프록시
아주 친절한 설명이 있어서 가져왔다. 프록시라는 것은 객체이다. 프록시가 있다는 것은 해당 값이 존재하다는 것이다. (NULL이 아니다) 지연로딩을 할경우 프록시를 생성하게 되는데 그렇다는 것은 해당 값이 존재하다는 것 -> 상태를 유추할 수 있어야한다는 것이다. 그러면 일대일 연관관계로 돌아와서 생각해보자 위 getPuiidBysummoner가 실행될때 member엔티티를 가져오게 되는데, member엔티티에서는 memberDetail이 존재한다. 그러면 memberDetail의 값이 프록시로 대체되야한다. 그러면 프록시를 넣기 위해서는 해당 값이 null이 아님을 보장되야한다.(넓게 생각하면 상태를 알 수 있어야하는게 맞는거 같음) 그러나 해당 값이 null일 수 있다.(null인데 프록시 객체를 넣어버리면 null이 아닌게 되어버림) 따라서 프록시를 생성하기 위해서는 해당값이 실제로 존재하는지 memberdetail을 봐야한다. 결국 즉시로딩이 발생한다.
다른 연관관계에서는 어떻게 프록시가 가능할까?
처음 이말을 듣고 내가 알던 모든 JPA지식이 무너졌다.(원리를 제대로 이해하지 못한 나의 업보다) 다대일과 같은 다른 연관관계에서도 그러면 지연로딩을 할 수 없는 것 아닌가?(비어있는지 알 수 없기 때문에)
결론은 상태를 알 수 있다. 다대일의 경우 컬렉션에 담아져 있다 컬렉션은 상태를 알 수 있다 (ArrayList.empty) 따라서 프록시를 사용 할 수 있다.
해결
찾아본 해결 방법은 일대일 -> 다대일 , 별도의 처리 추가 등 몇개가 있었는데 나의 경우 연관관계주인을 바꾸는 것으로 해결했다. member의 경우에는 memberDetail을 알 필요가 없는 경우가 있는데, memberDetail의 경우 거이 member를 알고 있어야하는 경우가 태반이라 이부분은 즉시로딩을 하기로 결정하였다.
'사이드 프로젝트' 카테고리의 다른 글
2024 KUIT 프로젝트 회고 (5) | 2024.08.30 |
---|---|
네이버 지도 크롤링하기 (0) | 2024.08.15 |
Riot API 파헤치기 (0) | 2023.08.03 |