[JPA] Auditing 추적 벗어나기 (@LastModifiedDate 업데이트 방지)

Hyunjin·2024년 9월 3일
0

🌱 JPA

목록 보기
1/1
post-thumbnail

서론

이번 글에서는 '온라인 메모장' 서비스를 개발하면서 발생한 Auditing 문제에 대해 다루고자 한다.
이 문제는 동일 엔티티 내 @LastModifiedDate 필드로 인해 발생했으며, 이러한 트러블 슈팅 과정에서 알게 된 개념과 해결 방안을 정리하여 공유하고자 한다.

실제 서비스에 적용된 원본 Full Code는 아래 링크에 첨부해두겠다.

문제 인식

→   : 메인페이지 시연  /   : DB memo 테이블

직접 QA 테스트를 진행하면서, 즐겨찾기 클릭시 modifiedTime 필드값이 바뀌는 이상 현상이 확인되었다.

modifiedTime  갱신 기준

  • [ 원했던 기준 ]  title, content  수정시
  • [ 현재 기준 ]  title, content, isStar  수정시

위의 기준처럼, 메모의 제목이나 내용을 수정할때만 수정시각 필드가 변경되어야 하지만,
본래 의도와는 다르게 즐겨찾기 수정시에도 변경되는 문제가 발생한 것이다.

원인 분석

Entity

@Getter
@NoArgsConstructor
@Entity
@Table(name = "memo")
public class Memo extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "memo_id")
    private Long id;

    @Column(name = "title")
    private String title;

    @Column(name = "content", columnDefinition = "MEDIUMTEXT")
    private String content;

    @Column(name = "is_star", columnDefinition = "TINYINT(1) default 0", length = 1)
    private Integer isStar;  // isStar 필드는 수정시각에 영향을 주지않아야함.
    
    // [ 방법 1 - X ]
    public void updateIsStar(Integer isStar) {
        this.isStar = isStar;
    }
}

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    @LastModifiedDate  // JPA Auditing
    @Column(name = "modified_time")
    protected LocalDateTime modifiedTime;  // 메모 수정시각
}

메모의 수정시각인 modifiedTime 필드를 @LastModifiedDate으로 관리했다.
그리고 엔티티 내부에 isStar 업데이트를 위한 setter 메소드를 구현하고, 이를 호출하여 더티 체킹을 통해 JPA 영속성 컨텍스트에서 업데이트하는 방식을 사용했다.

개념 속 실마리

  • @LastModifiedDate 어노테이션은 JPA의 Auditing 기능을 통해 엔티티의 마지막 수정 시간을 자동으로 기록한다.
  • 영속성 컨텍스트는 엔티티의 생명 주기를 관리하며, JPA는 이 환경에서 엔티티 상태를 지속적으로 모니터링한다.
  • 더티 체킹은 영속성 컨텍스트 내의 엔티티 상태를 비교하여, 트랜잭션 커밋 시 JPA가 변경 사항을 데이터베이스에 반영한다.
    →  JPA의 메커니즘인 @LastModifiedDate 필드는 더티 체킹의 영향을 받는다.

즉, [ 방법 1 ] 이라 명시한 updateIsStar 메소드가 이 문제의 원인이었다.
Memo 엔티티가 JPA 영속성 컨텍스트에 포함된 상태에서 위의 업데이트를 실행할 경우, isStar 필드의 변경이 감지되어 원치 않게 modifiedTime 필드가 업데이트되는 것이었다.

이를 해결하기 위해서는 영속성 컨텍스트에서 관리되지 않도록, JPA Auditing의 추적을 벗어나 특정 필드를 업데이트하는 방법을 사용해야만 했다.

해결 방안

Repository

public interface MemoRepository extends JpaRepository<Memo, Long> {

