19 분 소요

현대의 비즈니스 환경은 빠르게 변화하며, 고객의 요구사항도 시시각각 변합니다. 이러한 환경에서 성공적으로 가치를 전달하기 위해서는 소프트웨어가 변화에 유연하게 대응할 수 있어야 합니다. 요구사항의 대부분은 비즈니스에서 출발하기 때문에, 소프트웨어 개발의 초점은 비즈니스 도메인에 맞춰져야 합니다.

비즈니스 도메인과 구현 간의 간격을 좁히고, 도메인과 코드가 긴밀하게 연동되도록 해야 합니다. 코드는 도메인을 표현하는 하나의 방법이며, 도메인이 변하면 코드도 자연스럽게 수정되어야 합니다. 비즈니스 도메인과 구현 간의 간격이 크면, 작은 도메인 변화도 코드에 큰 영향을 미칠 수 있어 가치를 전달하는 시간이 길어질 수 있습니다. 반대로, 도메인과 코드의 간격이 좁다면 도메인 변화가 코드에 적은 수정만으로 반영되어 더 빠르게 대응할 수 있습니다.

도메인과 밀접하게 연결된 코드는 도메인이 정제됨에 따라 개념이 명확해지고, 의존성은 줄어들며, 응집성은 증가합니다. 도메인 전문가와 개발자가 긴밀하게 협력하면 더 설득력 있는 제품을 제공할 수 있습니다. 이를 통해 숨겨진 개념을 표면화하고, 비즈니스 가치를 제공하는 소프트웨어를 만들 수 있습니다.

따라서 비즈니스 도메인을 깊이 이해하고 이를 바탕으로 비즈니스 개념을 도메인 모델로 명확히 정의한 후, 이 모델을 기반으로 소프트웨어를 개발해야 합니다.

유비쿼터스 언어 (Ubiquitous Language)

언어는 종종 모호하고 혼란스러울 수 있습니다. 동일한 단어가 문맥에 따라 다른 의미를 지닐 수 있으며, 비슷한 표현이 사람마다 다르게 해석될 수 있습니다. 이러한 언어적 차이가 소통을 방해하면, 도메인 탐구와 지식 공유의 기회가 줄어들고, 결과적으로 제품이 고객의 기대에서 멀어질 수 있습니다.

도메인 전문가와 소프트웨어 개발자들이 공통된 심적 모델을 유지하려면 사용하는 언어를 통일하는 것이 필요합니다. 이를 위해 다양한 전문가들이 비즈니스 도메인에 대한 지식을 공유하고 심층적으로 탐구하여 공통적으로 사용되는 언어인 유비쿼터스 언어(Ubiquitous Language)를 정제해야 합니다. 이 과정에서 언어의 모순이나 오류를 발견하고, 도메인 내의 본질적인 개념을 도출하여 도메인 모델을 명확히 정의할 수 있습니다.

어휘와 그 간략한 정의를 포함한 사전을 만들어 유비쿼터스 언어를 체계화할 수 있습니다. 이 사전을 바탕으로 사용자 스토리를 작성하며, 스토리에서 나타나는 언어의 변화를 반영하여 사전을 지속적으로 업데이트하고 확장해 나갑니다.

유비쿼터스 언어를 담은 문서는 지속적으로 업데이트되어야 하며, 이 문서들을 유지하고 관리하는 작업은 결코 간단하지 않습니다. 코드는 유비쿼터스 언어의 가장 지속적이고 유일하게 보장된 표현이지만, 소프트웨어 개발자가 아닌 다른 구성원에게는 접근이 어려울 수 있습니다. 따라서 모든 구성원이 쉽게 이해하고 접근할 수 있는 방식으로 정보가 전달되어야 합니다. 이 문서들은 코드에서 자동으로 생성되거나, 코드의 변화에 맞춰 업데이트될 수 있지만, 소프트웨어 개발자가 아닌 사람들도 이해할 수 있어야 합니다.

유비쿼터스 언어는 코드, 구성원들이 사용하는 문장들, 그리고 이를 기반으로 작성된 문서들로 구성됩니다. 이 언어로 작성된 구문은 단순한 표현이 아니라 그 자체로 유비쿼터스 언어의 발현이자, 언어 자체를 나타냅니다.

유비쿼터스 언어는 특정 비즈니스 도메인의 개념을 포착하기 위해 사용되며, 코드에는 이 언어가 명확히 반영되어야 합니다. 유비쿼터스 언어에 포함된 명사, 형용사, 동사 등 다양한 표현들이 코드에 통합되어 명시적으로 드러나야 합니다.

바운디드 컨텍스트 (Bounded Context)

유비쿼터스 언어를 도출하는 과정에서 동일한 어휘가 서로 다른 개념을 나타내는 경우가 종종 발생합니다. 이는 어휘가 다양한 문맥에서 사용되기 때문입니다. 이러한 미세한 차이를 정제하여 하나의 일관된 개념으로 통합할지, 아니면 두 개의 개념 차이를 인정하고 각 문맥에 따라 다른 어휘를 사용할지, 혹은 두 개의 유비쿼터스 언어를 수용할지 결정해야 합니다.

이처럼 나누어진 문맥과 언어의 경계를 바운디드 컨텍스트(Bounded Context)라고 합니다. 바운디드 컨텍스트는 하나의 유비쿼터스 언어가 표현하는 도메인 모델을 명확하게 구분하는 경계입니다. 이 경계 내에서 유비쿼터스 언어의 어휘와 구문은 구체적인 의미를 가지며, 도메인 모델의 정확성과 일관성을 보장합니다. 각 바운디드 컨텍스트는 고유한 유비쿼터스 언어를 사용하며, 다른 컨텍스트와 격리됩니다. 복잡한 개념은 여러 바운디드 컨텍스트로 나뉘어 구체적이고 풍부한 의미를 드러내며, 같은 용어라도 각 컨텍스트에서 서로 다른 의미를 지닐 수 있습니다.

