🛠️ 도메인을 지키는 설계

이번 블로그에서는 상반기 동안 진행한 WMS 프로젝트에서 마주했던 문제들과 그 해결 방법을 공유하고자 합니다.
제가 정의하고 해결한 문제는 총 4가지인데, 4가지의 특징을 한데 모아보니 제가 공통적으로 고민했던 것은
“어떻게 하면 도메인을 지키는 설계를 할 수 있을까?” 였습니다.

여기서 말하는 도메인이란 비즈니스 로직을 의미하며 외부 인프라로 부터 독립된 소프트웨어가
해결해야 할 특정 비즈니스 문제나 상황을 의미합니다.

저는 이번 블로그에서 제가 도메인을 지키기 위해 어떤 고민과 노력을 했는지 공유 해보려고 합니다.

셀메이트컨퍼런스발표자료


저는 올 상반기에 WMS 프로젝트를 진행하면서, 여러 가지 문제를 직접 경험했습니다.
WMS는 셀메이트, 포스, 그리고 대한통운과 같은 외부 시스템과 통신이 굉장히 많았습니다.
그러다 보니 복잡한 트랜잭션 처리와 외부 시스템 연동이 비즈니스 로직과 얽히는 경우가 많았습니다.
이렇게 비즈니스 로직이 외부 시스템에 의존한 결과,
문제가 발생했을 때 원인을 파악하기가 어려워졌고,
테스트도 어렵고, 결국 시스템 유지보수 자체가 힘들어졌습니다.

실제 장애 사례를 보면서 “도메인을 보호하는 설계가 왜 중요한지” 보겠습니다.

셀메이트컨퍼런스발표자료


WMS에서 창고 내 재고 작업의 마지막 단계는 출고 처리입니다.
이때 WMS는 재고를 차감하면서 출고를 진행하고,
출고된 내역은 셀메이트 시스템에도 반영합니다.
그런데 당시에는 잘못된 트랜잭션 처리, 외부 서비스 호출에 의존적인 코드 설계, 게다가 로깅조차 없어서,
출고는 정상 처리됐지만,
셀메이트에는 출고 내역이 반영되지 않는 문제가 발생했습니다.

셀메이트컨퍼런스발표자료


결국 창고에서는 재고를 다시 파악해야 했고,
이 과정에서 천만 원이 넘는 재고 조사 비용이 발생했습니다.
뿐만 아니라 데이터 보정과 원인 파악을 위해
수많은 시간과 인력이 투입됐습니다.

이 사건을 계기로 저희 팀은
단순히 성능 문제가 아니라
잘못된 트랜잭션 처리와 외부 서비스 의존적 설계가 데이터의 정합성을 위협할 수 있다는 걸 절실히 깨달았습니다.

셀메이트컨퍼런스발표자료


그래서 오늘 발표에서는
도메인을 지키기 위한 핵심 설계 원칙 3가지를 공유하고,
실제 경험했던 4가지 문제 상황과 해결 전략을 소개하려고 합니다.

✅ 도메인을 지키는 핵심 원칙

  1. 핵심 도메인의 독립성 확보
  2. 외부 호출과 트랜잭션 처리의 분리
  3. 외부 시스템의 구현 방식 추상화

이 원칙들을 토대로, 본격적인 사례 소개를 시작하겠습니다.


🧩 첫 번째 사례: 반드시 동기적으로 수행해야 하는 외부 호출이 필요한 경우

셀메이트컨퍼런스발표자료

WMS에서는 출고 지시서를 만들고,
그에 맞춰 재고를 피킹하는 비즈니스 로직이 있습니다.
이때, 출고 지시서를 만들기 전에
셀메이트에 주문이 취소됐는지 확인해야 하는 요구사항이 있었습니다.

그런데 기존에는 지시서 생성과 외부 호출을 한 트랜잭션 안에서 처리하면서
락 타임이 길어지고, 동시성 문제가 발생했습니다.

셀메이트컨퍼런스발표자료

만약 외부 호출이 단순 조회가 아니라 수정이나 삭제였다면,
실패했을 때 롤백이 불가능해져 서비스 간 데이터 정합성도 무너질 수는 위험한 결과를 초래할 수 있었습니다.

셀메이트컨퍼런스발표자료

