오탈자

  • p.125 코드 if (account.isBlocked()) -> if (store.isBlocked())
  • p.153 다이어그램 Address → ArticleContent
  • p.164 애그리거트가 완전해야 이유는 → 애그리거트가 완전해야 하는 이유
  • p.264 다이어그램 수정 폼 요정 → 수정 폼 요청

1. 도메인 모델 시작하기

  • p.33 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.
  • p.35 도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다.
  • p.46 Value 타입은 개념적으로 완전한 하나를 표현할 때 사용한다.
  • p.50 Value 타입을 불변으로 구현하는 여러 이유가 있는데 가장 중요한 이유는 안전한 코드를 작성할 수 있다는 데 있다.
data class Money(val value: Int)
data class OrderLine(
  val product: Product,
  val price: Money,
  val quantity: Int,
  val amounts: Money,
)
  • p.52 식별자는 단순한 문자열이 아니라 도메인에서 특별한 의미를 지니는 경우가 많기 때문에 식별자를 위한 Value 타입을 사용해서 의미가 잘 드러나도록 할 수 있다.
public class Order {
  private OrderNo id;
  ...
  public OrderNo getId() {
    return id;
  }
}
  • p.55 도메인 객체가 불완전한 상태로 사용되는 것을 막으려면 생성 시점에 필요한 것을 전달해 주어야 한다. 즉 생성자를 통해 필요한 데이터를 모두 받아야 한다.
  • p.58 에릭 에반스는 도메인 주도 설계에서 언어의 중요함을 강조하기 위해 유비쿼터스 언어라는 용어를 사용했다. 전문가, 관계자, 개발자가 도메인과 관련된 공통의 언어를 만들고 이를 대화, 문서, 도메인 모델, 코드, 테스트 등 모든 곳에서 같은 용어를 사용한다. 이렇게 하면 소통 과정에서 발생하는 용어의 모호함을 줄일 수 있고 개발자는 도메인과 코드 사이에서 불필요한 해석 과정을 줄일 수 있다.

2. 아키텍처 개요

  • p. 63 Application 서비스는 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다.
class CancelOrderService {
  @Transactional
  fun cancelOrder(orderId: String) {
    val order = findById(orderId) ?: throw OrderNotFoundException(orderId)
    // 주문 취소 로직을 직접 구현하지 않고 Order 객체에 취소 처리를 위임
    order.cancel()
  }
  ...
}
  • p.64 인프라스트럭처 영역은 논리적인 개념을 표현하기보다는 실제 구현을 다룬다. 도메인 영역, Application 영역, Presentation 영역은 구현 기술을 삳용한 코드를 직접 만들기 않는다. 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.
  • p.69 인프라스트럭처에 의존하면 ‘테스트 어려움’과 ‘기능 확장의 어려움’이라는 두 가지 문제가 발생하는 것을 알게 되었다. 그렇다면 어떻게 해야 이 두 문제를 해소할 수 있을까? 해답은 DIP에 있다.
  • p.70 고수준 모듈이 제대로 동작하려면 저수준 모듈을 사용해야 한다. 그런데 고수준 모듈이 저수준 모듈을 사용하면 앞서 계층 구조 아키텍처에서 언급했던 두 가지 문제, 즉 구현 변경과 테스트가 어렵다는 문제가 발생한다.
  • p.71 DIP는 이 문제를 해결하기 위해 저수준 모듈과 고수준 모듈에 의존하도록 바꾼다. 저수준 모듈을 구현하려면 저수준 모듈을 사용해야 하는데, 반대로 저수준 모듈이 고수준 모듈에 의존하도록 하려면 어떻게 해야 할까? 비밀은 추상화한 인터페이스에 있다.
  • p.73 고수준 모듈이 저수준 모듈을 사용하려면 고수준 모듈이 저수준 모듈에 의존해야 하는데, 반대로 저수준 모듈이 고수준 모듈에 의존한다고 해서 이를 DIP Dependency Inversion Principle , 의존 역전 원칙이라고 부른다.
  • p.75 실제 구현 없이 테스트를 할 수 있는 이유는 DIP를 적용해서 고수준 모듈이 저수준 모듈에 의존하지 않도록 했기 때문이다.
  • p.77 인프라스트럭처 영역은 구현 기술을 다루는 저수준 모듈이고 Application 영역과 도메인 영역은 저수준 모듈이다. 인프라스트럭처 계층이 가장 하단에 위치하는 계층형 구조와 달리 아키텍처에 DIP를 적용하면 인프라스트럭처 영역이 Application 영역과 도메인 영역에 의존(상속)하는 구조가 된다.

