본문 바로가기
💻 트러블슈팅 (에러 해결)

영원한 JPA의 숙제! LazyInitializationException (no Session) 원인과 해결법 완벽 정리

by 데브프리 2026. 6. 30.

안녕하세요, 여러분의 든든한 IT 길잡이 데브프리입니다.

 

스프링 부트(Spring Boot)와 JPA(Spring Data JPA)를 사용해 프로젝트를 진행하다 보면, 분명히 코드는 잘 짠 것 같고 컴파일 에러도 없는데 화면을 띄우거나 API를 호출하는 순간 대문작만하게 500 에러가 터지는 경우가 있습니다.

 

로그를 살펴보면 다음과 같은 악명 높은 메시지가 적혀 있죠.

org.hibernate.LazyInitializationException: could not initialize proxy [...] - no Session

 

데이터베이스에 분명히 연관된 데이터가 존재하는데도 불구하고, 왜 데이터를 가져오지 못하고 '세션이 없다(no Session)'며 에러를 뱉어내는 걸까요?

 

오늘은 JPA를 쓸 때 무조건 한 번은 마주치게 되는 'LazyInitializationException'의 원인을 아주 직관적인 비유로 알아보고, 이를 똑똑하게 해결하는 2가지 핵심 방법까지 확실하게 정리해 드리겠습니다!

 

1. LazyInitializationException, 도대체 넌 누구냐? (퇴근한 창고 직원에 비유)

이 에러를 이해하려면 JPA의 핵심 기능 중 하나인 지연 로딩(Lazy Loading)을 알아야 합니다. 지연 로딩은 성능을 아끼기 위해 "당장 필요한 데이터만 DB에서 가져오고, 연관된 다른 데이터는 진짜로 사용할 때(호출할 때) DB에서 가져오는" 아주 똑똑한 기술입니다.

JPA LazyInitializationException 에러 원인 비유
이 예외는 데이터를 보관하는 창고(데이터베이스 세션)가 이미 문을 닫고 퇴근해 버렸는데, 뒤늦게 물건(연관 데이터)을 꺼내달라고 요청할 때 발생합니다.

일상생활에 비유해 볼까요?

게시글(Board)과 회원(User) 데이터가 연관되어 있습니다. 여러분이 퇴근 시간 직전에 창고(데이터베이스)에 가서 "게시글 데이터만 일단 먼저 주세요!"라고 요청했습니다. 창고 직원은 게시글을 건네주고는 문(Session)을 잠그고 퇴근(Transaction 종료)을 해버렸습니다.

 

그런데 잠시 후 화면을 그리다가 문득 "아 맞다, 이 게시글을 쓴 회원 이름도 보여줘야 하네?" 하고 뒤늦게 게시글 안에 비어있던 회원 객체를 호출했습니다. 하지만 이미 창고 문은 굳게 닫혔고 직원도 퇴근한 상태입니다. 자바에서는 데이터를 더 이상 DB에서 가져올 통로가 없기 때문에 "Could not initialize proxy - no Session" (통로가 없어서 가짜 객체를 진짜 데이터로 채울 수 없어!)이라며 에러를 던지는 것입니다.

 

2. 스프링 부트에서 이 에러를 해결하는 2가지 방법

이 에러를 해결하는 원리는 간단합니다. 창고 문을 닫지 않고 조금 더 열어두거나(트랜잭션 유지), 혹은 처음 창고에 갔을 때 연관된 데이터까지 한꺼번에 싹 다 들고 오는 것(Fetch Join)입니다.

스프링 부트 Fetch Join 지연로딩 에러 해결
처음 데이터를 조회할 때 연관된 데이터를 한 번에 묶어서 가져오는 fetch join을 사용하면 세션 종료 걱정 없이 안전하게 데이터를 쓸 수 있습니다.

스프링 부트 환경에서 이 에러를 해결하는 가장 대표적인 실무 방법 2가지를 소개합니다.

방법 ①: @Transactional 어노테이션 사용하기 (비즈니스 로직 안에서 해결)

데이터를 사용하는 시점까지 창고 문(Session)이 닫히지 않도록 영속성 콘텍스트의 생명 주기를 연장해 주는 방법입니다. 해당 로직이 실행되는 서비스(Service) 메서드 위에 어노테이션을 붙여줍니다.