도메인 모델은 바운디드 컨텍스트를 구성하는 핵심 요소이지만, 바운디드 컨텍스트 내부에는 도메인 모델과 상호작용을 지원하는 다른 요소들도 포함됩니다. 예를 들어, 데이터베이스 스키마가 바운디드 컨텍스트의 유비쿼터스 언어로 표현된다면, 그 스키마는 바운디드 컨텍스트 내부에 속합니다. 반면, 스키마가 유비쿼터스 언어로 표현되지 않고 별도로 관리된다면, 바운디드 컨텍스트 외부에 위치하게 됩니다.

또한, 바운디드 컨텍스트는 독립적인 서비스나 애플리케이션을 나타낼 수도 있습니다. 외부 서비스는 각각 독립된 문맥을 가지며, 고유한 언어를 사용합니다.

바운디드 컨텍스트를 설정할 때는 적절한 경계를 설정하는 것이 중요합니다. 경계가 너무 좁으면 필수적인 개념이 누락되어 표현력이 떨어지고, 점차 무기력해질 수 있습니다. 반면, 경계가 너무 넓으면 하나의 도메인 모델이 상충되는 개념을 담아내게 되어, 개념의 구체성과 충실함을 잃을 수 있습니다.

모든 것을 포괄하는 언어를 만드는 것은 거의 불가능하며, 유용하지도 않습니다. 모든 개념을 모순 없이 명확히 정의하고 구성원 모두의 동의를 받는 것은 매우 어렵기 때문입니다. 따라서 하나의 어휘가 여러 의미를 가질 수 있음을 인정하고, 바운디드 컨텍스트를 통해 이러한 차이점을 명확하게 표현하는 것이 필요합니다. 이를 통해 각 도메인 모델은 해당 컨텍스트의 요구에 맞춰 구체적이고 정확하게 기술될 수 있습니다.

바운디드 컨텍스트는 상호 의존성을 최소화합니다. 동일한 어휘로 표현된 도메인 개념이라도, 각 바운디드 컨텍스트는 고유한 언어를 내포하고 이를 서로 다른 의미로 해석하여 모델링합니다. 다른 바운디드 컨텍스트에서 정의된 모델을 활용하려면 해당 컨텍스트의 언어로 변환하는 과정이 필요합니다. 이로 인해 중복이 발생할 수 있지만, 바운디드 컨텍스트 간의 강한 연결을 피하고 각자의 목적에 맞게 설계될 수 있습니다.

도메인 (Domain)

비즈니스는 여러 도메인으로 구성됩니다. 이러한 도메인은 제품을 경쟁자와 차별화하는 핵심 도메인 (Core Domain), 비즈니스와 밀접하게 연관되어 있지만 경쟁 우위를 가질 필요가 없는 지원 서브 도메인 (Supporting Sub-Domain), 그리고 비즈니스와 직접적인 연관은 없지만 정상적인 운영을 위해 필요한 범용 서브 도메인 (Generic Sub-Domain)으로 구분됩니다.

핵심 도메인은 경쟁자들과의 차별화를 통해 경쟁 우위를 제공하며, 비즈니스 수익을 창출하는 중요한 요소입니다. 따라서 핵심 도메인은 경쟁자가 쉽게 모방하지 못하도록 진입 장벽을 높이는 것이 중요합니다. 지원 서브 도메인은 핵심 도메인처럼 차별화될 필요는 없지만, 핵심 도메인과 밀접하게 연결된 영역으로서 기성 제품만으로는 해결할 수 없는 문제를 다루는 경우가 많습니다. 반면, 범용 서브 도메인은 차별화가 필요하지 않으므로 기존 제품을 활용할 수 있습니다. 자원이 한정된 상황에서는 비즈니스 성공에 직접적인 영향을 미치는 핵심 도메인을 명확히 식별하고, 여기에 역량을 집중해야 합니다.

비즈니스는 문제점 공간해결책 공간으로 나눌 수 있습니다. 문제점 공간은 해결해야 할 비즈니스 문제에 초점을 맞추고, 해결책 공간은 이러한 문제를 해결하는 소프트웨어 구현에 집중합니다.

문제점 공간은 핵심 도메인과 그에 연관된 서브 도메인들로 구성됩니다. 명확하게 정의된 문제점 공간을 통해 비즈니스에 중요한 영향을 미치는 핵심 도메인과 이를 지원하는 서브 도메인들을 식별하고, 모든 구성원이 공감할 수 있는 비전과 목표를 수립해야 합니다.

해결책 공간은 하나 이상의 바운디드 컨텍스트로 구성된 구체적인 소프트웨어 모델의 집합입니다. 바운디드 컨텍스트는 문제를 해결하기 위한 특화된 해결책이자 소프트웨어 구현의 이정표가 되어 문제를 소프트웨어로 구현하는 데 활용됩니다. 해결책 공간은 새로 만들어질 시스템과 기술뿐만 아니라 기존에 존재하는 시스템과 기술에도 영향을 받습니다.

