🛠️ 도메인을 지키는 설계
이번 블로그에서는 상반기 동안 진행한 WMS 프로젝트에서 마주했던 문제들과 그 해결 방법을 공유하고자 합니다.
제가 정의하고 해결한 문제는 총 4가지인데, 4가지의 특징을 한데 모아보니 제가 공통적으로 고민했던 것은
“어떻게 하면 도메인을 지키는 설계를 할 수 있을까?” 였습니다.
여기서 말하는 도메인이란 비즈니스 로직을 의미하며 외부 인프라로 부터 독립된 소프트웨어가
해결해야 할 특정 비즈니스 문제나 상황을 의미합니다.
저는 이번 블로그에서 제가 도메인을 지키기 위해 어떤 고민과 노력을 했는지 공유 해보려고 합니다.
저는 올 상반기에 WMS 프로젝트를 진행하면서, 여러 가지 문제를 직접 경험했습니다.
WMS는 셀메이트, 포스, 그리고 대한통운과 같은 외부 시스템과 통신이 굉장히 많았습니다.
그러다 보니 복잡한 트랜잭션 처리와 외부 시스템 연동이 비즈니스 로직과 얽히는 경우가 많았습니다.
이렇게 비즈니스 로직이 외부 시스템에 의존한 결과,
문제가 발생했을 때 원인을 파악하기가 어려워졌고,
테스트도 어렵고, 결국 시스템 유지보수 자체가 힘들어졌습니다.
실제 장애 사례를 보면서 “도메인을 보호하는 설계가 왜 중요한지” 보겠습니다.
WMS에서 창고 내 재고 작업의 마지막 단계는 출고 처리입니다.
이때 WMS는 재고를 차감하면서 출고를 진행하고,
출고된 내역은 셀메이트 시스템에도 반영합니다.
그런데 당시에는 잘못된 트랜잭션 처리, 외부 서비스 호출에 의존적인 코드 설계, 게다가 로깅조차 없어서,
출고는 정상 처리됐지만,
셀메이트에는 출고 내역이 반영되지 않는 문제가 발생했습니다.
결국 창고에서는 재고를 다시 파악해야 했고,
이 과정에서 천만 원이 넘는 재고 조사 비용이 발생했습니다.
뿐만 아니라 데이터 보정과 원인 파악을 위해
수많은 시간과 인력이 투입됐습니다.
이 사건을 계기로 저희 팀은
단순히 성능 문제가 아니라
잘못된 트랜잭션 처리와 외부 서비스 의존적 설계가 데이터의 정합성을 위협할 수 있다는 걸 절실히 깨달았습니다.
그래서 오늘 발표에서는
도메인을 지키기 위한 핵심 설계 원칙 3가지를 공유하고,
실제 경험했던 4가지 문제 상황과 해결 전략을 소개하려고 합니다.
✅ 도메인을 지키는 핵심 원칙
- 핵심 도메인의 독립성 확보
- 외부 호출과 트랜잭션 처리의 분리
- 외부 시스템의 구현 방식 추상화
이 원칙들을 토대로, 본격적인 사례 소개를 시작하겠습니다.
🧩 첫 번째 사례: 반드시 동기적으로 수행해야 하는 외부 호출이 필요한 경우
WMS에서는 출고 지시서를 만들고,
그에 맞춰 재고를 피킹하는 비즈니스 로직이 있습니다.
이때, 출고 지시서를 만들기 전에
셀메이트에 주문이 취소됐는지 확인해야 하는 요구사항이 있었습니다.
그런데 기존에는 지시서 생성과 외부 호출을 한 트랜잭션 안에서 처리하면서
락 타임이 길어지고, 동시성 문제가 발생했습니다.
만약 외부 호출이 단순 조회가 아니라 수정이나 삭제였다면,
실패했을 때 롤백이 불가능해져 서비스 간 데이터 정합성도 무너질 수는 위험한 결과를 초래할 수 있었습니다.
그래서 반드시 동기적으로 수행해야 하는 외부 호출이 필요하다면,
트랜잭션을 시작하기 전에 모든 외부 호출을 완료하는 것이 안전합니다.
- 외부 서비스 호출 실패 → DB 작업에 영향 없음
- DB 작업 실패 → 외부 서비스 롤백 불필요
- 네트워크 통신 시간 제거 → 성능 개선
🧩 두 번째 사례: 외부 서비스 호출이 비동기로 수행 가능한 경우
WMS에서는 송장 채번을 위해 택배사 시스템과 연동을 합니다.
송장 채번은 비동기로 처리되도록 의사결정된 상황이었습니다.
그러나 기존에는 송장 채번을 지시서 생성과 같은 트랜잭션 안에서 처리했기 때문에,
외부 호출의 실패가 도메인 로직의 실패로 전이되는 문제가 있었습니다.
그래서 비동기로 처리 가능한 비즈니스 로직은
트랜잭션이 종료된 이후에 수행하게 해야 합니다.
- 외부 연동 실패 → 도메인 로직 영향 없음
- 재시도 로직 구현 용이
- 테스트 코드 작성 쉬움
- 비즈니스 로직 보호 가능
🧩 세 번째 사례: 다중 서비스 클래스 호출 시 트랜잭션 관리 전략
일반적인 백엔드 프레임워크들은 레이어드 아키텍처를 기반으로 구현됩니다.
도메인 레이어의 서비스 클래스들은 실제 비즈니스 로직 구현 책임을 갖습니다.
이 예에서는 출고지시서, 피킹지시서 등을
각각 shipping service
, picking service
클래스에서 구현하여
각 메서드 안에서 트랜잭션을 관리했습니다.
각 지시서 중 하나라도 실패하면 전체를 롤백하도록 의사결정이 되었지만,
현재 구조에서는 각 서비스가 개별적으로 트랜잭션을 관리하고 있어 문제가 발생했습니다.
💡 해결: DDD 기반 Application Service / Domain Service 분리
DDD(Domain Driven Design):
비즈니스 규칙을 최우선으로 고려하는 설계 방법론
- Application Service: 프로세스 조율 (도메인 직접 구현 ❌)
- Domain Service: 실제 비즈니스 규칙 구현 책임
트랜잭션은 Application Service가 관리하고,
Domain Service는 도메인 작업만 수행하도록 분리했습니다.
- 명확한 책임 분리
- 도메인 로직과 트랜잭션 관리 분리
- 외부 호출을 분리된 어댑터에서 처리 → 도메인 보호
🧩 네 번째 사례: 비동기 처리 구현 방식의 추상화
출고 연동 장애 원인:
잘못된 트랜잭션 처리와 외부 연동에 의존적인 설계
→ DDD 설계 원칙에 따라
셀메이트 출고 연동 서비스를 분리, Application Service에서 호출하도록 리팩토링
그러나…
- 현재 구현: HTTP 기반
- 추후 예정: RabbitMQ 기반의 이벤트 통신
- Application Service는 어떻게 처리되는지 몰라야 한다
🔧 해결: 헥사고날 아키텍처
- 내부와 외부를 명확히 구분
- 의존성은 내부로만 향함
- 핵심 비즈니스 로직 보호
구성요소: 도메인, 포트(인터페이스), 어댑터(구현체)
- 포트 인터페이스 정의
- Application Service에 의존성 주입
- 어댑터 클래스 (HTTP, RabbitMQ 등) 구현
- DI 설정으로 어댑터 선택 가능
결과: 외부 시스템 연동 방식이 바뀌어도 비즈니스 로직은 건드리지 않음
🎤 마무리
이번 블로그에서는 도메인을 지키는 설계를 주제로
4가지 문제와 그 해결 전략을 공유했습니다.
제가 셀메이트 개발팀에서 일하면서 가장 힘들었던 점은,
수많은 외부 서비스와 연동하며 개발을 해야 했지만,
비즈니스 로직이 외부 서비스에 지나치게 의존하는 구조 탓에 CS 업무의 부담이 너무 컸다는 점입니다.
매번 문제가 생길 때마다
고객응대, 재고 조사, 데이터 보정에 수많은 시간을 소모해야 했습니다.
앞으로 비즈니스 요구사항은 더 복잡해지고,
외부 연동도 점점 더 많아질 것입니다.
우리의 삶의 질을 지키기 위해서라도
반드시 도메인을 지키는 설계를 실천해야 합니다.
이 블로그가 앞으로 셀메이트의 CS 업무 강도를 줄이는 계기가 되기를 진심으로 바라며,
마지막으로 기술 구현보다 중요한 것은 도메인을 지켜내는 설계라는 점을 강조드리며 마무리하겠습니다.