그래서 반드시 동기적으로 수행해야 하는 외부 호출이 필요하다면,
트랜잭션을 시작하기 전에 모든 외부 호출을 완료하는 것이 안전합니다.

  • 외부 서비스 호출 실패 → DB 작업에 영향 없음
  • DB 작업 실패 → 외부 서비스 롤백 불필요
  • 네트워크 통신 시간 제거 → 성능 개선

외부 서비스 호출이 실패하는 경우에 데이터베이스 작업에 영향을 주지 않을 수 있고 반대로 데이터 베이스 작업이 실패했을 경우 외부 서비스의 롤백을 할 필요가 없는 안전한 구조로 서비스간 데이터 정합성을 안전하게 보호할 수 있습니다.

또한 네트워크 통신 시간을 락타임에서 제외할 수 있어 성능이 개선되는 효과까지 볼 수 있습니다.


🧩 두 번째 사례: 외부 서비스 호출이 비동기로 수행 가능한 경우

셀메이트컨퍼런스발표자료

WMS에서는 송장 채번을 위해 택배사 시스템과 연동을 합니다.
송장 채번은 비동기로 처리되도록 의사결정된 상황이었습니다.

그러나 기존에는 송장 채번을 지시서 생성과 같은 트랜잭션 안에서 처리했기 때문에,
외부 호출의 실패가 도메인 로직의 실패로 전이되는 문제가 있었습니다.

셀메이트컨퍼런스발표자료

그래서 비동기로 처리 가능한 비즈니스 로직트랜잭션이 종료된 이후에 수행하게 해야 함으로써 외부 연동 실패가 도메인 로직의 실패로 전이 되지 않도록 보호하는 것이 중요합니다.

  • 외부 연동 실패 → 도메인 로직 영향 없음
  • 재시도 로직 구현 용이
  • 테스트 코드 작성 쉬움
  • 비즈니스 로직 보호 가능

이 구조의 장점은 만약 송장 채번같은 외부 서비스 호출이 실패했을 때 지시서의 생성과 별개로 재시도 로직을 구현하기 용이하며, 테스트 코드 작성이 쉽고 무엇보다 비즈니스 로직을 안전하게 보호할 수 있습니다.


🧩 세 번째 사례: 다중 서비스 클래스 호출 시 트랜잭션 관리 전략

셀메이트컨퍼런스발표자료

일반적인 백엔드 프레임워크들은 레이어드 아키텍처를 기반으로 구현됩니다.
도메인 레이어의 서비스 클래스들은 실제 비즈니스 로직 구현 책임을 갖습니다.

이 예에서는 출고지시서, 피킹지시서 등을
각각 shipping service, picking service 클래스에서 구현하여
각 메서드 안에서 트랜잭션을 관리했습니다.

셀메이트컨퍼런스발표자료

각 지시서 중 하나라도 실패하면 전체를 롤백하도록 의사결정이 되었지만,
현재 구조에서는 각 서비스가 개별적으로 트랜잭션을 관리하고 있어 문제가 발생했습니다.

이에 따라, 서비스 내부의 단일 책임 원칙(SRP)을 지키면서도 비즈니스 로직을 트랜잭션 관리로부터 분리하고, 서비스 간 트랜잭션을 효과적으로 관리할 수 있는 새로운 방안이 필요해졌습니다.


💡 해결: DDD 기반 Application Service / Domain Service 분리

셀메이트컨퍼런스발표자료

DDD(Domain Driven Design):
비즈니스 규칙을 최우선으로 고려하는 설계 방법론

  • Application Service: 프로세스 조율 (도메인 직접 구현 ❌)
  • Domain Service: 실제 비즈니스 규칙 구현 책임

셀메이트컨퍼런스발표자료

트랜잭션은 Application Service가 관리하고,
Domain Service는 도메인 작업만 수행하도록 분리했습니다.

셀메이트컨퍼런스발표자료

  • 명확한 책임 분리
  • 도메인 로직과 트랜잭션 관리 분리
  • 외부 호출을 분리된 어댑터에서 처리 → 도메인 보호

이렇게 하니 명확한 책임 분리가 이뤄졌고, 도메인 로직을 트랜잭션 관리로 부터 안전하게 분리할 수 있게 되었습니다.

뿐만아니라, 만약 외부 호출이 필요한 상황이라면 외부 호출을 구현한 서비스를 별도로 구현하게 함으로써, 도메인 서비스를 안전하게 외부 연동으로 부터 보호할 수 있게되었습니다.


🧩 네 번째 사례: 비동기 처리 구현 방식의 추상화

