@Transactional 안에서 LazyInitializationException, JPQL @Modifying

    JPQL, @Query 어노테이션을 통해 Update/Delete/Insert와 같은 Command를 사용하려면 @Modifying 어노테이션을 함께 추가해야 합니다.

    아마, 사진 하나만으로도 제가 무슨말을 하려는지 예상하신 분들도 계실껍니다..

    지금 내가 보내는 요청이 Query, 그러니까 SQL의 Select문이 아니라 DB에 변경을 끼친다는 것을 알리는 것이죠.

    저도 프로젝트 내에서 JPQL을 사용하는 부분에 위와 같이 @Modifying을 붙여 사용했습니다.

    @Modifying은 다음과 같은 두 Element를 받습니다.

     

    https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/Modifying.html

    flushAutomatically는 해당 어노테이션이 붙은 modifying query를 실행 전에 영속성 컨텍스트에 있는 내용을 flush 하도록 합니다.

    clearAutomatically는 해당 어노테이션이 붙은 modifying query 실행 후에 영속성 컨텍스트에 있는 내용을 clear, 즉 비웁니다.

    계속해서, 설명을 위해 위 메서드를 호출하는 주변 코드부터 소개하겠습니다.

     

    스케줄 수정을 위한 메서드입니다.

    저희 프로젝트의 수정 로직은 특정 날짜를 넘기면, 해당하는 날짜가 포함된 주차의 스케줄을 모두 삭제하고 새로 작성하는 방식입니다.

    169번 라인에서 일주일 분량의 스케줄을 삭제하기 위해 앞에서 정의한 JPQL 메서드를 호출합니다.

    @Transactional이 붙은 것을 확인할 수 있습니다.

     

    위와 거의 비슷한 방식으로 동작하는 스케줄 생성입니다.

    삭제하는 부분이 없다는 것을 빼면 나머지는 거의 비슷합니다.

    여기까지는 테스트 수행 후 문제없이 잘 동작했습니다.

    문제는 SSE를 이용한 실시간 알림을 추가하면서 발생했습니다.

     

    새로 정의한 NotificationService는 다음과 같이 프로젝트와 멤버 엔티티를 받아, 클라이언트에게 반환할 메시지를 만들고 프로젝트에 속한 멤버에게 알림을 전송합니다.

    그리고 이 메시지에는 알림을 발송한 원인이 되는 sender의 이름이 들어가게 됩니다.

    다른 클래스에서 이를 사용하는 방법은 간단합니다.

     

    위와 같이 noticationService를 통해 프로젝트와 발송자(멤버)를 넘겨주면 됩니다.

     

    성공!

    스케줄 생성 로직 마지막에 알림 발송 로직을 추가한 모습입니다.

    이를 적용한 모습입니다. 이벤트스트림으로 메시지 발행 시 알림이 쌓이도록 만들었고 결과는 다음과 같습니다.

     

    import { EventSourcePolyfill } from 'event-source-polyfill';
    import React, { useState, useEffect } from 'react';
    import { ToastContainer, toast } from 'react-toastify';
    import 'react-toastify/dist/ReactToastify.css';
    
    function App() {
      const sseURL = "http://localhost:8080/v1/sse/subscribe";
      const token = '';
    
      useEffect(() => {
        const eventSource = new EventSourcePolyfill(sseURL, {
          headers: { 'Authorization': `Bearer ${token}` },
          withCredentials: false
        });
    
        eventSource.addEventListener('notification', (event) => {
          toast(event.data);
        });
    
        return () => {
          eventSource.close();
        };
      }, []);
    
      return (
        <div>
          <h1>Server-Sent Events Example</h1>
          <ToastContainer
            position="top-right"
            autoClose={5000}
            hideProgressBar={false}
            newestOnTop={true}
            closeOnClick
            rtl={false}
            pauseOnFocusLoss
            draggable
            pauseOnHover
          />
        </div>
      );
    }
    
    export default App;

    리액트 코드는 다음과 같습니다. (GPT의 도움을 받아 작성되었습니다.)

     

    그리고 같은 로직은 editSchedule에도 추가했습니다.

     

    다만, 여기서 스케줄 변경 API 호출 시 다음과 같은 에러 응답을 받게 됩니다.

     

    서버 콘솔에서 다음과 같은 로그를 발견합니다.

    지연 초기화?? editSchedule 메서드는 분명히 @Transactional이 선언되어 있는데,

    서버 로그 상에서는 LazyInitializationException으로 인해 롤백되었다고 합니다.

    문제를 찾기위해 해당하는 부분을 찾아가겠습니다.

     

    문제가 발생한 부분은 editSchedule의 마지막에 추가했던, 알림 발송 부분에서 였습니다.

    더 따라가보죠.

     

    알림 발송 로직의 첫 라인, 멤버 엔티티 sender의 닉네임을 가져오는 데에서 LazyIntializationException이 발생합니다.

    분명히 @Transactional이 붙어있는데 무슨 이유일까요?

     

    문제는 처음 정의했던 JPQL에 숨어있었습니다.

    editSchedule에서 스케줄 삭제를 위해 호출했던 ScheduleRepository의 메서드를 다음과 같이 선언했었는데 기억나시나요?

     

    네, @Query 어노테이션으로 Command를 보내기 위해 @Modifying 어노테이션을 사용했고.

    DB 업데이트 이후 영속성 컨텍스트에 남아있는 변경 전 정보로 인해 정합성이 깨질 것을 방지하기 위해 clearAutomatically를 true로 설정해두었습니다.

     

    혹시, 감이 오시나요..?

    @Query의 경우 다른 JpaRepository를 통한 질의와는 다르게 쓰기지연 SQL로 동작하지 않고 곧바로 DB에 질의하게 됩니다.

    네, LazyIntializationException은 스케줄 delete 이후 영속성 컨텍스트를 비웠기 때문에 프록시를 지연 초기화할 참조를 찾을 수 없어 발생하는 것이었습니다.

    modifying query(delete) 실행 전
    modifying query(delete) 실행 후

    더보기

    memberProject를 사용하는Schedule.of()는 어떻게 정상적으로 동작하는걸까?

    modifying query가 사용된 이후 영속성 컨텍스트는 초기화되지만, 현재 memberProject 인스턴스의 참조는 여전히 들고있기 때문!

     

    우선, 저희는 해당하는 부분뒤에 이미 삭제된 스케줄을 다루는 코드가 없어 @Modifying 어노테이션의 clearAutomatically를 지워(default: false) 이 문제를 해결할 수 있었습니다.

    성공!

    이번에는 기존에 잘 동작하던 코드가 새로운 로직을 추가하면서 동작하지 못하게 되었는데요.

    이러한 문제를 해결하기 위해서는 영속성 컨텍스트를 플러시 해주고나서 다시 조회해오는 부분을 추가하면 됩니다.

    다만 스트림 내에서 참조되는 지역변수는 파이널이거나 사실상 파이널이어야 하기 때문에 같은 이름의 변수로는 코드를 수정하지 않고 사용하기 어려울 것 입니다.

    추후 알림의 로직을 다른 부분으로 들어내는 것을 고려하고, JPQL로 modifying query를 다룰 시 영속성 컨텍스트와 지연초기화에 유념하여 작성해야겠습니다.

     

    마지막으로 @Query, @Modifying과 관련하여 깔끔하게 정리된 글 하나를 투척하고 가겠습니다.
    Spring Data JPA @Modifying 알아보기
    Spring Data JPA @Modifying Annotation

     

    '코딩 > [JAVA]' 카테고리의 다른 글

    Interface에는 왜 protected 접근지정을 사용할 수 없을까?  (0) 2023.06.25
    접근 지정자 비교하기  (0) 2023.06.18
    이것이 자바다. 3~4  (0) 2022.11.17
    [JAVA] 이것이 자바다.1-2  (0) 2022.11.08

    댓글