티스토리 뷰

이전 글: https://gojs.tistory.com/67

 

DDD에서 표현 계층과 응용 계층 구성하기

응용 계층 구성응용 계층은 도메인 객체를 사용해서 사용자가 원하는 기능을 제공한다.응용 계층은 표현 계층에 의존성을 가지지 않으며 어떤 통신을 이용하는지 등의 정보를 알 필요는 없다. 

gojs.tistory.com

 

하나의 애그리거트를 대상으로 동시에 다른 두 요청이 들어왔을 때 일관성이 깨질 수 있다.

두 개의 다른 요청

만약 위의 그림과 같이 하나의 계좌에 대해 1,000원 출금/입금 거래가 동시에 들어왔다고 가정해보자.

트랜잭션 처리가 정상적으로 이루어지지 않는다면 최종 커밋이 유효하게 처리되어 계좌 잔액이 2,000원이 되어 일관성이 깨지게 된다.

 

선점 잠금

선점 잠금은 애그리거트를 선점한 요청이 모두 끝나기 전까지 다른 요청이 해당 애그리거트를 수정할 수 없도록 하는 방식이다.

선점 잠금 동작 방식

위의 그림과 같이 두 번째 요청은 애그리거트에 접근하기 위해 첫 번째 요청이 끝날 때까지 블로킹한다.

두 번째 요청이 수행되면 첫 번째 요청의 결과 뒤에서 입금 거래가 수행되기 때문에 데이터의 일관성을 유지할 수 있게 된다.

 

일반적으로 선점 잠금을 위해서 DBMS가 자체적으로 제공하는 select ... for update 구문 등을 활용한다.

(DBMS가 자체적으로 위와 같은 락 기능을 제공한다)

Account account = entityManager.find(
    Account.class, 
    accountNumber, 
    LockModyType.PESSIMISTIC_WRITE);
public interface AccountRepository extends Repository<Account, String> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select a from Account a where a.accountNumber = :accountNumber")
    Optional<Account> findByAccountNumberForUpdate(String accountNumber);
    
    ...
}

JPA와 Spring Data JPA에서는 위와 같은 코드로 select ... for update 구문을 사용할 수 있다.

 

교착 상태

두 개 이상 애그리거트에 대해 선점 잠금을 활용하는 경우 교착 상태를 고려하여야만 한다.

이를 방지하기 위해서는 잠금에 대한 최대 대기 시간을 설정하여야 한다.

Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.lock.timeout", 2000);

Account account = entityManager.find(
    Account.class,
    accountNumber,
    LockModyType.PESSIMISTIC_WRITE,
    hints);
public interface AccountRepository extends Repository<Account, String> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @QueryHints({
        @QueryHint(name = "javax.persistence.lock.timeout", value = "2000")
    })
    @Query("select a from Account a where a.accountNumber = :accountNumber")
    Optional<Account> findByAccountNumberForUpdate(String accountNumber);
}

위와 같이 설정하게 되면 잠금에 대한 최대 대기 시간을 설정할 수 있다.

 

비선점 잠금

선점 잠금은 항상 변경 트랜잭션 내부에서 애그리거트를 조회하는 과정을 거쳐야만 한다.

또한 하나의 애그리거트에 대한 경합이 많이 발생하면 잠금 대기에 대한 성능적인 병목이 생긴다.

따라서 필요한 경우에는 비선점 잠금을 사용할 수도 있다.

 

비선점 잠금은 애그리거트에 버전으로 사용할 필드를 추가한다.

UPDATE aggregate_table
SET 
    version = [current_version] + 1,
    column1 = ?,
    column2 = ?
WHERE
    aggregate_id = ?
AND version = [current_version]

따라서 위의 쿼리를 수행하여 현재 버전에 대한 변경만을 수행한다.

만약 변경 전에 다른 요청에 의해 버전이 바뀌었다면 위 쿼리는 아무런 변경을 일으키지 않는다.

 

@Entity
@Table(name = "account")
@Access(AccessType.FIELD)
public class Account {

    @Id
    private String accountNumber;
    
    ...
    
    @Version
    private long version;
    
    ...
}

위와 같이 엔티티 클래스의 필드에 @Version 어노테이션을 설정하여 비선점 잠금 쿼리를 구현할 수 있다.

이 경우 update 쿼리를 실행하는 경우에 버전 필드를 조건으로 활용한다. (응용 서비스는 이에 대해 알 필요가 없다)

만약 update 쿼리의 결과로 수정된 행의 수가 0개이면 OptimisticLockingFailureException 예외가 발생한다.

 

강제 버전 증가

루트 엔티티의 하위 엔티티가 변경되는 경우 루트 엔티티의 버전은 변경되지 않는다.

그러나 이것은 논리적으로 맞지 않다.

왜냐하면 애그리거트의 일부 구성요소가 변경되었다면 애그리거트 자체가 변경된 것으로 봐야하기 때문이다.

이와 같은 경우 강제로 버전을 갱신할 수 있는 방법이 있다.

 

@Repository
public class JpaAccountRepository implements AccountRepository {

    ...
    
    @Override
    public Account findByAccountNumberOptimisticLockMode(String accountNumber) {
        return entityManager.find(
            Account.class,
            accountNumber,
            LockModeType.OPTIMISTIC_FORCE_INCREMENT);
    }
}
public interface AccountRepository extends Repository<Account, String> {

    @Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
    @Query("select a from Account a where a.accountNumber = :accountNumber")
    Optional<Account> findByAccountNumberForUpdate(String accountNumber);
    
    ...
}

 

다음 글: https://gojs.tistory.com/69

 

이벤트를 활용한 도메인 의존성 관리

예를 들어 이체 성공 시 Push 알림을 받아야하는 요구사항을 구현해야한다고 생각해보자.가장 단순하고 쉽게 생각하면 아래와 같은 코드를 떠올릴 것이다.public class TransferService { public TransferResult

gojs.tistory.com

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함