Skip to content

도메인 주도 개발 시작하기 #129

@raycon

Description

@raycon

https://hanbit.co.kr/store/books/look.php?p_code=B4309942517

도메인 모델 시작하기

도메인

  • 소프트웨어로 해결하고자 하는 문제 영역
  • 도메인 전문가, 관계자, 개발자가 같은 지식을 공유하고 직접 소통할수록 전문가가 원하는 제품을 만들 가능성이 높아진다

도메인 모델

  • 특정 도메인을 개념적으로 표현한 것
    • 도메인을 이해하는데 도움이 된다면 표현 방식(UML, 수학 공식)은 중요하지 않음
    • 모델의 각 구성 요소는 특정 도메인으로 한정할 때 비로소 의미가 완전해지기 때문에 각 하위 도메인마다 별도로 모델을 만들어야 한다.
  • 아키텍처 상의 도메인 계층을 구현할 때 사용하는 객체 모델
    • 표현(Presentation): 사용자의 요청을 처리하고 정보를 보여준다.
    • 응용(Application): 사용자가 요청한 기능을 실행한다. 업무 로직을 구현하지 않는다.
    • Domain: 시스템이 제공할 도메인 규칙을 구현한다.
    • Infrastructure: 외부 시스템과의 연동을 처리한다.

도메인 모델링

  • 요구사항으로부터 핵심 구성 요소, 규칙, 기능을 찾는다.
  • 문서화를 통해 지식을 공유한다.

엔티티

  • 식별자를 갖는다.
  • 식별자를 가지고 equals(), hashCode() 메서드를 구현할 수 있다. (식별자가 같으면 두 엔티티는 같다)

밸류

  • 개념적으로 완전한 하나를 표현 (예: 받는 사람, 배송 정보)
  • 의미를 명확하게 하기 위해 사용 (예: int 타입을 필드로 갖는 Money 타입)
  • 밸류 타입을 위한 기능을 추가할 수 있다.
  • 특별한 이유가 없다면 불변 객체로 정의해서 사용한다.
  • 모든 속성을 비교해서 equals(), hashCode() 메서드를 구현한다.
  • 식별자를 위한 밸류 타입을 사용해서 의미가 잘 드러나도록 할 수 있다. (예: OrderNo)

set 메서드

  • set 메서드는 필드값만 변경하고 끝나기 때문에 상태 변경과 관련된 도메인 지식이 코드에서 사라지게 된다.
  • 객체를 생성할 때 온전하지 않은 상태가 될 수 있다. -> 생성자를 통해 필요한 데이터를 모두 받아야 한다.
  • 도메인 모델에는 set 메서드를 넣지 않는다.

유비쿼터스 언어

  • 전문가, 관계자, 개발자가 도메인과 관련된 공통의 언어를 만든다.
  • 대화, 문서, 도메인 모델, 코드, 테스트 등에서 같은 용어를 사용한다.
  • 도메인 용어에 알맞은 단어를 찾는 시간을 아까워하지 말자.

아키텍처 개요

네개의 영역

  • 표현 영역: HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환
  • 응용 영역: 도메인 모델을 이용해서 사용자에게 제공할 기능을 구현한다. 실제 도메인 로직 구현은 도메인 모델에 위임한다.
  • 도메인 영역: 핵심 로직을 도메인 모델에서 구현한다.
  • 인프라스트럭처 영역: 논리적인 개념을 표현하기보다는 실제 구현을 다룬다.
  • 도메인 영역, 응용 영역, 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다. 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.

계층 구조 아키텍처

  • 하위 계층은 상위 계층에 의존하지 않는다.
  • 응용 영역과 도메인 영역이 인프라스트럭처에 의존하면 테스트 어려움기능 확장의 어려움이라는 두가지 문제가 발생한다.

