전체 서비스를 관통하는 도메인 모듈을 안전하게 분리하기
비스가 성장함에 따라 도메인은 함께 비대해집니다. 초기에는 단순했던 기능들이 점점 얽히기 시작하고, 특정 기능 하나를 수정하기 위해 관련 없는 코드까지 들춰봐야 하는 상황이 자주 발생합니다. 팀원 간의 커밋은 빈번히 충돌하고, 사소한 수정에도 예기치 못한 장애가 발생하면서 점점 ‘건드리면 안 되는 코드’가 늘어나게 됩니다. 새로운 기능을 추가할 때마다 전체 구조를 다시 이해해야 하는 일도 반복됩니다.
이러한 문제는 보통 하나의 도메인이 지나치게 많은 책임을 지고 있을 때 발생합니다. 단순히 코드가 복잡해졌다는 수준을 넘어서, 시스템 전체의 복잡도가 임계점을 넘어섰다는 신호이기도 합니다.
그리고 결국 우리는 이 결론에 도달하게 됩니다.
“도메인을 분리해야 하지 않을까?”
WMS플랫폼팀 역시, WMS라는 거대한 하나의 도메인 안에서 유사한 문제를 마주하게 되었고, 이를 해결하기 위해 도메인 분리 작업을 시작하게 되었습니다.
WMS와 재고 도메인
WMS플랫폼팀은 B마트 서비스의 물류 플랫폼 중에서도 WMS(Warehouse Management System)를 담당하고 있습니다.
WMS는 창고의 상태를 가시화하고, 무엇이, 어디에, 얼마나 있는지를 신뢰할 수 있는 데이터로 관리하는 시스템입니다. 이 목적을 달성하기 위해 다양한 기능과 프로세스를 제공하며, 이러한 기능들은 다음의 세 가지 주요 도메인으로 나눌 수 있습니다.
- 입고
외부에서 창고로 물류가 유입되는 전 과정을 관리하는 도메인입니다.
- 입고는 창고 내 재고가 새롭게 생성되는 과정을 담당합니다.
- 일반적으로는 발주를 기반으로 공급업체로부터 물품이 입고되며, 입고 검수 및 적치까지 포함됩니다.
- 재고
창고 내에 현재 보관 중인 물품의 상태와 수량을 관리하는 도메인입니다.
- 상품별 수량, 위치, 유효기간, 상태(정상/불량/보류 등) 등의 속성 정보를 관리합니다.
- 다른 모든 도메인(입고, 출고, 반품 등)과의 상호작용이 가장 많은 핵심 도메인입니다.
- 출고
창고에서 외부로 물류가 이탈하는 전 과정을 관리하는 도메인입니다.
- 출고는 재고를 감소시키는 트리거 역할을 합니다.
- 고객 주문, 다른 센터 이동, 반품 출고 등 다양한 형태를 가지며 피킹, 패킹, 출하 등 물리적 이동의 준비 과정까지 포함됩니다.
기존에는 위의 3개의 도메인을 WMS 라는 하나의 도메인으로 패키지조차 분리하지 않고 운영하고 있었습니다.
이 상황에서 재고 정확도 향상을 위한 고도화 과제가 생겼습니다.
재고 도메인은 다른 도메인들과 서로 강하게 연결되어 있어, 단순한 수정만으로도 연관된 코드들의 수정 범위가 넓어지고, 사이드 이펙트 발생 가능성이 매우 높았습니다. 또한 변경사항으로 인한 영향 범위 분석과 QA에 필요한 리소스 또한 무시할 수 없었습니다.
이러한 상황 속에서 재고 도메인의 고도화를 안전하게 수행하기 위해서는, 먼저 도메인 분리를 수행하는 것이 더 효과적이라는 판단을 내리게 되었습니다.
이 글에서는 재고 도메인을 분리하고 헥사고널 아키텍처를 적용하는 과정과, 개선 내용을 안전하게 배포한 경험을 소개합니다.
멈추지 않는 WMS에서 재고 도메인 분리하기
기존의 WMS는 각자 서비스를 제공하는 api, consumer, batch 모듈이 하나의 도메인 모듈에 의존하고 있었습니다. 이 서비스들은 멈출 수 없고, 새 기능들을 계속 추가해야 하는 상황에서 도메인 분리를 시작했습니다.

