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가 개인에 소유 될때만 사용하자.