DIP 의존 역전 원칙

  • 고수준 모듈: 의미 있는 단일 기능을 제공
  • 저수준 모듈: 하위 기능을 실제로 구현한 것
  • DIP: 저수준 모듈이 고수준 모듈에 의존하게 한다.
  • 저수준 모듈 없이 테스트가 가능해진다.
  • 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출한다. (인터페이스는 고수준 영역의 패키지에 위치)
  • 응용 영역과 도메인 영역에 영향을 최소화하면서 구현체를 변경하거나 추가할 수 있다.

도메인 영역의 주요 구성 요소

  • 엔티티: 고유의 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다. 데이터와 함께 도메인 기능을 제공한다.
  • 밸류: 개념적으로 하나인 값을 표현한다.
  • 애그리거트: 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것
    • 군집에 속한 객체를 관리하는 루트 엔티티를 갖는다.
    • 내부 구현을 숨겨서 애그리거트 단위로 구현을 캡슐화한다.
  • 리포지터리: 도메인 모델의 영속성을 처리한다.
    • 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다.
    • 리포지터리 인터페이스는 도메인 모델 영역에 속하며, 실제 구현 클래스는 인프라스트럭처 영역에 속한다.
  • 도메인 서비스: 특정 엔티티에 속하지 않은 도메인 로직을 제공한다.

요청 처리 흐름

  • 표현 영역은 사용자가 전송한 데이터를 응용 서비스가 요구하는 형식으로 변환해서 전달한다.
  • 응용 서비스는 도메인 모델을 이용해서 기능을 구현한다.
    • 도메인의 상태를 변경하므로 변경 상태가 물리저장소에 올바르게 반영되도록 트랜잭션을 관리해야 한다.

인프라스트럭처 개요

  • 응용 영역의 @Transactional, 도메인 영역의 @Entity, @Table 은 편리하다.
  • 응용 영역과 도메인 영역이 인프라스트럭처에 대한 의존을 완전히 갖지 않도록 시도하는 것은 자칫 구현을 더 복잡하고 어렵게 만들 수 있다.
  • 표현 영역은 항상 인프라스트럭처와 밀접한 관계가 있다.

모듈 구성

  • 도메인이 크면 하위 도메인 별로 모듈을 나눈다.
  • 애그리거트, 모델, 리포지터리는 같은 패키지에 위치시킨다.

애그리거트

애그리거트

  • 상위 수준에서 도메인 모델간의 관계를 파악
  • 한 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다.
  • 각 애그리거트는 자기 자신을 관리할 뿐 다른 애그리거트를 관리하지 않는다.
  • 함께 변경되는 빈도가 높은 객체는 한 애그리거트에 속할 가능성이 높다.
  • 두개 이상의 엔티티로 구성되는 애그리거트는 드물다.

애그리거트 루트

  • 애그리거트 전체를 관리할 주체
  • 도메인 규칙을 구현한 기능을 제공
  • 애그리거트 외부에서 내부의 상태를 변경할 수 없도록 해야한다.
    • 단순히 필드를 변경하는 set 메서드를 public 범위로 만들지 않는다.
    • 밸류 타입은 불변으로 구현한다.
  • 한 트랜잭션에서 한 애그리거트만 수정한다.
    • 다른 애그리거트를 변경하지 않는다.
    • 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 할 경우, 응용 서비스에서 각 애그리거트의 상태를 변경한다.

리포지터리와 애그리거트

  • 리포지터리는 애그리거트 단위로 존재한다.

ID를 이용한 애그리거트 참조

  • 필드를 이용한 애그리거트 참조의 문제점
    • 편리함 오용 (다른 애그리거트 수정)
    • 성능과 관련된 고민 (즉시 로딩, 지연 로딩 등)
    • 확장 (하위 도메인별로 시스템을 분리할 수 없게됨)
  • ID 참조를 사용하면 한 애그리거트에 속한 객체들만 참조로 연결된다.
  • 참조하는 애그리거트가 필요하면 응용 서비스에서 ID를 이용해서 로딩한다.
  • N+1 문제가 발생하지 않도록 하려면 조회 전용 쿼리를 사용한다.
    • 조회 메서드에서 조인을 이용해 한번의 쿼리로 필요한 데이터를 로딩한다.
    • 애그리거트 마다 서로 다른 저장소를 사용하면, 캐시를 적용하거나 조회 전용 저장소를 따로 구성한다.

