Spring/Spring Boot

[Spring] JPA 영속성 컨텍스트의 시작 범위 (Lazy Loding proxy 객체 조회 시 에러가 나는 이유)

민돌v 2022. 12. 15. 09:29

 

many to one 관계에 있는 엔티티를 jpa 즉시로딩에서 지연로딩으로 바꾸었더니 생각치 못했던 곳에서 생긴 문제의 기록입니다.

 

결론

더보기

jpa 지연로딩의 proxy 객체를 호출할 떄면, proxy 는 영속성 컨텍스트에서 데이터를 조회합니다.

즉 준영속 상태라면 proxy 객체와 실제객체를 매핑시킬 수 없습니다.

영속성 컨텍스트의 생명주기는 트랜잭션의 생명주기와 동일합니다.


 

📌 문제 상황

회사 코드에서 N+1 문제가 발생하는 부분이 있어 몇일 전 가벼운 마음으로 @ManyToOne 관계에 있는 엔티티를 Eager 로딩에서 Lazy 로딩으로 바꿔 주었다.

 

어치파 지연로딩으로 바꾼다고 하여도, 객체를 불러올 때 Proxy 객체에서 필요하면 엔티티를 조회하기 때문에 진정한 N+1 문제의 해결방안은 아니지만

마찬가지의 이유로, 연관 객체를 다시 조회하지 않기때문에 지연로딩으로 바꿔주었고, 내가 모르는 다른 곳에서(로직에서) 필요하면 proxy 객체 불러와서 사용하겠지 (N+1 다시 터지겠지~ 라는 안일한 마음ㅎ) 라는 생각에 에러는 나지 않을거라고 생각했다.

 

하지만 에러 ㅎ..

 

[Error Code]

ERROR [dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.hibernate.LazyInitializationException: could not initialize proxy [com.pood.server.entity.meta.Brand#0] - no Session] with root cause

 

 


❓즉시로딩에서 지연로딩으로 바꿨을 때 에러가 난 이유

문제는 역시 내가 알지 못하는 api 였고 (그래서 1주일 뒤에 발견함)

 

원복 후 다시 찬찬히 살펴보니, proxy 객체를 조회할 떄, jpa 에서 proxy 객체를 호출하니, 이제 진짜 객체를 불러와서 매핑해주어야하는데 이 부분에서 에러가 났습니다.

 

찬찬히 살펴봅시다..


📚  jpa 가 proxy 객체를 불러오는 과정

 

1) 지연 로딩 동작 과정 (LAZY LOADING)

엔티티 A를 조회시 관련(Reference)되어 있는 엔티티 B를 한번에 가져오지 않습니다. 프록시를 맵핑하고 실제 B를 조회할때 쿼리가 나갑니다.
  • 쿼리가 두번 나간다. A 조회시 한번,B 조회시 한번
  • fetch = FetchType.LAZY로 선언한다(어노테이션)
  • e.g.)
@ManyToOne(fetch = FetchType.LAZY) // 지연로딩
@JoinColumn
private Team team;

 

2) proxy 객체가 실제 객체를 조회할 때 일어나는 일

GoF 디자인패턴에서는 프록시 패턴을 한 문장으로 설명합니다.
Proxy Pattern - 사용할 객체의 제어권을 위임함으로써, 객체에 대한 클라이언트의 요청을 대신 받아 전달합니다.

 

👏🏻 즉 Poxy 란 대리인 역할을 한다는 뜻.  JPA에서의 프록시도 마찬가지로 진짜 엔티티가 아닌 대리인, 가짜 엔티티를 말합니다.

 

지연로딩 사용시 Member는 Team에 속해있고, Member조회시 Member의 엔티티인 Team을 조회하지 않고 일단 프록시 오브젝트로 Team을 구성하고 실제 Team을 조회할때 쿼리가 나갑니다.

반대인 즉시로딩은 Member 조회시 Team 엔티티도 한번에 가져옵니다.


3) JPA Proxy 특징

처음 한번만 초기화
  • 프록시가 실제 엔티티로 바뀌는게 아니고, 초기화시 프록시를 통해서 엔티티에 접근합니다.
  • 프록시는 원본 엔티티를 상속받습니다.
  • 영속성 컨텍스트에 이미 엔티티가 존재하면, getReference() 메소드를 호출해도 실제 엔티티가 반환됩니다.
  • 따라서 준영속상태일때 프록시를 쓰면 문제가 될수 있습니다. ✨

 

즉, 지연로딩 시 연관관계에 있는 객체를 프록시 객체(빈 객체)로 보내지만, 이걸 직접 호출해서 매핑 할떄에는, 프록시가 호출하는 객체가 영속성 컨텍스트에 관리가 되어있는 상태여야만 하는 겁니다.

jpa proxy 객체는 영속성 컨텍스트에 있는 객체만을 조회할 수 있다는 의미입니다. 

 


📚 영속성 컨텍스트의 주기

스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용합니다.

즉 트랜잭션 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻으로, 트랜잭션을 시작할 떄 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료합니다.

 

트랜잭션을 커밋하면 JPA는 먼저 영속성 컨텍스트를 플러시해서 변경 내용을 데이터베이스에 반영한 후에 데이터베이스 트랜잭션을 커밋합니다.

준영속 상태와 영속 상태

 

준영속 상태

  • 문제가 생겼던 이유는, 지연로딩으로 불러오는 레이어 (service 단) 에서 트랜잭션을 적용하지 않았기 때문입니다.
  • 트랜잭션이 없는 프레젠테이션 계층에서는 엔티티가 영속 상태가 아닌 준영속 상태입니다.
  • 따라서 변경 감지지연로딩이 동작하지 않습니다.

 

요약하자면 트랜잭션의 생명주기 = 영속성 컨텍스트의 생명주기이며, 트랜잭션이 적용되지 않는 레이어에서는 엔티티가 준영속 상태이기 때문에 지연로딩 시, proxy 객체가 엔티티를 호출할 수 없습니다!

 

 

 

트랜잭션을 잘걸자!

 

 


*참고