<aside> 🔑

요약 ReportEntity의 reporter 필드는 LAZY 로딩이 적용되어 있었다. 서비스 로직에서 report.getReporter().getEmail()을 호출했으나, 이미 Hibernate 세션이 닫힌 상태였다. 그 결과 LazyInitializationException 예외가 발생했었다.

이 문제는 Fetch Join 사용 혹은 트랜잭션을 명시하여 해결할 수 있었다. 트래픽 특성상 조회 빈도가 낮은 관리자 기능이므로 @Transactional(readOnly = true)를 적용했었다. 이 설정을 통해 Lazy 로딩에 필요한 영속성 컨텍스트가 유지되어 오류가 해결되었다.

정리하자면, 해당 문제는 대부분 세션 종료 후 지연 로딩 접근이 원인이다. 따라서 서비스 계층에서 readOnly 트랜잭션을 명시하는 습관이 필요함을 느끼는 이슈였다.

</aside>

문제배경

could not initialize proxy [com.mincho.herb.domain.user.entity.MemberEntity#9999] - no Session

개선

개선 방법은 총 두 가지가 있다

개선 방법1 ) Fetch Join 사용

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();

개선방법2) 서비스 메서드에 @Transactional(readOnly = true) 명시

읽기 전용 트랜잭션을 명시적으로 선언해 Hibernate 세션을 열어두고 Lazy 로딩이 문제없이 가능하게 한다. 서비스 레이어에 다음과 같이 추가하면 된다. 참고로 readOnly 는 해당 트렌잭션은 읽기 작업 전용으로 사용한다는 의미이다.

@Transactional(readOnly = true)
public List<ReportDTO> getReports(...) {
    ...
}

<aside> 💡

여기서 readOnly를 true 로 지정하게된 이유

여기서 적용하는 readOnly = true는 다음의 장점이 있기에 적용하였다.