애그리거트 간 집합 연관

  • 개념적으로 양방향 M-N 관계가 존재해도, 실제 구현에서는 단방향 M-N 연관만 적용하면 된다.
  • RDBMS 에서는 M-N 연관은 조인 테이블을 사용한다.
  • JPA 에서는 밸류 타입에 대한 컬렉션 매핑을 사용해서 M-N 단방향 연관을 구현한다.

애그리거트를 팩토리로 사용하기

  • 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면, 애그리거트에 팩토리 메서드를 구현한다.

리포지터리와 모델 구현

매핑 구현

  • 애그리거트 루트는 엔티티이므로 @Entity로 매핑 설정 한다.
  • 밸류는 Embeddable로 매핑 설정한다.
  • 밸류 타입 프로퍼티는 Embedded로 매핑 설정한다.
    • @AttributeOverrides를 사용해서 매핑할 컬럼 이름을 변경할 수 있다.
  • JPA 요구사항으로 @Entity, @Embeddable은 기본 생성자를 제공해야 한다.
    • 불변 타입은 기본 생성자가 필요 없으므로, 기본 생성자를 protected로 선언한다.
  • @AttributeConverter 를 사용해서 밸류 타입과 컬럼 데이터 간의 변환을 처리한다.
    • @Converter(autoApply=true) 를 적용하면 컨버터를 자동으로 적용한다.
  • 밸류 컬렉션을 별도 테이블로 매핑할 때는 @ElementCollection@CollectionTable을 함께 사용한다.
    • @OrderColumn 으로 인덱스를 지정할 수 있다.
  • 밸류 타입을 식별자로 매핑하면 @Id 대신 @EmbeddedId 애너테이션을 사용한다.
    • 식별자에 기능을 추가할 수 있다. (버전 관리 등)
    • 식별자로 사용할 밸류 타입은 equals(), hashcode() 메서드를 알맞게 구현해야 한다.
  • 밸류를 별로 테이블에 저장할 경우 @SecondaryTable@AttributeOverride를 사용한다.
    • 즉시 로딩으로 작동한다.
    • 밸류를 엔티티로 매핑하고 지연 로딩하도록 설정할 수 있지만, 좋은 방법은 아니다. -> 조회 전용 쿼리를 사용하는게 낫다.
  • 밸류에 상속을 사용해야 할 경우
    • @Embeddable 대신 @Entity를 이용한다.
    • @Inheritance, @DiscriminatorColumn 을 사용해서 테이블과 매핑한다.
    • 의존하는 객체에 cascade 설정을 적용하고, orphanRemovaltrue로 설정한다.
  • 컬렉션의 clear() 메서드 동작 방식이 다르다
    • @OneToMany: select 쿼리를 수행하고, 각각에 대해 delete 쿼리를 실행한다.
    • @ElementCollection: 객체를 로딩하지 않고, 한 번의 delete 쿼리를 실행한다.

애그리거트 로딩 전략

  • 카타시안 Cartesian 조인을 사용하면 쿼리 결과에 중복이 발생한다.
  • 하이버네이트가 중복된 데이터를 알맞게 제거해주지만, 애그리거트가 커지면 문제가 될 수 있다.
  • 애그리거트 내의 모든 연고나을 즉시 로딩으로 설정할 필요는 없다.
  • 상태 변경을 위해 지연 로딩을 사용할 때 발생하는 추가 쿼리로 인한 실행 속도 저하는 보통 문제가 되지 않는다.

애그리거트의 영속성 전파

  • @Embeddable 타입에 대한 매핑은 함께 저장되고 삭제된다.
  • @Entity 타입에 대한 매핑은 cascade 설정을 해야한다.

