could not initialize proxy - no Session

213kky·2024년 12월 22일

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(); // 강제 초기화

뭔가 의미 없는 코드 한 줄이 생겨 좋은 방식은 아닌것 같다.
좋은 방식을 찾게 된다면 추후 이어 작성하도록 할 것이다.

profile
since 2022

0개의 댓글