Blog

도메인 주도 개발 시작하기(8)

도메인 주도 개발 시작하기(8)

8-1. 애그리거트와 트랜잭션

일관성이 깨지는 문제가 발생하지 않도록 하려면 2가지 방법 중 1가지를 택해야 한다
1.
운영자가 배송지 정보를 조회하고 상태를 변경하는 동안, 고객이 애그리거트를 수정하지 못하게 막는다
2.
운영자가 배송지 정보를 조회한 이후에 고객이 정보를 변경하면, 운영자가 애그리거트를 다시 조회한 뒤 수정하도록 한다.
DBMS 가 지원하는 트랜잭션과 함께 애그리거트를 위한 추가적인 트랜잭션 처리 기법이 필요하다.
대표적인 트랜잭션 처리 방식
선점 (Pessimistic) 잠금 → 비관적 잠금
비선점 (Optimistic) 잠금 → 낙관적 잠금

8-2. 선점 잠금

선점 (Pessimistic) 잠금은 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 때 까지 다른 스레드가 해당 애그리거트를 수정하지 못하게 막는 방식이다
먼저 선점한 스레드가 작업을 끝날 때 까지 다른 스레드는 블로킹(Blocking) 된다
선점 잠금 방식을 사용하면 다른 스레드가 수정할 수 없으므로 동시에 애그리거트를 수정할 때 발생하는 데이터 충돌 문제를 해소할 수 있다
선점 잠금은 보통 DBMS 가 제공하는 행단위 잠금을 사용해서 구현한다
JPA EntityManager는 LockModeType 을 인자로 받는 find() 메서드를 제공한다

8-2-1. 선점 잠금과 교착 상태

선점 잠금 기능을 사용할 때는 잠금 순서에 따른 교착 상태(deadlock) 가 발생하지 않도록 주의해야 한다
선점 잠금에 따른 교착 상태는 상대적으로 사용자 수가 많을 때 발생할 가능성이 높고, 사용자수가 많아지면 교착 상태에 빠지는 스레드는 더 빠르게 증가한다
이러한 문제를 발생하지 않도록 하기 위해서는 잠금을 구할 때 최대 대기 시간을 지정해야 한다.
지정한 시간 이내에 잠금을 구하지 못하면 익셉션을 발생시킨다

8-3. 비선점 잠금

선점 잠금 방식이 강력해 보이긴 하지만, 모든 트랜잭션 충돌 문제가 해결되지는 않는다
비선점(Optimistic) 잠금 방식은 동시에 접근하는 것을 막는 대신, 변경한 데이터를 실제 DBMS 에 반영하는 시점에 변경 가능 여부를 확인하는 방식이다
비선점 잠금을 구현하려면 애그리거트에 버전으로 사용할 수 잇는 숫자 타입 프로퍼티를 추가해야 한다
애그리거트를 수정할 때 마다 버전으로 사용할 프로퍼티 값이 1씩 증가하게 된다
UPDATE aggtable SET version = version + 1, colx = ?, coly = ? WHERE aggid = ? and version = 현재버전
SQL
JPA 는 버전을 이용한 비선점 잠금 기능을 지원한다
응용 서비스는 버전에 대해 알 필요가 없다
리포지터리에서 필요한 애그리거트를 구하고 알맞은 기능만 실행하면 된다
기능 실행 과정에서 애그리거트 데이터가 변경되면 JPA 는 트랜잭션 종료 시점에 비선점 잠금을 위한 쿼리를 실행한다
비선점 잠금 방식을 여러 트랜잭션으로 확장하려면 애그리거트 정보를 뷰로 보여줄 때 버전 정보도 함께 사용자에게 전달해서 처리해야 한다
버전 충돌 상황에 대한 구분이 명시적으로 필요 없다면 응용 서비스에서 프레임워크용 익셉션을 발생시키는 것도 고려할 수 있다.

8-3-1. 강제 버전 증가

애그리거트 루트 외에 다른 엔티티가 존재하는데, 기능 실행 도중 루트가 아닌 다른 엔티티의 값만 변경되는 경우에는 루트 엔티티의 버전도 강제로 증가시켜줘야 한다.
루트 엔티티의 값은 변경되지 않았지만, 애그리거트의 구성 요소 중 일부 값이 바뀌면 논리적으로 그 애그리거트는 바뀐것이기 때문이다

8-4. 오프라인 선점 잠금

한 트랜잭션 범위에서만 적용되는 선점 잠금 방식이나 비선점 잠금 방식으로는 동시에 수정을 허용하지만 사전에 충돌 여부를 알려줄 수 없다
이 때 필요한 것이 오프라인 선점 (Offline Pessimistic Lock) 잠금이다
오프라인 선점 잠금은 여러 트랜잭션에 걸쳐 동시 변경을 막는다
첫번째 트랜잭션을 시작할 때 오프라인 잠금을 선점하고, 마지막 트랜잭션에서 잠금을 해제한다
잠금을 해제하기 전까지 다른 사용자는 잠금을 구할 수 없다
오프라인 선점 잠금 방식은 유효 시간이 지나면 자동으로 잠금이 해제해서 다른 사용자가 잠금을 일정 시간 후에 다시 구할 수 있도록 해야 한다

8-4-1. 오프라인 선점 잠금을 위한 LockManager 인터페이스와 관련 클래스

오프라인 선점 잠금은 크게 잠금 선점 시도, 잠금 확인, 잠금 해제, 잠금 유효시간 연장의 4가지 기능이 필요하다
package com.myshop.lock; public interface LockManager { LockId tryLock(String type, String id) throws LockException; void checklock(LockId lockId) throws LockException; void relaseLock(LockId lockId) throws LockException; void extendLockExpiration(LockId lockId, long inc) throws LockException; }
Java
잠금을 선점하는데 실패하면 LockException 이 발생한다
잠금을 선점한 이후에 실행하는 기능은 다음과 같은 상황을 고려하여 반드시 주어진 LockId 를 갖는 잠금이 유효한지 확인해야 한다
잠금 유효 시간이 지났으면 이미 다른 사용자가 잠금을 선점한다
잠금을 선점하기 않은 사용자가 기능을 실행했다면 기능 실행을 막아야 한다.

8-4-2. DB 를 이용한 LockManager 구현

각 잠금마다 새로운 LockId 를 생성하기 때문에 lockId 필드를 유니크 인덱스로 설정한다
create table locks ( `type` varchar(255), id varchar(255), lockid varchar(255), expiration_time datetime, primary key (`type`, `id`) ) character set utf8; create unique index locks_idx on locks (lockid);
SQL