에러 내용
org.hibernate.LazyInitializationException: could not initialize proxy [com.hyunrian.project.domain.Member#1] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:165)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:314)
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:44)
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:102)
at com.hyunrian.project.domain.Member$HibernateProxy$KTB3GaAm.toString(Unknown Source)
at java.base/java.lang.StringConcatHelper.stringOf(StringConcatHelper.java:453)
at com.hyunrian.project.domain.MusicPreference.toString(MusicPreference.java:14)
(이하 생략)
LazyInitializationException이 발생하여 프록시를 초기화할 수 없다는 오류 메시지다.
문제 배경
개인 프로젝트의 DB 조회 테스트 도중 발생했다.
Querydsl로 작성한 조회 쿼리가 있는데, 원하는대로 데이터가 잘 조회되는지 확인하기 위해 간단히 작성한 테스트 코드에서 막혀버렸다.
앞 뒤 부분은 생략하고, 테스트 코드에서 문제가 되는 부분은 다음과 같다.
List<MusicPreference> topPreference = preferenceService.getTopPreference(member);
for (MusicPreference musicPreference : topPreference) {
log.info("musicPreference={}", musicPreference);
}
조회 결과를 로그로 확인할 수 없었다.
Entity인 MusicPreference는 다음과 같다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class MusicPreference {
@Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "music_preference_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
private String genre;
private String trackId;
private LocalDateTime createdDate = LocalDateTime.now();
public MusicPreference(Member member, String genre, String trackId) {
this.member = member;
this.genre = genre;
this.trackId = trackId;
}
}
이 예외가 발생하는 원인을 찾아보니 다음과 같았다.
- Lazy Loading으로 연관된 객체는 바로 초기화되지 않고, 필요할 때 정보가 채워지는 프록시 객체로 채워짐
- 서비스 계층에 적용한 Transaction으로 인해 메서드가 종료되면 영속성 컨텍스트가 유지되지 않음
- 영속성 컨텍스트에서 관리되지 않으므로 필요한 값이 있을 때 프록시 객체를 채우지 않아 예외가 발생됨
문제 해결
방법 1. `@ToString` 삭제
우선 객체를 문자로 출력하는 과정에서 발생했기 때문에 `@ToString`을 제거하면 예외는 발생하지 않지만, 객체의 필드값을 확인할 수 없으니 원하는 해결방법이 아니다. 애초에 ToString은 개발 편의를 위한 메서드니까.
방법 2. Service 계층에서 실행
public List<MusicPreference> getTopPreference(Member member) {
List<MusicPreference> preferenceList = preferenceQueryRepository.findByMemberIdInOrder(member.getId());
for (MusicPreference musicPreference : preferenceList) {
log.info("musicPreference={}", musicPreference);
}
return preferenceList;
}
Transaction 내에서 실행하면 해결되는 부분이니 Service 계층에서 log를 찍어보면 정상적으로 출력된다.
하지만 log로 조회 결과를 확인하는 건 테스트를 위한 것이기 때문에, 서비스단에서 굳이 실행할 필요는 없었다.
방법 3. 엔티티의 연관 필드를 즉시 로딩 방식(Eager)으로 변경
@ManyToOne //FetchType: LAZY -> EAGER
@JoinColumn(name = "member_id")
private Member member;
이 방법 역시 문제는 해결되지만 연관된 엔티티의 모든 데이터가 항상 로드되기 때문에 성능 저하의 문제가 발생한다.
방법 4. `toString()`을 오버라이드하여 필요한 필드만 구현
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class MusicPreference {
@Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "music_preference_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
private String genre;
private String trackId;
private LocalDateTime createdDate = LocalDateTime.now();
public MusicPreference(Member member, String genre, String trackId) {
this.member = member;
this.genre = genre;
this.trackId = trackId;
}
@Override
public String toString() {
return "MusicPreference{" +
"genre='" + genre + '\'' +
", trackId='" + trackId + '\'' +
'}';
}
}
최종적으로 적용한 방법이다.
확인하고자 했던 데이터는 member가 아니라 genre와 trackId였기 때문에 이 두 개만 출력되도록 변경하였다.
JPA에 대해 공부할 때는 영속성 컨텍스트의 생명주기에 대해 이해를 했다 여겼는데, 막상 실제로 적용해 보니 이 부분을 생각하지 못했다.
그래도 이 경험을 통해 JPA의 개념을 한번 더 잡게 되었다.🤪