도메인 주도 설계(Domain-Driven Design) in Real Project — Bounded context

Minseok
Cross-Platform Korea
11 min readFeb 2, 2020

소프트웨어를 개발하면서 다양한 한계를 극복하고 더 나아가기 위해 여러가지 기술적인 요소들을 적용하기 시작합니다. 그리고 그로인한 또다른 기술적 한계에 직면합니다.

소프트웨어 개발의 한계는 어디서부터 나타나는 것일까요? 그리고 무엇을 고민하면 좋을까요?

TL;DR

  • 구조적 프로그래밍(Structured Programming, SP)은 소프트웨어의 복잡성을 줄이기 위한 프로그래밍 패러다임입니다.
  • 일반적으로 기술적 구현은 겉으로 드러나는 소프트웨어의 동작과 그것을 위한 내부 동작을 다르게 표현합니다.
  • 도메인에 집중하면 도메인이 설명하는 범위와 도메인 간의 경계들이 명확하게 드러나는데, 이를 Bounded context라고 합니다.
  • Bounded context는 해당 요소가 최소한이자 최대한으로 필요로하는 범위를 나타내고, 보편 언어(Ubiquitous language)를 자연스럽게 반영합니다.
  • 도메인에 필요한 요소들만을 뽑아내는 과정을 Distillation이라고 합니다.
  • 도메인의 경계(Bounded context)를 명확하게 하면 의존성을 관리할 수 있고, 불필요한 시간의 소비와 예상치 못한 side-effect를 최소화할 수 있습니다.
  • Bounded context와 보편 언어를 사용할 때의 문제점은 무엇일까요?

구조적 프로그래밍(Structured Programming, SP)

SOLID를 고민해보신 적이 있으신가요? SOLID는 객체 지향(Object-Oriented)에서 반드시 지켜야 할 5개의 원칙을 말합니다.

OOP(Object-Oriented Programming)를 이용하여 개발을 하거나, 꼭 그렇지 않더라도 개발에서 매우 중요하게 여겨지는 원칙들입니다. 소프트웨어 개발에서 이를 모두 지키게 되면, 이상적으로는 개발 시간과 디버깅 시간을 감소시키는 것은 물론, 추가/변경/삭제 등의 유지보수도 편하게 할 수 있습니다. 5가지 중 하나라도 지키지 못한다면 그 프로그램은 이슈가 발생할 수 있는 결함을 가지게됩니다.

구조적 프로그래밍(Structured Programming, SP)복잡성(Complexity)을 제어하기 위한 소프트웨어 설계 패러다임입니다. 결합도(Coupling)를 최소화하고 응집도(Cohesion)를 최대화함으로써 소프트웨어를 설계하며 개발해 나아갑니다. 다양한 디자인 패턴(Design pattern)을 적용하는 것도 이를 해결하기 위한 관습들이라고 할 수 있습니다.

SP를 통해 필요한 만큼의 의존성(Dependency)만을 유지하면서 개발해 나아가는 것입니다. 요소들 간의 관계를 정말 필요한 만큼만 이어준다면 개발자가 기억하거나 생각해야할 범위를 줄일 수 있고, 개발 속도와 소프트웨어의 질과 유지보수성을 향상시킬 수 있습니다.

DDD도 소프트웨어의 복잡성을 최소화하는 것이 목적입니다. 그렇다면, SP를 적용하면 복잡성이 최소화되는 것이니까 굳이 DDD까지 생각할 필요가 있을까요?

구조적 프로그래밍의 한계

SP와 DDD의 차이는 요소를 바라보는 관점에 있습니다. DDD는 사용자의 관점에서 요소들을 바라보는 것이라면, SP는 기술적인 관점에서 요소들을 바라봅니다.

기술적인 관점으로만 접근하게 되면, 소프트웨어의 규모가 커질수록 SP의 의미는 점점 소실됩니다.

다음의 상황을 가정해 보겠습니다.

  • 하나의 소프트웨어를 개발하였습니다.
  • 모듈(module), 변수(variable), 함수(function) 등의 의존성을 완벽하게 최소화하였습니다.
  • 같은 팀의 모든 개발자들이 이해하고 공유할 수 있는 프로그래밍 스타일(programming style)도 완벽하게 지켰습니다.

