기존 코드
/**
* 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 쿼리만 실행되고, 이후 AccountRepositoryImpl 의 modify 메소드에서는 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
복사
•
결국 AccountEntity 에 update() 메소드를 변경하는 방향으로 가기로 결정했다.
•
그런데 다음과 같이, 추가적으로 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는 정말 편리한 기능을 영속성 컨텍스트를 통해 제공하고 있다.
•
그래도 싫다. 함수가 너무 어지러움.