서브 도메인과 바운디드 컨텍스트를 명확히 정의하고 이들의 연관성을 정확히 파악해야 합니다. 서브 도메인과 바운디드 컨텍스트가 항상 일대일로 대응하지 않을 수 있지만, 만약 하나의 서브 도메인이 여러 바운디드 컨텍스트에 걸쳐 있거나, 하나의 바운디드 컨텍스트가 지나치게 많은 서브 도메인과 연결되어 있다면 구분이 적절히 이루어지지 않았다는 신호일 수 있습니다.

서브 도메인을 바운디드 컨텍스트와 일치시키는 것은 문제점 공간과 해결책 공간을 효과적으로 통합하고, 도메인 모델을 비즈니스 목표에 맞게 명확히 구분하는 데 유리합니다. 그러나 레거시 시스템을 고려하면 서브 도메인이 여러 바운디드 컨텍스트와 중첩될 수 있습니다. 예를 들어, 하나의 바운디드 컨텍스트가 여러 서브 도메인으로 구성되거나, 반대로 하나의 서브 도메인이 여러 바운디드 컨텍스트로 나뉘는 경우가 있을 수 있습니다.

컨텍스트 맵 (Context Map)

바운디드 컨텍스트 간의 경계와 관계를 명확히 표현한 컨텍스트 맵 (Context Map)을 통해 해결책 공간을 쉽게 이해할 수 있습니다. 컨텍스트 맵은 전체 시스템 구조를 파악하고, 바운디드 컨텍스트 간의 의존성을 관리하며, 통합 방식을 결정하는 데 도움을 줍니다.

컨텍스트 맵을 작성할 때, 각 바운디드 컨텍스트와 이들 간의 관계를 기록합니다. 이를 통해 바운디드 컨텍스트의 경계, 팀 간의 관계, 서로 다른 컨텍스트 간의 통합 방식, 그리고 모델 변환 방식을 높은 수준에서 표현하여 현재 프로젝트의 상태를 간단한 다이어그램으로 나타냅니다.

세부 사항이 너무 많아지면 컨텍스트 맵이 복잡해져 직관적인 이해가 어려워지고, 수정 및 관리가 힘들어질 수 있습니다. 따라서 컨텍스트 맵은 최대한 단순하게 유지하며, 변화하는 상황에 맞춰 유연하게 업데이트해야 합니다.

컨텍스트 맵은 현재 시스템 구조를 정확히 반영해야 합니다. 이상적인 모습을 그리기보다는 현재 상태를 명확히 이해할 수 있도록 매핑하고, 프로젝트가 진행됨에 따라 관계가 바뀌면 맵을 업데이트하여 최신 상태를 유지해야 합니다.

바운디드 컨텍스트는 여러 가지 정해진 관계를 가질 수 있습니다. 예를 들어, 컨텍스트 내 팀이 성공과 실패를 함께한다면, 이 관계를 파트너십 (Partnership)이라고 합니다. 이러한 팀들은 개발과 통합을 공동으로 관리하기 위해 잘 조정된 기획 과정을 갖추고, 양측의 요구를 수용할 수 있는 인터페이스를 설계하며 긴밀하게 협력해야 합니다.

도메인 모델에서 공유되는 코드는 밀접한 상호 의존성을 형성할 수 있습니다. 팀이 공유하기로 합의한 도메인 모델의 일부는 공유 커널 (Shared Kernel)로 지정할 수 있으며, 이 공유 커널은 다른 팀의 동의 없이 변경할 수 없고 신중하게 관리되어야 합니다.

업스트림 팀이 다운스트림 팀의 요구사항을 다양한 방법으로 수용하여 개발하는 것을 고객-공급자 개발 (Customer-Supplier Development)이라고 합니다. 이 경우, 업스트림 팀의 계획이 다운스트림 팀의 계획에 큰 영향을 미치므로, 업스트림 팀은 다운스트림 팀의 요구사항을 반드시 반영해야 합니다.

반대로, 업스트림 팀이 다운스트림 팀의 요구사항을 수용할 동기가 없는 경우, 다운스트림 팀은 업스트림 팀의 모델을 무조건적으로 준수해야 합니다. 이러한 관계를 순응주의자 (Conformist)라고 하며, 다운스트림 팀은 업스트림 팀의 모델을 그대로 따름으로써 컨텍스트 간의 복잡한 변환을 제거합니다.

잘 설계된 바운디드 컨텍스트의 경계를 연결할 때, 변환 계층은 간결합니다. 그러나 바운디드 컨텍스트 간의 거리가 멀어질수록 변환은 더 복잡해지고 방어적인 성격을 가지게 됩니다. 이러한 변환 계층을 부패 방지 계층 (Anticorruption Layer)이라고 하며, 다운스트림에서 분리된 변환 계층을 통해 업스트림의 모델을 다운스트림의 맥락에 맞는 모델로 조정합니다.

서비스에 접근할 수 있는 프로토콜이 명확히 정의된 공개된 서비스를 오픈 호스트 서비스 (Open Host Service)라고 합니다. 이 서비스는 공개된 프로토콜을 통해 상호작용하며, 새로운 요구사항에 대응하기 위해 프로토콜을 확장하고 조정할 수 있어야 합니다.

두 바운디드 컨텍스트 간의 상호작용을 위해 잘 문서화된 공유 언어를 사용할 수 있습니다. 이 언어를 발행된 언어 (Published Language)라고 하며, 이를 참고하여 바운디드 컨텍스트 간의 변환을 처리합니다. 발행된 언어는 종종 오픈 호스트 서비스와 함께 사용됩니다.