dip

  • p.80 도메인 영역의 주요 구성요소
    • 엔티티(Entity): 고유의 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다. 주문(Order), 회원(Member), 상품(Product)과 같이 도메인의 고유한 개념을 표현한다. 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능을 함께 제공한다.
    • 밸류(Value): 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 값을 표현할 때 사용된다. 배송지 주소를 표현하기 위한 주소(Address)나 구매 금액을 위한 금액(Money)와 같은 타입이 Value 타입이다. 엔티티의 속성으로 사용할 뿐만 아니라 다른 Value 타입의 속성으로도 사용할 수 있다.
    • 애그리거트(Aggregate): 애그리거트는 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것이다. 예를 들어 주문과 관련된 Order 엔티티, OrderLine 밸류, Orderer 밸류 객체를 ‘주문’ 애그리거트로 묶을 수 있다.
    • 리포지터리(Repository): 도메인 모델의 영속성을 처리한다. 예를 들어 DBMS 테이블에서 엔티티 객체를 로딩하거나 저장하는 기능을 제공한다.
    • 도메인 서비스(Domain service): 특정 엔티티에 속하지 않은 도메인 로직을 제공한다. ‘할인 금액 계산’은 상품, 쿠폰, 회원 등급, 구매 금액 등 다양한 조건을 이용해서 구현하게 되는데, 이렇게 도메인 로직이 여러 엔티티와 밸류를 필요로 하면 도메인 서비스에서 로직을 구현한다.
  • p.81 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다는 점이다. 도메인 모델의 에티티는 단순히 데이터를 담고 있는 데이터 구조라기보다는 데이터와 함께 기능을 제공하는 객체이다. 도메인 관점에서 기능을 구현하고 기능 구현을 캡슐화해서 데이터가 임의로 변경되는 것을 막는다.
  • p. 84 개별 객체뿐만 아니라 상위 수준에서 모델을 볼 수 있어야 전체 모델의 관계와 개별 모델을 이해하는 데 도움이 된다. 도메인 모델에서 전체 구조를 이해하는 데 도움이 되는 것이 바로 애그리거트 Aggregate 이다.
  • p.85 애그리거트는 군집에 속한 객체를 관리하는 루트 엔티티를 갖는다. 루트 엔티티는 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리거트가 구현해야 할 기능을 제공한다. 애그리거트를 사용하는 코드는 애그리거트가 제공하느 기능을 실행하고 애그리거트 루트를 통해서 간접적으로 애그리거트 내의 다른 엔티티나 밸류 객체에 접근한다. 이것은 애그리거트의 내부 구현을 숨겨서 애그리거트 단위로 구현을 캡슐화할 수 있도록 돕는다.

aggre_root

  • p.88 Application 서비스와 리포지터리는 밀접한 연관이 있다.
    • Application 서비스는 필요한 도메인 객체를 구하거나 저장할 때 리포지터리를 사용한다.
    • Application 서비스는 트랜잭션을 관리하는데, 트랜잭션 처리는 리포지터리 구현 기술의 영향을 받는다.

