[JPA] OneToMany 관계에서의 Lazy와 프록시 문제 해결

원모어깨찰빵·2024년 2월 19일

트러블 슈팅

목록 보기
5/7
post-thumbnail

문제 상황

OneToMany로 연결된 User과 Word 엔티티를 Fetch.LAZY(지연 로딩)으로 설정하였는데, Oauth를 통하여 받은 User객체로 Word List를 불러와 사용하는 과정에서 다음의 에러가 발생했다.

failed to lazily initialize a collection of role

User엔티티와 Word엔티티

User엔티티

public class Users implements UserDetails {

    @Builder.Default
    @OneToMany(mappedBy = "users")
    private List<Word> words = new ArrayList<>();
...
}

Word 엔티티

public class Word {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    @Setter
    private Users users;

다음과 같은 상황에서 에러가 난 코드는 서비스단에서 다음과 같다.

@Transactional(readOnly = true)
    public List<WordInfoDto> getWordDtoList(Users users) {        
     
        List<Word> wordList=users.getWords();
        List<WordInfoDto> wordInfoDtoList=new ArrayList<>();
        for(int i=0;i<wordList.size();i++){
           wordInfoDtoList.add(WordInfoDto.from(wordList.get(i)));
        } 
        return wordInfoDtoList;
    }

여기서 문제가 난 부분은 wordList가 프록시인 상태에서 wordList.size()를 호출하였기 때문인데, 이는 아래의 블로그에서 JPA Lazy Loading Select문 실행시점을 보며 알아낼 수 있었다.
https://velog.io/@byeongju/JPA-Lazy-Loading-%EB%82%B4-%EB%88%88%EC%9C%BC%EB%A1%9C-%ED%99%95%EC%9D%B8%ED%95%98%EA%B8%B0
이를 그림으로 표현하면 다음과 같다.

이제 정확하게 어느 부분에서 오류가 났는지 다시 확인해 보기 위하여 다음과 같이 디버깅을 하였다.

@Transactional(readOnly = true)
    public List<WordInfoDto> getWordDtoList(Users users) {
        System.out.println("before getWords()-----------------------------------");
        List<Word> wordList=users.getWords();
        List<WordInfoDto> wordInfoDtoList=new ArrayList<>();
        System.out.println("before wordList.size()-----------------------------------");
        for(int i=0;i<wordList.size();i++){
            System.out.println("before wordList.get(i)");
            wordInfoDtoList.add(WordInfoDto.from(wordList.get(i)));
        }
        return wordInfoDtoList;
    }

실행 결과는 다음과 같다.

이를 통하여 user.getWords()는 프록시 객체를 반환하기 때문에 쿼리가 발생하지 않았으며, 프록시 객체.size()를 실행하였기에 오류가 났음을 알 수 있었다!

해결 방법

wordList가 프록시 객체인 상태라 문제가 생기는 것이므로, 쿼리를 발생시켜 초기화 해주면 문제가 해결된다.
따라서 wordList=wordRepositry.findAllBy(user) 로 초기화 작업을 진행하여 주니 해결되었다.

@Transactional(readOnly = true)
    public List<WordInfoDto> getWordDtoList(Users users) {
        List<Word> wordList=wordRepository.findAllByUsers(users);
        List<WordInfoDto> wordInfoDtoList=new ArrayList<>();
        for(int i=0;i<wordList.size();i++){
            wordInfoDtoList.add(WordInfoDto.from(wordList.get(i)));
        }
        return wordInfoDtoList;
    }

이제 유저 확인(Oauth), 유저를 통한 word들 검색으로 2번의 쿼리로 정상 작동된다.

TODO

단어 저장 로직 오류처리
트랜잭션과 영속성 컨텍스트

profile
https://fuzzy-hose-356.notion.site/1ee34212ee2d42bdbb3c4a258a672612

0개의 댓글