만약 두 바운디드 컨텍스트 사이에 유의미한 관계가 없다면, 이들은 서로 관계가 없음을 명확히 선언해야 합니다. 이러한 관계를 분리된 방법 (Separate Ways)이라고 하며, 작은 범위 내에서 단순하고 전문적인 해결책을 찾을 수 있게 합니다.

기존 시스템에 여러 모델이 경계 없이 뒤섞여 있을 수 있습니다. 이러한 복잡한 시스템을 큰 진흙공 (Big Ball Of Mud)이라고 정의하며, 이 컨텍스트 안에서 세련된 모델링을 적용하지 않되, 다른 컨텍스트까지 오염되지 않도록 격리해야 합니다.

바운디드 컨텍스트 간의 관계를 명확히 표현한 컨텍스트 맵은 높은 수준의 관점을 제공하여 아키텍처 결정에 활용되거나, 조직 역학을 이해하고 프로젝트 진행의 장애 요소를 파악하는 데 도움을 줍니다.

엔티티 (Entity)

도메인 모델은 소프트웨어에서 다양한 방식으로 표현될 수 있습니다. 도메인 모델이 바운디드 컨텍스트 내에서 다른 모델과 구별될 수 있는 식별성을 가지며, 시간이 지나도 그 식별성이 유지되어야 한다면, 해당 도메인 모델은 엔티티(Entity)로 설계될 수 있습니다.

엔티티의 역할을 명확히 정의하려면 바운디드 컨텍스트 내의 유비쿼터스 언어를 면밀히 분석해야 합니다. 이를 통해 엔티티와 관련된 요구 사항을 정리하고, 엔티티가 반드시 가져야 할 특성을 도출할 수 있습니다. 이러한 특성을 바탕으로 엔티티의 식별자를 설계하게 되며, 이 식별자는 객체를 구별하거나 매칭하는 데 사용되어 엔티티의 정체성을 결정하는 중요한 요소가 됩니다.

사용자가 직접 고유 식별자를 제공하는 방식은 도메인 개념을 식별자에 직접 반영할 수 있어 직관적이고 간편합니다. 그러나 이러한 방식에서는 식별자의 품질을 일관되게 유지하기 어려울 수 있으며, 영속성 저장소에 적재할 때 성능 저하가 발생할 수 있습니다. 또한, 도메인에 대한 이해가 깊어져 식별자가 변경될 경우, 기존 엔티티의 식별자를 모두 업데이트해야 하며, 이를 참조하는 다른 객체들도 수정해야 하는 부담이 생길 수 있습니다.

이에 비해 서비스가 식별자를 생성하는 방식도 있습니다. 애플리케이션이 UUID와 같은 고유 식별자를 생성하거나, PostgreSQL, MongoDB와 같은 영속성 저장소에서 식별자를 생성하도록 맡길 수 있습니다. 이 경우, 고유 식별자의 품질과 일관성이 일정하게 유지됩니다. 그러나 이러한 식별자는 도메인 개념을 포함하지 않기 때문에, 엔티티를 검색할 때 추가적인 식별자가 필요할 수 있습니다. 자동 생성된 고유 식별자는 도메인 개념의 변화와 무관하게 유지되지만, 영속성 저장소의 개념이 도메인에 누수될 위험이 있으며, 기술적 한계로 인해 식별자를 변경해야 할 경우도 있을 수 있습니다.

식별자 생성 시점도 중요합니다. 식별자는 객체가 생성될 때 즉시 생성되거나, 영속성 저장소에 저장될 때까지 지연될 수 있습니다. 여러 엔티티를 배치 작업으로 동시에 생성하고 이들이 서로 연관된 경우, 배치 작업 요청 전에 식별자가 할당되어 있어야 엔티티 간의 의존관계를 표현할 수 있습니다. 그러나 기술적인 이유로 식별자가 영속성 저장소에서 생성되는 경우, 저장되기 전까지는 식별자를 할당할 수 없는 상황이 발생할 수 있습니다. 이럴 때는 도메인 요구 사항에 맞춘 식별자와 영속성 계층의 요구 사항에 맞춘 식별자를 모두 생성하여 문제를 해결할 수 있습니다. 다만, 영속성 계층의 요구 사항에 맞춘 식별자는 도메인 모델의 일부가 아니므로, 영속성 개념이 외부로 누수되지 않도록 주의해야 합니다.

식별자를 설계한 후에는 엔티티가 가져야 할 최소한의 속성을 정의해야 합니다. 이를 위해 엔티티의 요구 사항을 검토하고, 필수적인 특성을 도출하여 속성을 정리합니다. 이때, 엔티티의 속성은 단순한 데이터 타입에 그치지 않고 도메인 개념을 잘 반영한 객체로 설계해야 합니다.

다음으로 엔티티의 행동을 정의해야 합니다. 이 과정에서는 도메인 언어에서 반복적으로 등장하는 행위를 명확히 정리하고, 이를 엔티티의 메서드로 구현합니다. 엔티티의 메서드는 단순한 세터나 게터에 그치지 않고, 유비쿼터스 언어에서 사용되는 표현을 충실히 반영해야 합니다.

엔티티는 도메인에서 발생하는 다양한 행위를 기반으로 설계되어야 하며, 데이터 모델을 도메인 모델로 직접 투영해서는 안 됩니다. 많은 ORM 도구가 도메인 모델이 영속성 저장소의 개념에 영향을 받지 않도록 돕지만, 그 기능이 항상 충분하지는 않습니다. 이로 인해 엔티티가 영속성 개념에 노출될 수 있으며, 이런 경우 데이터 모델과 엔티티를 명확히 분리하는 것이 필요합니다. 데이터 모델과 도메인 모델 간에 변환 계층을 추가하면, 데이터 저장 방식에 대한 책임을 덜어 복잡성을 줄이고 도메인 모델이 도메인 개념에 집중할 수 있습니다.

