지난번 포스팅에 이어 최범균님의 도메인 주도 개발 시작하기 책 챕터 7,8 에 대해 정리한 내용을 포스팅 하려고 합니다.
chapter 7. 도메인 서비스
chapter 8. 도메인 서비스
지난번 포스팅은 아래 링크에서 보실 수 있습니다.
[책] 도메인 주도 개발 시작하기 - Chapter 1,2
[책] 도메인 주도 개발 시작하기 - Chapter 3,4
[책] 도메인 주도 개발 시작하기 - Chapter 5,6
chapter 7. 도메인 서비스
7.1 여러 애그리거트가 필요한 기능
- 도메인 영역의 코드를 작성하다 보면, 한 애그리거트로 기능을 구현할 수 없을 때가 있다.
- 한 애그리거트에 넣기 애매한 도메인 기능을 억지로 특정 애그리거트에 구현하면 안 된다.
- 자신의 책임 범위를 넘어 코드가 길어진다.
- 외부에 의존이 높아지게 되며 코드를 복잡하게 만들어 수정을 어렵게 만든다.
- 도메인 기능을 별도 서비스로 구현하여 이런 문제를 해소한다.
- 한 애그리거트에 넣기 애매한 도메인 기능을 억지로 특정 애그리거트에 구현하면 안 된다.
7.2 도메인 서비스
- 도메인 서비스는 도메인 영역에 위치한 도메인 로직을 표현할 때 사용한다.
- 계산 로직 : 여러 애그리거트가 필요한 계산 로직이나, 한 애그리거트에 넣기에는 다소 복잡한 계산 로직
- 외부 시스템 연동이 필요한 도메인 로직 : 구현하기 위해 타 시스템을 사용해야 하는 도메인 로직
7.2.1 계산 로직과 도메인 서비스
- 한 애그리거트에 넣기 애매한 도메인 개념을 구현하려면 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러내면 된다.
- 도메인 서비스는 상태 없이 로직만 구현한다.
애그리거트 객체에 도메인 서비스를 전달하는 것은 응용 서비스 책임
이다.- 도메인 서비스 객체를 애그리거트에 주입하지 않기
- 도메인 객체는 필드로 구성된 데이터와 메서드를 이용해서 개념적으로 하나의 모델을 표현한다.
- 도메인 서비스는 도메인 로직을 수행하지 응용 로직을 수행하지 않는다.
- 트랜잭선 처리와 같은 로직은 응용 로직으로 응용 서비스에서 처리해야 한다.
7.2.2 외부 시스템 연동과 도메인 서비스
- 외부 시스템이나 타 도메인과의 연동 기능도 도메인 서비스가 될 수 있다.
- 이 도메인 로직은 도메인 로직 관점에서 인터페이스를 작성할 수 있다.
7.2.3 도메인 서비스의 패키지 위치
- 도메인 서비스는 다른 도메인 구성요서와 동일한 패키지에 위치한다.
- 도메인 서비스의 개수가 많거나 엔티티나 밸류와 같은 다른 구성요소와 명시적으로 구분하고 싶다면 domain 패키지 밑에 하위 패키지를 구분하여 위치시켜도 된다.
7.2.4 도메인 서비스의 인터페이스와 클래스
- 도메인 서비스의 로직이 고정되어 있지 않은 경우 도메인 서비스 자체를 인터페이스로 구현하고 이를 구현한 클래스를 둘 수 있다.
- 도메인 로직을 외부 시스템이나 별도 엔진을 이용해서 구현할 때 인터페이스와 클래스를 분리하게 된다.
- 도메인 영역이 특정 구현에 종속되는 것을 방지할 수 있고 테스트가 쉬워진다.
- 도메인 로직을 외부 시스템이나 별도 엔진을 이용해서 구현할 때 인터페이스와 클래스를 분리하게 된다.
chapter 8. 도메인 서비스
8.1 애그리거트와 트랜잭션
- 애그리거트의 일관성이 깨지는 문제가 발생하지 않도록 하려면 다음 두가지 중 하나를 해야 한다.
- 운영자가 배송지 정보를 조회하고 상태를 변경하는 동안, 고객이 애그리거트를 수정하지 못하게 막는다.
- 운영자가 배송지 정보를 조회한 이후에 고객이 정보를 변경하면, 운영자가 애그리거트르 다시 조회한 뒤 수정하도록 한다.
- 애그리거트에 대해 사용할 수 있는 대표적인 트랜잭션 처리 방식에는 선점(Pessimistic) 잠금과 비선점(Optimistic) 잠금의 두 가지 방식이 있다.
8.2 선점 잠금
- 선점 잠금은 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날때까지 다른 스레드가 해당 애그리거트를 수정하지 못 하게 막는 방식이다.
- 스레드1이 선점 잠금 방식으로 애그리거트를 구한 뒤 이어서 스레드2가 같은 애그리거트를 구하고 있다.
- 이때 스레드2는 스레드1이 애그리거트에 대한 잠금을 해제할 때까지 블로킹 된다.
- 스레드1이 선점 잠금 방식으로 애그리거트를 구한 뒤 이어서 스레드2가 같은 애그리거트를 구하고 있다.
- 선점 잠금은 보통 DBMS가 제공하는 행단위 잠금을 사용해서 구현한다.
- 오라클을 비롯한 다수의 DBMS가 for update 와 같은 쿼리를 사용한다.
- JPA EntityManager 는 LockModeType 을 인자로 받는 find() 매서드를 제공한다.
- LockModeType.PESSIMISTIC_WRITE 값을 전달
- 하이버네이트의 경우 PESSIMISTIC_WRITE 를 잠금 모드로 사용하면 ‘for update’ 쿼리를 이용해서 선점 잠금을 구현한다.
- 스프링 데이터 JPA는 @Lock 애너테이션을 사용해서 잠금 모드를 지정한다.
- @Lock(LockModeType.PESSIMISTIC_WRITE)
8.2.1 선점 잠금과 교착상태
- 선점 잠금 기능을 사용할 때는 잠금 순서에 따른 교착 상태가 발생하지 않도록 주의해야 한다.
- 선점 잠금에 따른 교착 상태는 상대적으로 사용자 수가 많을때 발생할 가능성이 높고, 사용자 수가 많아지면 교착 상태에 빠지는 스레드는 더 빠르게 증가한다.
- 이런 문제가 발생하지 않도록 하려면 잠금을 구할 때 최대 대기 시간을 지정해야 한다.
- JPA 에서 hints.put(“javax.persistence.lock.timeout”, 2000) 과 같이 힌트를 사용한다.
- DBMS 에 따라 힌트가 적용되지 않을 수도 있다는 점
- 스프링 데이터 JPA 는 @QueryHints 애너테이션을 사용해서 쿼리 힌트를 지정할 수 있다.
8.3 비선점 잠금
- 선점 잠금이 강력해 보이긴 하지만 선점 잠금으로 모든 트랜잭션 충돌 문제가 해결되는 것은 아니다.
- 비선점 잠금은 동시에 접근하는 것을 막는 대신 변경한 데이터를 실제 DBMS 에 반영하는 시점에 변경가능 여부를 확인하는 방식이다.
- 비선점 잠금을 구현하려면 애그리거트에 버전으로 사용할 숫자 타입 프로퍼티를 추가해야 한다.
- 애그리거트를 수정할때마다 버전으로 사용할 프러퍼티 값이 1씩 증가하는데 수정할 애그리거트와 매핑되는 테이블의 버전 값이 현재 애그리거트의 버전과 동일한 경우에만 데이터를 수정한다.
- JPA 는 버전을 이용한 비선점 잠금 기능을 지원한다.
- 버전으로 사용할 필드에 @Version 애너테이션을 붙이고 매핑되는 테이블에 버전을 저정할 칼럼을 추가하면 된다.
- 비선점 잠금을 위한 쿼리를 실행할 때 쿼리 실행 결과로 수정된 행의 개수가 0 이면 이미 누군가 앞서 데이터를 수정한 것이다. 트랜잭션 종료 시점에 익셉션이 발생한다.
- 비선점 잠금을 구현하려면 애그리거트에 버전으로 사용할 숫자 타입 프로퍼티를 추가해야 한다.
8.3.1 강제 버전 증가
- 애그리거트 내에 어떤 구성요소의 상태가 바뀌면 루트 애그리거트의 버전값이 증가해야 비선점 잠금이 올바르게 동작한다.
- LockModeType.OPTIMISTIC_FORCE_INCREMENT 를 사용하면 해당 엔티티의 상태가 변경되었는지에 상관없이 트랜잭션 종료 시점에 버전 값 증가 처리를 한다.
8.4 오프라인 선점
- 단일 트랜잭션에서 동시 변경을 막는 선점 잠금 방식과 달리 오프라인 선점 잠금은 여러 트랜잭션에 걸쳐 동시 변경을 막는다.
- 첫번째 트랜잭션을 시작할 때 오프라인 잠금을 선점하고, 마지막 트랜잭션에서 잠금을 해제한다.
- 잠금을 해제하기 전까지 다른 사용자는 잠금을 구할 수 없다.
- 사용자 A 가 수정요청을 수행하지 않고 프로그램을 종료하면 잠금 해제가 되지 않아 영원히 잠금을 구할 수 없는 상황이 발생한다.
- 오프라인 선점 방식은 잠금 유효시간을 가져야 한다.
- 잠금 유효 시간이 지난 후 수정 요청시 수정이 실패하는 상황을 만들지 않기 위해 일정 주기로 유효 시간을 증가시키는 방식이 필요하다.
- 첫번째 트랜잭션을 시작할 때 오프라인 잠금을 선점하고, 마지막 트랜잭션에서 잠금을 해제한다.
8.4.1 오프라인 선점 잠금을 위한 LockManager 인터페이스와 관련 클래스
- 오프라인 선점은 크게 잠금 선점 시도, 잠금 확인, 잠금 해제, 잠금 유효시간 연장의 네 가지 기능이 필요하다.
- 이 기능을 위한 LockManager 인터페이스를 사용한다.
- 잠금을 선점한 이후에 실행하는 기능은 다음과 같은 상황을 고려하여 반드시 주어진 LockId가 갖는 잠금이 유효한지 확인해야 한다.
- 잠금 유효시간이 지났으면 이미 다른 사용자가 잠금을 선점한다.
- 잠금을 선점하지 않은 사용자가 기능을 실행했다면 기능 실행을 막아야 한다.
8.4.2 DB를 이용함 LockManager 구현
- 잠금 정보를 저장할 테이블과 인덱스를 생성한다.
- type 과 id 칼럼을 주요키로 지정해서 동시에 두 사용자가 특정 타입 데이터에 대한 잠금을 구하는 것을 방지했다.
- 각 잠금마다 새로운 LockId를 사용하므로 LockId 필드를 유니크 인덱스로 설정했다.
- 잠금 유효 시간을 보관하기 위해 expiration-time 칼람을 사용한다.
마무리
그럼 이만. 🥕👋🏼🖐🏼