Easy Understanding

DDD를 적용한 간단한 결제 컨텍스트 개발해보기(with CQRS) 본문

Study

DDD를 적용한 간단한 결제 컨텍스트 개발해보기(with CQRS)

appleg1226 2022. 4. 3. 21:31

DDD 학습을 하다보면

전략적 패턴(도메인을 도출하고 정의하는 과정)에는 어느 정도 일관된 결론이 있는 것을 확인할 수 있다.

각종 DDD 책이나 블로그 들에서 해당 과정은 타협할 수 없는 과정으로 여겨지고 있고,

실제로 어떤 문서를 읽어도 내용들이 크게 다르지는 않다.

 

하지만 전술적 패턴(실제로 코드를 작성하고 아키텍처를 결정하는 과정)에 대해서는 딱히 일관성이 없다.

왜냐하면 DDD에서는 코드를 작성하는 방법에 대해서 정답이 없다고 애초에 선언되었기 때문이다.

실제 DDD 예제 코드들을 깃헙에서 확인해보니, 정말 제각각의 다양한 형식의 코드 스타일들이 존재했다.

 

반 버논의 iDDD 책에서 제공하는 DDD에 대한 방법론은 많은 인사이트를 제공해주었지만,

실제로 고민해보고 코딩해보는 과정이 필요하다고 생각하여

가볍게 하나의 컨텍스트를 결정하기로 했고,

추후에 내가 DDD에 대해서 고민했던 것들을 추적할 수 있도록 프로젝트를 하나 진행해보기로 결심했다. 

 

우선 해당 프로젝트가 이루어진 github 주소는 다음과 같다. 

https://github.com/appleg1226/ddd-payment

 

GitHub - appleg1226/ddd-payment: example project for ddd

example project for ddd. Contribute to appleg1226/ddd-payment development by creating an account on GitHub.

github.com

 


 

도메인 결정 과정

해당 프로젝트는 전략적 패턴(도메인 도출)에 대한 고민은 좀 줄이고,

아키텍처에 대한 고민과 DDD를 스프링에서 어떻게 사용할 수 있을까에 대한 고민을 좀 더 해 보았다.

 

그래서 컨텍스트는 나에게 조금이나마 익숙한 '결제' 컨텍스트로 잡았으며

간단하게 다음의 시나리오가 들어갈 수 있도록 도메인을 구상했다.

 

유저는 특정 PG가 제공하는 결제창에서 결제를 진행하며, 이를 통해서 서비스가 제공하는 전용 재화(캐시)를 얻을 수 있다.

1. 결제: 결제창에서의 모든 과정이 완료되면, 결제 시스템에 결제가 완료되었다는 데이터를 본 시스템의 api를 호출하여 전달해주며, 이때 유저는 캐시를 획득한다.

2. 구매: 캐시를 사용하여 특정 아이템을 구매할 수 있게 된다. 아마 이러한 api는 메인 서비스 컨텍스트에서 결제 컨텍스트에 호출하는 식이 될 것이다. 이때 유저의 캐시가 차감된다. 

 

그리고 유저는 서비스의 특정 창을 통해서 다음의 내용을 조회할 수 있게 된다.

1. 본인의 결제 내역: 결제를 통해서 캐시가 증가된 내역을 확인할 수 있다.

2. 본인의 구매 내역: 구매를 통해서 캐시가 차감된 내역을 확인할 수 있다. 

 


Strategic 설계

다음으로는 도메인을 정의하기 위해서 몇 가지 유비쿼터스 언어들을 모아 보았다.

- 유저: 캐시를 결제하고 구매하는 주체. id 외에는 큰 정보가 필요 없다.

- 결제: 결제시스템에서 결제하여 캐시가 증가된 기록

- 캐시: 실제로 얻은 캐시 데이터

- 구매: 구매로 인해 캐시가 차감된 기록

- 아이템: 구매에서 캐시를 사용할 때 구매한 컨텐츠에 대한 정보. 결제에서는 id만 필요하다. 

 

이제 위의 것들을 조합해서 애그리거트와 엔터티, 값 객체를 도출하면 된다. 

 

