org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: Library.Library.Member.Member.heartList: could not initialize proxy - no Session 에러
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: Library.Library.Member.Member.heartList: could not initialize proxy - no Sessionorg.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: Library.Library.Member.Member.heartList: could not initialize proxy - no Session
발생상황
- 일대다(Member:Heart) 양방향 관계
- 등록되어있는 멤버는 특정 책에 대하여 찜목록에 추가한다.
- 따로 서비스 로직에 트랜잭션은 걸어두지 않은 상태
Member엔티티
@Getter
@Setter
@Entity(name="member_table")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
private String id;
private String pwd;
@OneToMany(mappedBy = "member",cascade = CascadeType.ALL )
private List<Heart> heartList = new ArrayList<>();
public void heartBook(Heart heartBook) {
this.heartList.add(heartBook);
}
public boolean notInHeartList(Long bookId){
return !(this.getHeartList().stream().anyMatch(heart->heart.getBookId().equals(bookId))); ★2★
}
}
- List<Heart> heartList
Member엔티티는 찜목록 리스트를 지니고 있으며
- heartBook(Heart heartBook)
Member엔티티를 통해서만 찜추가(heartBook) 할 수 있도록 구현하였다.
- notInHeartList(Long bookId)
- 해당 도서가 이미 찜이 되어있는지 판단하는 로직
- 양방향 관계로 매핑된 Heart엔티티들을 가져와 비교하여 판단
MemberService
@RequiredArgsConstructor
@Service
public class MemberService {
private final MemberRepository memberRepository;
...
public void heartBook(Long memberId, Long bookId) {
Member member = memberRepository.findMemberByMemberId(memberId); ★1★
if(member.notInHeartList(bookId)){ //존재하지 않는 찜도서ID인 경우
member.heartBook(Heart.builder().member(member).bookId(bookId).build());
memberRepository.save(member);
}
}
}
로직순서 설명
★1. PK를 통하여 Member엔티티 조회
★2. 조회된 Member 엔티티의 Heart리스트 내 동일한 책(bookId)을 가지고 있는지 확인
테스트 코드
@Test
public void 기존멤버가찜수행(){
//기존 멤버 ID(PK)
Long memberId= Long.valueOf(1);
//찜할 도서 ID(PK)
Long newHeartBookId=Long.valueOf(99);
memberService.heartBook(memberId,newHeartBookId);
}
결과
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: Library.Library.Member.Member.heartList: could not initialize proxy - no Sessionorg.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: Library.Library.Member.Member.heartList: could not initialize proxy - no Session
내 잘못된 예상은 아래와 같았다.
fetch 기본 전략은 Lazy이니깐 멤버테이블 엔티티만 조회하고
heart엔티티들은 ★2 라인의 Heart엔티티 get메소드를 통해 사용할 때 로딩이 될거야!! 문제없어!!
틀렸다. 왜??
JPA 영속성 컨텍스트는 보통 트랜잭션과 생명주기를 같이한다.
위 로직에선 트랜잭션을 걸어주지 않았으니 ★1라인 이후부턴 영속성관리 대상에서 벗어나게 된다.
★2 라인의 heart엔티티 get메소드 수행 시 이미 조회된 엔티티는 준영속 상태이니 변경감지 및 지연로딩이 동작하지 않게 되는 것이다.
해결 방안
1. Member엔티티 조회 시 관련 Heart엔티티까지 조회할 수 있도록 즉시로딩(Eager) 전략 사용
그러나 해당 방법은 비추
2. 서비스 계층 메소드 내에서 영속성 관리 대상이 될 수 있도록 트랜잭션 선언
3.
4.
5.
참고
@Transactional 옵션을 사용하지 않는다면? :: 혹독한 겨울에 자라 배울수록 겸손한 보리 (tistory.com)
@Transactional은 조회만 할 때 있어야할까? — 꾸준히 성장하는 개발자스토리 (tistory.com)
JPA/ could not initialize proxy - no Session (tistory.com)