글 작성자: beaniejoy

데이터 변경 로직에서 JPA 변경감지 기능을 자주 접할 수 있는데요. JPA 변경감지에 대해서 짤막한 정리와 함께 @Transactional과 같이 사용할 때 어떠한 점을 유의해야 되는지에 대해서 정리해보고자 합니다.

 

📌 데이터 변경을 위한 기본 로직

DB 테이블에 저장되어 있는 데이터의 정보를 수정하기 위해 update 하는 로직을 개발하고자 한다면 다음과 같은 논리적 절차를 거치게 될 것입니다.

  1. 트랜잭션 시작
  2. 변경하고자 하는 데이터의 id(주로 primary key)를 통한 where 조회
  3. 조회한 데이터에 수정할 내용들로 교체
  4. update 처리
  5. 트랜잭션 커밋

물론 DB 콘솔 상에서는 변경할 데이터의 id를 가지고 있다면 update 쿼리하나로 바로 수정하면 됩니다.

하지만 어플리케이션단에서는 변경할 대상을 먼저 가져와서 그 대상 안에 있는 데이터 일부를 수정하고 다시 DB에 update 처리하는 과정이 필요합니다. JDBC 영역에서는 직접 update 쿼리를 작성해야하는 번거로움이 있었는데 JPA에서는 이러한 데이터 변경 작업을 세련되게 대신 해줍니다.

JPA에서는 위의 데이터 변경 로직을 어떻게 처리해줄까요.

  1. 트랜잭션의 시작
  2. 영속성 Entity 조회 (없으면 DB 조회 후 영속화)
  3. 조회한 영속성 Entity의 데이터 수정
  4. 트랜잭션 커밋

위 과정에서도 볼 수 있듯이 따로 update 쿼리를 요청하는 부분이 없습니다.
여기서 JPA의 장점 중 하나가 나오는데 바로 변경감지(Dirty Checking)입니다.

 

📌 변경감지

변경감지는 트랜잭션 커밋시 영속화된 Entity에서 가지고 있었던 최초 정보(스냅샷)와 바뀐 Entity 정보를 비교해서 바뀐 부분을 update 해주는 기능입니다.

위의 내용을 하나하나 살펴보면

  • 트랜잭션 커밋시
    JPA는 트랜잭션 커밋시 EntityManager에서 flush를 자동 호출해줍니다. flush는 영속성 컨텍스트의 변경 내용을 DB에 반영하는 것이라고 생각하면 됩니다. 트랜잭션 커밋할 때 DB에 변경 내용을 update하지 않고 커밋하게되면 아무것도 변경되지 않기 때문에 JPA에서 트랜잭션 커밋할 때 이를 자동으로 수행해줍니다.
  • 스냅샷과 바뀐 정보
    flush를 호출하게 되면 스냅샷과 Entity의 바뀐 정보를 서로 비교하게 됩니다. 스냅샷은 DB에서 데이터를 가져와 영속성 컨텍스트에 저장해 Entity를 영속화할 때의 최초 정보들입니다. 가져온 Entity 최초 정보를 가지고 로직 중간에 수정한 데이터들과 비교를 하는 것입니다.
  • update 처리
    스냅샷 정보와 Entity 바뀐 정보를 비교해 최초 정보에서 바뀐 부분들을 기준으로 update 쿼리를 작성하게 됩니다. update 쿼리는 잠시 특정 저장소에 담겨져있다가 마지막에 한꺼번에 DB로 요청을 보내게 됩니다. (JPA 쓰기지연 기능입니다.)
@Transactional
fun updateEntityData(id: Long, changed: String) {
    val entity = memoRepository.findEntityById(id) 
        ?: throw RuntimeException("존재하지 않는 Entity 입니다.")

    // this.changed = changed 작업
    // 이후 update 쿼리를 보내기 위한 작업을 안해도 됨
    entity.updateInformation(changed)

    // 메소드가 끝나면 트랜잭션 커밋 발생 
    // -> flush(변경감지 작동 후 쓰기지연을 통해 update 쿼리 요청)
}

JPA의 변경감지를 이용하기 위해서는 다음과 같은 조건이 있습니다.

  • 변경하려는 Entity가 영속 상태여야 합니다. (영속성 컨텍스트 안에 관리되는 상태)
  • 트랜잭션 안에 묶여 있어야 합니다.
  • 트랜잭션이 제대로 커밋되어야 합니다. (그래야 flush가 작동하기 때문)

 

📌 @Transactional 사용과 변경감지

@Transactional
fun updateEntityData(id: Long, changed: String) {
    val entity = memoRepository.findEntityById(id) 
        ?: throw RuntimeException("존재하지 않는 Entity 입니다.")

    entity.updateInformation(changed)
}
fun updateEntityData(id: Long, changed: String) {
    val entity = memoRepository.findEntityById(id) 
        ?: throw RuntimeException("존재하지 않는 Entity 입니다.")

    entity.updateInformation(changed)
}

위 두 개의 예시에서 차이점은 @Transactional의 존재유무입니다.

@Transactional이 없는 아래의 코드에서는 변경감지가 잘 작동될까요? 당연히 아닙니다.
위에서 JPA 변경감지가 작동되기 위한 조건 중에 Entity가 영속상태여야 하고 트랜잭션 안에 묶여 있어야 한다는 것이 있었습니다.

@Transactional이 있는 경우에는 해당 메소드가 트랜잭션으로 묶여있어 메소드가 끝나는 지점에 트랜잭션 커밋이 발생하게 되고 flush가 자동으로 작동하게 됩니다.

하지만 해당 어노테이션이 없으면 repository 메소드에 @Transactional 어노테이션이 잡혀있어도 해당 쿼리메소드에만 트랜잭션이 적용이 되기 때문에 메소드가 끝나도 트랜잭션 커밋이 일어나지 않습니다.
(쿼리메소드 호출하는 부분에서 트랜잭션이 시작되고 커밋완료 됐으니 당연한 얘기겠죠?)

그렇기 때문에 혹여나 Service 단에서 트랜잭션을 묶지 않고 Repository 단위로 트랜잭션을 묶는 전략을 사용한다면 update 작업에서 마지막 save 처리를 꼭 해줘야 합니다.

fun updateEntityData(id: Long, changed: String) {
    val entity = memoRepository.findEntityById(id) 
        ?: throw RuntimeException("존재하지 않는 Entity 입니다.")

    entity.updateInformation(changed)

    // save를 통해 update 처리
    memoRepository.save(entity) 
}