치춘짱베리굿나이스
도메인 주도 설계와 아키텍처 본문
내용이 굉장히 방대하고 어려워서 간략하게 메모 느낌으로만 적는 게시글…
나중에 책을 사서 읽던지 해야 할 것 같다
- 도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지
- 클린 아키텍처
- 도메인 주도 설계란 무엇인가? 쉽고 간략하게 이해하는 DDD
도메인 주도 설계
의미
도메인별로 나누어 설계하는, 도메인이 중심이 되는 설계 방식
도메인의 복잡한 문제를 모델링하고 구현할 수 있다
큰 도메인을 전략적 설계를 이용해서 작은 도메인과 컨텍스트 모델로 분류하고, 작은 모델들을 전술적 설계를 이용해서 실체화하는 일련의 과정을 도메인 주도 설계라고 부른다
- 도메인 그 자체와 도메인 로직에 초점을 맞춘다
- 데이터 중심의 접근법 탈피
- 유비쿼터스 언어를 사용한다
- 도메인 전문가와 소프트웨어 설계자, 개발자 간의 커뮤니케이션 문제를 없앤다
- 모든 문서와 코드를 동일한 표현과 단어로 구성하게끔 단일화된 언어체계를 구축한다
- 소프트웨어 엔티티와 도메인 컨셉을 가능한 가까이 일치시킨다
- 도메인 모델부터 코드까지 함께 움직이는 구조의 모델
도메인
사전적 의미로 ‘영역, 범위’ 를 뜻한다
구현해야 할 대상, 해결해야 할 문제의 영역
- 게시글, 댓글, 사용자 등등 하나하나가 도메인이라고 볼 수 있다
- 그 도메인의 하위에 게시글 작성 및 삭제 등의 이벤트와 게시글 작성자 등, 서브도메인이 존재한다
상위 도메인은 여러 개의 하위 도메인을 가질 수 있고, 각자가 기능을 제공한다
- 예를 들어, ‘게시글’ 의 하위 도메인은 아래와 같은 것들이 올 수 있다
- 게시글 작성
- 게시글 열람
- 게시글 삭제
- 게시글 수정
- 작성자
- 댓글
- 게시글 좋아요
- 하위 도메인은 서브도메인 이라고도 한다
전략적 설계
Strategic Design
복잡한 도메인의 컨텍스트 경계를 명확히 정의하는 과정
후술할 이벤트 스토밍을 통해 도메인에 대해 빠르게 이해하고 서브도메인들로 구성된 바운디드 컨텍스트를 분류한 뒤 서로의 관계를 매핑하는 과정이다
도메인 내에서 서브도메인을 파악하는 단계라고 할 수 있다
도메인 전문가
해당 도메인을 잘 아는 전문가
비행 항로 제어 시스템을 구축한다 ⇒ 항공 교통 관제사가 도메인 전문가가 된다
쇼핑몰 시스템을 구축한다 ⇒ 쇼핑몰 운영자 또는 판매자가 도메인 전문가가 된다
개발 주체들과 도메인 전문가들은 끝없이 이야기를 나누면서 지식을 교환하고, 이는 정제되어 도메인 모델의 밑거름이 된다
이벤트 스토밍
구성원 (팀원) 들이 함께 도메인을 학습하기 위한 브레인스토밍 방식
도메인 전문가, 개발자, 설계자들이 큰 벽에 형형색색의 포스트잇을 붙여가면서 수행한다고 한다
- 생각나는 모든 이벤트를 주황색 포스트잇으로 작성해서 붙인다
- 이벤트 발생 순서대로 왼쪽 → 오른쪽으로 부착하고, 동시 수행되는 이벤트는 위 → 아래로 부착한다
- 이벤트가 발생하는 커맨드를 파란색 포스트잇으로 작성해서 붙인다
- 이벤트가 발생하는 주체 (사용자 또는 역할) 를 노란색 포스트잇으로 작성해서 붙인다
- 애그리거트를 정의한다
- 바운디드 컨텍스트를 정의한다
- 바운디드 컨텍스트 간 관계를 묘사한다 (컨텍스트 맵)
유비쿼터스 언어
사용자, 개발자, 프로젝트 매니저, 디자이너… 등등이 보편적으로 사용할 용어
도메인 전문가와 개발자는 의사소통에 장벽이 발생할 수밖에 없다
- 도메인 전문가는 자신의 분야에서 전문가이고, 전문용어를 사용하는 경우가 많다
- 개발자는 세상을 프로그램적으로 바라보는 경향이 있다
서로 다른 용어를 사용함으로써 발생하는 불필요한 커뮤니케이션을 줄이고, 도메인의 구조를 쉽게 파악하는 데에 도움을 준다
- 같은 대상을 사용자, 유저, 프로필 등 다른 용어로 지칭할 수 있다
- 유비쿼터스 언어는 이를 하나로 통합하는 역할을 한다
유비쿼터스 언어는 설계의 모든 분야에 관여한다
- 어색하거나 오해를 일으키는 용어를 사용하면 안 된다
- 모호함과 불일치를 줄여야 한다
모델 (= 도메인 모델)
소프트웨어와 도메인이 교차하는 지점이자, 도메인의 체계화된 표현
도메인 모델을 잘 구성하는 것이 소프트웨어 설계의 기반이자 출발점이 된다
대부분의 의사소통이 모델 기반으로 이루어진다
- 정보를 조직화, 체계화하고 특정 부분을 과감히 제외하는 등, 도메인에서 불필요한 정보를 쳐내고 정제하는 과정이 필요하다
컨텍스트
상황, 맥락, 문맥
도메인이 가지는 맥락을 의미한다
바운디드 컨텍스트
도메인들을 고유한 비즈니스 맥락 별로 묶어둔 것이다
바운디드 컨텍스트 안에서는 동일한 유비쿼터스 언어를 사용하며, 바운디드 컨텍스트는 유비쿼터스 언어의 개념이 달라지는 지점을 기준으로 나뉘어진다
- 게시물 도메인에서의 사용자는 작성자를 의미한다
- 로그인 / 회원가입 (Auth) 도메인에서의 사용자는 로그인하는 주체를 의미한다
- 프로필 도메인에서의 사용자는 프로필의 주인을 의미한다
- 게시물 - Auth - 프로필 도메인이 바운디드 컨텍스트라고 할 수 있다
바운디드 컨텍스트끼리는 컨텍스트간 결합이 느슨하기 때문에, 바운디드 컨텍스트 하나당 마이크로서비스 하나씩을 구성하기 쉽다
컨텍스트 맵
서로 다른 분할된 컨텍스트 (바운디드 컨텍스트) 와 그들 간의 관계를 표현한 문서
컨텍스트를 분할하긴 했지만 결국 컨텍스트들 하나하나가 모여 하나의 소프트웨어 (시스템) 를 구성하여 적절하게 동작하는 것이 중요하다
전술적 설계
Tactical Design
전략적 설계에서 만든 바운디드 컨텍스트의 내부를 모델링하는 과정
애그리거트, 엔티티, 값 객체 등을 구성하고 실제로 구현하는 과정이 여기에 포함된다
전략적 설계와 달리 전술적 설계는 개발을 진행하는 과정에서 반복적으로 수행되고 끝없이 개선된다
엔티티
사전적 의미로 ‘실재, 본체, 객체’ 를 뜻한다
업무 (개발) 에 필요한 정보를 저장하고 관리하기 위한 집합적인 개념 (추상적) 이라고 볼 수 있다
- 유일한 식별자를 가지고 있어야 한다
- 이 식별자는 인스턴스들을 구분하는 데에 이용된다
- 회원 ID, 전화번호, 이메일 등이 주로 사용된다
- 2개 이상의 인스턴스가 존재해야 한다
- [동물 엔티티]: [강아지], [고양이], [독수리] 등…
- 속성을 가지고 있어야 한다
- [동물 엔티티]: [분류], [다리 개수], [초식 / 육식 여부] 등…
소프트웨어가 여러 상태를 거치는 동안 ‘동일한 값을 유지하는’ 식별자를 가진 객체이다
- 연속성과 식별성을 지닌다
값
value
값 그 자체이다
데이터를 표현하기 위한 객체라고 생각하자
엔티티와 다르게 식별자가 없으며, 불변하다
식별자와 변경이 필요한 특정 객체만을 엔티티로 만들고, 나머지는 값 객체로 만드는 것이 설계를 단순화하는 방법이다
값 객체는 쉽게 생성 및 폐기가 가능하며, 수정은 불가능하다
값은 단순하게 유지하자
애그리거트
Aggregate
비슷한 객체들끼리의 묶음, 객체들을 대표하는 추상화된 객체
- 예를 들어, 사용자명이나 이메일 같은 값과 사용자라는 엔티티는 사용자 애그리거트로 묶일 수 있다
애그리거트 안의 엔티티 중 해당 애그리거트를 대표하는 엔티티는 애그리거트 루트 (Aggregate Root) 가 된다
- 엔티티 중 다른 객체들과 가장 연관성이 높은 엔티티가 애그리거트 루트가 된다
- 보통 애그리거트 안에는 애그리거트 루트 단 한 개의 엔티티만 존재한다 (나머지는 값 객체?)
- 한 애그리거트가 다른 애그리거트에 접근하기 위해서는 루트 엔티티를 통할 수밖에 없다
애그리거트 내의 객체들은 비슷한 생명주기를 갖는다 (생성, 삭제, 수정의 타이밍)
팩토리
복잡한 엔티티 또는 애그리거트를 생성하는 주체
객체를 생성하는 절차와 필요한 지식 자체를 캡슐화한 개념이 바로 팩토리이다
복잡한 객체 생성 절차를 다른 객체에 맡기고자 팩토리를 사용한다
- 객체 생성에 관여한다
레포지토리
대부분의 객체는 데이터베이스의 접근 및 조회를 통해 얻어낼 수 있는데, 이러한 접근 방식은 오히려 데이터베이스에서 불필요하거나 은닉되어야 하는 정보에까지 접근할 수 있는 가능성이 있어 도메인적인 관점에서 좋지 않다
전역 인터페이스를 통해 접근 가능한 레포지토리를 작성하고, 이를 통해서만 도메인 객체의 참조를 얻어올 수 있게 한다
- 외부에서는 데이터 저장소에 직접 접근할 수 없고, 레포지토리를 통해서만 접근 가능하기 때문에 관심사 분리에 탁월하다
- 도메인 모델은 객체의 저장이나 참조와 연관성이 사라진다
- 하부의 영속성을 보장하는 인프라스트럭처에 접근할 필요가 없어진다
- 인터페이스는 단순하게 유지되는 것이 좋다
- 이미 생성된 객체를 대상으로 한다
계층형 아키텍처
소스 코드의 역할과 관심사에 따라 계층으로 분리한 아키텍처
한 계층 당 하나의 관심사에만 집중할 수 있도록 하는 아키텍처이다
계층형 아키텍처의 레이어 구성
- 표현 영역 (Presentation / User Interface Layer)
- 사용자의 요청 / 응답 등 사용자와 직접 상호작용하는 계층
- View, Controller 등이 이에 속한다
- 프론트엔드라면 사용자 인터페이스를 구성하고 상호작용을 수행하는 계층이다
- 백엔드라면 사용자 (클라이언트) 로부터 요청을 받고 응답을 반환한다
- 응용 영역 (Application / Business Layer)
- 기능적인 요구 사항을 수행하는 데 필요한 것들을 처리한다
- 비즈니스 로직은 포함하지 않는다 (비즈니스 로직은 도메인 영역에서…)
- 도메인 객체 간 실행 흐름을 제어하고, 모델의 정합성을 검증한다
- 도메인 영역 (Domain Layer)
- 핵심 비즈니스 로직이 담기는 곳
- 어떠한 외부 관심사에도 의존하지 않으며, 순수한 비즈니스 로직만을 담아야 한다
- 인프라스트럭처 영역 (Infrastructure / Persistence / Database Layer)
- 데이터베이스에 직접 접근하여 데이터를 관리하는 역할을 한다
- 상위 계층에 기술적인 부분을 지원해주기도 한다
- 라이브러리와 비슷한 느낌이라고 생각하면 된다
특징
- 각 계층 내부적으로는 응집도가 높아야 한다
- 다른 계층과는 낮은 결합도를 가져야 한다 (느슨한 결합)
- 상위 계층은 하위 계층을 이용할 수 있지만, 하위 계층은 상위 계층에 무엇이 있는지 알지 못해야 한다
- 격리성 (상위 계층에서 하위 계층으로 이동할 때, 한번에 두세 계층을 뛰어내려갈 수 없으며 무조건 한 단계씩 거쳐서 내려가야 한다) = 닫혀 있다
계층화의 이유
- 관심사 분리
- 한 계층에서 변경이 발생했을 때, 다른 계층에선 영향을 받지 않아야 한다
- 코드의 재사용성과 유지보수성을 향상시킨다
- 유연함
- 각 계층이 독립적으로 개발, 확장, 변경이 가능하므로 유연하다고 볼 수 있다
- 테스트 용이성
- 각 계층별로 테스트를 독립적으로 수행할 수 있어서 좋다
클린 아키텍처
엄청 많이 볼 수 있는 그 사진
계층형 아키텍처만으로는 다양한 인터페이스를 감당하는 데에 한계가 있고, 이를 해결하기 위해 클린 아키텍처와 육각형 아키텍처를 많이 도입하는 편이다
밥아저씨 (로버트 마틴) 의 블로그에서 처음 제시되었다
클린 아키텍처는 종속성 규칙을 준수하여 계층적으로 기능을 분리하는 기법이다
- 종속성 규칙: 각 코드의 종속성은 외부에서 내부로만 (안쪽 방향으로) 가리킬 수 있다
- 고수준 정책이 저수준 정책의 변경에 영향을 받으면 안 된다
- 고수준 정책이 저수준 정책에 의존하면 안 된다 = 의존성 역전 원칙
- 고수준 정책: 비즈니스 영역과 엔티티 등, 거의 변하지 않는 정책들
- 저수준 정책: UI, 인프라 등 변경점이 자주 일어나는 정책들
클린 아키텍처의 레이어 구성
- Enterprise Business Rules (Domain Layer)
- 어플리케이션의 주제를 표현하는 엔티티와, 엔티티를 조작하는 코드가 포함된다
- 어플리케이션을 구성하는 핵심적인 요소가 여기 들어간다고 생각하면 된다
- 도메인은 전체 프레임워크가 바뀌거나 다른 레이어들이 다 변경되어도 변하지 않는 핵심적인 (코어한) 부분이라고 생각하자
- Application Business Rules (Application Layer)
- Use Cases 가 들어있는 레이어
- 유즈케이스: 유저의 시나리오
- 버튼을 누르면 알림이 나온다, 폼에 입력하면 값을 검증한다 등
- 어플리케이션 규모의 비즈니스 규칙을 포함한다
- 포트를 포함한다
- 어플리케이션이 외부와 어떻게 소통하기를 원하는지에 대한 명세
- 인바운드 포트: 외부에서 어플리케이션으로 들어오는 데이터에 대한 명세
- 아웃바운드 포트: 어플리케이션에서 외부로 나가는 데이터에 대한 명세
- Use Cases 가 들어있는 레이어
- Interface Adapters (Adapter Layer)
- 외부 서비스와 연결되는 어댑터를 포함한다
- 호환되지 않는 외부 서비스 API들을 필요에 맞게 변환하는 역할
- 바깥 계층 (웹, UI 또는 DB 등) 에서 사용하기 편리하도록, 이전 계층 (어플리케이션 계층, 도메인 계층) 에서 데이터를 변환하는 어댑터들이 포함된다
- 어댑터
- driving 어댑터 (인바운드): 외부에서 어플리케이션 방향으로 들어오는 데이터의 어댑터 (예시: UI에서의 상호작용)
- driven 어댑터 (아웃바운드): 어플리케이션에서 외부 방향으로 나가는 데이터의 어댑터 (예시: 서비스 API)
- 외부 서비스와 연결되는 어댑터를 포함한다
- Frameworks & Drivers
- 프레임워크, 데이터베이스, 웹 서버 등 외부 도구적인 요소들이 담기는 곳이다
- 가장 의존도가 낮은 계층이다
클린 아키텍처의 특징, 원칙
- 바깥쪽 레이어가 안쪽 레이어에 의존성을 지닌다
- 반드시 지킬 필요는 없지만 가급적 그렇게 하는 편이 낫다
- 의존성의 방향을 제대로 통제하지 않으면 복잡하고 어려운 코드가 될 수 있다
- 내부 레이어는 외부 레이어를 모르게 해야 좋으며, 가급적 영향을 받지 않도록 해야 한다
- 내부 레이어가 가장 추상화된 영역으로, 바깥 영역으로 향할 수록 세부 사항이 구현된다
- 내부 레이어로 갈 수록 변경 가능성이 낮아야 한다
- 프레임워크에 의존적이지 않다 (UI, 데이터베이스 등…)
- 프레임워크는 도구일 뿐
- 클린 아키텍처에서 Use Cases 가 비즈니스 로직을 담당하므로, 전체 어플리케이션에서 핵심을 맡는다
- SOLID 를 따르며, 이 중에서도 의존성 역전 원칙이 가장 중요하다
육각형 아키텍처
포트와 어댑터 아키텍처라고도 불린다
- 어댑터를 통해 어플리케이션의 코어 부분과 외부 세계를 연결한다
- Incoming Adapter: 외부에서 어플리케이션으로 데이터를 전달 (UI 등)
- Outgoing Adapter: 어플리케이션에서 외부로 데이터를 전달 (서버 API 호출 등)
- 포트를 통해 애플리케이션 코어의 경계를 정의한다
- Incoming Port: 외부로부터의 데이터가 어플리케이션 코어로 들어오는 경로 정의
- Outgoing Port: 어플리케이션 코어로부터 외부로 서비스를 제공하는 경로 정의
- 애플리케이션 코어는 어댑터 덕에 외부와의 결합도를 최소화한다
그외
고수준 모듈, 저수준 모듈 (or 고수준 정책, 저수준 정책)
- 고수준 모듈: 의미 있는 기능을 제공하는 모듈
- 저수준 모듈: 고수준의 모듈을 제공하기 위해 필요한 하위 모듈
SOLID
- 단일 책임 원칙 (Single Responsibility Principle)
- 한 클래스는 하나의 책임 (기능) 만 가져야 한다
- 한 클래스가 여러 책임을 가지고 있다면, 기능 변경 시에 수정해야 할 코드가 연쇄적으로 많아질 수 있다
- 책임 영역이 확실해지기 때문에, 연쇄적인 책임 변경의 위험에서 자유로울 수 있다
- 프로그램의 유지보수성을 높여 준다
- 개방 - 폐쇄 원칙 (Open - Closed Principle)
- 소프트웨어의 요소는 확장에는 열려 있지만, 변경에는 닫혀 있어야 한다
- 기능을 추가할 때, 클래스 확장 등을 통해 구현하되, 클래스를 직접 변경하는 것은 최소화하라는 뜻
- 리스코프 치환 원칙 (Liskov Substitution Principle)
- 자식 객체는 부모 객체를 대체 (치환) 할 수 있어야 한다
- 부모 클래스인 A를 자식 클래스인 B로 치환했을 때 프로그램이 오작동 없어야 한다는 뜻
- 인터페이스 분리 원칙 (Interface Segregation Principle)
- 자신이 이용하지 않는 메서드에 의존하면 안 된다
- 클라이언트가 꼭 필요한 메서드만 이용할 수 있어야 한다
- 클라이언트 입장에서 사용하고 싶은 기능만 이용할 수 있도록 인터페이스를 잘 분리해야 한다는 뜻
- 의존성 역전 원칙 (Dependency Inversion Principle)
- 고수준 모듈은 저수준 모듈에 의존하면 안 된다
- 고수준 모듈과 저수준 모듈 모두 추상화에 의존해야 한다
- 저수준 모듈은 자주 변경되므로 직접 의존하면 안 된다
- 대신 저수준 모듈을 추상화한 계층에 의존하는 것이 좋다
- 추상에 의존하며 구체에는 의존하면 안 된다
- 추상화는 세부 사항에 의존하면 안 되고, 세부 사항이 추상화에 의존해야 한다
- 자신보다 변하기 쉬운 것 (구체) 에 의존하지 말라는 뜻
- 변하기 쉬운 것에 영향을 받지 않도록 하고, 변화가 비교적 적은 추상 클래스에 의존하라는 것
- 고수준 모듈은 저수준 모듈에 의존하면 안 된다
의존성 역전 예시
function 밥먹기(식기) {
if (식기 === 숟가락) 숟가락_들기();
else if (식기 === 포크) 포크_들기();
else if (식기 === 젓가락) 젓가락_들기();
else 맨손();
}
고수준 모듈은 저수준 모듈에 의존하면 안 되고, 고수준 모듈과 저수준 모듈은 모두 추상화에 의존해야 한다
위의 예시에서 밥 먹기
가 저수준 모듈인 숟가락 들기
에 의존하면 안 된다
저수준 모듈인 포크 들기
, 젓가락 들기
등등이 추가될 때마다 고수준 모듈이 계속 변경되는 것을 볼 수 있다
class 식기 {
...
들기() {
팔_뻗기();
손가락_구부리기();
}
}
function 밥먹기(식기) {
식기.들기();
}
대신 추상화 모듈인 식기
를 추가하였다
식기
에 포크와 젓가락 뿐만 아니라 나이프, 티스푼, 국자 등등이 추가되어도 밥먹기
고수준 모듈은 변경되지 않는 것을 볼 수 있다
의존성 주입
Dependency Injection
객체가 필요로 하는 어떤 것을 외부에서 내부로 전달 (주입) 해주는 것
class User {
constructor() {
this.beverage = new Coffee();
}
}
내가 오늘 마실 음료가 커피일 수도 있고 밀크티일 수도 있고 그냥 물일 수도 있다
클래스 내부에서 Coffee
객체를 생성해 버리면 User
클래스는 Coffee
에만 의존하게 된다
class User {
constructor(beverage: Beverage) {
this.beverage = beverage;
}
}
const me = new User(new Milktea());
외부에서 음료를 주입받으면 특정한 음료에만 의존하는 것이 아니라 여러 음료를 인자로 받아올 수 있다
- 장점
- 코드의 재사용성과 유연성이 높아진다
- 의존성이 줄어들면서, 객체간 결합도가 낮아져 한 코드를 수정했을 때 다른 코드도 수정해야 할 일이 줄어든다
- 유지보수 및 테스트 용이
참고 자료
https://happycloud-lee.tistory.com/94
https://coding-factory.tistory.com/870
https://velog.io/@maketheworldwise/DevOps-도메인-주도-설계
https://ittrue.tistory.com/252
https://devfunny.tistory.com/869
https://blog.bespinglobal.com/post/domain-driven-design-1부-strategic-design/
https://www.nextree.co.kr/p6960/
https://tecoble.techcourse.co.kr/post/2021-04-27-dependency-injection/
https://tech.kakao.com/2022/12/12/ddd-of-recommender-team/
https://daryeou.tistory.com/280
https://blog.toktokhan.dev/요즘-대세-clean-architecture-67b80df66c6
'이론적인 부분들 > 기타' 카테고리의 다른 글
블로킹, 논블로킹, 동기, 비동기 (0) | 2023.09.18 |
---|---|
디자인 패턴이란 (0) | 2022.09.24 |