이전 글에서 낙관적 잠금(@Version 어노테이션)을 활용하여 동시성 이슈를 해결할 수 있었습니다. 이번에는 @Embeddable 어노테이션을 활용하여 엔티티의 가동성을 높이고 역할을 분리하고자 했습니다. 하지만, 예상치 못한 동작이 발생했고, 이에 대해 남겨보고자 합니다.
기존 구조
@Entity
public class Petition {
@OneToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY, mappedBy = "petition")
private List<Agreement> agreements = new ArrayList<>();
@OneToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "answer_id", referencedColumnName = "id")
private Answer answer;
@Version
Long version;
public void agree() {
this.agreements.add(new Agreement("content", this));
}
}
//PetitionService
@Transactional
public void agree(Long petitionId) {
Petition petition = petitionRepository.findById(petitionId).orElseThrow();
petition.agree();
}
기존 구조를 간단하게 표현해 보면 위와 같습니다. 이때 agree 트랜잭션을 실행시키면 아래의 insert문 만이 실행됩니다.
위의 상태에서 List<Agreement> 를 일급 컬렉션으로 감싸 해당 로직을 분리하고자 했습니다.
@Embeddable 적용 이후
@Entity
public class Petition {
@Embedded
private Agreements agreements = new Agreements();
@OneToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "answer_id", referencedColumnName = "id")
private Answer answer;
@Version
Long version;
public void agree() {
this.agreements.add(new Agreement("content", this));
}
}
@Embeddable
public class Agreements {
@OneToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY, mappedBy = "petition")
private List<Agreement> agreements = new ArrayList<>();
public void add(Agreement agreement) {
this.agreements.add(agreement);
}
}
큰 어려움 없이 분리해낼 수 있었습니다. 하지만 이 구조로 변경하게 되었을 때, 이유를 알지 못하는 쿼리가 추가로 발생하게 되었습니다. 전체 청원의 상태를 update 하는 쿼리가 나갔습니다. 어떤 칼럼의 변화가 발생했는지 확인하기 위해 @DynamicUpate 어노테이션을 추가한 후 확인했을 때는 단지 version만을 증가시키고 있었습니다. 이는 낙관적 잠금을 사용하게 되었을 때 치명적인 문제로 남게 됩니다. (같은 청원에 대한 동시 청원에 대해 OptimisticLockException이 발생하게 됩니다)
생각해본 점
1. Agreements 객체 자체에 대해 더티 체킹이 발생해서 인가?
equals 항상 true, hashCode를 항상 0으로 재정의해서 실행시켜 봤을 때에도, 동일하게 version을 올리는 쿼리가 나갔습니다.
실제로는 hibernate의 dirty 체킹은 equals/hashcode와 관련없이 진행된다고 한다.
2. Embeddable한 경우에는 변화를 주게 된다면 의도적으로 이렇게 동작하도록 만든 것일까?
OneToOne관계를 별도로 만들어 보고 @Embeddable을 적용해봤습니다.
@Entity
@DynamicUpdate
public class Petition {
@Embedded
private AgreeCountWrapper agreeCount = new AgreeCountWrapper();
public void agree() {
this.agreeCount.increment();
}
}
@Embeddable
public class AgreeCountWrapper {
@OneToOne(cascade = CascadeType.ALL)
private AgreeCount agreeCount = new AgreeCount();
public void increment() {
agreeCount.increment();
}
}
@Entity
@Getter
public class AgreeCount {
private Integer count = 0;
public void increment() {
this.count += 1;
}
}
하지만, 이 경우에는 별도의 version update 쿼리가 발생하지 않았습니다. @OneToMany상태의 컬렉션을 @Embeddable로 담게 되었을 때만 발생하는 문제로 파악됩니다.
정리
아직 hibernate의 동작 방식에 대해 이해가 부족해 이 부분에 대한 명확한 답을 찾지는 못했습니다. 임시로 @OptimisticLock(excluded = true) 설정을 통해 급한 불을 껐지만, 이유를 알고 싶네요..
'프로젝트' 카테고리의 다른 글
우리 프로젝트에서 동시성 이슈를 해결하는 방법 - 3. 비관적 잠금 (0) | 2022.03.13 |
---|---|
우리 프로젝트에서 동시성 이슈를 해결하는 방법 - 2. 낙관적 잠금 (0) | 2022.03.06 |
간단한 Spring AOP 적용기 (0) | 2022.02.24 |
우리 프로젝트에서 동시성 이슈를 해결하는 방법 - 1. DB Unique 조건을 활용하여 (0) | 2022.02.13 |
Spring Data Envers를 활용하여 Entity의 변경 이력을 관리하기 (0) | 2022.02.09 |