Enjoy My Posts

DDD - 아키텍처

Posted on By Geunwon Lim

이 포스트는 IDDD 4장을 보고 작성합니다.

DDD의 장점 중 하나는 특정 아키텍처의 사용을 요구하지 않는다는 점이다.

아키텍처 스타일과 패턴을 선택할 때는 유스케이스와 사용자 스토리, 도메인 모델의 시나리오 등과 같은 기능적 요구사항을 사용할 수 있는지를 고려해야한다. 즉 기능적 요구사항 없이는 필요한 소프트웨어 품질을 결정할 수 없다.

4 계층

사용자 인터페이스에서 찾을 수 있는 유효성 검사의 유형은 도메인 모델에 속해 있는 유형과는 다르다. 깊은 비즈니스 지식을 표현하는 대단위 유효성 검사는 모델로만 제한하는 편이 좋다.

사용자 인터페이스 컴포넌트가 도메인 모델의 객체를 사용하더라도 일반적으론 데이터를 투명한 유리에 올려두는 수준으로 제한된다.

애플리케이션 계층에서는 도메인 로직이 전혀 없고, 영속성 트랜잭션과 보안을 제어할 수 있다. 또한 이벤트 기반의 알림을 다른 시스템으로 보내거나, 사용자에게 보낼 이메일 메시지의 작성을 담당할 수도 있다. 보통 애그리거트를 리포지토리에서 가져와서 몇몇 커맨드 오퍼레이션을 수행한다. 애플리케이션 서비스는 새로운 애그리게잇 인스턴스를 생성하고 리포지토리로 저장시킨다. 무상태 오퍼레이션으로 설계된 일부 도메인별 태스크를 완성하기 위해 도메인 서비스를 사용할 수도 있다.

애플리케이션 계층은 이벤트에 구독자를 등록할 수 있다. 이렇게 함으로써 이벤트를 저장하거나 전달하거나 애플리케이션이 책임지고 처리토록 할 수 있다. 이를 통해 도메인 모델이 고유한 핵심 문제만 알면 되는 자유로움을 갖고, 도메인 이벤트 게시자를 경량으로 유지할 수 있도록 해주며, 메시징 인프라스트럭처 의존성으로부터 해방된다.

헥사고날(육각형) 아키텍처

DI를 적용하면 실제론 계층이 나눠지지 않다고 할 수 있을지도…? 상,하위 모두 추상화에 의존하니 스택처럼 쌓여있던 레이어 형태가 무너지는듯 보인다!

이 아키텍처에서는 다양한 이질적 클라이언트가 동등한 지위에서 시스템과 상호작용하도록 한다. 새로운 클라이언트가 필요하다고 해도 내부 애플리케이션의 API가 클라이언트의 입력을 이해하도록 변환해주는 어댑터만 추가해주면 된다. 이로써 시스템이 사용하는 그래픽, 영속성, 메시징 같은 출력 메커니즘이 다양해지고 쉽게 대체할 수 있게 된다. 애플리케이션 결과를 지정된 출력 메커니즘이 허용하는 형태로 변환하기 위해 어댑터를 생성했기 때문이다.

의존성 주입이 무조건 헥사고날을 의미하진 않는다. 아키텍처를 만드는 과정에서 자연스럽게 포트와 어댑터 스타일의 방향으로 흘러가게 해줄 뿐이다.

일반적으로 프론트 엔드(클라이언트가 시스템과 상호작용하는 곳), 백 엔드(애플리케이션이 저장된 영속성 데이터를 가져오거나 새로운 영속성 데이터를 저장하거나 출력을 내보내는 곳)로 나눈다. 그런데 헥사고날은 시스템을 다른 방식으로 바라본다.

여기엔 외부와 내부의 두 주요 영역이 있다. 외부 영역은 이질적 클라이언트가 입력을 보낼 수 있도록 해주고 영속 데이터를 가져오거나 애플리케이션 출력을 저장(db)하거나 다른 위치로 전송(메시징)하는 메커니즘을 제공한다.

외부 타입마다 어댑터가 존재하고 외부 영역은 애플리케이션의 API를 통해 내부 영역과 이어진다.

각 헥사고날의 면은 입력이든 출력이든 각기 다른 종류의 포트를 표현한다. 예를 들어 하나의 포트는 HTTP를 사용하고 다른 하나는 AMQP를 사용할지도. 포트를 정의하는 엄격한 틀이 있는 것은 아니고 따라서 포트는 유연하게 해석된다. 포트를 어떻게 나눴든 클라이언트의 요청이 도착하면 그에 해당하는 각각의 어댑터가 이 입력을 변환해준다.

헥사고날을 사용하면 유스케이스를 염두에 두고 설계할 뿐이지, 지원되는 클라이언트의 수를 고려하진 않는다.

헥사고날의 큰 장점은 테스트를 위해 어댑터를 쉽게 개발할 수 있다는 점이다. 클라이언트와 저장소 메커니즘이 없더라도, 전체 애플리케이션과 도메인 모델을 설계해서 테스트할 수 있다. HTTP, SOAP 등 메시징 포트를 지원할지 결정하기 전에 테스트를 생성해 서비스를 구동해볼 수 있다.

REST: 표현 상태 전송

아키텍처 스타일이란 특정 설계를 위한 설계 패턴이 무엇인지에 관한 구조적인 큰 그림이다. 이는 여러 구체적 구현에서 일반적으로 사용되는 측면을 추상화하며, 이를 통해 기술적 세부사항에만 집착하다가 길을 잃지 않게 해주고 어떤 부분에서 이점이 있는지 논의할 수 있도록 해준다.

왜 REST를 시스템을 만드는 방법이나 웹 서비스를 만드는 방법으로 생각하게 됐을까? 웹 프로토콜을 여러 가지 방법으로 활용할 수 있다는 점을 발견했기 때문이다.

레스트풀 서버의 주요 특징

  1. 리소스가 핵심 개념이다. 시스템 설계자로서, 외부에서 접근 가능하도록 노출하고 싶은 의미 있는 대상이 무엇인지 결정ㅎ라고, 각각에 구분된 식별자를 부여한다. 일반적으로 각 리소스는 하나의 URI를 가지는데, 각 URI는 반드시 하나의 리소스를 가리켜야 한다는 점이 더욱 중요하며, 이를 통해 외부로 노출한 ‘대상’을 각각 불러낼 수 있어야 한다.

  2. 자술적 메시지를 사용해 무상태로 의사소통 한다는 것. http 요청이 서버가 처리할 때 필요한 모든 정보를 담고 있음. 클라이언트와 서버가 암시적 컨텍스트(세션)를 설정하기 위해 개별 요청에 의존하지 않는다는 점이 중요하다. 이는 다른 요청과는 독립적으로 각각의 리소스를 액세스할 수 있도록 해주며, 이를 바탕으로 대규모 확장성을 달성할 수 있다.
  3. 레스트풀 서버는 하이퍼미디어라는 방법으로 클라이언트가 애플리케이션에서 일어날 수 있는 상태 변경에 맞는 경로를 찾을 수 있도록 해준다. 개별 리소스는 스스로 자립할 수 없으며, 리소스는 서로 연결돼 동작한다. 서버 입장에선 답을 줄 때 연결 정보를 포함시켜야 함을 의미하며, 클라이언트가 연결된 리소스와 상호작용할 수 있도록 해준다.

REST와 DDD를 합치는 법

도메인 모델을 레스트풀 HTTP로 바로 노출하는 것은 좋지 않다. 도메인 모델 내의 변경 하나하나가 시스템 인터페이스로 반영되기 때문에 필요 이상으로 취약한 시스템이 된다.

  1. 시스템의 인터페이스에 별도의 바운디드 컨텍스트를 생성하고, 시스템의 인터페이스 모델에서 실제 핵심 도메인으로 액세스하기 위해 적절한 전략을 사용하는 것이다. 이는 인터페이스를 서비스나 원격 인터페이스가 아닌 단순한 리소스 추상화로 노출된 하나의 응집된 묶음으로 보는 것이다.

    예시로, 레스트풀 리소스의 집합으로서 원격 인터페이스를 제공하는데, 이 리소스는 클라이언트가 요구하는 유스케이스를 반영한다. 이는 순수한 도메인 모델과는 거리가 멀다. 각 리소스는 핵심 도메인 모델에 속한 하나 이상의 애그리게잇으로부터 생성된다.

  2. 리소스 메소드의 매개변수로 도메인 객체를 사용할 수도 있다. 그런데 이때 도메인 객체 구조에 일어나는 모든 변경이 아무리 외부 세계와는 완전히 무관한 변경이었을 뿐이라 해도 즉시 원격 인터페이스에 반영된다. 좋지 않다.

일반적으로 첫 번째 방법이 낫다.

REST 원리에 맞게 설계된 시스템은 일반적으로 느슨한 결합의 조건을 충족한다. 새로운 리소스를 추가해서 기존의 리소스 표현으로 연결하는 일은 매우 쉽다.

커맨드 쿼리 책임 분리

  1. 메소드가 객체의 상태를 수정한다면 이 메소드는 커맨드이며 값을 반환하면 안된다.
  2. 메소드가 값을 반환한다면 이 메소드는 쿼리이며 직접적이든 간접적이든 객체 상태의 수정을 야기해선 안된다.

쿼리 모델의 업데이트는 동기적으로 수행돼야 할까 비동기적으로 수행돼야 할까? 이는 시스템의 일반적 부하와 쿼리 모델 데이터베이스의 저장 위치에 달려 있다. 데이터 일관성 제약과 성능 요구사항이 결정에 영향을 주게 된다.

동기식으로 업데이트하기 위해선 일반적으로 쿼리 모델과 커맨드 모델이 같은 데이터베이스를 공유하며, 도 모델을 같은 트랜잭션으로 업데이트한다. 이를 통해 두 모델 사이에 일관성을 완벽하게 유지한다. 그러나 여러 테이블을 업데이트하려면 더 많은 처리 시간이 필요하고, 이에 따라 서비스 수준 계약을 만족시키지 못할 수도 있다.

일반적으로 시스템이 큰 부하에 걸려 있고 쿼리 모델 업데이트 프로세스가 길다면 비동기적 업데이트를 사용하자.

결국 일관성이 유지되는 쿼리 모델 다루기

쿼리 모델 업데이트가 비동기적으로 일어나는 경우 사용자 인터페이스 내에서 처리해야 하는 이질성이 존재하게 된다. 예를들어 한 사용자가 커맨드를 보냈을 때 다음 사용자 인터페이스 뷰에서도 쿼리 모델을 반영해 완전히 업데이트되고 일관성 있는 데이터를 가질 수 있을까? 최악을 대비하자.

  1. 방금 실행된 커맨드의 매개변수로서 성공적으로 보내진 데이터를 일시적으로 사용자 인터페이스에 보여주도록 설계. 일종의 속임수지만 사용자는 즉시 확인 가능.
  2. cqrs로 사용자가 현재 보고 있는 쿼리 모델의 데이터에 해당하는 시간과 날짜를 항상 사용자 인터페이스에 명시적으로 보여주는 방법. 이러려면 쿼리 모델의 각 기록은 최신 업데이트의 시간과 날짜를 유지해야 한다.

이벤트 주도 아키텍처

파이프와 필터

각 유틸리티(리눅스 커맨드)에서 데이터 집합을 수신하고 처리한 뒤 다른 데이터 집합을 출력한다.

파이프와 필터 프로세스의 특성

  1. 파이프는 메시지 채널이다. 필터는 들어오는 파이프에서 메시지를 수신하고 나가는 파이프로 메시지를 보낸다.
  2. 포트는 필터를 파이프에 연결한다. 필터는 포트를 통해 들어오고 나가는 파이프로 연결된다.
  3. 필터는 처리기다. 필터는 실제 필터링 없이도 메시지를 처리할 수 있다.
  4. 분리된 처리기. 각 필터 ㅊㅓ리기는 별도의 컴포넌트이고, 신중한 설계를 통해 적절한 컴포넌트의 단위성을 달성할 수 있다.
  5. 느슨한 결합. 각 필터 처리기는 다른 모든 것으로부터 독립적인 프로세스로 구성된다. 필터 처리기 컴포지션은 구성으로 정의할 수 있다.
  6. 교환 가능성. 처리기가 메시지를 받는 순서는 유스케이스 요구사항마다 재구성할 수 있으며 이 또한 구성에 따른 컴포지션을 사용한다.
  7. 필터에는 여러 파이프를 연결할 수 있다. 커맨드라인 필터는 읽고 쓰기 동작에 각각 하나의 파이프만 사용하는 반면 메시징 필터는 여러 파이프를 통해 읽거나 쓸 수 있다. 이는 병력적이고 동시적임을 뜻한다.
  8. 병렬 처리에는 같은 타입의 필터를 사용하라. 가장 바쁘고 가장 느릴 가능성이 있는 필터는 처리량의 증가를 위해 여러 개 사용할 수 있다.

cat, grep, wc (type, find 도 가능) 와 같은 유틸리티를 이벤트 주도 아키텍처의 컴포넌트로 생각해볼 수 있다.

장기 프로세스

사가패턴으로도 불림. 태스크들은 병렬로 수행된다.

문제: 컴포넌트 입장에서 두 완료 이벤트가 어떤 병렬 프로세스에 왔는지 구분할 수 없다. 회사의 비즈니스 도메인을 다룰 때라면 잘못 정렬된 장기 프로세스는 재앙이 될 수 있다.

해결을 위해

첫째, 각 도메인 이벤트에 고유 프로세스 식별자 부여. 그 이벤트의 완섣을 추적하기 위해 각 프로세스 실행자의 인스턴스는 애그리게잇과 유사한 새로운 상태 객체를 생성한다. 상태 객체는 프로세스가 시작될 때 생성되고 각각의 연관된 도메인 이벤트가 반드시 실어 옮겨야만 하는 고유 식별자와 연결된다.

둘째, 전체 프로세스의 한 부분을 추적하는 애그리게잇의 구현이야말로 문제를 해결할 수 있는 방법이 될 수 있다. 반드시 있어야 하는 애그리게잇 외에 추가적으로 상태 머신으로 동작하는 별도의 추적자를 개발할 필여가 없어진다.

장기 프로세스를 설계하는 여러 방법

  1. 컴포지트 태스크로 프로세스를 설계하면 영속성 객체를 사용해 태스크의 단계와 완성을 기록하는 실행 컴포넌트가 이 태스크를 추적
  2. 프로세스는 일련의 활동에서 서로 협력하는 파트너 애그리게잇의 집합으로 설계.
  3. 이벤트를 포함하고 있는 메시지를 수신한 메시지 핸들러 컴포넌트가 수신한 이벤트에 태스크 진행 정보를 더 추가해 다음 메시지로 내보내도록 무상태 프로세스 설계

병렬 프로세싱은 도메인 이벤트를 모두 수신할 때까지 완료되지 않았다고 간주된다. 완료된 후에는 병렬 처리의 결과가 하나로 합쳐진다.