또한, 너무 많은 도메인 모델을 엔티티로 모델링하는 것을 피해야 합니다. 도메인 모델이 식별성을 가진 것처럼 보일 수 있지만, 그것이 데이터 기반의 사고에서 나온 것인지, 아니면 유비쿼터스 언어에서 정의된 특성인지 명확히 구분해야 합니다. 많은 도메인 모델이 영속성 저장소에서 식별 가능한 고유 키가 필요하다는 이유로 엔티티로 오해받지만, 실제로는 식별성을 유지할 필요가 없으며 엔티티가 요구하는 복잡한 추적과 검색이 필요하지 않을 수 있습니다.

값 객체 (Value Object)

도메인 모델이 식별성을 일관되게 유지할 필요가 없다면, 값 객체(VO, Value Object)로 모델링할 수 있습니다. 값 객체는 도메인 내에서 대상을 측정하거나 수량화하고 설명하는 데 사용됩니다. 값 객체는 식별자를 가지지 않으며, 다른 객체와의 비교 시 등가성을 기준으로 판단합니다. 또한, 값 객체는 불변성을 가지도록 설계되어 있어 동일한 속성을 가진 다른 객체로 쉽게 교체할 수 있습니다.

강한 연관성을 가지는 하나 이상의 속성이 모여 값 객체를 구성됩니다. 이러한 속성들은 개별적으로는 완전한 개념을 표현하지 못할 수 있지만, 함께 모여 값 객체를 이루면 응집력 있는 의미를 제공합니다. 즉, 값 객체는 도메인의 연관된 특성을 모은 필수 단위로, 개념적 전체를 나타냅니다. 값 객체는 주로 속성을 중심으로 모델링되지만, 단순한 데이터 홀더에 그쳐서는 안 됩니다. 엔티티와 마찬가지로 값 객체도 도메인 개념을 풍부하게 표현하고 유비쿼터스 언어를 충실히 반영해야 합니다.

값 객체는 변하지 않게 설계되어야 합니다. 값 객체의 메서드는 부작용 없이 (Side-Effect Free) 작성되어야 하며 생성 시점에 스스로 유효성을 검사하도록 설계되어야 합니다. 엔티티가 가진 값 객체가 변경될 경우에는 전체 값을 완전히 대체하는 새로운 객체를 사용하여 상태를 업데이트해야 합니다. 엔티티나 다른 값 객체는 자신의 속성을 표현하기 위해 값 객체를 포함할 수 있으며, 값 객체의 불변성 덕분에 엔티티나 값 객체의 무결성을 쉽게 유지할 수 있습니다. 값 객체는 불변하므로 여러 객체에서 손쉽게 공유될 수 있습니다.

객체 등가성을 통해 값 객체는 다른 객체와 비교될 수 있습니다. 값 객체는 자신의 속성을 기반으로 동일성을 판단하며, 타입과 모든 속성이 동일하다면 두 값 객체는 동일하다고 간주됩니다. 즉, 값 객체는 내부 상태가 같다면 동일한 것으로 간주됩니다. 동일한 속성을 가진 값 객체는 동일하기 때문에, 값 객체는 동일한 속성을 가진 다른 값 객체로 대체될 수 있습니다.

영속성 저장소에 값 객체를 저장할 때는 값 객체를 영속성 계층이 허용하는 타입으로 변환하여 저장해야 합니다. 값 객체를 소유한 엔티티는 값 객체의 속성을 개별적으로 저장할 수도 있고, 값 객체 전체를 하나의 값으로 인코딩하여 저장할 수도 있습니다. 때로는 값 객체가 영속성 저장소에 독립적으로 저장되어야 할 수 있으며, 이 경우 값 객체는 영속성 저장소에서 요구하는 대리 식별자를 가질 수 있습니다. 이러한 대리 식별자는 도메인 개념과는 무관하므로 외부로 노출되지 않아야 하며, 오직 영속성 저장소에서만 사용되어야 합니다. 대리 식별자가 존재하더라도 값 객체의 모든 특성은 그대로 유지되어야 합니다.

애그리게잇 (Aggregate)

강하게 연관된 엔티티와 값 객체는 동일한 애그리게잇(Aggregate)으로 묶여 일관성을 함께 유지해야 합니다. 애그리게잇은 공통의 루트 엔티티를 중심으로 구성된 긴밀한 객체 그래프로, 내부의 엔티티와 값 객체는 트랜잭션 일관성을 유지하며 관리됩니다.

바운디드 컨텍스트 내에서 트랜잭션 일관성이 필요한 경계를 식별해 애그리게잇을 정의할 수 있습니다. 애그리게잇은 트랜잭션 일관성을 보장하는 경계를 나타내며, 하나의 트랜잭션은 하나의 애그리게잇만 수정해야 합니다. 이 경계는 도메인의 핵심 개념을 포함하되 가능한 한 작게 유지되어야 합니다. 애그리게잇이 너무 커지면 트랜잭션 처리 시 과도한 잠금이 발생하여 시스템 확장성에 부정적인 영향을 미치며, 불필요한 엔티티나 값 객체까지 로드해야 할 수 있습니다.

애그리게잇 내에서 엔티티와 값 객체를 사용할 때, 영속성 저장소의 개념이 노출되어서는 안 됩니다. 엔티티와 값 객체를 지연 로딩하더라도, 이들을 담는 컨테이너는 영속성 저장소와 무관한 표준적이고 추상적인 자료형을 사용해야 합니다. 애그리게잇은 단순히 엔티티와 값 객체를 모으는 역할을 넘어, 이를 활용해 실제적인 도메인 연산을 수행해야 합니다.

