본문 바로가기
Spring

[Spring] LazyInitializationException 해결 방법

by worldcenter 2024. 11. 8.

이슈

Springboot 통합 테스트를 진행할 때 다음과 같은 에러가 발생했습니다.

failed to lazily initialize a collection of role: com.sparta.myselectshop.entity.Product.productFolderList: could not initialize proxy - no Session org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.sparta.myselectshop.entity.Product.productFolderList: could not initialize proxy - no Session at org.hibernate.collection.spi.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:636) at org.hibernate.collection.spi.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:219) at org.hibernate.collection.spi.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:615) at org.hibernate.collection.spi.AbstractPersistentCollection.read(AbstractPersistentCollection.java:138) at org.hibernate.collection.spi.PersistentBag.iterator(PersistentBag.java:369)

 

 

원인

디버깅을 해본 결과 ProductService에서 productList가 ResponseDto로 넘어갈 때 문제가 발생한 것으로 보였습니다.

public Page<ProductResponseDto> getProducts(User user, int page, int size, String sortBy, boolean isAsc) {
        // 페이징 처리
        Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
        Sort sort = Sort.by(direction, sortBy);
        Pageable pageable = PageRequest.of(page, size, sort);

        // 사용자 권한 가져와서 ADMIN이면 전체 조회, USER면 본인이 추가한 부분 조회
        UserRoleEnum userRoleEnum = user.getRole();

        Page<Product> productList;
        if (userRoleEnum == UserRoleEnum.USER) {
            // 사용자 권한이 USER일 경우
            productList = productRepository.findAllByUser(user, pageable);
        } else {
            productList = productRepository.findAll(pageable);
        }
        return productList.map(ProductResponseDto::new); // 문제 발생 지점
    }

 

LazyInitializationException이 언제 일어나는가 구글링 해보니 영속성 컨텍스트는 엔티티를 보관해두었다가 필요에 의해 lazy loading(지연 로딩)이 발생하게 됩니다. 하지만, 영속성 컨텍스트가 종료된 후 지연 로딩이 발생하면 상기와 같은 에러가 발생합니다.

 

Product에서 productFolderList는 OneToMany로 기본적으로 Lazy loading이 default 값이 됩니다.

따라서, productList.getproductFolderList() 를 하게 되면 Lazy loading이 동작하면서 조회 SQL 쿼리가 날라가게 됩니다.

public class Product extends Timestamped {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String image;

    @Column(nullable = false)
    private String link;

    @Column(nullable = false)
    private int lprice;

    @Column(nullable = false)
    private int myprice;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @OneToMany(mappedBy = "product")
    private List<ProductFolder> productFolderList = new ArrayList<>();
    
}

 

하지만, 이전 ProductService 코드의 경우 지연 로딩으로 인해 Product 객체에 productFolderList 정보가 들어가지 않은 채로 ResponseDto 로 넘어가는 과정에서 영속성 컨텍스트가 종료되었습니다. 그 이후, ResponseDto에서 뒤늦게 productFolerList를 조회하다보니 Lazy 예외가 발생한 것 입니다.

@Getter
@NoArgsConstructor
public class ProductResponseDto {
    .
    .
    .

    public ProductResponseDto(Product product) {
        this.id = product.getId();
        this.title = product.getTitle();
        this.link = product.getLink();
        this.image = product.getImage();
        this.lprice = product.getLprice();
        this.myprice = product.getMyprice();
        for (ProductFolder productFolder : product.getProductFolderList()) {
            productFolderList.add(new FolderResponseDto(productFolder.getFolder()));
        } // 영속성 컨텍스트가 종료된 후 productFolder 호출
    }
    
}

 

 

 

해결

이를 해결하기 위한 방법으로 다음 2가지가 존재합니다.

 

1. Product Entity 내 productFolderList Fetch type을 LAZY -> EAGER로 변경

public class Product extends Timestamped {

    .
    .
    .

    @OneToMany(mappedBy = "product", fetch = FetchType.EAGER)
    private List<ProductFolder> productFolderList = new ArrayList<>();
    
}

 

이 경우 정상적으로 테스트가 실행되는 것을 볼 수 있습니다.

 

 

 

2. Fetch Join 활용하기

fetch type이 Lazy여도 JSQL의 fetch join이 우선합니다.

// 예시
@Query("SELECT p FROM Product p JOIN FETCH p.productFolderList WHERE p.user = :user")
List<Product> findAllByUserWithProductFolder(@Param("user") User user);

 

하지만, 해당 소스코드의 경우 페이지네이션을 사용하기 때문에 Fetch Join을 사용하지 않았습니다. 그 이유는 Page<Product>를 반환하면서 JOIN FETCH를 사용하게 되면, Hibernate  제한으로 페이징이 제대로 동작하지 않기 때문입니다.

 

페이징 쿼리에서 fetch join을 사용하되, 별도의 카운트 쿼리를 작성하여 페이징 처리가 가능하도록 설정할 수 도 있습니다.

이와 관련된 내용은 해당 본문에서는 더 깊게 다루지 않고 관련 블로그 글을 링크 걸어두겠습니다.

 

 

KEEP

해당 이슈를 해결하면서 알게된 점은 리소스 성능을 생각해서 무조건 EAGER을 지양하는 것보다 위와 같이 항상 Join으로 데이터를 가져와야 하는 상황에서는 EAGER을 사용하는 것이 유용하다는 것을 알게 되었습니다.