본문 바로가기
DevLog/Tech Notes

🚨[JPA] LazyInitializationException: failed to lazily initialize a collection of role

by sunsetk 2025. 9. 12.

1. 문제 상황

Spring Boot + JPA 환경에서 온도 조회 API를 개발하던 중 아래와 같은 예외가 발생했다.

failed to lazily initialize a collection of role: goorm.ddok.member.domain.User.positions: 
could not initialize proxy - no Session

 

즉, `User` 엔티티에서 연관된 컬렉션(positions)을 접근하려고 했는데 Hibernate 세션이 이미 닫혀 있어서 Lazy 로딩을 수행하지 못한 상황이다.

 

---

 

2. 원인 분석

내 서비스 코드는 대략 이런 구조였다.

public TemperatureMeResponse getMyTemperature(CustomUserDetails currentUser) {

    User user = currentUser.getUser(); // Authentication 객체에서 바로 꺼냄
    
    String mainPosition = user.getPositions().stream()
            .filter(pos -> pos.getType() == UserPositionType.PRIMARY)
            .map(UserPosition::getPositionName)
            .findFirst()
            .orElse(null);
    ...
}

 

여기서 문제는 currentUser.getUser()가 영속성 컨텍스트에 관리되지 않는 Detached 객체라는 점이다.
Spring Security의 CustomUserDetails 안에 User 엔티티를 직접 넣어두면, 인증 시점에는 영속성이 유지되지만 서비스 계층에 도달할 때는 세션이 끊겨 있다.

 

따라서 user.getPositions() 호출 시 Hibernate는 DB에서 조회하려고 했지만, 세션이 없어서 LazyInitializationException을 던진 것이다.

 

---

 

3. 해결 방법

나는 다음과 같이 UserRepository를 통해 다시 DB에서 조회하도록 수정했다.

User user = userRepository.findByUserId(currentUser.getId())
        .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND));

 

즉, currentUser에서 꺼낸 엔티티를 직접 쓰는 대신 ID 기반으로 fresh 영속 엔티티를 다시 가져오는 방식이다.
이렇게 하면 @Transactional 범위 안에서 조회되므로 positions 같은 LAZY 필드도 안전하게 초기화된다.

 

---

 

4. 정리

 

 

  • currentUser.getUser()  Detached 엔티티일 가능성 높음 → LAZY 로딩 시 위험
  • currentUser.getId() → 안전한 원시 값 접근, 영속성 보장 없음 → DB에서 다시 조회 필요

 

  • CustomUserDetails 안의 엔티티는 인증 시점의 스냅샷일 뿐, 영속성 컨텍스트에 묶여 있지 않다.
  • 서비스 계층에서 연관 컬렉션을 안전하게 쓰려면 Repository를 통해 새로 영속화된 엔티티를 조회해야 한다.
  • JPA의 기본 로딩 전략(LAZY/EAGER)과 Hibernate 세션의 라이프사이클을 이해하는 게 중요하다.