애그리게잇은 다른 애그리게잇을 참조할 수 있지만, 이 참조는 반드시 다른 애그리게잇의 식별자를 통해 이루어져야 하며, 각 애그리게잇은 자신의 일관성 경계를 유지해야 합니다. 만약 애그리게잇 내에서 리포지토리를 사용해 다른 애그리게잇을 로드하면, 영속성 저장소의 개념이 애그리게잇에 침투하거나 일관성 경계가 혼란스러워질 수 있습니다. 이를 방지하기 위해 서비스 계층에서 리포지토리를 활용해 여러 애그리게잇을 로드하거나, 애그리게잇의 메서드에 필요한 다른 애그리게잇을 로드할 수 있는 의존성을 주입하는 것이 필요합니다.

애그리게잇 내부에서만 트랜잭션 일관성이 유지되기 때문에, 애그리게잇과 연관된 모든 외부 의존성을 항상 최신 상태로 유지하는 것은 어렵습니다. 만약 애그리게잇이 수정될 때 외부 리소스에서 추가적인 연산이 필요하다면, 결과적 일관성을 활용해야 합니다.

대규모 트래픽을 처리하는 마이크로서비스 아키텍처에서는 모든 연관된 리소스의 트랜잭션 일관성을 보장하기 어려운 경우가 많습니다. 이는 수많은 마이크로서비스 간의 트랜잭션 조정을 요구하며, 연산들이 강하게 결합되어 장애에 취약해지고, 연산이 완료될 때까지 기다리는 시간이 늘어나 사용자 경험에도 부정적인 영향을 미칠 수 있습니다. 대신, 애그리게잇은 도메인 이벤트를 비동기적으로 구독자에게 전파해 결과적 일관성을 유지하면서 내구성과 유연성을 높일 수 있습니다.

리포지토리 (Repository)

영속성 저장소에 저장된 애그리게잇은 리포지토리(Repository)를 통해 접근할 수 있습니다. 리포지토리는 애그리게잇을 영속성 저장소에 저장하고, 필요한 시점에 이를 메모리로 로드합니다. 메모리로 로드된 애그리게잇이 변경되면, 그 변경사항은 영속성 저장소에 반영되어야 합니다. 모든 영속성 애그리게잇은 리포지토리를 통해 관리되며, 일반적으로 애그리게잇과 리포지토리는 일대일로 대응됩니다. 그러나 애그리게잇이 계층 구조를 가질 경우, 여러 애그리게잇이 동일한 리포지토리를 공유할 수 있습니다.

애그리게잇은 직접적으로 접근할 수 있는 유일한 영속성 객체이므로, 애그리게잇 루트가 아닌 엔티티에 대해서는 리포지토리를 제공하지 않아야 합니다. 하위 엔티티에 접근할 때는 리포지토리를 통해 애그리게잇의 루트 엔티티를 먼저 조회한 후, 루트 엔티티가 제공하는 메서드를 사용해 접근해야 합니다. 만약 애그리게잇 내의 하위 객체에 대해 지연 로딩이 필요하다면, 애그리게잇이 이를 로드할 수 있는 리포지토리를 소유할 수 있습니다. 그러나 이 리포지토리는 애그리게잇의 맥락 내에서만 사용되어야 하며, 독립적으로 사용되어서는 안 됩니다.

리포지토리를 설계할 때 표준 컬렉션 인터페이스를 구현하거나 이를 유사하게 흉내내어 컬렉션 기반 리포지토리를 만들 수 있습니다. 이 방법은 영속성 개념이 도메인 모델에 침투하는 것을 막아, 개발자가 도메인 로직에 집중할 수 있도록 도와줍니다. 또한 다양한 구현체와의 호환성을 높여 유연한 설계가 가능합니다. 그러나 표준 컬렉션 인터페이스를 사용하는 방식은 영속성 저장소의 종류에 따라 구현이 복잡해질 수 있으며, 저장소가 제공하는 강력한 기능을 충분히 활용하는 데 제약이 있을 수 있습니다.

컬렉션 기반의 리포지토리를 사용하면, 메모리에 로드된 애그리게잇의 변경 사항이 자동으로 영속성 저장소에 반영되도록 해야 합니다. 이를 위해, 애그리게잇을 로드할 때 복사본을 반환하고, 커밋 시 원본과 복사본을 비교해 변경 사항을 추적하여 영속성 저장소에 반영할 수 있습니다. 또 다른 방법으로, 애그리게잇을 로드할 때 직접 반환하지 않고 프록시 객체를 반환하는 방식이 있습니다. 이 경우 애그리게잇이 수정되면 프록시는 원본을 직접 변경하지 않고 속성들의 복사본을 저장해 두며, 커밋 시점에 이 변경 사항을 영속성 저장소에 반영합니다.

영속성 저장소가 객체의 변화를 감지하고 추적하기 어렵거나 표준 인터페이스 이상의 기능이 필요할 때는, 영속성 지향 리포지토리를 활용할 수 있습니다. 이러한 리포지토리는 표준 컬렉션 인터페이스를 넘어서는 다양한 기능을 제공하며, 여러 영속성 저장소에서 더 쉽게 지원할 수 있는 유연한 인터페이스를 제공합니다. 하지만 영속성 지향 리포지토리가 영속성 저장소의 세부 구현을 과도하게 노출해서는 안 됩니다. 호환성을 유지하기 위해 다양한 영속성 저장소에서 공통적으로 지원하는 추상적인 기능들만을 노출하도록 인터페이스를 설계해야 합니다.