그리고 그 소프트웨어에 추가적인 요구사항을 반영해야할 시기가 다가왔습니다.

  1. 구현 1년 후, 특정 기능에 버그가 발생했습니다. 어떤 1개의 기능에 대해 100개의 모듈이 존재한다면, 어떻게 찾아야 할까요?
  2. 내가 개발한 기능에서 다른 개발자가 새로운 동작을 추가해야 합니다. 기존에 이 기능과 관련된 100개의 동작이 있었습니다. 추가할 위치를 어떻게 찾고, 어떻게 추가해야 할까요?
  3. 내가 또는 다른 개발자가 개발한 기능을 수정하면서 side-effect가 발생하지 않을 것이라는 것을 무엇을 근거로 신뢰해야 할까요?
  4. 구현 1년 후, 각 요소들이 왜 그렇게 적용되었는지 어떻게 설명해야 할까요?
  5. 다른 개발자가 내가 개발한 기능과 함께 전체 로직을 설명해야할 상황이 발생하였습니다. 그 기능의 무엇 때문에 그렇게 구현하였는지 어떻게 설명해야 할까요?

실무에서 소프트웨어 개발하다보면, 흔히 접하는 상황들입니다. 그 상황들을 어떻게 해결하셨나요?

현실적으로, 의존성을 최소화하고 이를 위해 좋은 도구들을 사용하더라도 소프트웨어가 커질수록 그 구현은 더 복잡해지고 일관성을 잃어버립니다.

  • 사용자가 사용하는 동작은 유지하고 개선하지만 생각해야 하는 범위가 너무 넓어집니다. (1, 2, 3)
  • 의존성을 줄이기 위해 다양한 디자인 패턴과 SOLID를 적용하더라도, 왜 그렇게 하였는지 해석하는 것에 점점 더 많은 시간이 소요됩니다. (4, 5)

SP에서 사용되는 패턴과 원칙의 공통점은 설계 및 구현 시 사용되는 각 요소들의 역할을 명확하게 분리하고 통합하는 데 목적을 가지고 있습니다. 그렇지만, 요소가 많아질수록 그 관계(의존성)는 지속적으로 증가합니다. 시간이 흐를수록 개발자는 이 요소들을 항상 기억할 수 없고, 개발자가 속한 조직 또는 기술적 트렌드 등의 변화와 동시에 개발자가 생각하는 방식과 도구도 변화합니다. 이는, 소프트웨어의 일관성을 유지하기 힘들게 합니다.

개발자의 기술적 설계와 코드의 구현은 사실상 소프트웨어의 표현과 동작을 추상화시켜 사람이 이해할 수 있도록 한 것에 불과합니다. 다시 말하면, 겉으로 드러나는 소프트웨어의 동작과 내부 동작은 다르게 구현됩니다. 예를 들어, 아래의 그림에서 A와 data1, B와 data2가 같다는 것을 보장할 수 없습니다. A와 data1의 관계는 개발자가 개발하면서 암묵적인 약속으로 생각하고 구현하며, 다른 개발자는 A를 d1이라는 명칭으로 표현할 수도 있습니다.

Domain-Driven Design — Traditional software implementation

코드의 변경

어느 한 기능과 관련된 코드를 얼마나 자주 개선하시나요?

설계와 코드를 자주 개선하고 재검증하는 것은, 수정하기 어려운 버그나 위험요소(risk)를 줄이기 위한 방법 중 하나입니다. 소프트웨어를 유지보수(software maintenance)하는 것은 실무에서 매우 중요합니다. 그렇지만, 기업은 한정된 자본 내에서 시간은 돈으로 직결되어 불필요한 곳에 소비하는 것을 최소화해야 하기 때문에, 유지보수에 시간이 많이 소요되면 새로운 기능을 개발할 여지를 가질 수 없습니다.

이는 기업이 개발자들의 역량 중 하나로 시간을 얼마나 잘 관리하는가를 중요하게 생각하도록 합니다. 꼭 그렇지 않더라도, 개발자들도 짧은 시간 내에 많은 것을 시도해보고 역량을 키우거나 빠르게 프로젝트를 끝내고 싶어합니다.

애자일(agile) 개발방법론은 미래에 발생할 위험요소를 사전에 예방하기 위해 사용됩니다. 스크럼(Scrum), 칸반(Kanban), 익스트림 프로그래밍(eXtreme Programming, XP) 등 여러가지 방식이 있고, 현대 사회의 빠른 발전과 기술의 트렌드 등으로 인해 많은 개발 팀들이 사용하고 있습니다. 잦은 개선을 통해 버그를 발생시킬 수 있는 코드를 최소화시키면서 나아갈 수 있습니다. 향후 개발하는 시간을 조금씩 더 줄일 수 있다는 기대감과 함께 소프트웨어의 개선과 버그 수정을 반복합니다. 실무에서는 애자일을 정형화하기 힘들기 때문에 조직에 따라 그 형태가 모두 다를 수 있고, 바라보는 관점이 모두 다를 수 있습니다. 공통점은, 잦은 코드 변경이 이루어진다는 것입니다. 장기적으로 보았을 때 개발 시간이 정말 줄어들까요?

