지연 로딩과 Transactional

ghltjd369·2023년 7월 14일
0

지연로딩 네 이놈!


지연로딩...
강의를 들으면서, 그리고 jpa 스터디를 진행하면서 지연로딩이 좋다는 것은 너무너무 잘 알았다.
그리고 사용 방법도 엄청 쉬워보였다.
그냥 @ManyToOne(fetch = FetchType.LAZY) 이렇게 쓰면 지연로딩이 되니까..
그래서 별 생각 없이 모든 연관관계에 저렇게 지연로딩을 적용했다.

그런데 웬걸!

failed to lazily initialize a collection of role: com.goodjob.article.domain.article.entity.Article.likesList: could not initialize proxy - no Session

이런 오류가 발생했다.

오류가 발생한 부분은 이곳이다.

Article.java

@OneToMany(mappedBy = "article", cascade = {CascadeType.ALL}, fetch = FetchType.LAZY)
@Builder.Default
private List<Likes> likesList = new ArrayList<>();

ArticleService.java

public Page<ArticleResponseDto> findByCategory(int page, int id, int sortCode, String category, String query) {
    Pageable pageable = PageRequest.of(page, 12);

    List<Article> articles = articleRepository.findQslBySortCode(id, sortCode, category, query);

    List<ArticleResponseDto> articleResponseDtos = articles
            .stream()
            .map(articleMapper::toDto)
            .collect(Collectors.toList());

    return convertToPage(articleResponseDtos, pageable);
}

ArticleMapplerImple.java

@Override
public ArticleResponseDto toDto(Article article) {
    if ( article == null ) {
        return null;
    }

    ArticleResponseDto articleResponseDto = new ArticleResponseDto();

    articleResponseDto.setId( article.getId() );
    articleResponseDto.setCreatedDate( article.getCreatedDate() );
    articleResponseDto.setModifiedDate( article.getModifiedDate() );
    articleResponseDto.setTitle( article.getTitle() );
    articleResponseDto.setContent( article.getContent() );
    articleResponseDto.setLikesList( likesListToLikesResponseDtoList( article.getLikesList() ) );
    articleResponseDto.setViewCount( article.getViewCount() );
    articleResponseDto.setCommentList( commentListToCommentResponseDtoList( article.getCommentList() ) );
    articleResponseDto.setCommentsCount( article.getCommentsCount() );
    articleResponseDto.setMember( article.getMember() );
    articleResponseDto.setHashTagList( hashTagListToHashTagResponseDtoList( article.getHashTagList() ) );
    articleResponseDto.setCategory( article.getCategory() );

    return articleResponseDto;
}

protected List<LikesResponseDto> likesListToLikesResponseDtoList(List<Likes> list) {
    if ( list == null ) {
        return null;
    }

    List<LikesResponseDto> list1 = new ArrayList<LikesResponseDto>( list.size() );
    for ( Likes likes : list ) {
        list1.add( likesToLikesResponseDto( likes ) );
    }

    return list1;
}

이 오류가 발생한 이유는 다음과 같다.
우선, 내가 Article에서 likesList를 지연 로딩으로 가져왔다.
그래서 mapstruct를 이용하여 article을 articleResponseDto로 변환하는 과정에서 likesList가 프록시 상태인데 list.size()를 하려니까 오류가 나는 것이었다.
아무것도 없는데 size를 하려니까!!

정말 정말 시간을 많이 잡아먹었다.
별의별 방법을 다 써봤지만 전부 안됐다.
하지만 사실 이 문제는 엄청엄청엄청 단순한 문제였다.
그냥 @Transactional만 붙여주면 해결됨....

ArticleService.java

@Transactional
public Page<ArticleResponseDto> findByCategory(int page, int id, int sortCode, String category, String query) {
    Pageable pageable = PageRequest.of(page, 12);

    List<Article> articles = articleRepository.findQslBySortCode(id, sortCode, category, query);

    List<ArticleResponseDto> articleResponseDtos = articles
            .stream()
            .map(articleMapper::toDto)
            .collect(Collectors.toList());

    return convertToPage(articleResponseDtos, pageable);
}

그럼 왜 @Transactional을 붙이지 않으면 오류가 발생할까?

@Transactional을 안 붙이면 오류가 나는 이유


우선 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 사용한다.
이는 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻이다.
좀 더 쉽게 말하면 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다는 뜻이다.

그러니까 내가 처음에 작성했던 코드에서 article은 준영속 상태인 것이다.
준영속 상태에서는 지연 로딩을 사용할 수 없다.
그래서 article.likeList를 사용하는 findByCategory 메소드에 @Transactional을 붙여서 영속성 컨텍스트로 만드는 것이다.

그렇게 되면 article은 findByCategory 메소드가 시작하면 영속성 컨텍스트가 되고 메소드가 종료되면 영속성 컨텍스트가 종료된다.
그렇기 때문에 어디에 @Transactional을 붙이는지가 굉장히 중요하다.
처음에는 막 이상한 메소드에 붙여서 오류가 전혀 해결되지 않았었다.

내가 어디부터 어디까지 영속성 컨텍스트로 사용할 것인지 잘 확인해야 하는 것 같다.

회고


어떻게 보면
아 아니 어떻게 보면이 아니라 그냥.
엄청 단순한 오류였다.
트랜잭션과 영속성 컨텍스트의 성질에 대해서 알고 있다면 간단하게 해결할 수 있는...
나는 jpa 스터디를 진행했음에도 이 문제를 제대로 해결하지 못했다.
그만큼 제대로 이해하지 못했다는 뜻이겠지.

정말 다행히도 이번 이슈를 해결하면서 너무너무 깊게 이해되었다.
물론 100% 이해한 것은 아니겠지만 언제 어떻게 @Transactional을 사용해야 하는지는 감을 조금 잡은 것 같다.

0개의 댓글