<aside> 🔑
요약
ReportEntity의 reporter 필드는 LAZY 로딩이 적용되어 있었다.
서비스 로직에서 report.getReporter().getEmail()
을 호출했으나, 이미 Hibernate
세션이 닫힌 상태였다. 그 결과 LazyInitializationException
예외가 발생했었다.
이 문제는 Fetch Join
사용 혹은 트랜잭션을 명시하여 해결할 수 있었다. 트래픽 특성상 조회 빈도가 낮은 관리자 기능이므로 @Transactional(readOnly = true)
를 적용했었다. 이 설정을 통해 Lazy 로딩에 필요한 영속성 컨텍스트가 유지되어 오류가 해결되었다.
정리하자면, 해당 문제는 대부분 세션 종료 후 지연 로딩 접근이 원인이다. 따라서 서비스 계층에서 readOnly 트랜잭션을 명시하는 습관이 필요함을 느끼는 이슈였다.
</aside>
ReportEntity
조회 시, 연관된 MemberEntity reporter
필드는 @ManyToOne(fetch = FetchType.LAZY)
설정으로 인해 지연 로딩(Lazy Loading) 된다.
조회 쿼리에서 report.getReporter().getEmail()
을 호출했으나, 해당 필드는 초기화되지 않았고, 이미 Hibernate 세션이 닫힌 상태였다. 이로 인해 다음과 같은 예외가 발생하는 상황에 직면했다.
could not initialize proxy [com.mincho.herb.domain.user.entity.MemberEntity#9999] - no Session
개선 방법은 총 두 가지가 있다
reporter
를 명시적으로 함께 로딩하여 Lazy 로딩 오류를 방지할 수 있다.QReportEntity report = QReportEntity.reportEntity;
QMemberEntity reporter = QMemberEntity.memberEntity;
List<ReportDTO> reports = queryFactory
.selectFrom(report)
.leftJoin(report.reporter, reporter).fetchJoin() // 여기서 페치 조인 명시
.where(builder)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch()
.stream()
.map(r -> ReportDTO.builder()
.id(r.getId())
.targetId(r.getTargetId())
.targetType(r.getTargetType())
.reporter(r.getReporter() != null ? r.getReporter().getEmail() : null)
.status(r.getStatus().name())
.reasonSummary(r.getReasonSummary())
.reason(r.getReason())
.handleTitle(r.getHandleTitle())
.handleMemo(r.getHandleMemo())
.handledAt(r.getHandledAt())
.createdAt(r.getCreatedAt())
.build())
.toList();
@Transactional(readOnly = true)
명시읽기 전용 트랜잭션을 명시적으로 선언해 Hibernate 세션을 열어두고 Lazy 로딩이 문제없이 가능하게 한다. 서비스 레이어에 다음과 같이 추가하면 된다. 참고로 readOnly 는 해당 트렌잭션은 읽기 작업 전용으로 사용한다는 의미이다.
@Transactional(readOnly = true)
public List<ReportDTO> getReports(...) {
...
}
<aside> 💡
여기서 readOnly를 true 로 지정하게된 이유
여기서 적용하는 readOnly = true
는 다음의 장점이 있기에 적용하였다.