식별자 생성 기능

  • 식별자 생성 규칙은 도메인 규칙이므로, 도메인 영역에 식별자 생성 기능을 위치시켜야 한다.
  • 도메인 서비스, 리포티저리에서 식별자를 생성한다.

도메인 구현과 DIP

  • 도메인 모델에서 @Entity, @Table 를 사용
  • 리포지터리 인터페이스도 도메인 패키지에 위치하는데, JPA Repository 를 상속받는다.
  • 원칙적으로 도메인 영역은 구현 기술에 의존하면 안된다.
  • 하지만 구현 기술은 거의 바뀌지 않는다. 타협이 필요하다.

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

스펙

  • Specification
  • 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스
  • List 를 리턴 받을 경우 COUNT 쿼리를 실행하지 않는다.
  • 여러 스펙 조합시 스펙 필더 클래스를 만들어서 사용한다.

동적 인스턴스 생성

  • JPA 쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있다.
  • JPQL 의 select 절에 new 키워드 사용

하이버네이트 @Subselect 사용

  • 뷰를 사용하는 것처럼 쿼리 실행 결과를 Entity 에 매핑 할 수 있다.
  • @Entity에 다음 어노테이션을 적용한다.
    • @Immutable: 필드가 변경되었을 경우에 update쿼리가 수행되는 문제를 방지한다
    • @Subselect: 조회 쿼리를 값으로 갖는다. 값으로 지정된 쿼리가 from 절의 서브 쿼리로 적용된다.
    • @Synchronize: 트랜잭션이 커밋되기 전에 다른 엔티티의 변경 내역을 동기화 하기 위해 사용한다.

응용 서비스와 표현 영역

  • 표현 영역은 응용 서비스가 요구하는 형식으로 사용자 요청을 변환한다.
  • 응용 서비스는 표현 영역에 의존하지 않는다.

응용 서비스의 역할

  • 도메인 객체 간의 흐름을 제어
  • 트랜잭션 처리
  • 접근 제어
  • 이벤트 처리

응용 서비스의 구현

  • 표현 영역과 도메인 영역을 연결하는 파사드와 같은 역할
  • 기능을 한 클래스에서 모두 구현
    • 코드 중복을 제거하기 쉬움 (메소드 추출)
    • 클래스 크기가 커짐
  • 구분되는 기능별로 서비스 클래스를 구현
    • 코드 품질을 일정 수준으로 유지하는데 도움
    • 별도 클래스 (~ServiceHelper)에 로직을 구현해서 코드 중복 방지 가능
  • 인터페이스가 필요한가?
    • 응용 서비스는 런타임에 교체하는 경우가 거의 없고, 한 응용 서비스의 구현 클래스가 두 개인 경우도 드물다
    • 표현 영역부터 개발을 시작한다면 인터페이스가 필요함
    • 도메인 영역부터 개발을 시작한다면 응용 서비스가 먼저 만들어져서 필요 없음
  • 응용 서비스에서 애그리거트를 리턴할 경우, 도메인 로직 실행을 응용 서비스와 표현 영역 두 곳에서 할 수 있게 된다
  • 응용 서비스는 표현 영역에서 필요한 데이터만 리턴하는 것이 기능 실행 로직의 응집도를 높이는 확실한 방법이다

표현 영역의 역할

  • 사용자가 시스템을 사용하도록 알맞은 흐름을 제공
  • 사용자의 요청에 맞게 응용 서비스에 기능을 실행 (입력 변환)
  • 응용 서비스의 실행 결과를 사용자에게 알맞은 형식으로 제공 (응답 변환)
  • 세션 관리

