Search

JPA Persistence Context & Dirty Checking

Created time
2024/05/05 18:43
Tags
SpringBoot
프로젝트

기존 코드

/** * AccountServiceImpl.kt */ @Transactional override fun modify(accountData: AccountData, accountModify: AccountModify): Account { var account: Account = getById(accountData.id) if (passwordService.isValidPassword(accountModify.password, account.password)) { if (!accountModify.equal(account)) { account = accountRepository.modify(account.update(accountModify)) } else { throw IllegalArgumentException("변경사항이 없습니다.") } } else { throw IllegalArgumentException("올바르지 않은 비밀번호입니다.") } return account } /** * AccountRepositoryImpl.kt */ override fun modify(account: Account): Account { accountJpaRepository.save(AccountEntity(account)) }
Kotlin
복사
계정 정보를 수정하는 api(put)를 개발하고 있다.
getById(accountData.id) 에서 SELECT 쿼리만 실행되고, 이후 AccountRepositoryImplmodify 메소드에서는 INSERT 쿼리가 실행되는 머리아픈 문제 발생했다.
생각해보면 AccountEntity(account) 는 앞의 getById 로 생성됐던 AccountEntity 와는 별개로 구분되는 것이 당연하다. Account 도메인 객체에서 id 를 식별자로 갖고 있다고 해서 AccountEntity 엔티티에 auto_increment 로 실행되는 id를 직접 할당해 줄 순 없기 때문이다.
도메인과 엔티티를 분리해서 개발하는 방향 자체가 처음이어서 더 어떻게 해야할 지 머리가 아팠다.

코드 수정

/** * AccountServiceImpl.kt */ @Transactional override fun modify(accountData: AccountData, accountModify: AccountModify): Account { var account: Account = getById(accountData.id) if (passwordService.isValidPassword(accountModify.password, account.password)) { if (!accountModify.equal(account)) { account = accountRepository.modify(account.id, accountModify) } else { throw IllegalArgumentException("변경사항이 없습니다.") } } else { throw IllegalArgumentException("올바르지 않은 비밀번호입니다.") } return account } /** * AccountRepositoryImpl.kt */ override fun modify(id: Long, updated: AccountModify): Account { val entity: AccountEntity = accountJpaRepository.findById(id) .orElseThrow {RuntimeException("DB 에러 발생")} entity.update(updated) // accountJpaRepository.save(entity) return entity.to() }
Kotlin
복사
결국 AccountEntityupdate() 메소드를 변경하는 방향으로 가기로 결정했다.
그런데 다음과 같이, 추가적으로 SELECT 쿼리가 실행되지 않고 UPDATE 쿼리가 바로 실행되는 것이 아닌가.
Hibernate: select ae1_0.id, ae1_0.created_at, ae1_0.email, ae1_0.is_agree_to_email, ae1_0.is_agree_to_personal_info, ae1_0.is_certificated_email, ae1_0.password, ae1_0.riot_id, ae1_0.riot_name, ae1_0.riot_tag, ae1_0.updated_at from account ae1_0 where ae1_0.id=? Hibernate: /* update for boaz.lol.co.storage.entity.account.AccountEntity */ update account set email=?, is_agree_to_email=?, is_agree_to_personal_info=?, is_certificated_email=?, password=?, riot_id=?, riot_name=?, riot_tag=?, updated_at=? where id=?
SQL
복사

영속성 컨텍스트(Persistence Context)

JPA에서는 엔티티를 영속성 컨텍스트에서 관리한다.
영속성 컨텍스트는 쉽게 말하면 엔티티 저장소라고 생각할 수 있다.
영속성 컨텍스트는 트랜잭션이 시작할 때 만들어지고, 종료할 때 사라진다.
DB에서 데이터를 긁어오면(위의 경우 getById), 영속성 컨텍스트에 엔티티가 저장된다.
영속성 컨텍스트 내부에는 캐시가 있다.
1차 캐시: 정확하게 이야기하면, 엔티티는 영속성 컨텍스트 내부의 캐시에 저장된다.
modify 메소드에서 repository.findById 명령이 실행되었을 때, 해당 메소드는 같은 트랜잭션 안에서 동일한 엔티티를 조회했는지 1차 캐시에서 먼저 찾아봄으로서 확인할 것이다.
때문에 영속성 컨텍스트의 특징을 통해, 같은 트랜잭션 안에서 동일한 엔티티를 조회하는 것임으로, SELECT 쿼리는 한 번만 실행된다.

Dirty Checking

entity.update(updated) // accountJpaRepository.save(entity)
Kotlin
복사
영속성 컨텍스트는 엔티티의 변경을 감지한다.
Dirty Checking은 기존의 순수한 엔티티가 Dirty한지 확인한다. == 동일한 값을 가져야 할 엔티티가 변경된 부분이 있는지 감지한다.
따라서 별도의 repository.save() 메소드 없이도, 트랜잭션이 종료되기 전에 update 쿼리를 실행하고 커밋한다.
아무리 생각해도 update 쿼리가 명시적으로 지원되지 않는 건 별로인 것 같다.
하지만 기술을 잘 이해하고 있기만 하면 jpa는 정말 편리한 기능을 영속성 컨텍스트를 통해 제공하고 있다.
그래도 싫다. 함수가 너무 어지러움.