3. 애그리거트

  • p.99 복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들려면 상위 수준에서 모델을 조망할 수 있는 방법이 필요한데, 그 방법이 바로 애그리거트다.
  • p.100 모델을 보다 잘 이해할 수 있고 애그리거트 단위로 일관성을 관리하기 때문에, 애그리거트는 복잡한 도메인을 단순한 구조로 만들어준다. 애그리거트는 관련된 모델을 하나로 모았기 때문에 한 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다.
  • p.101 경계를 설정할 때 기본이 되는 것은 도메인 규칙과 요구사항이다. 도메인 규칙에 따라 함께 생성되는 구성요소는 한 애그리거트에 속할 가능성이 높다.
  • p.103 애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 애그리거트 전체를 관리할 주체가 필요한데, 이 책임을 지는 것이 바로 애그리거트의 루트 엔티티다. 애그리거트에 속한 객체는 애그리거트 루트 엔티티에 직접 또는 간접적으로 속하게 된다.
  • p.104 애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 하는 것이다. 애그리거트 루트가 제공하는 메서드는 도메인 규칙에 따라 애그리거트에 속한 객체의 일관성이 깨지지 않도록 구현해야 한다.
  • p.106 불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들려면, 도멩니 모델에 대해 다음의 두 가지를 습관적으로 적용해야 한다.
    • 단순히 필드를 변경하는 set 메서드를 공개(public) 범위로 만들지 않는다.
    • 밸류 타입은 불변으로 구현한다.
  • p.107 밸류 타입의 내부 상태를 변경하려면 애그리거트 루트를 통해서만 가능하다. 그러므로 애그리거트 루트가 도메인 규칙을 올바르게만 구현하면 애그리거트 전체의 일관성을 올바르게 유지할 수 있다.
  • p.109 트랜잭션 범위는 작을수록 좋다. 한 트랜잭션이 한 개 테이블을 수정하는 것과 세 개의 테이블을 수정하는 것을 비교하면 성능에서 차이가 발생한다. 한 개 테이블을 수정하면 트랜잭션 충돌을 막기 위해 잠그는 대상이 한 개 테이블의 한 행으로 한정되지만, 세 개의 테이블을 수정하면 잠금 대상이 더 많아진다. 잠금 대상이 많아진다는 것은 그만큼 동시에 처리할 수 있는 트랜잭션 개수가 줄어든다는 것을 의미하고 이것은 전체적인 성능(처리량)을 떨어뜨린다.
  • p.110 한 트랜잭션에서 한 애그리거트만 수정한다는 것은 애그리거트에서 다른 애그리거트를 변경하지 않는다는 것을 의미한다. 한 애그리거트에서 다른 애그리거트를 수정하면 결과적으로 두 개의 애그리거트를 한 트랜잭션에서 수정하게 되므로, 애그리거트 내부에서 다른 애그리거트의 상태를 변경하는 기능을 실행하면 안 된다.
  • p.110 만약 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면 애그리거트에서 다른 애그리거트를 직접 수정하지 말고 Application 서비스에서 두 애그리거트를 수정하도록 구현한다.
class ChangeOrderService {
  // 두 개 이상의 애그리거트를 변경해야 한면,
  // Application 서비스에서 각 애그리거트의 상태를 변경한다.
  @Transactional
  fun changeShipping(id: OrderId, newShipping: Shipping, useNewShppingAddr: Boolean) {
    val order = orderRepository.findById(id) ?: throw OrderNotFoundException(id)
    order.shipTo(newShpping)
    if (useNewShppingAddr) {
      val member = findMember(order.getOrderer())
      member.changeAddress(newShpping.getAddress())
    }
  }
}
  • p.113 애그리거트를 영속화할 저장소로 무엇을 사용하든지 간에 애그리거트의 상태가 변경되면 모든 변경을 원자적으로 저장소에 반영해야 한다. 애그리거트에서 두 개의 객체를 변경했는데 저장소에는 한 객체에 대한 변경만 반영되면 데이터 일관성이 깨지므로 문제가 된다.
  • p.115 한 애그리거트 내부에서 다른 애그리거트 객체를 접근할 수 있으면 다른 애그리거트의 상태를 쉽게 변경할 수 있게 된다. 트랜잭션 범위에서 언급한 것처럼 한 애그리거트가 관리하는 범위는 자기 자신으로 한정해야 한다. 그런데 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 구현의 편리함 때문에 다른 애그리거트를 수정하고자 하는 유혹에 빠지기 쉽다.
  • p.116 한 애그리거트에서 다른 애그리거트의 상태를 변경하는 것은 애그리거트 간의 의존 결합도를 높여서 결과적으로 애그리거트의 변경을 어렵게 만든다.
  • p.117 ID 참조를 사용하면 모든 객체가 참조로 연결되지 않고 한 애그리거트에 속한 객체들만 참조로 연결된다. 이는 애그리거트의 경계를 명확히 하고 애그리거트 간 물리적인 연결을 제거하기 때문에 모델의 복잡도를 낮춰준다. 또한 애그리거트 간의 의존을 제거하므로 응집도를 높여주는효과도 있다.