값 검증

  • 표현 영역, 응용 영역 두 곳에서 모두 수행할 수 있음
  • 원칙적으로 모든 값에 대한 검증은 응용 서비스에서 처리한다.
    • 표현 영역: 응용 서비스가 발생시킨 검증 에러 목록을 뷰에서 사용할 형태로 변환한다.
  • 응용 서비스를 사용하는 표현 영역 코드가 한 곳이면
    • 표현 영역: 필수 값, 값의 형식, 범위 등을 검증
    • 응용 서비스: 데이터의 존재 유무와 같은 논리적 오류를 검증
  • 저자는 가능하면 응용 서비스에서 필수 값 검증과 논리적인 검증을 모두 하는 편

권한 검사

  • 도메인에 맞게 보안 프레임워크를 확장하려면 프레임워크에 대한 높은 이해가 필요하다.
  • 이해도가 높지 않으면 권한 검사 기능을 직접 구현하는 것이 코드 유지 보수에 유리하다.
  • PermissionService를 따로 구현

조회 전용 기능과 응용 서비스

  • 조회 전용 기능은 트랜잭션이 필요하지 않다
  • 조회를 위한 응용 서비스가 단지 조회 전용 기능을 실행하는 코드밖에 없다면 응용 서비스를 생략해도 무방하다.

도메인 서비스

  • 한 애그리거트에 넣기 애매한 도메인 기능을 억지로 특정 애그리거트에 구현하면 안된다.
  • 도메인 서비스는 도메인 영역에 위치한 도메인 로직을 표현할 때 사용
  • 상태 없이 로직만 구현한다.
  • 애그리거트 객체에 도메인 서비스를 전달하는 것은 응용 서비스 책임
  • 애그리거트의 상태를 변경하거나 계산할 경우 도메인 서비스로 구현
  • 외부 시스템과 연결된 도메인 서비스는 도메인 영역에 인터페이스로 선언하고, 인프라 스트럭처 영역에 구현 클래스를 위치시키며, 응용 서비스에서 사용한다.

애그리거트 트랜잭션 관리

  • DBMS가 지원하는 트랜잭션과 함께 애그리거트를 위한 추가적인 트랜잭션 처리 기법이 필요

선점 잠금 (Pessimistic Lock)

  • 먼저 애그리거트를 구한 스레드가 끝날 때 까지 다른 스레드가 해당 애그리거트를 수정하지 못하게 막는 방식
  • JPA의 @Lock(LockModeType.PESSIMISTIC_WRITE) 사용
  • 하이버네이트는 for update 쿼리를 사용해서 DBMS 가 제공하는 행단위 잠금을 사용한다

선점 잠금과 교착상태

  • A -> B, B -> A 순으로 잠금 기능을 사용하는 두 스레드가 있을 경우 교착 상태가 발생한다
  • JPA의 @QueryHint 를 사용해서 javax.persistence.lock.timeout 힌트를 밀리초 단위로 지정한다.

비선점 잠금

  • 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식
  • JPA의 @Version 을 사용해서 엔티티의 버전을 관리한다.
  • 트랜잭션 충돌이 발생하면 OptimisticLockingFailureException이 발생한다.
  • 비선점 잠금 방식을 여러 트랜잭션으로 확장하려면, 버전 정보도 사용자에게 전달해야 한다.
  • 사용자가 전달한 버전과 현재 버전이 맞지 않을 경우 예외로 처리한다.
  • 루트가 아닌 다른 엔티티가 수정될 경우
    • @Lock 어노테이션에 비선점 강제 버전 증가 잠금 모드 LockModeType.OPTIMISTIC_FORCE_INCREMENT를 사용한다.

오프라인 선점 잠금

  • 여러 트랜잭션에 걸쳐 동시 변경을 막는다.
  • LockManager와 같은 인터페이스를 정의하고, 별도의 구현을 통해서 잠금 기능을 구현한다.

도메인 모델과 바운디드 컨텍스트