	// [ 방법 2 - O ]
    @Modifying(clearAutomatically = true)  // 수정시각에 영향을 주지않도록, isStar를 JPQL로 직접 업데이트.
    @Query("UPDATE Memo m SET m.isStar = :isStar WHERE m.id = :memoId")
    void updateIsStar(@Param("memoId") Long memoId, @Param("isStar") Integer isStar);
}

Service

@Transactional
@Override
public void updateMemo(Long memoId, MemoDto.UpdateRequest updateRequestDto) {
    Long loginUserId = SecurityUtil.getCurrentMemberId();
    userMemoService.checkUserInMemo(loginUserId, memoId);  // 사용자의 메모 접근권한 체킹.
    Memo memo;

    if(updateRequestDto.getIsStar() != null) {  // 메모의 즐겨찾기 여부 수정인 경우
        memo = findMemo(memoId);
        printTime(memo.getModifiedTime(), memo.getIsStar());

        // [ 방법 1 - X ]
        // memo.updateIsStar(updateRequestDto.getIsStar());
        // memoRepository.flush();
        
        // [ 방법 2 - O ]
        memoRepository.updateIsStar(memoId, updateRequestDto.getIsStar());

        memo = findMemo(memoId);
        printTime(memo.getModifiedTime(), memo.getIsStar());
    }

    /* 생략 */
}

private static void printTime(LocalDateTime modifiedTime, Integer isStar) {
    String modifiedTimeStr = TimeConverter.timeToString(modifiedTime);
    String printStr1 = String.format("[ isStar : %d ]", isStar);
    String printStr2 = String.format("- modifiedTime : %s", modifiedTimeStr);
    System.out.println(printStr1 + "\n" + printStr2);
}

기존의 엔티티 내 updateIsStar 메소드를 사용한 [ 방법 1 ] 방식을 버리고,
isStar 필드를 JPQL로 직접 업데이트하는 효과적인 [ 방법 2 ] 방식으로 전환했다.

JPQL 쿼리는 영속성 컨텍스트를 우회하여 데이터베이스에 직접적으로 쿼리를 실행하기에,
JPA가 해당 엔티티의 상태를 업데이트하지 않아 더티 체킹이 발생하지 않는다.

참고로, JPQL 대신 Native Query를 사용하여도 동일한 결과를 얻을 수 있다.
아래 표에는 위의 코드로 테스트한 출력 결과를 정리해 두었다.

JPA vs JPQL

Before  -  JPA (방법 1)After  -  JPQL (방법 2)
-  즐겨찾기 수정 :  isStar  0 → 1
-  수정시각 변경 :  modifiedTime  4:50:34 → 4:51:06
-  메모의 수정시각이 유지되어야 하나, 변경되는 문제 발생
-  즐겨찾기 수정 :  isStar  0 → 1
-  수정시각 유지 :  modifiedTime  4:51:06 → 4:51:06
-  메모의 수정시각이 이전과 동일하게 유지됨

위의 표에서 볼 수 있듯이, 비교 결과는 확연히 달랐다.
기존 JPA 방식은 isStar를 수정할 때마다 modifiedTime이 함께 변경됐다.
반면 JPQL 방식은 modifiedTime은 변하지 않고, isStar 필드만 단독으로 수정됐다.
==>  JPQL 업데이트 방식을 채택함으로써 발생했던 문제를 해결할 수 있었다.

마치며

이번 트러블 슈팅을 통해 JPA Auditing과 더티 체킹의 메커니즘을 깊이 이해할 수 있었다.
특히, JPA의 영속성 컨텍스트와 Auditing 기능이 어떻게 상호작용하는지를 명확히 알게 되었고, 이로 인해 발생하는 문제를 효과적으로 해결하는 방법도 배웠다.

이 경험을 통해 JPA에 대한 접근 관점이 보다 넓어졌고, 개발자로서의 역량도 한층 강화된 것 같다.
내 정리가 비슷한 문제를 겪고 있는 개발자들에게 도움이 되기를 바라며, 이 글을 마친다.

참고 링크

profile
Success is the sum of small efforts.

0개의 댓글

관련 채용 정보