프록시와 지연로딩

지연로딩.

예를 들어, 다음과 같은 entity가 있다고 가정하자.

publi class Member{

       private String name;

       @ManyToOne
       @JoinColumn(name="team_id")
       private Team team;
}

위 엔티티를 정의하고
em.find(memberName)을 호출해서, 특정 member를 가져오자.

SQL은 teamjoin 되어서 team에 대한 정보도 가져오게된다.
하지만 지금은 team정보를 필요로 하지 않을 수도 있다.
그렇다면 쿼리에서, 굳이 join해서 모든 정보를 가져오는 것은 손해일 것이다.
즉, team정보를 필요로할 때 가져온다면, 리소스 낭비를 줄일 수 있을 것이다.

따라서, JPA는 지연로딩이라는 개념을 지원한다.

publi class Member{

      private String name;

      @ManyToOne(fetchType = FetchyType.LAZY)
      @JoinColumn(name="team_id")
      private Team team;
}

지연로딩은 단어 그대로 지연해서 로딩하겠단 뜻이다.
위와 같이 fetchTypeLAZY를 준다면,
해당 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를 전략으로 사용하자.

하지만, 한 예로, 항상 MemberTeam과 조인해서 데이터가 필요하다고 가정하자.
그렇다면, 굳이 LAZY 전략을 사용해서 쿼리를 두번 요청할 필요가 없을 수도 있다 생각 할 수 있다.

하지만, 그래도 LAZY 전략을 사용해야한다.

LAZY는 의도치 않은 쿼리를 항상 발생시킨다.
개발자 모르게 JOIN이 발생해서 쿼리가 요청되는것이다.
또 한 다음 경우를 생각해보자. @ManyToOne 연관관계에서, entity를 참조하고 참조하고 참조한다 생각해보자.
entity가 서로 참조의 참조를 @ManyToOne 연관관계로 10단계 의 참조를 가지고 있다면? 10번의 JOIN이 발생할 것이다.

그러니 실무에서는 LAZY를 쓰도록하자

또한, EAGER 전략은 N + 1 버그가 발생한다.
이 문제는 fetchJoin() 키워드로 찾아 해결 할 수있다.

<br>
<br>

그렇다면 이 지연로딩이 어떻게 가능한 것일까?

<br>

프록시를 기억하자.

JPA는 프록시를 사용한다.
프록시는 다음과 같이 생겼다.

<img src="https://file.podo-dev.com/blogs/images/2019/10/16/origin/324031f2-6978-4224-857a-e216519e2309.PNG" style="width:170px;">

프록시는 entity를 상속받고,
entity가 가진 똑같은 메소드를 가지고 있다.
그리고 또한 target을 가지고 있다.
target은 진짜 entity를 가지고 있는 참조값이다.

LAZY 로딩 전략을 선택했다면,
memberteam의 프록시를 가지고 있는다.

    // EAGER 전략
    member.getTeam().getClass() // 난 진짜 entity야!!

    // LAZY 전략
    member.getTeam().getClass() // 나프록시야!!

<br>
<br>

LAZY 로딩 전략인 상태에서
다음과 같이 내부정보를 조회한다면,

    member.getTeam().getTeamName();

다음과 같은 단계를 거치게된다.

  1. 영속성컨텍스트는 select쿼리를 요청하여 entity를 가져온다.
  2. proxytargetentity를 주입한다.
  3. proxy 메소드가 호출되어질 때, target의 메소드를 호출하여 반환한다. 예를 들어, proxygetTeamName()을 호출하면, target.getTeamName()을 호출하여 값을 리턴하는 것이다.

<br>
<br>

따라서 지연로딩되는 proxyentity인 것처럼 보이지만
명확히 말하면 proxyentity가 아니다.

다음 코드를 보면 확인 할 수 있다.

    // LAZY 전략
    member.getTeam().getClass() == Team.class // false

그래서 == 연산을 통해서 type을 비교하는 것은 옳지 않다.
대신에 instanceof 를 사용한다면 true를 확인 할 수 있다.

<br>
<br>

JPA 동일성을 보장한다.
데이터베이스의 데이터를 마치 Collection처럼 쓸 수 있는 것이다.
즉, 한 영속성컨텍스트 내에서 호출한 똑같은 entity는 다르지 아니하여야 한다.

하지만 entityproxy는 명백히 다른 객체이다.

    //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 쿼리를 요청 해서 proxytargetentity를 주입 할 수 없는 것이다. 때문에 이런 상황에서는 exception이 발생하니 주의하도록 하자

0
이전 댓글 보기
등록
TOP