org.hibernate.LazyInitializationException: could not initialize proxy [com.example.demo.domain.member.entity.Member#7] - no Session
db에서 findById를 사용하여 이를 가져오는 controller 작성 후 테스트를 돌리는 과정에서 이와 같은 오류가 발생하였다.
개략적으로 다음과 같은 상황이다.
해당 erd다이어그램에서 멤버 테이블과 전시회 테이블은 각각 스토리를 1:n관계를 맺고있다. 즉, 멤버와 전시회로 스토리라는 값이 생성되는 것이다. 이때 스토리id값을 가지고 스토리의 데이터와 멤버id와 멤버데이터 일부, 전시회id과 전시회 데이터 일부 값들을 가져오는 요구사항이 존재했다.
이에 해당 논리를 다음과 같이 작성하였다.
@Service
@RequiredArgsConstructor
public class StoryServiceImpl implements StoryService{
// 생략
public StoryResponseDto.StorySpecificResponseDto getStoryById(Long storyId) {
Story story = storyRepository.findById(storyId).orElseThrow();
return storyConverter.convertToSpecificResponseDto(story);
}
// 생략
}
결과적으로 테스트 코드를 실행시켰을 때 위와 같은 오류가 발생하였다.
원인은 Story Entity에서 설정한 연관관계에서 있었다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "exhibition_id")
private Exhibition exhibition;
이와같은 코드로 Story와 Member, Exhibition 사이의 연관관계를 설정하였다. 이때 @ManyToOne(fetch = FetchType.LAZY)로 지연로딩을 사용하였다.
JPA로 데이터를 조회할때 두가지 방식이 있다. 하나는 지연로딩(LAZY), 마지막 하나는 즉시로딩(EAGER)이다.
지연로딩은 데이터를 조회할 때 연관된 데이터는 불러오지 않고, 필요할 때 불러오는 방식이다.
즉시로딩은 데이터를 조회할 때 연관된 데이터를 즉시 모두 불러오는 방식이다.
이때 굳이 지연로딩을 사용 안하고 즉시로딩만 쓰면 되지 않을까라는 의문이 생겼다. 하지만 필요없는 데이터까지 조회하는데 사용될 필요없는 쿼리의 수가 증가하여 성능을 떨어뜨릴 수 있다는걸 깨닫고 가급적 지연로딩을 사용해야함을 알게 되었다.
Story Entity에서 Member와 Exhibition의 데이터까지 가져오고 싶었지만 지연로딩으로 db에 조회하였기 때문에 연관된 데이터인 Member, Exhibition에 접근하지 못하므로 에러가 발생하였다.
추가로 에러 문구 중 "could not initialize proxy" 의 원인은 다음과 같다.
지연로딩에서 Story를 조회할 때, Story와 연관된 객체인 Member, Exhibition의 객체는 프록시 객체로 대체된다. 이에 Member, Exhibition의 데이터는 불러와지지 않으므로 프록시 객체는 초기화되지 못한다.
다음과 같은 이유로 지연로딩으로 연관관계에 있는 데이터를 불러오지 못할 때, 프록시 객체를 초기화하지 못했다는 에러를 만날 수 있다.
해결방안으로 Story Entity에서 Member, Exhibition의 데이터를 불러오지 못했다면, Story로 연관된 Member, Exhibition의 객체를 찾고 찾은 객체로 하여금 데이터를 가져오는 방법을 생각했다.
public StoryResponseDto.StorySpecificResponseDto getStoryById(Long storyId) {
Story story = storyRepository.findById(storyId).orElseThrow();
Member member = story.getMember();
Exhibition exhibition = story.getExhibition();
return storyConverter.convertToSpecificResponseDto(story, member, exhibition);
}
간단하게, @Transactional을 사용하여 해당 오류를 해결할 수 있다. @Transactional 어노테이션이 붙은 메서드는 하나의 트랜잭션에서 실행된다. 트랜잭션 내에서는 JPA는 영속성 컨테스트를 활성화하여 지연로딩을 사용해도 연관된 데이터를 안전하게 사용할 수 있도록 해준다.