우선 결제 컨텍스트에서 100% 필요하지 않은 다음의 도메인 두 가지에 대해서 생각해 보았다.

'유저'와 '아이템' 인데 해당 도메인들은 다른 컨텍스트에서 관리되는 도메인들이다.

유저 도메인은 따로 컨텍스트 하나로 관리될 것이고, 아이템 도메인은 메인 서비스 컨텍스트에서 관리될 것이다.

 

'유저'는 id만 필요하지만, 유저의 잔액 정보 등 같이 관리할 데이터가 많아 하나의 애그리거트로 선언하기로 결정했다.

다만 원래대로라면 컨텍스트에 맞게 PaymentUser 같은 컨텍스트에 맞춘 이름으로 진행해야 하지만,

이번에는 그냥 User로 진행했다.

 

'아이템'은 과연 이 컨텍스트에 필요할지가 의문이 들었다.

아이템 구매에 대한 기록은 다른 컨텍스트에서 관리하는 것이 맞으며,

결제 시스템에서는 캐시 차감에 대한 기록만 관리하는 것이 맞지 않나하는 생각이 들었다.

그래서 아이템은 해당 컨텍스트에서는 제외하기로 했다.

만약에 사용하려면 구매 도메인 내부에 하나의 값 객체로 설정하면 좋지 않을까 생각은 든다.

 

다음으로 '결제'와 '구매' 컨텍스트는 확실하게 애그리거트가 되어야 한다고 생각한다.

캐시가 증가하고 차감되는 두 행위 자체는 결제의 핵심 도메인이라고 할 수 있기 때문에 무조건 애그리거트가 맞다고 결론을 내렸다.

 

마지막으로 '캐시'는 좀 고민을 했다. 

캐시를 관리하는 방법은 크게 두 가지가 있다.

(1) 유저 애그리거트 내부에서 amount(금액) 감싸는 값 객체로 선언해 필드로 관리하는 방법

(2) 유저 애그리거트 내부의 엔터티로 지정하여 데이터를 쌓는 방법

실무라면 무조건 후자로서 각 캐시에 대한 이력을 남기고 사용해야 하는 것이 맞지만,

이번 프로젝트에서는 도메인 로직에 대한 세세한 디테일은 포기하기로 했기 때문에 1번 방법으로 결정했다.

 


Tactical 설계

1. 개발 스펙

이번 프로젝트에서는 스프링에 DDD를 적용해보고, 이를 기록으로 남기고자 한 목적이 크다.

또한 CQRS를 연습해보기 위해서 메시지 큐를 둬야했고, 이를 위해서 카프카도 도입했다.

정리하자면 아래의 간단한 스펙으로 개발했다.

스프링부트(3.0.0 M2) + JPA(Mysql) + Kafka 

 

2. 셋팅

docker-compose.yml 파일을 작성하여 mysql 2개(command, query), kafka, zookeeper 를 띄웠으며,

gradle로 command, query 모듈을 나눠 놓았다.

 

3. 패키지 구조

iDDD의 내용을 참고하고, 헥사고날 패턴과 관련된 예제들을 참고하고 고민하면서 나만의 패키지 구조를 생각해 보았다.

https://vaadin.com/learn/tutorials/ddd/ddd_and_hexagonal

 

우선 가장 크게는 Bounded Context 위주로 패키지를 나눴다. 

 

또한 Payment의 내부 패키지를 열어보면 다음과 같이 구성했다.

 

큰 범주로는 아래의 세 가지로 패키지를 구성했다.

- application

각종 Use-case를 담당하는 Application Service가 여기에 포함된다. 

 

- domain

도메인 엔터티, 값 객체, 이벤트, 리파지토리, 도메인 서비스 등이 여기에 포함된다.

 

- infra  

http(controller), persistence(mysql)가 여기에 포함되었으나 다른 것들도 추가될 수가 있다.

참고로 이 프로젝트에서는 메시지 관련해서는 common에서 한번에 관리한다. 

 

