본문 바로가기

Knowledge/Software Design

도메인(Domain) 관점 모듈 분리 (With. DDD, MSA)

시스템이 점점 거대해지면서, 우리 팀 또한 공통 모듈의 저주에 빠지고 말았다.

(공통 모듈의 저주? https://techblog.woowahan.com/2637/)

 

거대한 하나의 모듈을 분리할 필요성을 느꼈고, 그 과정에서 논의하며 정리했던 내용들을 기록해둔다.

(도메인과 관련된 글이나 자료는 많았지만, 대부분 추상적이고 실효성이 있는 자료를 찾지 못했다..)

도메인은 무엇일까?

도메인이란 단어 뜻 자체는 영토, 분야, 영역, 범위 등의 의미를 가진다.

인터넷 주소의 의미로도 사용된다.

 

다만, MSA와 DDD에서 흔히 사용하는 도메인의 단어는 [영역]의 의미를 가진다고 볼 수 있다.

Domain이라고 하면, DDD에 대한 얘기를 하려고 하는걸까? 라는 인식이 있어서 꺼려진다.
하지만, DDD 개발방법론에 대한 얘기를 하고자 하는 것은 전혀 아니다.

 

예를 들어 온라인 예약이 있다. 개발자 입장에서는 온라인 예약은 개발해야하는 소프트웨어 대상이 된다.

즉, 온라인 예약은 소프트웨어로 해결하고자 하는 문제영역, 도메인에 해당된다.

 

온라인 예약을 다시 하위 도메인으로 분류할 수 있다.

  • 회원
  • 예약
  • 상품
  • 정산
  • 등등..

도메인 관점에서의 모듈 분리?

지금까지 보통 소프트웨어를 개발할 때, MVC 패턴에 기초해서 메소드 또는 객체지향의 관점으로만 코드를 작성해왔다.

그러다보니 시스템이 커질수록 각 도메인 로직들이 서로 얽혀있고, 경계 없이 거대한 하나의 모듈이 만들어졌다.

모듈 분리가 왜 필요한가?

그냥 이대로 놔두면 되지, 왜 이 거대한 만능 모듈을 분리해야 하는가?

  • 유연성과 확장성을 높이기 위해
  • 유지보수성을 높이기 위해
  • 빠른 빌드 속도를 위해
  • SPOF 방지
  • 등등..

결국 모듈 분리가 필요한 이유를 나열해보면, 자연스레 MSA의 장점과 일맥상통하는 느낌이다.

 

당장 우리의 문제는, 새로운 멀티 모듈 프로젝트를 만들더라도, 이 거대한 모듈을 Import 해야했기 때문에, 유연성과 확장성이 떨어졌다.

또한, 비즈니스 로직들이 서로 얽혀있어 가독성이 떨어지고, 유지보수가 어려운 문제로 이어졌다.

 

가장 불편했던 문제는 사소한 수정이 이뤄지더라도 이는 곧 거대한 공통 모듈의 빌드가 필수적이였고, 테스트 코드 또한 마찬가지였으므로, 개발 속도를 저해하는 큰 요소 중 하나로 작용했다.

어떤 기준으로 분리할 것인가?

비즈니스의 도메인을 식별해서, 도메인 기준으로 모듈을 분리한다.

어디까지 분리할 것인가?

도메인이 식별되었다면, 식별된 모든 도메인을 분리하는 것이 과연 맞을까? 

 

실효성을 따져보며 판단해야한다.

만약, 결제 도메인과 카드관리 도메인이 비즈니스 로직 상 정말 결합이 강하다면, 굳이 코드 상에서도 모듈을 분리할 필요가 있을까?

소프트웨어를 설계할 때 가장 중요한 것은 항상 트레이드 오프를 따져봐야 한다는 것이다.
단점없이 장점만 취할 수 있는 소프트웨어 구조는 없다.

도메인 모듈 간의 경계

분리 작업을 할 때, 도메인 경계를 지키며 모듈 분리를 하는 것이 중요한데, 이는 곧 모듈 간의 결합을 약하게 한다는 뜻이다.

겉으로 모듈 패키지만 분리되어 있더라도, 실질적으론 모듈 간의 결합이 강하다면, 위에서 언급했던 모듈 분리의 장점을 취하지 못한채 단점만 가져가게 된다.

 

그렇다면, 구체적으로 도메인 경계를 지키며 모듈을 분리한다는 건 어떻게 하는걸까?

비즈니스 관점으로 분리

비즈니스을 그대로 코드에 반영하는 것은 중요하다. 비즈니스 로직의 흐름을 알 수 있기에 지속 성장 가능한 코드로 만들 수 있다.

 

비즈니스 관점으로 모듈을 분리하는 방법은 간단하다.

예를 들어 온라인 예약 서비스가 있고, 그 안에서 결제를 이용할 수 있다는 가정을 해본다.

예약과 결제, 이렇게 2개의 영역이 도출되어 도메인으로 식별할 수 있고, 예약 관련된 로직은 예약 모듈에 위치시키고, 결제 관련된 로직은 결제 모듈이 위치시키면 된다.

 

하지만, 이렇게 각 로직을 각 모듈로 자연스레 분리가 될까? 분명히 각 도메인 모듈간의 관계는 존재한다.

 

예약 서비스에서 결제를 사용하는 것이므로, 모듈 간의 관계도 그대로 비즈니스를 코드에 투영한다.

예약 모듈에서 결제 모듈을 import해서, 예약 + 결제 로직을 조합한다.

  • 결제 금액 계산, 결제 처리 등등 > 결제 모듈
  • 결제 금액과 예약 금액 비교 검증, 결제 모듈 호출 등 > 예약 모듈

 

만약, 결제 서비스도 주체가 되고 거기서 예약까지 가능해야한다면?

예약 & 결제 두 모듈 둘다 로직의 주체가 될 수도 있고, 서로 이용이 가능한 경우이므로, Circular Dependency가 발생한다.

이런 경우엔, 두 모듈을 조합하는 상위 Layer 모듈을 둔다.

그렇다면 Persistence Level 에서의 Mysql Join 등도 분리해야하나?

예를 들어, 예약 모듈에서 결제 모듈을 통해 데이터를 구해서 사용하는 것이 아닌, Mysql Join 등을 이용해서 데이터를 얻는 경우이다.

성능 상의 이유가 있다면, Persistence Level에서 다른 도메인의 경계를 침범하도록 한다.
(아래에서 다시 한번 정리하겠지만, 극단적인 모듈 분리는 오히려 단점이 생길 수 있다. 트레이드 오프를 항상 생각..)

데이터 모델 관점으로 분리

비즈니스 모델을 따라 그대로 도메인을 분리시켰다 하더라도, 인터페이스 모델(DTO 혹은 VO)에서 결합이 강할 수 있다.

 

예를 들어, 결제 모듈에서 다음과 같은 유저의 일부 정보가 필요하다는 가정을 해본다.

- User.age (Type: Int)
- User.type (UserType / Type: Enum)
- User.extraInfo (UserExtraInfo / Type: Class)

 

위 데이터를 결제 모듈로 어떻게 전달할 수 있을까?

이렇게 데이터 모델을 만들어서 넘기면 정말 모듈이 분리됐다고 말할 수 있을까?

 

의미 상 분리됐다고 볼 수도 있다.

유저 필드를 이용해 데이터 모델을 새롭게 정의했으므로, 어떻게 보면 분리되었다고 말할 수도 있다.

 

하지만 모듈 간의 결합도가 약한가?

 

결합도를 더 낮추려면, Int, UserType, UserExtraInfo 타입까지 모두 분리해야한다.

타입도 분리한다는 뜻은, UserType을 결제 모듈에서 의미를 갖는 PaymentType 등으로 변환해서 받아야 된다는 것을 뜻한다.

 

다만, 분리하는 목적은 각 모듈 간의 결합도를 낮춰서 영향도를 낮추기 위함이다.

그렇다면, 서비스 전반에 걸쳐 변할 가능성이 거의 없는 것은 굳이 타입을 변환할 필요가 없다.

  • Java의 Primitive Type인 Int가 그것의 대표적인 예다.
  • Enum인 UserType 또한, 서비스 전반에 걸쳐 변할 가능성이 없다면, 타입을 굳이 변환해서 결제 모듈로 가져갈 필요는 없다.
  • UserExtraInfo 클래스가 변할 가능성이 많다면, 결제 모듈안에서 의미를 갖는 데이터로 변환해서 받아야한다.
여기서 데이터 모델을 만들어서 다른 모듈로 넘기기 전에 가장 중요하게 확인해야 되는 것은 정말 그 모듈에서 필요한 데이터인지를 확인하는 것이다.

정말 결제 모듈 로직에서 유저 데이터가 필요한걸까? 유저 도메인 로직인데, 결제 도메인에서 함께 처리하고 있는 것은 아닐까?

그래서 추구하는 방향은 무엇인가?

모듈 분리를 극단적으로는 하지 않을 것이다. 극단적으로 하면 오히려 장점보다 단점이 많아질 수도 있을 것이다.

(각 모듈마다 기술 스택이 달라져도 각 모듈의 영향은 없어야한다... 라는 생각까지라던가)

 

또한, 앞서 살펴본 듯이 모듈을 극단적으로 분리하면, 기존보다 현저히 성능이 떨어지고, 수많은 보일러 플레이트 코드가 발생할 수 있다.

 

  1. 비즈니스 니즈를 잘 파악해서 도메인을 식별하는 것이 가장 중요한 첫번째 스텝이 될 것이다.
  2. 도메인을 식별해서 로직을 분리한 뒤, 비즈니스 흐름에 맞춰 도메인 간의 관계를 만드는 것이 두번째 스텝이 된다.
  3. 그 다음, 모듈 간 데이터 모델의 결합도를 살펴본다. (결합도를 극단적으로 낮추는 것은 오히려 독이될 수 있다)
  4. 결합도를 최대한 낮춰서, 모듈 분리의 이점을 취한다.