could not initialize proxy - no Session는 주로 Lazy 로딩을 사용하는 객체를 유효한 세션 범위 밖에서 접근하려고 할 때 발생한다.
엔티티나 연관 관계에 fetch = FetchType.LAZY가 설정되어 있으면, 데이터는 처음 조회 시점에 즉시 로드되지 않는다.
대신 Hibernate는 프록시 객체(가짜 객체)를 생성하고, 실제 데이터를 사용하려고 할 때 필요한 데이터를 DB에서 로드한다.
나의 경우는 프로젝트에 답변 작성 부분에서 발생했는데
@Transactional
public ConversationResponse addAnswer(CreateConversationRequest request) {
ConversationMST conversationMST = conversationMSTRepository.findById(request.mstId())
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 대화입니다."));
MemberInfo writer = SecurityUtil.getCurrentMember().getMemberInfo();
Conversation answer = Conversation.builder()
.content(request.content())
.isPrivate(request.isPrivate())
.isQuestion(false) // true = 질문, false = 답변
.memberInfo(writer)
.build();
answer.setConversationMST(conversationMST);
Conversation conversation = conversationRepository.save(answer);
applicationEventPublisher.publishEvent(
new NotificationEvent(writer, conversationMST.getGuest(), conversation, NotificationType.COMMENT)
);
return convertToResponse(conversation);
}
아래 코드에서 문제가 발생하게 되었다.
applicationEventPublisher.publishEvent(
new NotificationEvent(writer, conversationMST.getGuest(), conversation, NotificationType.COMMENT)
);
conversationMST.getGuest() 에서 객체가 초기화 되지 않고 이벤트로 넘어가
@TransactionalEventListener
public void notification(NotificationEvent notificationEvent) {
Conversation conversation = notificationEvent.getConversation();
MemberInfo sender = notificationEvent.getSender();
MemberInfo receiver = notificationEvent.getReceiver();
NotificationType notificationType = notificationEvent.getNotificationType();
notificationService.saveAndSendNotification(receiver, sender, notificationType, conversation);
if (notificationType == NotificationType.QUESTION || notificationType == NotificationType.COMMENT) {
if (receiver != null && receiver.getMemberSetting().getEmailNotice()) {
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String domainName = mailSendService.getDomainName(httpServletRequest);
mailSendService.sendEmailNotice(
receiver.getEmail(),
domainName,
notificationType.label(),
"/api/conversations/details/" + conversation.getConversationMST().getId(),
conversation.getContent(),
receiver.getNickname()
);
}
}
}
saveAndSendNotification메소드의 String receiverEmail = receiver.getEmail();에서 초기화 되지 않은 프록시 객체를 접근하려해서 문제가 발생하게 된다.
@Async("notificationTaskExecutor")
public void saveAndSendNotification(MemberInfo receiver, MemberInfo sender, NotificationType type, Conversation conversation) {
Notification notification = notificationRepository.save(createNotification(receiver, sender, type, conversation.getId()));
String receiverEmail = receiver.getEmail();
String eventId = makeTimeIncludeId(receiverEmail);
long countIsRead = notificationRepository.countByIsReadFalseAndReceiverInfo(receiver);
Map<String, SseEmitter> emitters = emitterRepository.findAllEmitterStartWithByMemberId(receiverEmail);
if (emitters.isEmpty()) {
return;
}
emitters.forEach(
(key, emitter) -> {
NotificationResponse notificationResponse = toNotificationResponse(notification, conversation);
emitterRepository.saveEventCache(key, notificationResponse);
sendToClient(emitter, eventId, key, notificationResponse, countIsRead);
}
);
}
saveAndSendNotification 메소드가 비동기로 처리되어 트랜잭션을 벗어나 프록시에 접근하게 되어 초기화를 할 수 없는 상황이 되었다.
해결방법은 비동기 메소드 이전에 객체를 초기화 한 뒤 넘기거나 비동기 메소드에서 새로 조회해야 했는데 나는 초기화 해서 넘기는 것을 선택하였다
초기화 하는 방법으로 아래 두 방법 중 선택하면 되는데 나는 2번을 선택하였다.
// 1. Hibernate.initialize(receiver);
// 2. receiver.getEmail(); // 강제 초기화
뭔가 의미 없는 코드 한 줄이 생겨 좋은 방식은 아닌것 같다.
좋은 방식을 찾게 된다면 추후 이어 작성하도록 할 것이다.