데이터 액세스 객체(DAO, Data Access Object)와 리포지토리가 모두 영속성 저장소를 추상화하여 개념이 자주 혼동될 수 있습니다. 데이터 액세스 객체는 보통 데이터베이스 테이블과 직접 상호작용하며, 데이터 중심의 CRUD 인터페이스를 제공합니다. 반면, 리포지토리는 도메인 모델 중심으로 설계되어 데이터 중심 접근을 지양합니다. 애그리게잇에서 데이터 액세스 객체를 사용하면 데이터 중심의 CRUD 연산을 애그리게잇이 직접 처리하게 되어, 도메인의 비즈니스 로직을 넘어 영속성 저장소의 세부적인 개념까지 다루게 됩니다. 리포지토리는 데이터 액세스 객체와 유사하지만, 데이터 중심보다는 컬렉션 지향으로 설계되어야 하며 도메인 모델과 비즈니스 로직에 집중할 수 있도록 지원해야 합니다.

서비스 (Service)

엔티티나 값 객체에서 수행하기에 적절하지 않은 작업이 있을 수 있습니다. 만약 이러한 작업이 해당 엔티티나 값 객체의 자연스러운 책임으로 보기 어렵다면, 이를 독립적인 연산으로 분리하여 서비스(Service)로 모델링할 수 있습니다. 서비스는 유비쿼터스 언어를 반영하며, 상태를 가지지 않는 방식으로 정의되어야 합니다.

서비스는 중요한 비즈니스 프로세스를 수행하거나 도메인 모델을 변경하여 다른 컴포넌트로 전달하거나, 여러 도메인 모델을 조정하는 데 유용합니다. 이러한 작업은 특정 절차를 중심으로 이루어지기 때문에, 서비스는 보통 무상태로 작성됩니다. 하지만, 서비스가 정적으로 결정된 의존성만을 사용해야 한다는 뜻은 아닙니다. 서비스는 다른 서비스나 리포지토리뿐만 아니라 다양한 동적으로 변하는 의존성을 가질 수 있으며, 이를 통해 보다 풍부한 기능을 제공할 수 있습니다.

도메인 개념을 서비스로 과도하게 모델링하는 것은 바람직하지 않습니다. 서비스를 남발하면 도메인 개념이 엔티티나 값 객체에 제대로 녹아들지 못하고, 서비스에만 집중되면서 엔티티나 값 객체가 단순히 데이터를 전달하는 역할로 전락할 수 있습니다. 이는 도메인 모델이 빈약해지는 결과를 초래할 수 있습니다. 도메인 개념을 서비스로 구현하기 전에, 먼저 해당 개념을 엔티티나 값 객체에 녹일 수 있는지 충분히 검토해야 합니다. 만약 독립된 객체로 만드는 것이 필요하다면, 이를 유비쿼터스 언어로 명확히 정의하고, 서비스가 도메인 개념을 풍부하게 표현할 수 있도록 설계해야 합니다.

애플리케이션 (Application)

여러 바운디드 컨텍스트가 모여 사용자에게 실질적인 가치를 제공하는 애플리케이션을 구성합니다. 애플리케이션은 다양한 바운디드 컨텍스트의 유스케이스를 조정하여 핵심 도메인에 인증 및 권한 관리와 같은 추가 기능을 통합하고, 이를 통해 사용자 인터페이스를 제공합니다.

도메인 모델은 사용자가 이해하기 쉬운 방식으로 변환되어 표현됩니다. 사용자에게 제공되는 데이터는 도메인 내부에서 작업을 처리하기 위해 사용하는 데이터보다 많을 수 있습니다. 데이터가 여러 애그리게잇에 분산되어 있을 경우, 이를 하나의 뷰로 제공하기 위해 여러 애그리게잇에서 데이터를 읽어 하나의 객체로 조합해야 합니다. 이와 같은 데이터 전달을 목적으로 렌더링된 객체를 데이터 전송 객체(DTO, Data Transfer Object)라고 부릅니다.

애그리게잇은 사용자 인터페이스에 의존해서는 안 됩니다. 사용자 인터페이스에서 데이터를 쿼리하다 보면 애그리게잇의 내부 구조가 지나치게 노출될 수 있습니다. 이로 인해 애그리게잇이 사용자 인터페이스의 요구에 맞추어 데이터 형식이 영속성 저장소에 그대로 반영될 수 있습니다. 결국, 사용자 인터페이스에서 유래한 데이터 형식이 애그리게잇을 거쳐 영속성 저장소까지 영향을 미쳐 모든 계층이 강하게 결합되고, 애그리게잇의 표현력이 제한될 수 있습니다.

애그리게잇 다수에 대한 통계나 도메인 연산에서 외부로 직접 노출할 필요가 없는 데이터를 필요로 하는 경우, 애그리게잇을 통해 데이터를 얻기 어려울 수 있습니다. 이런 상황에서는 리포지토리에서 적절한 값 객체를 제공받을 수 있으며, 이 값 객체는 애플리케이션이 아닌 도메인 중심으로 작성되어야 합니다.