public class Order {
  private Orderer orderer;
  ...
}
public class Orderer {
  private MemberId memberId;
  private String name;
  ...
}
public class Member {
  private MemberId id;
  ...
}
  • p.118 ID를 이용한 참조 방식을 사용하면 복잡도를 낮추는 것과 함께 한 애그리거트에서 다른 애그리거트를 수정하는 문제를 근원적으로 방지할 수 있다. 외부 애그리거트를 직접 참조하지 않기 때문에 애초에 한 애그리거트에서 다른 애그리거트의 상태를 변경할 수 없는 것이다.
  • p.119 ID 참조 방식을 사용하면서 N+1 조회와 같은 문제가 발생하지 않도록 하려면 조회 전용 쿼리를 사용하면 된다.
  • p.127 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면 애그리거트에 팩토리 메서드를 구현하는 것을 고려해보자.
  • p.127 다른 팩토리에 위임하더라도 차단 상태의 상점은 상품을 만들 수 없다는 도메인 로직은 한곳에 계속 위치한다.
class Store {
  fun createProduct(newProductId: ProductId, product: ProductInfo): Product {
    if (isBlocked()) {
      throw StoreBlockedException()
    }
      return ProductFactory.create(newProductId, getId(), product)
  }
}

4. 리포지터리와 모델 구현

  • p.131 가능하면 리포지터리 구현 클래스를 인프라스트럭처 영역에 위치시켜서 인프라스트럭처에 대한 의존성을 낮춰야 한다.
  • p.151 JPA에서 식별자 타입은 Serializable 타입이어야 하므로 식별자로 사용할 밸류 타입은 Serializable 인터페이스를 상속받아야 한다.
  • p.152 애그리거트에서 루트 엔티티를 뺀 나머지 구성요소는 대부분 밸류이다. 밸류가 아니라 엔티티가 확실하다면 해당 엔티티가 다른 애그리거트는 아닌지 확인해야 한다. 특히 자신만의 독자적인 라이프 사이클을 갖는다면 구분되는 애그리거트일 가능성이 높다.
  • p.153 애그리거트에 속한 객체가 밸류인지 앤티티인지 구분하는 방법은 고유 식별자를 갖는지를 확인하는 것이다.
  • p.164 애그리거트가 완전해야 하는 이유. 첫 번째는 상태를 변경하는 기능을 실행할 때 애그리거트 상태가 완전해야 하기 때문이고, 두 번째는 Presentation 영역에서 애그리거트의 상태 정보를 보여줄 때 필요하기 때문이다.
  • p.165 일반적인 애플리케이션은 상태 변경 기능을 실행하는 빈도보다 조회 기능을 실행하는 빈도가 훨씬 높다. 그러므로 상태 변경을 위해 지연 로딩을 사용할 때 발생하는 추가 쿼리로 인한 실행 속도 저하는 보통 문제가 되지 않는다.
  • p.166 애그리거트가 완전한 상태여야 한다는 것은 애그리거트 루트를 조회할 때 뿐만 아니라 저장하고 삭제할 때도 하나로 처리해야 함을 의미한다.
    • 저장 메서드는 매그리거트 루트만 저장하면 안 되고 애그리거트에 속한 모든 객체를 저장해야 한다.
    • 삭제 메서드는 애그리거트 루트 뿐만 아니라 애그리거트에 속한 모든 객체를 삭제해야 한다.
  • p.167 식별자는 크게 세 가지 방식 중 하나로 생성한다. 사용자가 직접 생성, 도메인 로직으로 생성, DB를 이용한 일련번호 사용.
  • p.167 식별자 생성 규칙은 도메인 규칙이므로 도메인 영역에 식별자 생성 기능을 위치시켜야 한다.
  • p.170 인터페이스는 도메인 패키지에 위치하는데 구현 기술인 Spring data jpa의 repository 인터페이스를 상속하고 있다. 즉, 도메인이 인프라에 의존하는 것이다.
  • p.171 특정 기술에 의존하지 않는 순수한 도메인 모델을 추구하는 개발자는 <도메인에서 구현="" 기술에="" 대한="" 의존을="" 없애려면="" 클래스를="" 인프라에="" 위치시켜야="" 한다.=""> 와 같은 구조로 구현한다. 이 구조를 가지면 구현 기술을 변경하더라도 도메인이 받는 영향을 최소화할 수 있다.
  • p.171 DIP를 적용하는 주된 이유는 저수준 구현이 변경되더라도 고수준이 영향을 받지 않도록 하기 위함이다.
  • p.172 DIP를 완벽하게 지키면 좋겠지만 개발 편의성과 실용성을 가져가면서 구조적인 유연함은 어느 정도 유지했다. 복잡도를 높이지 않으면서 (즉 JPA 애너테이션을 도메인 모델에 사용하면서) 기술에 따른 구현 제약이 낮다면 합리적인 선택이라고 생각한다.