Step1. 재고 도메인 모듈 만들기
먼저, 신규 모듈(domain-inventory)을 생성하여 재고 도메인에 해당하는 클래스들을 모았습니다. 이 과정에서 다른 도메인과의 결합은 최대한 제거했습니다.
1-1 응집도 올리기
가장 먼저 뿔뿔이 흩어진 재고 도메인을 모으는 작업을 진행했습니다.
재고 도메인에 해당하는 클래스를 모으려면 무엇이 재고 도메인에 해당하는 것인가?를 정의해야 했습니다.
단순히 재고 정보에 해당하는 도메인뿐만 아니라, 재고 이력, 재고 실사 등 재고와 관련된 도메인들을 포함하도록 했습니다.
관련 엔터티를 정의하고 기본 리포지토리와 서비스들을 신규 재고 모듈 하위로 이동시켰습니다.
아직은 기존 모듈(domain-wms)에서는 신규 재고 모듈(domain-inventory)을 의존하고 있는 상태입니다.

1-2 결합을 끊어야 하는 대상을 특정하기
기존 모듈과 신규 재고 모듈의 의존성을 제거하기 위해서 재고 도메인과 결합된 입출고 도메인의 로직을 파악해야 했습니다.
가장 빠른 방법으로 기존 모듈에서 신규 재고 모듈의 의존성을 제거하고 빌드가 실패하는 파일들을 별도 모듈(service-wms)로 옮겼습니다. 이후에는 이 모듈을 결합 모듈이라고 부르겠습니다.
이 결합 모듈에 다음과 같은 클래스를 이동했습니다.
- 입고/출고 도메인과 재고 도메인 사이에 연관관계를 선언한 엔터티
- 입고/출고 도메인과 재고 도메인을 직접 join한 QueryDSL 리포지토리
- 위의 엔터티 혹은 리포지토리를 사용한 서비스
- 입고/출고 도메인과 재고 도메인의 서비스를 조합하여 사용하는 서비스
또, 이 과정에서 도메인에서 공통으로 사용하는 마스터 정보를 모아두는 모듈(domain-master)이 추가되었습니다.

1-3 결합도 낮추기 1 – 엔터티, 리포지토리의 결합 제거
목표는 결합 모듈의 도메인 간 결합 로직을 모두 수정하여 재고 도메인은 신규 재고 모듈로 입고/출고 도메인의 로직은 기존 모듈로 보내는 것이었습니다.
먼저 위에서 언급한 4가지 유형의 클래스 중 입고/출고 도메인과 재고 도메인의 서비스를 조합하여 사용하는 서비스을 제외한 엔터티와 리포지토리 관련하여 아래와 같은 작업을 수행하여 의존이 필요한 최소한의 서비스 파일만 남겼습니다.
- 입고/출고 도메인과 재고 도메인 사이에 연관관계를 선언한 엔터티
- 연관관계 제거
- 입고/출고 도메인과 재고 도메인을 직접 join한 QueryDSL 리포지토리
- join 절을 제거하고 서비스 레이어에서 조합
- 위의 엔터티나 리포지토리를 사용한 서비스
- 위 수정 사항으로 이동 가능한 서비스는 각 도메인에 맞게 이동
- 타 도메인의 서비스와 결합된 메서드를 제외하고는 별도 파일로 분리하여 이동

1-4 결합도 낮추기 2 – 서비스 결합 제거
입고/출고 도메인과 재고 도메인의 서비스를 조합하여 사용하는 서비스의 결합은 쉽게 분리할 수 없었습니다. 예를 들어, 입고 작업이 완료되면 입고 작업의 상태를 변경함과 동시에 재고를 생성하거나 증가시켜야 합니다. 이처럼 입고 작업의 상태를 변경하는 입고 도메인 서비스와 재고를 생성하는 재고 도메인 서비스를 함께 조합하는 오케스트레이션 서비스가 필요했습니다.
이런 오케스트레이션 로직을 그대로 하나의 결합 모듈에 모아둘 수도 있었습니다. 하지만 모든 도메인에 접근할 수 있는 이 모듈은 금세 비대해졌고, 결국 다시 하나의 거대한 모놀리스로 되돌아갈 수 있다는 우려가 생겼습니다.
실제로 과거에도 유사한 구조가 점점 복잡해지며 관리가 어려워졌던 경험이 있었기 때문에, 이번에는 같은 실수를 반복하지 않기로 했습니다.
이에 결합 모듈의 완전한 제거를 목표로 삼고, 모듈 간 통신 방식으로 헥사고널 아키텍처를 도입했습니다. port-adapter 패턴(비즈니스 로직을 중심에 두고, 그 바깥에 입출력 어댑터를 붙여서 언제든 기술 스택을 갈아끼울 수 있게 해주는 구조)을 적용하여, 모듈 간에는 접근 가능한 인터페이스(port)만을 정의하고, 실제 구현(adapter)은 각 도메인 내부에 두는 방식으로 구성했습니다.


