본문 바로가기
Server/🌱 Spring Boot (java)

[Spring boot jpa] 연관된 엔티티 불러올때 LazyInitializationException 발생하는 문제 해결

by 코딩하는 동현😎 2025. 1. 25.

지연 로딩하려 할 때 세션/entityManager이 닫혀 LazyInitializationException이 발생하는 문제

 

@OneToMany 관계에서 fetch의 기본값은 LAZY이다.

@OneToMany(mappedBy = "house", cascade = CascadeType.ALL)
private List<Room> rooms = new ArrayList<>();

원인

지연 로딩(Lazy Loading) 관련 오류

FetchType.LAZY는 엔티티 관계에서 연관된 다른 엔티티를 지연 로딩 방식으로 로딩하겠다는 의미이다.

그러나 지연 로딩을 사용하는 엔티티가 프록시 객체로 반환되기 때문에, 실제로 해당 엔티티가 사용될 때까지 로딩되지 않는다.

이 때문에, 관계된 엔티티가 사용될 때 LazyInitializationException이 발생할 수 있다. 특히, @ManyToOne 관계가 설정된 엔티티가 트랜잭션 밖에서 접근될 때 발생한다.

 

public HouseDetailsResponseDto getHouseDetails(Long houseId) {
    // 트랜잭션 없이 House 조회
    House house = houseRepository.findById(houseId)
            .orElseThrow(() -> new RuntimeException("House not found"));

    // Lazy 로딩된 rooms 접근 (트랜잭션이 없으면 LazyInitializationException 발생)
    List<Room> rooms = house.getRooms();  // 여기가 LazyInitializationException을 일으킬 수 있음

    // DTO 변환 로직 생략

    return new HouseDetailsResponseDto(house.getId(), house.getName(), roomDtos);
}

 

 

houseRepository.findById(houseId).orElseThrow(() -> new RuntimeException("House not found"));

위 함수가 종료되면(트랜잭션이 종료되면) jpa 영속성 컨텍스트가 닫히게 되고, Lazy 로딩된 엔티티에 접근할 때 LazyInitializationException이 발생한다.


해결 방법

1. 트랜잭션 내에서 사용(불가)

LAZY 로딩을 사용할 때, 해당 엔티티를 사용하는 곳에서 트랜잭션이 유지되도록 하여, LazyInitializationException이 발생하지 않도록 해야 한다. 예를 들어, 서비스 레벨에서 트랜잭션을 관리하고, 그 안에서 관련 데이터를 접근할 수 있게 한다. 즉, 하나의 트랜잭션 함수 안에서 필요한 작업들을 전부 해야된다.

house를 불러와서 rooms에 대해서 dto 변환등 여러 동작을 findById 함수와 같이 연속적으로 실행하는것은 불가능하기 때문에 2번으로 해결하면 된다.


2. @Transactional 어노테이션을 이용해서 트랜잭션를 유지 (범위를 늘린다)

@Transactional 어노테이션은 Spring Framework에서 트랜잭션 관리를 지원하는 핵심 어노테이션이다.

메서드 또는 클래스에 적용할 수 있으며, 해당 메서드 또는 클래스 내에서 실행되는 모든 작업을 하나의 트랜잭션으로 묶어주고, 트랜잭션의 시작과 종료를 자동으로 관리한다.

 

1. 기본 개념

@Transactional을 사용하면, 트랜잭션을 자동으로 관리할 수 있다. 트랜잭션은 데이터베이스에서 일련의 작업이 모두 성공하거나 모두 실패하도록 보장하는 ACID(Atomicity, Consistency, Isolation, Durability) 속성을 제공한다.

 

2. 어노테이션의 기본 동작

자동 커밋/롤백: 기본적으로 @Transactional은 메서드 내의 모든 작업을 자동으로 커밋하거나, 예외가 발생하면 롤백합니다.

트랜잭션 범위: @Transactional이 적용된 메서드가 호출되면, 해당 메서드 내에서 실행되는 DB 작업은 하나의 트랜잭션 안에 포함됩니다. 메서드가 정상적으로 완료되면 트랜잭션이 커밋되고, 예외가 발생하면 롤백됩니다.

 

 

아래와 같이 @Transactional을 사용하면 getHouseDetails 함수 전체에서 트랜잭션이 유지되므로 영속성 컨텍스트가 함수 전체가 끝날때까지 유지하게 되고 지연로딩을 통해서 전부 구현할 수 있게 된다.

@Transactional //  추가!!
public class HouseService {

    private final HouseRepository houseRepository;

    public HouseDetailsResponseDto getHouseDetails(Long houseId) {
        // 트랜잭션 내에서 house 객체를 가져옴
        House house = houseRepository.findById(houseId)
                .orElseThrow(() -> new RuntimeException("House not found"));

        // 트랜잭션 내에서 Lazy 로딩된 rooms 접근
        List<Room> rooms = house.getRooms();
        
        ///...
}

3. FetchType.EAGER 로 설정

연관된 데이터를 항상 불러오게 되나, 그러면 항상 불러올때 조인해서 불러온다는 성능상 단점이 있다.

즉, 해당 객체 자체만 불러오고 싶을때마다 불필요한 조인 하지 않도록 별도의 커스텀 jpa 함수를 작성해야되므로 비효율적이라고 느꼈다.

 

fetch = FetchType.EAGER 추가

@OneToMany(mappedBy = "house", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<Room> rooms = new ArrayList<>();

4. 커스텀 jpa함수 작성

조인이 필요할때만 필요한 만큼 조인하고 그 외에는 jpa 기본 함수 쓰는게 성능상 3번보다 제일 안전하다고 생각했다.

커스텀 jpa함수를 정의해서 조인해서 받아 오도록 했다.

@Query("SELECT h FROM House h " +
            "LEFT JOIN FETCH h.rooms r " +
            "WHERE h.id = :houseId")
    Optional<House> findHouseWithRoomsById(@Param("houseId") Long houseId);
반응형

댓글