5. 스프링 데이터 JPA를 이용한 조회 기능

  • p.175 스펙 Specification 은 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스다.
  • p.189 프로퍼티를 비교하는 findBy프로퍼티 형식의 메서드는 Pageable 타입을 사용하더라도 리턴 타입이 List 면 Count 쿼리를 발생하지 않는다.
fun findByBlocked(blocked: Boolean, pageable: Pageable): Page<User>
fun findByNameLike(String name, pageable: Pageable): List<User>
  • p.189 스펙을 사용하는 findAll 메서드에 Pageable 타입을 사용하면 리턴 타입이 Page가 아니어도 COUNT 쿼리를 실행한다.
fun findAll(spec: Specification<User>, pageable: Pageable): List<User>
  • p.194 조회 전용 모델을 만드는 이유는 Presentation 영역을 통해 사용자에게 데이터를 보여주기 위함이다. 동적 인스턴스의 장점은 JPQL을 그대로 사용하므로 객체 기준으로 쿼리를 작성하면서도 동시에 Lazy/Eager 로딩과 같은 고민 없이 원하는 모습으로 데이터를 조회할 수 있다는 점이다.

6. Application Service와 Presentation Layer

  • p.200 Presentation 영역은 사용자의 요청을 해석한다. 실제 사용자가 원하는 기능을 제공하는 것은 Application 영역에 위치한 서비스다.
  • p.201 사용자와 상호작용은 Presentation 영역이 처리하기 때문에, Application 서비스는 Presentation 영역에 의존하지 않는다. Application 영역은 사용자가 웹 브라우저를 사용하는지 REST API를 호출하는지, TCP 소켓을 사용하는지를 알 필요가 없다. 단지 기능 실행에 필요한 입력 값을 받고 실행 결과만 리턴하면 될 뿐이다.
  • p.202 Application 서비스의 주요 역할은 도메인 객체를 사용해서 사용자의 요청을 처리하는 것이므로 Presentation(사용자) 영역 입장에서 보았을 때 Application 서비스는 도메인 영역과 Presentation 영역을 연결해 주는 창구 역할을 한다.
  • p.203 Application 서비스는 트랜잭션 처리도 담당한다. Application 서비스는 도메인의 상태 변경을 트랜잭션으로 처리해야 한다.
  • p.205 도메인 로직을 도메인 영역과 Application 서비스에 분산해서 구현하면 코드 품질에 문제가 발생한다. 코드의 응집성이 떨어진다. 여러 Application 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.
  • p.206 소프트웨어의 가치를 높이려면 도메인 로직을 도메인 영역에 모아서 코드 중복을 줄이고 응집도를 높여야 한다.
  • p.210 한 클래스가 여러 역할을 갖는 것보다 각 클래스마다 구분되는 역할을 갖는 것을 선호한다. 한 도메인과 관련된 기능을 하나의 Application 서비스 클래스에서 모두 구현하는 방식보다 구분되는 기능을 별도의 서비스 클래스로 구현하는 방식을 사용한다.
  • p.215 Application 서비스에서 Aggregate 자체를 리턴하면 코딩은 편할 수 있지만 도메인의 로직 실행을 Application 서비스와 Presentation 영역 두 곳에서 할 수 있게 된다. 이것은 기능 실행 로직을 Application 서비스와 Presentation 영역에 분산시켜 코드의 응집도를 낮추는 원인이 된다. Application 서비스는 Presentation 영역에서 필요한 데이터만 리턴하는 것이 기능 실행 로직의 응집도를 높이는 확실한 방법이다.
  • p.216 Application 서비스에서 표현 영역에 대한 의존이 발생하면 Application 서비스만 단독으로 테스트하기가 어려워진다. 게다가 Presentation 영역의 구현이 변경되면 Application 서비스의 구현도 함께 변경해야 하는 문제도 발생한다.
  • p.218 Presentation 영역의 책임은 크게 다음과 같다.
    • 사용자가 시스템을 사용할 수 있는 흐름(화면)을 제공하고 제어한다.
    • 사용자의 요청을 알맞은 Application 서비스에 전달하고 결과를 사용자에게 전달한다.
    • 사용자의 세션을 관리한다.
  • p.225 Presentation 영역과 Application 서비스가 값 검사를 나눠서 수행하는 것.
    • Presentation 영역: 필수 값, 값의 형식, 범위 등을 검증
    • Application 서비스: 데이터의 존재 유무와 같은 논리적 오류를 검증
    • 요즘은 가능하면 Application 서비스에서 필수 값 검증과 논리적인 검증을 모두 하는 편이다. Application 서비스에서 필요한 값 검증을 모두 처리하면 프레임워크가 제공하는 검증 기능을 사용할 때보다 작성할 코드가 늘어나는 불편함이 있지만, 반대로 Application 서비스의 완성도가 높아지는 이점이 있다.