바운디드 컨텍스트

  • 경계를 갖는 컨텍스트
  • 논리적으로 같은 존재처럼 보이지만 하위 도메인에 따라 다른 용어를 사용하는 경우, 한 개의 모델로 모든 하위 도메인을 표현 할 수 없다
  • 하위 도메인마다 모델을 만들고, 명시적으로 구분되는 경계를 가져서 섞이지 않도록 해야한다
  • 조직 구조에 따라 바운디드 컨텍스트가 결정된다.
  • 물리적인 바운디드 컨텍스트가 한 개 이더라도, 내부적으로 패키지를 활용해서 논리적으로 바운디드 컨텍스트를 만든다.

바운디드 컨텍스트 구현

  • 바운디드 컨텍스트는 도메인 기능을 제공하는 데 필요한 모든 요소를 포함한다
    • 표현 영역, 응용 서비스, 도메인, 인프라스트럭처, DBMS
  • 모든 바운디드 컨텍스트를 도메인 주도로 개발할 필요는 없다.
    • 표현 영역, 서비스, DAO, DBMS
  • 각 바운디드 컨텍스트는 서로 다른 구현 기술을 사용할 수도 있다.

바운디드 컨텍스트 간 통합

  • 직접 통합
    • 도메인 서비스를 인터페이스로 정의하고 인프라스트럭처 영역에 구현한다
    • 모델 간 변환이 복잡하면 별도의 변환기(Translator)를 정의한다.
  • 간접 통합
    • 큐를 사용할 수 있다.
    • 큐를 누가 제공하느냐에 따라 데이터 구조가 결정된다.
    • 큐를 제공 받을 경우 비동기로 데이터를 전달하는 것 이외에 REST API 를 사용해서 데이터를 전달하는 것과 차이가 없다.

바운디드 컨텍스트 간 관계

  • 가장 흔한 관계는 한쪽에서 API를 제공하고, 다른 한쪽에서 그 API를 호출하는 관계
  • 하류는 상류에서 제공하는 데이터와 기능에 의존한다.
  • 여러 하류 팀의 요구사항을 수용할 수 있는 API를 공개호스트서비스(OHS: Open Host Service)라고 한다.
  • 상류 컴포넌트는 상류 바운디드 컨텍스트의 도메인 모델을 따른다.
  • 하류 컴포넌트는 상류 서비스의 모델이 자신에게 영향을 주지 않도록 완충지대를 만들어야 한다.
    • 안티코럽션 계층 (ACL: Anticoruption Layer)
    • 인프라스트럭처 영역에 구현
  • 두 바운디드 컨텍스트가 같은 모델을 공유하는 경우
    • 두 팀이 공유하는 모델을 공유 커널(Shared Kernel)이라고 부른다
  • 서로 통합하지 않는 방식: 동립 방식(Separate Way)
    • 수동으로 바운디드 컨텍스트를 통합한다.
    • 별도의 시스템을 만들어야 할 수도 있다.

컨텍스트 맵

  • 바운디드 컨텍스트 간의 관계를 표시한다.
  • 시스템 전체 구조를 보여준다.

이벤트

시스템 간 강결합 문제

  • 도메인 객체에 서비스를 전달하면
    • 트랜잭션 처리를 어떻게 해야 할지 애매하다
    • 외부 서비스의 성능에 직접적인 영향을 받는다
    • 서로 다른 두가지 로직이 섞이게 된다
    • 기능 추가가 어렵다
  • 비동기 이벤트를 사용하면 두 시스템 간의 결합을 크게 낮출 수 있다.

이벤트 개요

  • 이벤트: 과거에 벌어진 어떤 것
  • 생성 주체: 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체
  • 이벤트 핸들러: 이벤트 생성 주체가 발생한 이벤트에 반응
  • 이벤트 디스패처: 생성 주체와 핸들러를 연결
  • 구조
    • 이벤트 종류
    • 이벤트 발생 시간
    • 추가 데이터
  • 이벤트 이름은 과거 시제를 사용
  • 용도
    • 트리거
    • 서로 다른 시스템간의 데이터 동기화