데이터 조회가 도메인에서 중요한 역할을 한다면, 읽기와 쓰기 작업을 분리하는 CQRS(Command and Query Responsibility Segregation) 패턴을 고려해볼 수 있습니다. CQRS는 읽기와 쓰기의 책임을 분리함으로써 복잡한 도메인 모델을 단순화된 여러 모델로 관리할 수 있게 합니다. 관심사를 분리함으로써 읽기와 쓰기 모두에 유연하고 단순한 모델을 사용할 수 있으며, 쓰기 연산의 부수적인 효과를 쉽게 제거하여 연산의 추적, 재시도, 취소 등을 용이하게 처리할 수 있습니다.

애플리케이션은 여러 바운디드 컨텍스트에 대한 클라이언트 역할을 하여 이를 통합된 뷰로 구성합니다. 사용자 유즈케이스를 지원하고 바운디드 컨텍스트를 조정하는 객체를 애플리케이션 서비스 (Application Service)라고 하며, 최대한 얇게 유지되어야 합니다. 애플리케이션 서비스는 여러 바운디드 컨텍스트를 조정하여 사용자 인터페이스를 제공하고 필요한 인프라 기능을 지원하는 데 집중합니다.

애플리케이션은 인프라 기능을 사용하지만, 그 세부적인 구현 사항에 직접 접근할 필요는 없습니다. 애플리케이션은 인프라 개념이 포함되지 않은 추상적인 도메인 모델에 의존하되, 이 도메인 모델의 인터페이스는 특정 인프라에 맞추어 구현될 수 있습니다. 애플리케이션은 외부에서 주입받은 의존성을 통해 인프라에 접근하므로 인프라에 대해 직접 알 필요는 없습니다.

아키텍처 (Architecture)

도메인은 각기 다른 비즈니스 영역을 가지고 있습니다. 이러한 비즈니스의 특성 때문에 바운디드 컨텍스트는 서로 다른 비기능적 요구사항을 가지게 되며, 함께 배포하기 어려울 수 있습니다. 따라서 바운디드 컨텍스트는 고유한 특성에 맞는 적절한 아키텍처(Architecture)를 가져야 하며, 서로 다른 특성을 가진 바운디드 컨텍스트는 각기 별도의 배포 단위로 분리될 수 있습니다.

바운디드 컨텍스트의 아키텍처를 결정하기 위해서는 해당 도메인을 분석하여 아키텍처 특성을 도출해야 합니다. 아키텍처 특성은 설계의 구조적 결정에 영향을 미치는 요소로, 비즈니스에서 비롯된 비기능적 고려사항을 명시합니다. 이러한 특성은 바운디드 컨텍스트가 해결해야 하는 핵심 문제와 직결되므로 프로젝트의 성공에 큰 영향을 미칩니다. 모든 특성을 지원하는 범용 아키텍처를 설계하면 구조가 지나치게 복잡해져, 비즈니스 문제를 해결하는 데 필요한 역량을 충분히 발휘하지 못할 수 있습니다. 따라서 비즈니스 성공에 필수적인 최소한의 특성만 도출하여 설계를 최대한 단순하게 유지해야 합니다.

기능적 응집도가 높고 동기적 호출 관계를 가진 바운디드 컨텍스트들은 아키텍처 특성을 공유하기 때문에 동일한 배포 단위로 묶을 수 있습니다. 그러나 아키텍처 특성뿐만 아니라 바운디드 컨텍스트를 관리하는 조직 간의 관계도 함께 고려해야 합니다. 서로 독립적인 조직이 관리하는 바운디드 컨텍스트는 서로 다른 운영 주체를 가지므로 동일한 배포 단위로 묶는 데 많은 조율이 필요할 수 있습니다.

바운디드 컨텍스트는 일반적으로 여러 배포 단위로 나뉘며, 각 배포 단위는 고유한 특성을 지니고 이에 맞는 아키텍처를 갖추게 됩니다. 이러한 이유로 서비스의 전반적인 구조는 종종 마이크로서비스 아키텍처를 따르게 됩니다. 그러나 이렇게 분산된 아키텍처는 완전히 신뢰하기 어렵고, 제한된 대역폭의 불안정한 네트워크를 통해 통신하기 때문에 로컬 호출보다 비용이 높고 안정성이 낮습니다. 따라서 다른 서비스에 대한 접근은 항상 실패할 가능성을 염두에 두어야 하며, 한 서비스에서 발생한 장애가 다른 서비스로 전파되지 않도록 격리해야 합니다. 높은 안정성과 유연성을 확보하기 위해 동기 호출보다는 비동기 호출을 선호해야 하며, 트랜잭션 일관성보다는 결과적 일관성을 통해 무결성을 유지하는 것이 중요합니다.

마이크로서비스 아키텍처는 각 바운디드 컨텍스트가 적합한 아키텍처를 선택할 수 있도록 하여, 바운디드 컨텍스트 간의 커플링을 제거함으로써 확장 가능하고 유연한 설계를 구현할 수 있게 합니다. 그러나 모든 서비스에 공통적으로 적용되어야 하는 모니터링, 로깅, 회로 차단기와 같은 관심사도 존재합니다. 이러한 관심사를 모든 서비스에 일관되게 적용하는 것은 애플리케이션의 복잡도를 줄이는 데 도움이 됩니다. 이와 같은 횡단 관심사는 사이드카(Sidecar) 패턴을 활용하여 모든 서비스에 공통적으로 적용할 수 있습니다. 각 서비스에 공통적인 관심사를 처리하는 별도의 사이드카 컴포넌트를 함께 배포하고, 이 사이드카들을 동일한 서비스 플레인에 연결하여 서비스 메쉬를 구성함으로써, 일관된 사용자 인터페이스를 통해 모든 서비스에 필요한 기능을 제공할 수 있습니다.

참고 문서

댓글남기기