<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는 다음의 장점이 있기에 적용하였다.