요구사항의 변경이 필요하면 상황에 따라 이미 소프트웨어에 구현된 설계를 변경해야할 수도 있고, 특정 부분의 작은 코드를 변경할 수도 있습니다. 그리고 변경이 된 기능들이 정상적으로 동작하는지 다시 확인하는 과정이 필요합니다. 이러한 변경 과정들은 소프트웨어를 개발한 자기 자신, 협업하는 개발자들과 테스트 코드들을 신뢰함으로써 가능합니다. 변경하기 전에 잠깐이라도 고민이 필요할 것입니다. 정말 신뢰할 수 있을까요?

조금 더 나아가면, 코드를 변경한다는 것은 관련된 코드를 다시 읽고 해석해야 한다는 것을 의미합니다. 코드 변경이 쉬우려면 변경 이전에 다음이 고민되어야 합니다.

  • 자신의 코드를 얼마나 빠르게 해석할 수 있는가?
  • 다른 개발자의 코드를 얼마나 빠르게 해석할 수 있는가?
  • 다른 개발자가 자신의 코드를 얼마나 빠르게 해석할 수 있는가?

개발자들은 소프트웨어가 완벽하지 않다는 것을 알고 있습니다. 기능을 추가하기 위해 관련 코드들을 확인하는 것은 물론이고, 디버깅 또는 버그를 예방하기 위해 소프트웨어를 개선하면서 코드를 지속적으로 읽습니다.

반복해서 코드를 해석하며 개선하고 버그를 수정하는 일들은 생각보다 많은 시간이 소요됩니다. 즉, 반복적으로 소프트웨어를 수정한다는 것은, 코드를 해석하고 소프트웨어를 개선하거나 디버깅하는 시간의 소비가 자주 발생할 수 있다는 것을 의미합니다.

실무에서는 일정이 정해져 있을 수도 있고, 그렇지 않더라도 더 좋은 소프트웨어를 만들기 위해서는 하염없이 한 가지만 보고 있을 수도 없습니다. 주어진 개발 기간 동안, 이러한 시간 소비를 어떤 근거로부터 예상하고, 얼마나 할당할 수 있을까요?

Bounded context

우리는 소프트웨어 개발을 하면서 항상 기술적/비즈니스적 한계를 마주하게됩니다. 어떤 이슈를 발견하게되면 그 한계로 인해 side-effect가 발생하게 되는데 언제 어디서 어떤 일이 발생할지 쉽게 알 수 없습니다. 소프트웨어를 적절한 패러다임과 구조로 매우 잘 설계한다고 하더라도, 그것을 신뢰하고 변경하는 것은 생각보다 쉽지 않습니다.

이를 위해 도메인(Domain)을 통해 side-effect를 관리할 수 있습니다. 도메인은 소프트웨어에서 사용자가 알 수 있는 곳에 있으므로, 무언가 변경이 필요하면 관련된 요소들이 무엇인지 쉽게 찾을 수 있습니다. 그리고 도메인에 집중하면 도메인이 설명하는 범위와 도메인 간의 경계들이 명확하게 드러나는데, 이를 Bounded context라고 합니다. Bounded context는 하나의 도메인 또는 도메인의 집합이며, DDD에서 소프트웨어를 쉽게 바라볼 수 있도록 하는 시야를 제공합니다.

Bounded context는 해당 요소가 최소한이자 최대한으로 필요로하는 범위를 나타내고, 보편 언어(Ubiquitous language)를 자연스럽게 반영합니다. 그리고 그 관련된 도메인에 집중하고 그렇지 않은 도메인들은 생각하지 않을 수 있습니다. 이렇게 도메인에 필요한 요소들만을 뽑아내는 과정을 Distillation이라고 합니다. 예를 들어, 도메인 A, X, Y가 있을 때, A와 X가 관련되어 있다면 Bounded context는 다음과 같이 표현할 수 있습니다.

Domain-Driven Design — Bounded context
Domain-Driven Design — Ubiquitous lannguage

도메인 A가 변경된다면 도메인 X가 영향을 받고, 도메인 Y는 관련이 없다는 것을 알 수 있습니다. 이와 같이 개발자에 의해 구현된 요소가 도메인으로 정의되고 그 관계와 경계를 명확하게 알 수 있다면 코드의 가독성(readability)과 함께 복잡성(의존성)을 관리할 수 있습니다. 코드를 쉽게 읽고 의존성들을 쉽게 파악할 수 있다면, 주어진 문제를 해결하기 위해 무엇을 해야하는 지 쉽게 접근할 수 있고, 불필요한 시간의 소비와 예상치 못한 side-effect를 최소화할 수 있습니다.

단순히 SP만을 사용할 때 문제점과 코드의 변경함으로써 고려해야하는 것들을 Bounded context가 어떻게 보완할 수 있는지 살펴보았습니다. 그렇다면, Bounded context와 보편 언어를 사용할 때의 문제점은 무엇일까요?

--

--