본문 바로가기

프로젝트

@Version + @Embeddable 이상 동작 현상에 대한 기록

이전 글에서 낙관적 잠금(@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이 발생하게 됩니다)

@Embeddable 적용 후 쿼리 (오른쪽은 @DynamicUpdate 적용)

생각해본 점

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) 설정을 통해 급한 불을 껐지만, 이유를 알고 싶네요..