@PrePersist 트러블 슈팅

크리링·2024년 5월 29일
0

실무 트러블 슈팅

목록 보기
2/5
post-thumbnail

문제 상황

업무 중에 동시에 입력되는 데이터를 시간으로 역추적하는 기능을 만들려다가 기존 레거시 코드에서 문제를 확인했다. 문제를 정리하자면

  1. 동시에 입력되는 데이터가 List형식으로 동시에 서버로 들어오는 것이 아닌 프론트에서 하나씩 들어온다.
  2. 두개의 조인된 테이블에 @PrePersist 어노테이션이 걸려있는데 데이터 생성 시간이 다르다.



예제 생성

환경

start.spring.io에서 위와 같은 설정으로 예제 프로젝트 생성



문제 상황

유저가 상품을 구매할 때 주문 결제구매 내역에 각각 저장하는데 후에 두 테이블의 정보를 연결해서 조회 가능할 수 있도록 최대한 생성 시간 에 초점을 맞추어 조회하려고 한다.

생성 시간은 부모 엔티티의 기본 정보의 createDateTime@PrePersist 어노테이션을 걸어 자동으로 생성되게 되어있다.

문제는 현재 상태로 저장이 되면 각각의 반복문에서 저장되는 주문 결제구매 내역다른 생성 시간으로 저장되는 것이다.

어떻게 해결할지는 몇가지 떠올랐다.

  1. 결제 시간 칼럼을 추가한다.
    -> 좋은 방법이라고 생각되지만 가장 마지막으로 고려해야된다고 생각되는게 DB에 칼럼을 추가하는 일이어서 최대한 있는 칼럼으로 해결할 수 있는지 먼저 고려하려한다.

  2. 영속성 컨텍스트의 flush()@PrePersist 어노테이션을 잘 활용해서 잘 활용해서 한번에 일괄적으로 들어가는 방법이 있지 않을까? -> 할 수 있으면 최선

  3. 임의의 Date 변수를 만들어서 일괄적으로 모두 저장후 한번에 saveAll() 로 변경한다. -> 좋은 방법. saveAll() 사용으로 성능 향상을 할 수 있다. 하지만 기존에 부모에서 사용되는 어노테이션인 @PrePersist의 문제를 해결해야함

1번은 최후의 보루이므로 2번을 먼저 시도해보자






영속성 컨텍스트와 @PrePersist

일단 이전에 JPA 강의에서 배웠지만 다시 한번 영속성 컨텍스트에 대해 복습해보자

영속성 컨텍스트 개념

엔티티를 영구 저장하는 환경

  • EntityManager.persist(entity); : persist()로 db에 객체를 저장하는 것이라고 배웠지만 실제로는 저장하는 것이 아니라, 영속성 컨텍스트를 통해서 엔티티를 영속화 한다는 뜻
  • 영속성 컨텍스트는 논리적인 개념



영속성 컨텍스트의 이점

  • 1차 캐시
    • 엔티티를 영속성 컨텍스트에 저장하는 순간 1차 캐시에 저장
    • key : @Id로 선언한 필드 값, value : 해당 엔티티 자체로 캐시에 저장
    • find()가 일어나는 순간, 엔티티 매니저 내부의 1차 캐시를 먼저 찾는다. -> 존재시 DB 접근 없이 반환
    • 1차 캐시에 없다면? -> DB에서 조회후 1차 캐시에 저장하고 반환
    • 글로벌하지 않다. -> 스레드 하나가 시작할 때부터 끝날때까지 쓰는 공유하지 않는 캐시이다.
  • 동일성 보장
    • 1차 캐시 덕분에 두번 조회해도 다를 객체가 아니다.
    • 1차 캐시로 반복 가능한 읽기 등급의 트랜잭션 격리 수준을 데이터 베이스가 아닌 애플리케이션 차원에서 제공
  • 트랜잭션을 지원하는 쓰기 지연 - 엔티티 등록
    • 엔티티들을 1차 캐시에 저장하고, 논리적으로 쓰기 지연 SQL 저장소 라는 곳에 INSERT 쿼리들을 생성해서 쌓아놓는다.
    • commit() 하는 시점에 DB에 동시에 쿼리를 보낸다. (옵션에 따라 다를 수 있음)
    • 쌓여있는 쿼리를 DB에 보내는 동작 flush() - 1차 캐시를 지우지 않음
    • 트랜잭션 커밋 = flush()+ `commit()``
  • 변경 감지 - 엔티티 수정
    • 변경 감지 : Dirty Checking
    • 1차 캐시에 저장할 때 동시에 스냅샷 필드도 저장
    • flush() 가 일어날 때 엔티티와 스냅샷을 비교, 변경사항 있을시 UPDATE SQL 만들어서 DB 저장



플러시

영속성 컨텍스트 변경 내용을 DB에 반영
-> 영속성 컨텍스트의 변경 사항들과 DB를 싱크하는 역할

  • 플러시 발생
    • Dirty Checking
    • 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
    • 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송한다.(등록, 수정, 삭제 SQL)
    • 플러시가 발생한다고 커밋이 이뤄지는게 아니고, 플러시 다음에 커밋이 일어남.




내가 궁금한 구체적인 내용은 @PrePersist 어노테이션이 걸리면 1차 캐시에 들어갈 때 해당 어노테이션이 동작할까?1차 캐시가 아닌 플러시에서 동작하게 변경할 수는 있을까? 이다.

첫번째 물음을 해결하기 위해 예제 테스트 코드를 만들었다.

결과는

두 값이 같고, Null이 아닌 것을 보아 @PrePersistpersist() 동작에서 호출되는 것으로 보여진다. 혹시 다른 예외가 있을까봐 찾아보았지만 @PrePersistpersist() 동작에서 엔티티에 동작하고, flush()에서 DB에 저장된다.

그렇다면 두번째 물음을 실행할 방법을 찾아보았다.
부모 @PrePersist에 개입 또는 무시, 그러면 saveAll()을 사용하면 동시에 동작하는거 아닌가?
방법을 찾지 못했다. (혹시나 아시는 분 있으면 댓글로 알려주시길 바랍니다.)

saveAll()은 코드를 보면 개별로 save()를 호출하지만 그 일련의 과정이 트랜잭션으로 하나로 묶여있는 장점이 있는데 위 상황에서는 개별로 호출할 때 @PrePersist가 동작하므로 해결 방법이 아니다.






상속 포기

결론적으로 상속을 포기하는 방식으로 결론지었다.
상속을 포기해도 다른 코드와 DB가 영향을 받지 않도록 구현한다면 가장 좋은 방법이라고 생각들었다.

코드를 비교하자면 아래와 같다.

  • Entity

    • BEFORE

    • AFTER



  • Service
    • 결과

결과적으로 saveAll() 사용으로 성능의 약 1/3 정도가 개선되었고, 모두 한개의 변수 값을 builder()에서 지정할 수 있어 정확성이 높아진다. 시간과 정확도가 모두 올라갈 수 있어 이 방식을 적용했다.






참고 및 출처

[JPA] 영속성 컨텍스트와 플러시 이해하기

0개의 댓글