즉시 로딩과 지연 로딩(feat : 프록시 객체)

건 초이·2025년 1월 22일

JPA

목록 보기
2/3

즉시 로딩과 지연 로딩이란?

특히, **지연 로딩**에 대해서 깊이 알아가자.


즉시 로딩(Eager Loading) :

데이터를 조회할 때, 연관된 모든 객체의 데이터까지 한 번에 불러오는 방법이다.
= 처음부터 연관된 모든 정보를 한 번에 가져오는 것
= 엔티티 조회 시, 연관 엔티티까지 한 번에 로딩하는 방법이다.

연관 엔티티란?
한 엔티티의 필드로 정의되어 연관 관계가 매핑된 다른 엔티티를 말합니다.
예를 들어, Product 엔티티 클래스에 ProductImage 엔티티가 필드로 있다면, ProductImage는 Product의 입장에서 연관 엔티티가 됩니다.

필드에 없었다면? Product 입장에서는 ProductImage를 모름..

연관 엔티티에 대해 즉시 로딩으로 설정하면 (= 개별 연관 엔티티마다 설정 가능하다)
즉시 로딩으로 설정된 연관 엔티티들은 해당 엔티티 조회 시, 항상 함께 로딩된다..
당연하게도, 지연 로딩으로 설정한 연관 엔티티들은 로딩되지 않는다.

But,

즉시 로딩은 예상치 못한 쿼리가 많이 나가고, 필요하지 않은 쿼리까지 나가 위험하다.

예시 : Member 입장에서 Team이 즉시 로딩 관계라면?
Member를 조회할 때, 매핑된 Team도 항상 같이 로딩된다.

그렇더라도, Team 입장에서 Member는 지연 로딩일 수 있다. 설정을 통해 정하면 된다
그런 경우에는 Team을 조회할 때, 매핑된 Member는 프록시 객체로 로딩된다.

중요한 것은 즉시 로딩은 쿼리 튜닝이 어렵다. 따라서, 즉시 로딩은 지양해야 한다..
= 쿼리 튜닝하기도 전에 이미 지맘대로 쿼리가 나가버렸으니.. 튜닝이 어려움!


지연 로딩(Lazy Loading) :

연관된 객체의 필드를 사용하기 전까지 객체의 로딩을 미루다가,
연관된 엔티티를 실제 사용할 때, 그제서야 쿼리로 데이터를 조회하는 방법이다.
= 필요한 연관 엔티티를 그때 그때 요청하는 로딩 방법
= 연관 엔티티를 로딩하기 전까지는 프록시 객체(비어있는 가짜 객체)를 로딩해둔다.
= 실제 데이터가 필요한 시점에 DB에 접근해 실제 객체를 로딩합니다.

Member 엔티티 조회시, 지연 로딩 설정된 연관엔티티 Team을 프록시 객체로 가져온다
이때, getTeam()으로 Team을 조회하면 프록시 객체가 반환되며,쿼리는 나가지 않는다.

실제 객체를 사용하는 시점에 (=Team의 필드를 사용하는 시점에) DB에 쿼리가 나간다.
예를 들어 getTeam().getName()으로 팀의 필드에 접근 할 때는, 쿼리가 나간다.

그럼 member.getTeam().getId()는 어떻게 될까?
일반적인 생각으로는 연관 엔티티의 필드값을 사용했으니 쿼리가 나가지 않을까??
답은 ? 아닙니다!
왜냐하면.. 식별자 ID는 프록시 객체 생성 시 저장됐던 식별자로 이미 알고 있던 값이다!

아하, 그럼 getTeam(), getTeam().getId()는 쿼리가 안 나가는 구나..
프록시 객체를 반환하거나, 이미 알고 있던 식별자 값이라서!
그럼 getTeam().getName() 같은 실제 값을 쓰는 것은 쿼리가 나가겠군!

Tip : 지연 로딩을 사용하면 모든 쿼리에 대한 통제가 가능해집니다.

즉시 로딩을 사용하면, 필요하지 않을 때에도 연관 엔티티까지 로딩됩니다..
불필요한 쿼리가 나가게 됩니다.
쿼리 튜닝을 위해서는 개발자의 의도대로 쿼리가 나가는 지연 로딩을 사용합니다
결론 : 명시적으로 모든 엔티티의 로딩 전략을 Lazy로 변경합니다.


지연 로딩 시에 쓰이는 프록시 객체란?

JPA에서 프록시 객체는 실제 엔티티 객체 대신 데이터베이스 조회를 지연할 수 있는
가짜 객체를 의미합니다. 

