JPA를 공부하자 02 - 프록시, 지연로딩편
프록시와 지연로딩
지연로딩.
예를 들어, 다음과 같은 entity
가 있다고 가정하자.
publi class Member{
private String name;
@ManyToOne
@JoinColumn(name="team_id")
private Team team;
}
위 엔티티를 정의하고
em.find(memberName)
을 호출해서, 특정 member
를 가져오자.
SQL은 team
과 join
되어서 team
에 대한 정보도 가져오게된다.
하지만 지금은 team
정보를 필요로 하지 않을 수도 있다.
그렇다면 쿼리에서, 굳이 join
해서 모든 정보를 가져오는 것은 손해일 것이다.
즉, team
정보를 필요로할 때 가져온다면, 리소스 낭비를 줄일 수 있을 것이다.
따라서, JPA는 지연로딩이라는 개념을 지원한다.
publi class Member{
private String name;
@ManyToOne(fetchType = FetchyType.LAZY)
@JoinColumn(name="team_id")
private Team team;
}
지연로딩은 단어 그대로 지연해서 로딩하겠단 뜻이다.
위와 같이 fetchType
을 LAZY
를 준다면,
해당 team
객체의 내부정보를 필요로 할 때, select
쿼리가 호출되고 team
의 값을 가져온다.
여기서 내부정보
란 말을 정확히 구분해야한다.
memeber.getTeam() // 내부정보 조회하지 않음, select 호출 하지 않음
member.getTeam().getTeamName(); // 내부정보 조회, select 호출
<br>
<br>
fetchType
은 두 가지로 나뉜다
- LAZY // 지연로딩
- EAGER // 즉시로딩
<br>
연관관계 매핑에 따라 로딩 전략에 Default가 정해져있다.
- @OneToOne /// EAGER
- @ManyToOne // EAGER
- @OneToMany // LAZY
- @ManyToMany // 쓰지마..
<br>
실무에서는 구분없이 LAZY
를 전략으로 사용하자.
하지만, 한 예로, 항상 Member
는 Team
과 조인해서 데이터가 필요하다고 가정하자.
그렇다면, 굳이 LAZY
전략을 사용해서 쿼리를 두번 요청할 필요가 없을 수도 있다 생각 할 수 있다.
하지만, 그래도 LAZY
전략을 사용해야한다.
LAZY
는 의도치 않은 쿼리를 항상 발생시킨다.
개발자 모르게 JOIN
이 발생해서 쿼리가 요청되는것이다.
또 한 다음 경우를 생각해보자. @ManyToOne
연관관계에서, entity
를 참조하고 참조하고 참조한다 생각해보자.
entity
가 서로 참조의 참조를 @ManyToOne
연관관계로 10단계 의 참조를 가지고 있다면? 10번의 JOIN
이 발생할 것이다.
그러니 실무에서는 LAZY
를 쓰도록하자
또한, EAGER
전략은 N + 1
버그가 발생한다.
이 문제는 fetchJoin()
키워드로 찾아 해결 할 수있다.
<br>
<br>
그렇다면 이 지연로딩이 어떻게 가능한 것일까?
<br>
프록시를 기억하자.
JPA는 프록시
를 사용한다.
프록시는 다음과 같이 생겼다.
<img src="https://static.podo-dev.com/blogs/images/2019/10/16/origin/324031f2-6978-4224-857a-e216519e2309.PNG" style="width:170px;">
프록시는 entity
를 상속받고,
entity
가 가진 똑같은 메소드를 가지고 있다.
그리고 또한 target
을 가지고 있다.
이 target
은 진짜 entity
를 가지고 있는 참조값이다.
LAZY
로딩 전략을 선택했다면,
member
는 team
의 프록시를 가지고 있는다.
// EAGER 전략
member.getTeam().getClass() // 난 진짜 entity야!!
// LAZY 전략
member.getTeam().getClass() // 나프록시야!!
<br>
<br>
LAZY
로딩 전략인 상태에서
다음과 같이 내부정보를 조회한다면,
member.getTeam().getTeamName();
다음과 같은 단계를 거치게된다.
- 영속성컨텍스트는
select
쿼리를 요청하여entity
를 가져온다. proxy
의target
에entity
를 주입한다.proxy
메소드가 호출되어질 때,target
의 메소드를 호출하여 반환한다. 예를 들어,proxy
의getTeamName()
을 호출하면,target.getTeamName()
을 호출하여 값을 리턴하는 것이다.
<br>
<br>
따라서 지연로딩되는 proxy
가 entity
인 것처럼 보이지만
명확히 말하면 proxy
는 entity
가 아니다.
다음 코드를 보면 확인 할 수 있다.
// LAZY 전략
member.getTeam().getClass() == Team.class // false
그래서 ==
연산을 통해서 type
을 비교하는 것은 옳지 않다.
대신에 instanceof
를 사용한다면 true
를 확인 할 수 있다.
<br>
<br>
JPA 동일성
을 보장한다.
데이터베이스의 데이터를 마치 Collection
처럼 쓸 수 있는 것이다.
즉, 한 영속성컨텍스트 내에서 호출한 똑같은 entity
는 다르지 아니하여야 한다.
하지만 entity
와 proxy
는 명백히 다른 객체이다.
//LAZY
Team team1= member.getTeam();
//EAGER
Team team2 = member.getTeam();
team1 == team2 // ????? 다를거같은데
<br>
하지만 , JPA는 이를 보장해준다.
언제든 한 영속성컨텍스트 내에서는 똑같은 객체를 보장해준다.
다음 두 가지 케이스를 보자.
//LAZY
Team team1= member.getTeam(); // 나프록시야!!
//EAGER
Team team2 = member.getTeam(); // 어? 맞춰야하는데 .. 나도 프록시!!
//EAGER
Team team1= member.getTeam(); // 나 진짜 entity야
//LAZY
Team team2 = member.getTeam(); // 어? 맞춰야하는데 .. 아 캐시에 있구나 나도 진짜 entity!!!
<br>
LAZY
, 다음을 주의하자.
LAZY
전략을 씀에 있어서 주의해야 할 점이 있다.
다음과 같은 상황이다.
//LAZY
Team team = member.getTeam()
~~ 트랜잭션 종료이거나
~~ 영속성 컨텍스트가 닫히거나
~~ team이 detach 됬다.
team.getTeamName() // Exception!
이미 영속성 컨텍스트가 닫힌 상황,
proxy
를 어느 방식이든 영속성 컨텍스트가 관여하지 않은 상황이 주어졋을때이다.
team
의 내부정보를 조회하면, 영속성컨텍스트가 관여하지 않기 때문에 proxy
를 초기화 할 수 없다.
즉 select
쿼리를 요청 해서 proxy
의 target
에 entity
를 주입 할 수 없는 것이다. 때문에 이런 상황에서는 exception
이 발생하니 주의하도록 하자
CaseCade
, OrphanRemoval
참조하는 곳이 한 곳일때,
특정 Entity가 개인에 소유 될때만 사용하자.