7. 도메인 서비스

  • p.235 한 Aggregate에 넣기 애매한 도메인 기능을 억지로 특정 Aggregate에 구현하면 안 된다. 억지로 구현하면 Aggregate는 자신의 책임 범위를 넘어서는 기능을 구현하기 때문에 코드가 길어지고 외부에 대한 의존이 높아지게 되며 코드를 복잡하게 만들어 수정을 어렵게 만드는 요인이 된다.
  • p.237 도메인 서비스
    • 계산 로직: 여러 Aggregate가 필요한 계산 로직이나, 한 Aggregate에 넣기에는 다소 복잡한 계산 로직
    • 외부 시스템 연동이 필요한 도메인 로직: 구현하기 위해 타 시스템을 사용해야 하는 도메인 로직
  • p.237 도메인 영역의 Aggregate나 Value와 같은 구성 요소와 도메인 서비스를 비교할 때 다른 점은 도메인 서비스는 상태 없이 로직만 구현한다는 점이다. 도메인 서비스를 구현할 때 필요한 상태는 다른 방법으로 전달받는다.
  • p.239 도메인 서비스 객체를 파라미터로 전달하는 것은 Aggregate가 도메인 서비스에 의존한다는 것을 의미한다. 도메인 객체는 필드(Property)로 구성된 데이터와 메서드를 이용해서 개념적으로 하나인 모델을 표현한다. 일부 기능을 위해 굳이 도메인 서비스 객체를 Aggregate에 의존 주입할 필요는 없다.
  • p.240 도메인 서비스는 도메인 로직을 수행하지 Application 로직을 수행하지 않는다. 트랜잭션 처리와 같은 로직은 Application 로직이므로 도메인 서비스가 아닌 Application 서비스에서 처리해야 한다.
    • 특정 기능이 Application 서비스인지 도메인 서비스인지 감을 잡기 어려울 때는 해당 로직이 Aggregate의 상태를 변경하거나 Aggregate의 상태 값을 계산하는지 검사해 보면 된다. 예를 들어, 계좌 이체 로직은 계좌 Aggregate의 상태를 변경한다. 결제 금액 로직은 주문 Aggregate의 주문 금액을 계산한다. 이 두 로직은 각각 Aggregate를 변경하고 Aggregate의 값을 계산하는 도메인 로직이다. 도메인 로직이면서 한 Aggregate에 넣기에 적합하지 않으므로 이 두 로직은 도메인 서비스로 구현하게 된다.