셀메이트컨퍼런스발표자료

마지막은 비동기 처리 구현 방식의 추상화 입니다.발표 초반 소개했던 출고 연동 장애 발생 원인은 잘못된 트랜잭션 처리와 외부 연동에 의존적인 설계로 인해 발생했습니다.

해당 로직의 결함을 파악하고 DDD의 설계 원칙에 따라 셀메이트로 출고 연동을 위한 서비스를 구현하고 어플리케이션 서비스에 호출하도록 리팩토링했습니다.

문제는 출고 연동은 HTTP 방식으로 구현되었지만, 추후에는 대부분의 비동기 처리 구현은 RabbitMQ를 사용한 이벤트 통신방식으로 변경할 예정이었습니다.

출고 연동 장애 원인:
잘못된 트랜잭션 처리와 외부 연동에 의존적인 설계

→ DDD 설계 원칙에 따라
셀메이트 출고 연동 서비스를 분리, Application Service에서 호출하도록 리팩토링

그러나…

  • 현재 구현: HTTP 기반
  • 추후 예정: RabbitMQ 기반의 이벤트 통신
  • Application Service는 어떻게 처리되는지 몰라야 한다

어플리케이션 서비스는 무엇을 할것인지에 대해서만 알뿐 어떻게 처리 되는가를 알아서는 안됩니다. 만약 서비스 구현 방식에 의존하게 되는 순간 기술의 변경이 비즈니스 규칙에 영향을 줄수있는 잘못된 설계로 인해 외부 기술의 변경이 어려워지는 문제가 발생할 수 있다고 판단했습니다.

셀메이트컨퍼런스발표자료

🔧 해결: 헥사고날 아키텍처

헥사고날 아키텍처는 제가 이 문제를 해결하기 위해 참고한 설계 방식입니다. 헥사고날 아키텍처의 설계 원칙은 어플리케이션의 핵심 비즈니스로직이 외부 세계의 변화에 영향을 받아서는 안된다고 강조하고 있습니다.

헥사고날 아키텍처는 어플리케이션의 내부와 외부를 명확하게 구분하고 모든 의존성이 내부로 향하게 함으로써 도메인을 안전하게 보호하는 것을 강조하고 있습니다.

  • 내부와 외부를 명확히 구분
  • 의존성은 내부로만 향함
  • 핵심 비즈니스 로직 보호

구성요소: 도메인, 포트(인터페이스), 어댑터(구현체)

헥사고날 아키텍처의 핵심 구성요소로 도메인, 포트, 그리고 어댑터 구현체가 있습니다. 포트는 내부 영역과 외부가 소통할 수 있는 인터페이스이며, 어댑터는 이 포트 인터페이스를 실제로 구현하는 구현체 입니다.

저는 이 포트 어댑터 구조를 활용해서 출고 처리 구현을 추상화 할 수 있었습니다.

셀메이트컨퍼런스발표자료

  • 포트 인터페이스 정의
  • Application Service에 의존성 주입
  • 어댑터 클래스 (HTTP, RabbitMQ 등) 구현
  • DI 설정으로 어댑터 선택 가능

셀메이트컨퍼런스발표자료

결과: 외부 시스템 연동 방식이 바뀌어도 비즈니스 로직은 건드리지 않음


셀메이트컨퍼런스발표자료

🎤 마무리

이번 블로그에서는 도메인을 지키는 설계를 주제로
4가지 문제와 그 해결 전략을 공유했습니다.

제가 셀메이트 개발팀에서 일하면서 가장 힘들었던 점은,
수많은 외부 서비스와 연동하며 개발을 해야 했지만,
비즈니스 로직이 외부 서비스에 지나치게 의존하는 구조 탓에 CS 업무의 부담이 너무 컸다는 점입니다.

매번 문제가 생길 때마다
고객응대, 재고 조사, 데이터 보정에 수많은 시간을 소모해야 했습니다.

앞으로 비즈니스 요구사항은 더 복잡해지고,
외부 연동도 점점 더 많아질 것입니다.

우리의 삶의 질을 지키기 위해서라도
반드시 도메인을 지키는 설계를 실천해야 합니다.

이 블로그가 앞으로 셀메이트의 CS 업무 강도를 줄이는 계기가 되기를 진심으로 바라며,
마지막으로 기술 구현보다 중요한 것은 도메인을 지켜내는 설계라는 점을 강조드리며 마무리하겠습니다.