TIL

LazyInitializationException

하얀잔디 2025. 5. 19. 21:32

"서비스 잘 돌다가 갑자기 500? 뭐야, 또 DB 터졌나?"

 

에러 로그를 봤다.

 

org.hibernate.LazyInitializationException: could not initialize proxy – no Session

 

 

아, 또 얘야?

처음엔 이 에러가 Hibernate나 JPA의 버그인 줄 알았다. 근데 이제는 안다.
내가 트랜잭션을 조절 못해서 그런 거였다.


LazyInitializationException이 왜 터지는지 제대로 이해해보자

 

Spring Data JPA에서 @OneToMany(fetch = LAZY) 같은 관계는, 실제 DB 쿼리를 지연 실행한다.
즉, 엔티티를 들고 왔지만, 관계 필드는 프록시 상태로 남겨둔다. 필요할 때 쿼리를 날리려고.

 
@Entity
classMember {
     @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
     private List<Order> orders;
}

문제는 이걸 트랜잭션 밖에서 꺼내려 할 때 발생한다.

 
// 이 메서드가 트랜잭션 범위 안에서 orders를 안 꺼냈다면...
member.getOrders().size(); // 💥 LazyInitializationException 발생
 

  1. Controller에서 DTO로 바꿀 때 Lazy 로딩 접근함
  2. Service에서 로그 찍다가 entity.getXXX() 하면서 에러 남
  3. 테스트 코드에서 트랜잭션 안 걸고 엔티티 필드 접근함

해결책?

1. 트랜잭션 범위를 넓히기

  • @Transactional(readOnly = true)를 Service 레벨이 아니라 Controller까지 걸 수도 있음
  • 근데 이건 좋은 방법은 아님. 트랜잭션 너무 넓히면 부작용 많음.

2. JOIN FETCH로 아예 한방 쿼리

 
@Query("SELECT m FROM Member m JOIN FETCH m.orders WHERE m.id = :id")
Optional<Member> findMemberWithOrders(@Param("id") Long id);

→ 가장 안정적인 방법.

3. EntityGraph 사용

 
@EntityGraph(attributePaths = {"orders"})
@Query("SELECT m FROM Member m WHERE m.id = :id")
Optional<Member> findMember(@Param("id") Long id);

 

→ 재사용성이 좋고, 네이밍도 깔끔해서 선호하는 방법.


✋ 근데 주의해야 할 점

  • LAZY 전략은 성능 이점이 분명히 있음
  • 무조건 EAGER로 바꾸는 건 최악의 해결법
  • 진짜 중요한 건, 트랜잭션 범위와 쿼리 타이밍을 예측 가능하게 유지하는 것

정리

LazyInitializationException은 JPA 잘못이 아니다.
우리가 지연 로딩의 시점과 트랜잭션의 범위를 헷갈려서 나는 거다.

'TIL' 카테고리의 다른 글

DB 마이그레이션 적용하는 법  (1) 2025.05.20
fetch join vs EntityGraph  (0) 2025.05.19
Kotlin vs Java  (0) 2025.05.13
조인 vs 서브쿼리  (0) 2025.05.13
LoadBalancer에 대해서  (0) 2025.05.12