이벤트 구현

  • 스프링에서 제공하는 ApplicationEventPublisher 사용
    • 스프링 컨테이너는 ApplicationEventPublisher를 구현하고 있다.
  • publishEvent() 를 사용해서 이벤트를 발생시킨다.
  • @EventListener 를 사용해서 이벤트를 처리한다.
  • 응용 서비스와 동일한 트랜잭션 범위에서 이벤트 핸들러를 실행한다. (동기)

동기 이벤트 처리 문제

  • 외부 서비스의 성능 저하가 내 시스템의 성능 저하로 연결된다.

비동기 이벤트 처리

  • A 하면 이어서 B 하라 -> A 하면 최대 언제까지 B 하라로 바꿀 수 있다.
  • 로컬 핸들러 비동기 실행
    • @EnableAsync를 설정하고, 핸들러에 @Async를 지정한다.
  • 메시징 시스템을 이용한 비동기 구현
    • Kafka, RabbitMQ 와 같은 메시징 시스템을 사용한다.
    • 글로벌 트랜잭션을 사용해야 될 수 있다.
  • 이벤트 저장소를 이용한 비동기 처리
    • 이벤트를 일단 DB에 저장한 뒤에 별도 프로그램을 이용해서 이벤트 핸들러에 전달한다.
    • 도메인 상태 변화와 이벤트 저장이 한 트랜잭션으로 처리된다.
    • 포워더: 이벤트를 주기적으로 읽어와서 핸들러에 전달한다. 어디까지 전달했는지 추적한다.
    • API: 이벤트 목록을 제공한다. 클라이언트가 어디까지 처리했는지 기억해야한다.

이벤트 적용시 추가 고려사항

  • 이벤트 소스를 저장할지 여부
  • 포워더에서 전송 실패를 얼마나 허용할 것인지
  • 이벤트 손실 (로컬 핸들러를 이용해서 비동기로 처리하는 도중 실패하면 이벤트를 유실하게 된다)
  • 이벤트 순서 (메시징 시스템은 발생 순서와 전달 순서가 다를 수 있다)
  • 이벤트 재처리

이벤트 트랜잭션

  • 주문 취소로 외부 시스템에 결제 취소를 했는데 -> 주문 상태 업데이트 실패한 경우 문제
  • 주문 취소로 주문 상태 업데이트를 했는데 -> 외부 시스템 결제 취소가 실패한 경우 문제
  • 트랜잭션이 성공할 때만 이벤트 핸들러를 실행
  • 스프링이 제공하는 @TransactionalEventListenerphase = TransactionPhase.AFTER_COMIT 사용
  • 이제 이벤트 처리 실패만 고민하면 된다

CQRS

  • 화면 조회시 여러 애그리거트의 데이터가 필요한 경우
  • 식별자를 이용해서 애그리거트를 참조하는 방식을 사용하면 즉시 로딩 방식과 같은 JPA의 쿼리 관련 최적화 기능을 사용할 수 없다.
  • 이러한 고민의 원인은 변경과 조회에 단일 도메인 모델을 사용하기 때문
  • 해결책은 상태 변경을 위한 모델(Command Model)과 조회를 위한 모델(Query Model)을 분리하는 것
  • 조회 기능 때문에 도메인 모델이 복잡해지는 것을 막을 수 있다.
  • 조회 모델에는 응용 서비스 없이 DAO를 실행한다.
  • 명령 모델과 조회 모델이 서로 다른 데이터 저장소를 사용할 수도 있다.
    • 이벤트를 활용
    • 동기 이벤트와 글로벌 트랜잭션을 사용해서 실시간으로 동기화 할 수 있다.
    • 특정 시간 안에만 동기화해도 된다면 비동기로 데이터를 전송하면 된다.
  • 트래픽이 높은 서비스인데 단일 모델을 고집하면 유지 보수 비용이 오히려 높아질 수 있으므로 CQRS 도입을 고려한다

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions