DDD - 아키텍처 패턴(계층형 / 포트와 어댑터 / CQRS)
in DEV on DDD, Architecture-pattern, Layered-architecture, Oltp, Cqrs, Olap
지금까지는 전술적 패턴을 통해 비즈니스 로직을 모델링하고 구현하는 다양한 방법에 대해 알아봤다면 이 포스트에서는 좀 더 높은 수준으로 관점을 옮겨서 구성 요소를 구조화하는 3가지 아키텍처 패턴에 대해 알아본다.
즉, 시스템의 구성 요소 간 상호 작용과 의존성을 조율하는 다양한 방법을 살펴본다.
- 계층화된 아키텍처
- 포트와 어댑터
- CQRS
각 아키텍처 패턴의 본질과 각 패턴을 사용해야 하는 사례에 대해 알아본다.
목차
- 1. 비즈니스 로직과 아키텍처 패턴
- 2. 계층형 아키텍처(Layered architecture)
- 3. 포트와 어댑터
- 4. CQRS(Command-Query Responsibility Segregation)
- 5. 범위
- 정리하며..
- 참고 사이트 & 함께 보면 좋은 사이트
1. 비즈니스 로직과 아키텍처 패턴
소프트웨어 시스템은 비즈니스 로직 외에 다른 요소들도 많다.
코드베이스는 기능/비기능 요구사항을 구현하기 위해 많은 책임을 담당한다.
코드베이스가 처리해야 할 다양한 관심사로 인해 비즈니스 로직이 다양한 구성 요소로 흩어지기 쉽다.
관심사를 구현할 때 엄격하게 구성하지 않으면 코드베이스의 변경이 어려워진다.
예) 비즈니스 로직이 변경될 때 코드베이스의 어떤 부분이 영향을 받는지 분명하지 않을 수 있거나, 겉으로 보기에 관련없는 부분에 기대치않게 변경이 영향을 미칠 수도 있음
이러한 문제들은 코드베이스의 유지보수 비용을 크게 증가시킨다.
아키텍처 패턴은 코드베이스의 다양한 측면에 대한 구성 원칙을 도입하여 이들 사이의 명확한 경계를 제시한다.
여기서는 3가지 주요 아키텍처 패턴인 계층형 아키텍처, 포트와 어댑터, CQRS 에 대해 알아본다.
2. 계층형 아키텍처(Layered architecture)
계층형 아키텍처는 코드베이스를 수평 계층으로 조직하고, 각 계층은 사용자와 상호 작용/비즈니스 로직 구현/데이터 저장과 같은 기술적 관심사 중 하나를 다룬다.
Object Storage 는 AWS S3 또는 구글 클라우드 스토리지 등을 말한다.
메시지 버스의 경우 메시비 버스를 내부 용도로 사용하는 것을 말하며, 외부로 노출된다면 그 때는 프레젠테이션 계층에 속한다.
2.1. 프레젠테이션 계층
프레젠테이션 계층은 프로그램 동작을 촉발하는 모든 동기식/비동기식 수단과 같은 범주를 포함한다.
- GUI
- CLI
- 다른 시스템과 연동하는 API
- 메시지 브로커에서 이벤트에 대한 구독
- 나가는 이벤트를 발행하는 메시지 토픽
프레젠테이션 계층은 프로그램의 퍼블릭 인터페이스이다. (= 외부 환경으로부터 요청을 받고 결과를 소통하는 수단)
2.2. 비즈니스 로직 계층
비즈니스 로직 계층은 프로그램의 비즈니스 로직을 구현하고 묶는 것을 담당한다.
액티브 레코드 패턴 또는 도메인 모델 패턴과 같은 비즈니스 로직 패턴을 이 계층에서 구현한다.
2.3. 데이터 접근 계층
데이터 접근 계층은 영속성 메커니즘에 접근할 수 있게 해준다.
데이터 접근 계층은 프로그램의 기능을 구현하는데 필요한 다양한 외부 정보 제공자와 연동하는 것을 포함한다.
예) 언어 변역, 오디오 녹음과 같은 외부 시스템에서 제공되는 API 또는 클라우드 벤더의 관리형 서비스와 연동한다.
2.4. 계층 간 커뮤니케이션
위 그림에서 보이듯이 계층은 top-down 커뮤니케이션 모델에 따라 연동한다.
즉, 각 계층은 바로 아래 계층에만 의존한다.
이렇게 함으로써 구현 관심사의 결합도를 낮추고, 계층 간에 공유할 지식을 줄인다.
2.5. Optional: 서비스 계층
계층형 아키텍처 패턴을 확장해서 서비스 계층을 추가하는 것을 흔히 볼 수 있다.
서비스 계층은 프레젠테이션 계층과 비즈니스 로직 계층 사이의 중간 역할을 한다.
아키텍처 패턴의 컨텍스트에서 서비스 계층은 물리적 서비스가 아닌 논리적 경계이다.
서비스 계층은 비즈니스 로직 계층으로의 관문 역할을 한다.
즉, 하부 계층을 조율하는데 필요한 기능들을 감싸서 퍼블릭 인터페이스 메서드에 상응하는 인터페이스로 노출한다.
interface TestService {
boolean createTest(String tt);
}
프레젠테이션에서는 관련된 구현 상세를 포함하지 않고, 서비스 계층에서 요구하는 입력을 제공하여 결과를 호출자에게 반환하는 것까지만 책임진다.
class TestServiceImpl extends TestService {
boolean createTest(String tt) {
// ...
return true;
}
}
<서비스 계층 도입 시 장점>
- 동일한 서비스 계층을 여러 퍼블릭 인터페이스에서 재사용 가능 (= 중복된 조율 로직 제거)
- 모든 관련 메서드를 한 곳에 모음으로써 모듈화 개선
- 프레젠테이션 계층과 비즈니스 로직 계층의 결합도 낮춤
- 비즈니스 계층을 테스트하기 쉬워짐
서비스 계층이 항상 필요한 것은 아니다.
예를 들어 비즈니스 로직이 트랜잭션 스크립트 패턴으로 구현된 겅우 이미 퍼블릭 인터페이스를 구성하는 일련의 메서드를 노출함으로써 기본적으로 서비스 계층 역할을 한다.
이 때 서비스 계층을 추가해봤자 복잡성을 추상화하거나 감싸는 것이 아니라 단순히 트랜잭션 스크립트의 퍼블릭 인터페이스를 되풀이하기만 할 뿐이므로 비즈니스 로직 계층 하나로 충분하다.
반면, 액티브 레코드 패턴을 사용하는 경우 비즈니스 로직 패턴에서 외부 조율을 해야하는 경우 서비스 계층이 필요하다.
이 때 서비스 계층은 트랜잭션 스크립트 패턴을 구현하고, 이것이 실제로 동작하는 액티브 레코드는 비즈니스 로직 계층에 둔다.
2.6. 계층형 아키텍처를 사용하는 경우
계층형 아키텍처에서 비즈니스 로직과 데이터 접근 계층 간에는 의존성이 있으므로, 비즈니스 로직이 트랜잭션 스크립트나 액티브 레코드 패턴으로 구현된 경우 계층형 아키텍처 패턴이 적합나다.
반면, 도메인 모델을 구현하는데 계층형 아키텍처 패턴을 적용하기는 어렵다.
도메인 모델에서는 비즈니스 엔티티(= 애그리거트와 밸류 엔티티)가 하부의 인프라스트럭처에 대해 의존성이 없어야 하고, 몰라야 하기 때문이다.
2.7. 용어
아래 용어들은 혼용되어 사용되기도 한다.
- 프레젠테이션 계층 = 사용자 인터페이스 계층
- 서비스 계층 = 애플리케이션 계층
- 비즈니스 로직 계층 = 도메인 계층 = 모델 계층
- 데이터 접근 계층 = 인프라스트럭처 계층
2.8. 계층과 티어(Tier)
계층형 아키텍처와 N-티어 아키텍처를 혼동될 때가 많은데 이 둘은 개념적으로 다르다.
계층은 논리적 경계인 반면에, 티어는 물리적 경계이다.
계층형 아키텍처에서 모든 계층은 동일한 수명 주기를 갖는다. (= 단일 단위로 구현되고, 배포됨)
반면 티어는 독립적으로 배포될 수 있는 서비스나 서버이다.
브라우저 - 리버스 프록시 - 애플리케이션 서버 - DB 서버 는 N-티어 시스템이다.
위는 웹 기반 시스템에 관련된 물리적 서비스의 연동을 표현한다. 이 구성 요소들은 동일한 물리 서버에서 컨테이너 등으로 동작하거나, 여러 서버에 분산되어 작동할 수 있지만, 각 구성 요소는 서로 독립적으로 배포되고 관리된다. 이런 것은 계층이 아니라 티어이다.
3. 포트와 어댑터
포트와 어댑터 아키텍처는 계층형 아키텍처보다 좀 더 복잡한 비즈니스 로직을 구현하는데 적합하다.
3.1. 용어
본질적으로 프레젠테이션 계층과 데이터 접근 계층 모두 DB, 외부 서비스, 사용자 인터페이스 프레임워크 등 외부 구성요소와 연동하는 것을 표현하지만 비즈니스 로직을 반영하지는 못하므로 아래처럼 이들을 모두 단일 인프라스트럭처 계층으로 통합한다.
3.2. 의존성 역전 원칙(DIP, Dependency Inversion Principle)
의존성 역전 원칙은 상위 수준 모듈을 하위 수준 모듈에 의존해서는 안된다.
포트와 어댑터 아키텍처에서 비즈니스 로직 계층은 더 이상 어떠한 하위 계층에도 의존하지 않는다.
계층형 아키텍처에서의 서비스 계층처럼, 애플리케이션 계층은 시스템이 노출하고 있는 모든 오퍼레이션을 설명하고, 이를 실행할 때 시스템의 비즈니스 로직을 조율한다.
3.3. 인프라 구성요소의 연동
왜 포트와 어댑터라고 부르는지에 대한 답을 알려면 인프라스트럭처 구성요소가 어떻게 비즈니스 로직과 연동하는지 보아야 한다.
포트와 어댑터 아키텍처의 핵심 목적은 인프라스트럭처 구성요소로부터 비즈니스 로직을 분리하는 것이다.
인프라스트럭처 구성요소를 직접 참조하고 호출하는 대신, 비즈니스 로직 계층은 인프라스트럭처 계층이 구현해야 할 ‘포트’를 정의하고, 인프라스트럭처 계층은 ‘어댑터’를 구현한다. (= 즉, 다양한 기술을 사용하기 위해 정의된 포트의 인터페이스를 구체적으로 구현함)
추상 포트는 인프라스트럭처 계층에서 의존성 주입을 통해 구체적인 어댑터로 나타낸다.
포트와 어댑터 아키텍처는 헥사고날 아키텍처, 어니언(onion) 아키텍처, 클린 아키텍처로 알려졌다.
- 애플리케이션 계층 = 서비스 계층 = 유스케이스 계층
- 비즈니스 로직 계층 = 도메인 계층 = 핵심 게층
3.4. 포트와 어댑터를 사용하는 경우
모든 기술적 관심사로부터 비즈니스 로직을 분리하는 것이 포트와 어댑터 아키텍처의 목적이므로 포트와 어댑터 아키텍처는 도메인 모델 패턴을 사용하여 구현한 비즈니스 로직에 매우 적합하다.
4. CQRS(Command-Query Responsibility Segregation)
CQRS 패턴은 포트와 어댑터와 동일한 비즈니스 로직과 인프라스트럭처 관심사에 기반하지만 시스템의 데이터를 관리하는 방식이 다르다는 점에 차이가 있다.
CQRS 패턴이 왜 필요하고, 어떻게 구현하는지 알아보자.
4.1. 폴리글랏(Polyglot) 영속성 모델링
단일 비즈니스 도메인 모델로 모든 요구사항을 해결하기 어려울 때가 있다.
여러 모델로 작업하는 또 다른 이유는 다양한 언어를 사용하는 영속성 개념과 관련이 있다.
완벽한 DB 는 없기 때문에 그에 대한 대안으로 폴리글랏 영속성 모델을 사용한다.
폴리글랏 영속성 모델은 관련 요구사항을 구현하기 위해 여러 DB 를 사용하는 것이다.
예) 실시간 데이터 처리는 docDB 를 사용, 견고한 검색 기능을 위해 검색 엔진 사용
CQRS 는 이벤트 소싱과 밀접하게 관련이 있다.
원래 CQRS 는 이벤트 소싱 모델의 질의 한계를 극복하기 위해 정의되었다. 즉, 이벤트 소싱 모델은 한 번에 하나의 애그리거트 인스턴스에 대한 이벤트를 질의할 수 있다.
CQRS 패턴은 프로젝션된 모델을 물리적 DB 에 머터리얼라이즈(Materialized) 해서 유연한 질의에 사용할 수 있게 해준다. (= 빈번한 질의의 결과를 물리 테이블에 저장하여 성능을 높이는 메커니즘)
CQRS 와 이벤트 소싱의 추가 설명
이벤트 소싱 모델에서는 애그리거트 상태를 이벤트 로그를 기반으로 재구성해야 하기 때문에 특정 시점의 상태를 빠르게 조회하는 것이 어려움
즉, 이벤트 로그를 하나씩 재생하면서 상태를 만들어야 하므로 실시간으로 복잡한 질의를 수행하는 것은 비효율적임CQRS 패턴을 적용하면 명령(Command) 과 질의(Query) 작업을 분리하여 각각 최적화된 모델을 사용할 수 있음
이를 통해 프로젝션된 데이터 모델을 별도의 DB 나 캐시 시스템에 저장하여 질의를 더욱 유연하고 효율적으로 수행할 수 있음
즉, CQRS 에서는 이벤트를 기반으로 Materialized View(사전 계산된 질의 결과) 를 생성하고 유지하여 질의 성능을 개선함
CQRS 와 이벤트 소싱을 함께 사용하여 활용하는 예시는 아래와 같다.
- Command 모델
- 애그리거트의 상태를 변경하는 이벤트 저장(Event Store 활용)
- 상태를 수정하는 오퍼레이션을 전담으로 수행하는 단일 모델
- 비즈니스 로직을 구현하고, 규칙을 검사하며, 불변성을 강화하는데 사용됨
- 강력한 일관성을 가진 데이터를 표현하는 유일한 모델
- Query 모델(프로젝션)
- 이벤트를 기반으로 여러 프로젝션 테이블을 생성하여 빠른 조회 제공
- 예) Elasticsearch, Redis
- 사용자에게 데이터를 보여죽나, 다른 시스템에 정보를 제공하기 위해 필요하 만큼 모델을 정의할 수 있음
- 캐시에서 언제든 다시 추출할 수 있는 프로젝션으로 DB, 파일, 인메모리 캐시에 위치할 수 있음
- 읽기 전용이므로 어떠한 오퍼레이션도 잉ㄹㄱ기 모델의 데이터를 직접 수정할 수 없음
- Event Handler(이벤트 프로세싱)
- 저장된 이벤트를 소비하여 프로젝션된 DB 를 업데이트
4.2. Query 모델의 프로젝션
Query 모델이 동작하려면 Command 모델에서 변경을 모든 Query 모델로 프로젝션해야 한다.
Query 모델의 프로젝션을 DB 의 Materialized View 의 개념과 유사하다.
프로젝션을 생성하는 방식은 2가지가 있다.
- 동기식 프로젝션
- 비동기식 프로젝션
4.2.1. 동기식 프로젝션: OLTP
동기식 프로젝션은 OLTP(Online Transaction Processing) 데이터의 변경 사항을 트랜잭션 내에서 즉시 반영한다.
즉, 변경이 발생하면 즉시 프로젝션을 업데이트한다. (OLTP 트랜잭션 내에서 동기적으로 처리)
OLTP(Online Transaction Processing) vs OLAP(Online Analytical Processing)
OLTP 데이터는 온라인 트랜잭션 처리 시스템에서 생성되고 관리되는 데이터를 의미하며, OLTP 시스템은 빠른 트랜잭션 처리와 실시간 데이터 업데이트를 목적으로 설계된 DB 임
OLTP 는 은행 계좌 이체, 온라인 쇼핑 결제, 항공권 예약 등 다수의 짧고 빈번한 트랜잭션을 처리하는데 최적화되어 있음
OLTP | OLAP | |
---|---|---|
목적 | 빠른 트랜잭션 처리 | 데이터 분석 및 리포팅 |
데이터 구조 | row 기반 저장(RDBMS) | column 기반 저장(Data Warehouse) |
트랜잭션 유형 | CURD 연산 중심 | 대량의 데이터 분석, 집계 |
성능 최적화 | 빠른 읽기/쓰기 성능 | 복잡한 분석 질의 최적화 |
예시 | 은행 시스템, 전자상거래, 예약 시스템 | 비즈니스 인텔리전스(BI), 데이터 마이닝 |
동기식 프로젝션은 이벤트가 발생할 때 즉시 프로젝션을 업데이트하는 방식이다.
예) 애그리거트 상태가 변경되면 변경 사항을 즉시 Query 모델(프로젝션)에 반영
주로 트랜잭션 내에서 프로젝션을 즉시 업데이트하는 경우가 많으며, 이는 일관성 유지에는 유리하지만 성능에 영향을 줄 수 있다.
4.2.2. 비동기식 프로젝션: 격차 해소 구독 모델
비동기식 프로젝션은 격차 해소 구독 모델(catch-up subscription model) 을 통해 이벤트 로그에서 데이터를 읽어와 변경 사항을 반영한다.
즉, Command 실행 모델이 모든 커밋된 변경 사항을 메시지 버스에 발행하고, 프로젝션 엔진은 발행된 메시지를 구독하여 이벤트 로그에서 변경사항을 가져와 업데이트하며 이 때 격차 해소 구독 모델이 가능하다.
격차 해소 구독 모델은 이벤트 소싱에서 사용되는 비동기적 이벤트 처리 방식 중 하나로, 이벤트 로그에서 이벤트를 읽어와 프로젝션을 업데이트한다.
이벤트가 순차적으로 쌓이기 때문에 구독자가 처음부터 혹은 특정 시점부터 이전 이벤트들을 처리하면서 점진적으로 최신 상태를 따라가는 방식이다.
즉, OLTP 데이터베이스에서 직접 변경 사항을 가져오는 것이 아니라 이벤트 로그를 통해 데이터를 동기화하는 것이다.(= 이벤트 로그 또는 체크포인트 기반으로 변경 사항을 비동기로 읽어와 Query 모델을 갱신하는 방식)
4.3. 도전과제
비동기식 프로젝션 방식의 확장성과 성능의 장점에도 불구하고 분산 컴퓨팅 구조에서는 메시지의 순서가 잘못되거나 중복 처리되여 Query 모델에 일관성 없는 데이터가 프로젝션 될 수 있다.
따라서 가능하면 동기식 프로젝션 방식으로 구현하고, 그 위에 선택적으로 비동기식 프로젝션 방식을 추가하는 것을 권장한다.
4.4. CQRS 를 사용해야 하는 경우
CQRS 패턴은 여러 모델, 궁극적으로는 다양한 종류의 DB 에 저장된 동일한 데이터와 작동할 필요가 있는 애플리케이션에 유용하다.
또한 이벤트 소싱 도메인 모델에도 적합하다.
이벤트 소싱 모델에서는 애그리거트의 상태에 기반한 레코드 조회가 불가능하지만, CQRS 는 상태를 질의할 수 있는 DB 에 상태를 프로젝젼하므로 이것이 가능하다.
5. 범위
계층형 아키텍터, 포트와 어댑터 아키텍처, CQRS 를 시스템 전체에 적용하는 원칙으로 취급하면 안된다.
동일한 타입의 하위 도메인도 다양한 비즈니스 로직과 아키텍처 패턴이 필요할 수 있다.
바운디드 컨텍스트에 단일 아키텍처를 강요하면 의도치않은 복잡성을 유발할 수 있다.
정리하며..
- 계층형 아키텍처
- 기술적 관심사에 따라 코드베이스 분해
- 비즈니스 로직과 데이터 접근 구현을 결합시키므로 액티브 레코드 기반 시스템에 적합함
- 포트와 어댑터 아키텍처
- 관계를 역전시킴
- 비즈니스 로직을 중심에 두고 모든 인프라스트럭처와의 의존성을 분리함
- 도메인 모델 패턴을 구현하는 비즈니스 로직에 적합함
- CQRS 패턴
- 여러 모델에서 동일한 데이터를 표현함
- 이벤트 소싱 도메인 모델에 기반한 시스템에 적합하지만, 다양한 영속 모델을 사용할 필요가 있는 모든 시스템에 사용할 수 있음
참고 사이트 & 함께 보면 좋은 사이트
본 포스트는 블라드 코노노프 저자의 도메인 주도 설계 첫걸음을 기반으로 스터디하며 정리한 내용들입니다.