처음 이해할 때는 프록시 객체에 대한 로직을 깊게 파고 들기 보다는
연관 엔티티의 필드에 접근하기 전까지는 연관 엔티티를 로딩하는 것을 미루다가,

실제 객체의 값이 필요할 때 그제서야 연관 엔티티를 로딩하는 정도로만 이해하고 넘어가면 될 것 같습니다.

지연 로딩을 사용하면, 실제 엔티티 객체가 아닌 프록시 객체가 들어가게 됩니다. 

프록시 객체가 초기화되면 실제 엔티티 참조가 로딩되고, 이후 프록시를 통해 

실제 엔티티의 메서드가 호출됩니다. 하지만 프록시 객체가 실제 엔티티로 대체되지는 않고,
  
계속해서 프록시 객체를 통해 참조로 실제 엔티티의 메서드가 호출됩니다.

지연 로딩과 프록시 객체 예시

지연 로딩 시 프록시 객체 예시 : 

회사에 직원(MEMBER)(TEAM)이 있다고 가정합니다. 
각 직원은 특정 팀에 속해 있습니다. 그런데 직원 정보를 불러올 때,
팀 정보는 그 순간에 필요하지 않을 수도 있습니다.

Member member = memberRepository.findById(1L);  // 1번 직원만 가져옴

이 코드는 1번 직원의 정보를 불러오지만,
직원이 속한 팀 정보는 그 순간 필요하지 않다고 가정합니다.

이때 JPA는 팀 객체를 즉시 불러오지 않고, 대신 프록시 객체를 만들어 둡니다. 
이 프록시는 아직 진짜 팀 객체의 참조를 갖고 있지 않지만, 진짜 팀 객체처럼 행동합니다. 
→  Team 객체의 참조가 없는 프록시 객체는, 실제 팀의 정보가 없습니다.

Team team = member.getTeam(); // 이 순간에도 데이터베이스 쿼리는 나가지 않음.

하지만 getTeam().getName()을 호출하면, 그제서야 JPA는 데이터베이스에 쿼리를 날려 팀 정보를 
불러옵니다., 실제 팀 정보가 필요해진 시점에 데이터베이스에서 값을 가져옵니다.

MEMBERTEAM이 있을 때, MEMBER만 조회를 할 때는 TEAM 필드는 프록시 객체로 채워집니다.
그 후에 TEAM의 필드에 접근하게 되면 프록시 객체는 아직 실제 데이터를 가지고 있지 않기 때문에
실제 데이터베이스에 쿼리를 실행하여 프록시 객체에 실체 엔티티의 참조를 채워옵니다. 

프록시 객체가 초기화 된 후에는 프록시 객체를 통해 실제 엔티티의 메서드를 호출하게 됩니다.
  
프록시 객체 -> 프록시 객체에서 실제 엔티티 참조 -> 실제 객체의 메서드 호출
프록시 객체를 거쳐 프록시 객체에서 실제 엔티티에 대한 참조를 통해 실제 객체의 메서드를 호출합니다.

프록시 객체 초기화 과정

프록시 객체 초기화 과정

1.어플리케이션이 객체에 getName()을 요청하면, 프록시 객체가 가로챈다.

2.프록시 객체에 getName()을 요청했으나 초기화가 안 된 상태라, 값이 없다면?
  
3.프록시 객체는 영속성 컨텍스트에 프록시 초기화를 요청한다.

4.영속성 컨텍스트는 프록시 초기화 요청을 받으면 DB로 쿼리를 날리고,
    실제 객체 참조를 Target에 넣어준다., **프록시 초기화**는 실제 객체의 참조를 Target에 넣어줌을 의미한다.

5.프록시 초기화가 된 상태에서 member.getName() 요청하면 Target.getName()으로
참조를 통해 메서드를 호출하여 값을 반환 함.
❗프록시 객체가 초기화 됐다고 실제 객체로 바뀌는 건 아니다. 

프록시 객체인 건 변함 없으나 Target에 실제 객체의 참조를 갖게 되면,
**프록시 객체가 실제 객체의 메서드를 호출할 수 있다 ←** 중요

예시) member.getName()을 하면 초기화 된 프록시 객체는 실제 객체의 참조를 갖게 돼 
target.getName() 메서드를 호출하여 member.getName()을 반환하면 된다!  

!프록시 객체에 대한 더 자세한 설명은 아래의 래퍼런스를 참고해주세요.

자세한 설명 참조 : https://woo-chang.tistory.com/28

끝.

profile
하면 된다

0개의 댓글