Java
 
import org.springframework.transaction.annotation.Transactional;
import org.springframework.stereotype.Service;

@Service
public class BoardService {

    private final BoardRepository boardRepository;

    // 💡 @Transactional을 붙이면 이 메서드가 끝날 때까지 DB 연결 통로(세션)가 유지됩니다!
    @Transactional(readOnly = true) 
    public BoardResponseDto getBoardDetail(Long id) {
        Board board = boardRepository.findById(id).orElseThrow();
        
        // 통로가 열려있으므로 지연 로딩된 유저 객체를 호출해도 에러 없이 DB에서 스륵 가져옵니다.
        String username = board.getUser().getUsername(); 
        
        return new BoardResponseDto(board, username);
    }
}

가장 간편한 해결책이지만, 만약 컨트롤러(Controller)나 화면(View) 단까지 넘어가서 뒤늦게 연관 객체를 호출하려고 하면 여전히 에러가 날 수 있으므로 주의해야 합니다.

방법 ②: 페치 조인(Fetch Join) 사용하기 (가장 확실한 실무 해결책)

지연 로딩이고 뭐고, 처음부터 Query를 짤 때 두 테이블을 조인(Join)해서 필요한 데이터를 한 방에 다 가져오는 마법 같은 방법입니다. Repository에 직접 JPQL 쿼리를 작성해 줍니다.

Java
 
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface BoardRepository extends JpaRepository<Board, Long> {

    // 💡 join fetch를 사용하면 Board를 가져올 때 연관된 User까지 한 번에(Eager처럼) 다 긁어옵니다!
    @Query("select b from Board b join fetch b.user where b.id = :id")
    Optional<Board> findByIdWithUser(@Param("id") Long id);
}

이렇게 페치 조인을 쓰면 창고가 열려있는지 닫혀있는지 고민할 필요가 없습니다. 이미 내 손에 모든 데이터(Board와 User)가 다 들려있기 때문이죠. 데이터베이스에 날아가는 요청 횟수도 줄어들어 성능 최적화에도 매우 유리하므로 실무에서 적극 권장하는 방식입니다.

 

3. 요약: 트러블슈팅 체크리스트

LazyInitializationException이 발생했다면 당황하지 말고 아래 3가지를 체크하세요.

  1. 에러 발생 위치 파악: 컨트롤러나 DTO 변환 과정 등, 혹시 @Transactional 범위(서비스 레이어)를 벗어난 곳에서 연관 객체(.getUser())를 꺼내 쓰려고 하지는 않았는지 코드를 확인합니다.
  2. 연관 관계 흐름 파악: 어떤 엔티티와 어떤 엔티티 사이에서 충돌이 났는지 로그를 읽어봅니다. (...and no Session 앞부분에 에러가 난 엔티티의 이름이 적혀 있습니다.)
  3. 페치 조인 적용 검토: 만약 성능 저하 없이 깔끔하게 해결하고 싶다면, 해당 데이터를 조회하는 Repository 메서드에 join fetch 쿼리를 적용할 수 있는지 검토합니다.

 

마무리

JPA의 지연 로딩은 시스템 자원을 아껴주는 고마운 기술이지만, 그 생명 주기(Transaction)를 완벽히 이해하지 못하면 오늘 배운 하얀 화면과 시뻘건 로그로 우리를 찾아오곤 합니다.

 

내가 작성한 코드가 잘못되었다기보다는, "데이터를 달라고 하는 타이밍과 통로가 열려있는 타이밍이 서로 맞지 않아서" 생긴 일종의 시차 오류일 뿐입니다. 오늘 정리해 드린 트랜잭션 범위 확장과 페치 조인을 적절히 활용하신다면, 더 이상 JPA 데이터 조회 오류로 밤을 지새우는 일은 없을 것입니다.

 

오늘 준비한 트러블슈팅 가이드가 여러분의 붉은 에러 창을 깨끗하게 지워주었기를 바랍니다. 오류를 고친 경험은 여러분의 가장 큰 자산이 됩니다. 막히는 에러가 있다면 언제든 댓글로 남겨주세요.

 

지금까지 데브프리였습니다. 감사합니다!