8. Aggregate 트랜잭션 관리

  • p.248 Pessimistic Lock은 먼저 Aggregate를 구한 스레드가 Aggregate 사용이 끝날 때까지 다른 스레드가 해당 Aggregate를 수정하지 못하게 막는 방식이다.
  • p.249 한 스레드가 Aggregate를 구하고 수정하는 동안 다른 스레드가 수정할 수 없으므로 동시에 Aggregate를 수정할 때 발생하는 데이터 충돌 문제를 해소할 수 있다.
  • p.250 LockModeType.PESSIMISTIC_WRITE 를 값으로 전달하면 해당 엔티티와 맵핑된 테이블을 이용해서 Pessimistic Lock 방식을 적용할 수 있다.
  • p.251 JPA의 javax.persistence.lock.timeout 힌트는 Lock을 구하는 대기 시간을 밀리초 단위로 지정한다. 지정한 시간 이내에 Lock을 구하지 못하면 익셉션을 발생시킨다.
  • p.256 JPA는 엔티티가 변경되어 UPDATE 쿼리를 실행할 때 @Version 에 명시한 필드를 이용해서 Optimistic Lock 쿼리를 실행한다. Optimistic Lock을 위한 쿼리를 실행할 때 쿼리 실행 결과로 수정된 행의 개수가 0이면 이미 누군가 앞서 데이터를 수정한 것이다. 이는 트랜잭션이 충돌한 것이므로 트랜잭션 종료 시점에 익셉션이 발생한다(OptimisticLockingFailureException).
  • p.262 LockModeType.OPTIMISTIC_FORCE_INCREMENT를 사용하면 해당 엔티티의 상태가 변경되었는지에 상관없이 트랜잭션 종료 시점에 버전 값 증가 처리를 한다. 이 Lock 모드를 사용하면 Aggregatea 루트 엔티티가 아닌 다른 엔티티나 Value가 변경되더라도 버전 값을 증가시킬 수 있으므로 Optimistic Lock 기능을 안전하게 적용할 수 있다.
  • p.264 단일 트랜잭션에서 동시 변경을 막는 Pessimistic Lock 방식과 달리 Offline Pessimistic Lock은 여러 트랜잭션에 걸쳐 동시 변경을 막는다.
  • p.265 Offline Pessimistic Lock 방식은 잠금 유효 시간을 가져야 한다. 유효 시간이 지나면 자동으로 Lock을 해제해서 다른 사용자가 Lock을 일정 시간 후에 다시 구할 수 있도록 해야 한다.
interface LockManager {
  fun tryLock(type: String, id: String): LockId
  fun checkLock(lockId: LockId)
  fun releaseLock(lockId: LockId)
  fun extendLockExpiration(lockId: LockId, inc Long)
}

9. 도메인 모델과 Bounded Context

  • p.279 한 개의 Bounded Context가 여러 하위 도메인을 포함하더라도 하위 도메인바다 구분되는 패키지를 갖도록 구현해야 하며, 이렇게 함으로써 하위 도메인을 위한 모델이 서로 뒤섞이지 않고 하위 도메인마다 Bounded Context를 갖는 효과를 낼 수 있다.
  • p.292 각 Bounded Context는 모델의 경계를 형성하는데 Bounded Context를 마이크로서비스로 구현하면 자연스럽게 컨텍스트별로 모델이 분리된다. 코드로 생각하면 마이크로서비스마다 프로젝트를 생성하므로 Bounded Context마다 프로젝트를 만들게 된다. 이것은 코드 수준에서 모델을 분리하여 두 Bounded Context의 모델이 섞이지 않도록 해준다.