조금 독특한 점이라면 다른 예제들에서는 PaymentController가 어플리케이션 레이어에 있는데 여기에는 인프라에 있다는 점이다.

헥사고날 아키텍처 때문인데, Controller가 adaptor로서 infra의 영역이 맞다고 생각했기 때문이다.

 

inbound인 http의 경우 adapter가 컨트롤러이고, port는 서비스 인터페이스에 해당한다.

나는 Controller가 http 요청을 라우팅한다는 기능만 한다는 점에서 infra에 해당한다고 결론을 내렸으며, 

이 때문에 controller에서는 http와 관련된 로직외에 비즈니스/어플리케이션 로직을 처리하면 안 된다고 결정했다.

 

https://vaadin.com/learn/tutorials/ddd/ddd_and_hexagonal

 

PaymentRepository, PaymentJpaRepository, PaymentRepositoryImpl 

 

이 세 가지를 분리한 것에 대한 설명은 이전에 다뤄놓았으므로 해당 내용을 참고하면 된다.(https://appleg1226.tistory.com/45)

 

(5) DDD와 아키텍처 - Layered, Hexagonal, CQRS, Event-Sourcing

DDD의 가장 큰 장점 중 하나는 특정 아키텍처의 사용을 요구하지 않는다는 점이라고 한다. 하지만 책을 읽고 공부를 하다보면, 사실상 DDD에서 선택할 수 있는 괜찮은 아키텍처는 결국은 몇개 안

appleg1226.tistory.com

4. CQRS

결제 과정이 CQRS로 이루어지는 과정을 그림으로 정리해 보았다. 

위의 순서대로 코드를 작성하기만 되어서 별다른 어려움은 없었다.

하지만 일반적인 API보다 많은 노력이 들어가기 때문에 프로젝트 틀을 잡는 것이 오래 걸렸다.

하지만 한 번 정리해놓으면 해당 구조를 따라서 이후에 빠른 속도로 만들 수 있을 것 같다.

 

구현 난이도는 Command가 조금 복잡한 편이다.

이벤트를 언제 발행할 건지도 신경써야 하고,

식별자(아이디)에 대한 생성 시점도 스스로 결정해야 한다.

또한 CUD만 처리하기 때문에 비즈니스 도메인에 대한 복잡한 로직들은 다 여기에서 얽히고 섥힌다. 

이렇게 복잡한 일이 일어나는데 조회는 빠져서그나마 다행이다.

 

반면 Query 모델의 개발난이도는 Command 보다는 쉬우며, 웬만하면 쉽게 가는 것을 지향해야 한다.

내가 작성하면서 몇 가지 아키텍처의 원칙을 깨도 된다고 생각한 포인트가 있다.

 

(1) Repository를 Controller에 직접 주입

어차피 Repository 내부에서 대부분의 로직이 이루어진다. 쿼리모델에 비즈니스 로직은 존재하면 안되므로 이렇게 Repository가 대부분의 역할을 하는 것이 맞다. 대부분의 경우 Service의 역할을 여기에서 하게 될 것이다. 만약 복잡한 상황이 발생해서 Service가 필요하다면 그때 만들면 된다. 

 

(2) Response용 DTO를 따로 작성하지 않기

애초에 QueryDB는 Response를 염두하고 스키마가 만들어져야 한다. DTO를 도입했다면 사용했을 이름 그대로 동일하게 저장하는 것이 맞다.  이건 좀 더 고민해보는 걸로...

 


DDD는 정해진 패턴이 없다는 점에서 참 무궁무진한 방법론인 것 같다.

전반적으로 DDD에 대한 패턴들은 나름 핫하고 세련된 패턴들의 적용 사례가 많기는 하다.

CQRS는 물론이고 이 프로젝트에서는 다루지 않았지만 Event-Sourcing과도 함께 다뤄지기도 한다.

 

에릭 에반스의 DDD 서적도 한 번 제대로 읽어보고,

여러 번의 고민이 쌓여야 조금 더 견고하게 DDD를 사용할 수 있을 것 같다.

 

이번 프로젝트 이후에도 조금 더 보완할 점들이 있으면 commit을 더해볼 예정이다.