10. 이벤트

  • p.302 주문 도메인 객체의 코드를 결제 도메인 때문에 변경할지도 모르는 상황은 좋아 보이지 않는다. 주문 Bounded Context와 결제 Bounded Context 간의 강결합(high coupling) 때문이다.
  • p.318 외부 시스템과의 연동을 동기로 처리할 때 발생하는 성능과 트랜잭션 범위 문제를 해소하는 방법은 이벤트를 비동기로 처리하거나 이벤트와 트랜잭션을 연계하는 것이다.
    • 로컬 핸들러를 비동기로 실행하기
    • 메시지 큐를 사용하기
    • 이벤트 저장소와 이벤트 포워드 사용하기
    • 이벤트 저장소와 이벤트 제공 API 사용하기
  • p.338 이벤트를 구현할 때 추가로 고려할 점
    • 특정 이벤트에서 계속 전송에 실패하면 어떻게 될까?
    • 로컬 핸들러를 이용해서 이벤트를 비동기로 처리할 경우 이벤트 처리에 실패하면 이벤트를 유실하게 된다.
    • 메시징 시스템은 사용 기술에 따라 이벤트 발생 순서와 메시지 전달 순서가 다를 수 도 있다.
    • 동일한 이벤트를 다시 처리해야 할 때 이벤트를 어떻게 할 지 결정해야 한다.
  • p.339 연산을 여러 번 적용해도 결과가 달라지지 않는 성질을 멱등성(idempotent)이라고 한다. 이벤트 처리도 동일 이벤트를 한 번 적용하나 여러 번 적용하나 시스템이 같은 상태가 되도록 핸들러를 구현할 수 있다. 이벤트 핸들러가 멱등성을 가지면 시스템 장애로 인해 같은 이벤트가 중복해서 발생해도 결과적으로 동일 상태가 된다. 이는 이벤트 중복 발생이나 중복 처리에 대한 부담을 줄여준다.
  • p.341 이벤트 처리를 동기로 하든 비동기로 하든 이벤트 처리 실패와 트랜잭션 실패를 함께 고려해야 한다. 경우의 수를 줄이는 방법은 트랜잭션이 성공할 때만 이벤트 핸들러를 실행하는 것이다.
  • p.341 TransactionalEventListener 애너테이션의 phase 속성 값으로 TransactionPhase.AFTER_COMMIT 을 사용하면 스프링은 트랜잭션 커밋에 성공한 뒤에 핸들러 메서드를 실행한다. 중간에 에러가 발생해서 트랜잭션이 롤백되면 핸들러 메서드를 실행하지 않는다. 이 기능을 사용하면 이벤트 핸들러를 실행했는데 트랜잭션이 롤백 되는 상황은 발생하지 않는다. 트랜잭션이 성공할 때만 이벤트 핸들러를 실행하게 되면 트랜잭션 실패에 대한 경우의 수가 줄어들어 이제 이벤트 처리 실패만 고민하면 된다.

11. CQRS

  • p.346 도메인 모델 관점에서 상태 변경 기능은 주로 한 Aggregate의 상태를 변경한다. 상태를 변경하는 범위와 상태를 조회하는 범위가 정확하게 일치하지 않기 때문에 단일 모델로 두 종류의 기능을 구현하면 모델이 불필요하게 복잡해진다. 단일 모델을 사용할 때 발생하는 복잡도를 해결하기 위해 사용하는 방법이 CQRS(Command Query Responsibility Segregation)이다.
  • p.351 복잡한 도메인은 주로 상태 변경 로직이 복잡한데 명령 모델과 조회 모델을 구분하면 조회 성능을 위한 코드가 명령 모델에 없으므로 도메인 로직을 구현하는 데 집중할 수 있다.

참고