도메인 주도 설계 철저 입문

2. 값 객체

  • 시스템 특유의 값을 표현하기 위해 정의하는 객체
  • 특정 값을 원시 데이터 타입이 아니라 객체(클래스)로 표현
  • 값의 성질
    • 변하지 않는다: 한 번 생성된 값을 변경할 수 없다
    • 주고받을 수 있다: 값을 변경할 때는 새로운 값을 생성해서 대입한다
    • 등가성을 비교할 수 있다: 같은 타입의 값들은 비교할 수 있다. 이 때 값 객체의 필드를 직접 비교하는 것이 아니라 비교 메서드를 가진다
  • 값 객체로 만드는 기준
    • 규칙이 존재하는가: 이름의 글자 수 또는 문자 제한이 있는 경우
    • 낱개로 다루어야 하는가
  • 독자적인 행동을 메서드로 만들 수 있다
    • 데이터와 행동을 한 곳에 모아두는 효과
    • 객체가 할 수 있는 행위를 정의
  • 값 객체를 사용함으로써 얻는 장점
    • 표현력이 증가: 여러 필드를 가지는 객체가 원시 데이터보다 데이터 표현력이 좋다
    • 무결성 유지: 값의 유효성 검사가 쉽다
    • 잘못된 대입을 방지: 다른 타입 변수에 대입하는 것을 컴파일 타임에 방지
    • 로직이 흩어지는 것을 방지: 유효성 검사, 행동(메서드)를 한 곳에 모아서 반복을 방지(DRY)

3. 엔티티

  • 도메인 모델을 구현한 도메인 객체를 의미
  • 값 객체와 차이점
    • 값 객체는 속성(필드)를 통해 식별되고, 엔티티는 동일성을 통해 식별된다
      • 값 객체는 속성이 같으면 동일하다
      • 엔티티는 식별자(identity)를 통해 구분한다
    • 값 객체는 불변이고, 엔티티는 가변이다
  • 엔티티의 판단 기준 - 어떤 것을 엔티티로 정의할 것인가
    • 생애주기와 연속성
      • 시간이 흐름에 따라 변화를 겪으며 태어나고 죽을 수 있는 객체
  • 완전히 같은 개념이라도 시스템에 따라 값 객체로 만들어야 할 수도 있고 엔티티로 만들 수도 있다
  • 도메인 객체를 정의할 때의 장점
    • 자기 서술적인 코드가 된다
      • 도메인 모델에 관한 규칙을 쉽게 이해할 수 있다
    • 도메인에 변경사항이 있을 시 코드에 반영하기 쉽다
      • 도메인에 일어난 변경을 코드에 쉽게 반영할 수 있다

4. 도메인 서비스

  • 도메인 주도 설계에서 서비스는 크게 두 가지로 나뉜다
    • 도메인 서비스
    • 어플리케이션 서비스
  • 도메인 서비스는 값 객체나 엔티티로 구현하기 어색한 행동을 해결해주는 객체
  • 부자연스러운 행동의 예시
    • User 엔티티에서 직접 유저 중복 확인을 하는 경우 → UserService를 만들어서 처리
  • 하지만 부자연스러운 처리만 도메인 서비스에서 처리해야 하며, 남용할 경우 다른 도메인 객체는 제공할 정보가 없는 빈혈 도메인 모델이 된다
    • 그러므로 우선 엔티티나 값 객체에 행위를 정의하는 것이 좋다
  • 도메인에 기초한 개념과 관련이 있으면 도메인 서비스로 만들고, 어플리케이션에 필요한 개념이면 어플리케이션 서비스로 만들어야 한다

5. 리포지토리

  • 개념
    • 리포지토리는 데이터를 저장하고 복원하는 처리를 추상화하는 객체. 영속적인 데이터를 처리할 때 사용
    • 리포지토리는 도메인 개념으로부터 나온 도메인 객체가 아니고, 인프라에 관련된 객체다
      • 이런 기술적 요소와 관련된 코드를 한 곳에 모아 도메인 문제 해결을 위한 코드가 침식되는 것을 막는다
  • 책임
    • 리포지토리의 책임은 객체의 퍼시스턴시까지다.
      • 데이터의 저장, 그리고 데이터를 불러오는 동작만 정의한다
      • 예를 들어 사용자명의 중복 확인은 도메인 규칙에 가까우므로 리포지토리에 정의하지 않고, 도메인 서비스에 정의하는 것이 옳다
    • 객체를 저장할 때는 저장 대상 객체를 인자로 받아야한다. 수정 항목을 인자로 받게 메서드를 정의하면 수정 항목에 따라 수많은 수정 메서드가 생기게 된다
  • 장점
    • 데이터스토어에 데이터를 기록하고 읽을 때 리포지토리에게 구체적인 행동을 맡기기 때문에 소프트웨어를 유연하게 만들 수 있다
    • 다른 도메인 모델들은 리포지토리 인터페이스에 의존하고, 구체적인 리포지토리 동작은 모르기 때문에 리포지토리를 교체하기 쉬워진다
      • 그렇기 때문에 DB를 교체하는 작업이나, 테스트 작성도 쉬워진다

6. 어플리케이션 서비스

  • 유스케이스를 구현하는 객체
  • 도메인 객체, 도메인 서비스를 조합해 이용자의 목적을 달성하는 역할을 한다 (대표적으로 CRUD)
  • 어플리케이션 서비스에서 클라이언트로 도메인 객체를 반환하는 경우, 도메인 객체를 그대로 노출하는 것보다 DTO에 데이터를 옮겨서 데이터만 노출시키는 것이 좋다
  • 어플리케이션 서비스는 도메인 객체가 수행하는 태스크를 조율하는 데만 전념해야 하며, 어플리케이션 서비스에 도메인 규칙을 기술해서는 안된다
    • 도메인 규칙이 어플리케이션 서비스에 기술되면 같은 코드가 여러 곳에서 중복되는 현상이 나타난다
  • 더 유연한 코드를 위해 어플리케이션 서비스의 인터페이스를 만들 수 있다
    • 어플리케이션 서비스의 클라이언트는 어플리케이션 서비스의 구현체를 직접 호출하는 것이 아니라 인터페이스를 통해 호출하게 된다
    • 인터페이스가 있으면 이를 구현한 목업 객체나 다른 객체로 쉽게 교체가 가능하다
  • 서비스는 경우에 따라 상태를 가질 수 있지만, 상태를 만들지 않을 방법을 먼저 생각해 보는 것이 좋다

7. 의존 관계 제어

  • 의존은 어떤 객체가 다른 객체를 참조하면서 발생한다
  • 또는 인터페이스와 그 구현체가 되는 구상 클래스 사이에도 의존 관계가 생긴다
  • 의존 관계 역전 원칙을 이용하면 비즈니스 로직이 특정 구현에서 해방될 수 있다
  • 의존 관계 역전 원칙
    • 추상 타입에 의존하라
      • 추상화 수준은 입출력으로부터의 거리를 뜻한다
      • 일반적으로 추상 타입은 자신을 사용할 클라이언트가 요구하는 정의다
    • 주도권을 추상 타입에 둬라
  • 의존 관계를 제어하는 수단
    • 환경에 따라 다른 의존성 객체가 필요할 때가 있다(예를 들면 테스트). 이 때 코드를 여기저기 교체해야 하는 문제가 발생한다. 이런 문제를 해결하기 위한 패턴들이 있다.
    • Service Locator 패턴
      • ServiceLocator 객체에 의존 해소 대상이 되는 객체를 미리 등록하고, 인스턴스가 필요한 곳에서 받아 사용하는 패턴
      • 의존 관계를 외부에서 보기 어렵고, 테스트 유지가 어렵다는 단점이 있다
    • IoC Container 패턴
      • 생성자, 메서드 등으로 의존성을 전달받는 것을 의존성 주입(Dependency Injection) 패턴이라고 한다
      • 컨테이너가 미리 등록된 설정대로 의존성을 주입하여 인스턴스를 생성하는 방식

8. 소프트웨어 시스템 구성하기

  • 어플리케이션은 사용자 인터페이스를 교체할 수 있다
  • 사용자 인터페이스를 소프트웨어의 핵심과 분리하는 것이 좋다
  • 사용자 인터페이스를 교환 가능한 상태라는 것은 단위 테스트를 수행할 수 있는 상태라는 의미다

9. 팩토리 패턴

  • 복잡한 객체는 객체를 생성하는 처리도 그만큼 복잡하다
  • 객체 생성을 도메인 모델에서 하면 모데인 모델을 나타내는 취지가 불분명해지고, 클라이언트에 맡기는 것도 좋은 방법이 아니다
  • 여기서 객체 생성 과정을 담당하는 팩토리 객체의 필요성이 생긴다
  • 예를 들어, User 엔티티를 생성할 때 User id를 테스트 환경에서는 임의의 값으로 넣고, 테스트 환경이 아닐 때는 데이터베이스의 id값을 넣고 싶을 때 그 역할을 유저 팩토리에게 담당하게 하면 좋다
  • 도메인 모델링에 따라 모델을 생성하는 것이 도메인 객체의 행위로 정의된다면, 도메인 모델에 팩토리 역할을 하는 메서드를 추가할 수도 있다
  • 클래스의 생성자 메서드 안에서 다른 객체를 생성하고 있다면 팩토리가 필요하지 않은지 검토해볼 필요가 있다
    • 생성 절차가 간단하다면 그냥 생성자 메서드를 호출하는 쪽이 더 낫다

10. 데이터의 무결성

  • 무결성이란 모순이 없고 일관적이라는 뜻
  • 데이터의 무결성을 지키는 방법
  • 방법 1 - 유일 키 제약
    • 장점
      • 중복 검사를 데이터베이스에서 하기 때문에 코드가 간단해진다
    • 단점
      • 비즈니스 로직이 숨어있기 때문에 이해하기 힘들다
      • 비즈니스 로직이 특정한 기술에 의존하게 된다
    • 유일 키 제약은 규칙을 준수하는 주 수단이 아니라 안전망 역할로 활용해야 한다
  • 방법 2 - 트랜잭션
    • 무결성을 유지하기 위한 더 일반적인 수단
    • 의존적인 조작을 한꺼번에 완료하거나 취소하는 방법으로 데이터의 무결성을 지킨다
    • 단점
      • 어플리케이션 서비스에서 데이터베이스에 의존하게 될 수 있다
    • 트랜잭션을 이용하면서 특정 기술에 의존하지 않게 만드는 패턴
      • C#에서 트랜잭션 범위를 지정하는 기능
      • AOP(관점 지향 프로그래밍)를 적용 : 자바의 Transactional 어노테이션
      • UnitOfWork 패턴 : 객체의 변경사항을 기록하는 객체
    • 트랜잭션은 일관성 유지를 위해 데이터에 락(Lock)을 건다
      • 락의 범위가 넓어지면 그에 비례해 실패 가능성이 높아진다
      • 1개에 트랜잭션으로 저장하는 객체의 수를 1개로 제한하고 객체의 크기를 가능한 한 줄이는 방법으로 락의 범위를 최소화할 수 있다

11. 어플리케이션 밑바닥부터 만들기

  • 어플리케이션을 만드는 과정
    1. 어떤 기능이 필요한 지 확인
    2. 기능을 결정했다면 그 기능의 기반이 될 유스케이스를 수립
    3. 필요한 유스케이스를 모두 수립한 다음, 도메인 개념과 규칙으로부터 지식을 추출해 도메인 객체를 정의
    4. 도메인 객체로 유스케이스를 실제 기능으로 제공할 어플리케이션 서비스를 구현

12. 애그리게이트

  • 애그리게이트는 불변 조건을 유지하는 단위로 꾸혀지며 객체 조작의 질서를 유지한다
  • 애그리게이트는 경계와 루트를 가지며 외부에서 애그리게이트를 다루는 조작은 모두 루트를 거쳐야 한다
    • 예를 들어 서글 애그리게이트가 있을 때, 서클 멤버를 추가하는 조작은 서클 객체를 통해서 이루어져야 한다
      • circle.members.add(member) (X)
      • circle.join(member) (O)
    • 데메테르의 법칙
      • 게터를 만들지 않아야 할 이유
      • 객체 내부를 감추면 하나의 규칙이 중복 구현되는 일을 막을 수 있고 유지 보수성을 향상시킨다
      • 객체 내부 데이터를 완전히 감추면 리포지토리가 객체를 데이터스토어에 저장할 수 없게 된다
        • 해결책 1 - 리포지토리만 예외적으로 접근하도록 코드를 작성한다
        • 해결책 2 - 노티피케이션 객체를 이용한다
  • 애그리게이트의 경계를 정하는 원칙
    • 주로 ‘변경의 단위’를 경계로 한다
  • 다른 애그리게이트를 포함하는 경우
    • 다른 애그리게이트의 인스턴스를 가지는 것보다 식별자를 포함하게 할 수 있다
      • 적어도 부주의하게 애그리게이트 너머의 영역을 변경하는 일을 방지할 수 있다
  • 애그리게이트의 크기와 조작의 단위
    • 애그리게이트의 크기가 크면 클수록 트랜잭션으로 걸리는 락의 크기도 커진다
    • 따라서 애그리게이트의 크기는 가능한 한 작게 유지하는 것이 좋다
    • 그리고 한 트랜잭션에서 여러 애그리게이트를 다루는 것도 가능한 피해야 한다
    • 여러 애그리게이트에 걸친 트랜잭션이 꼭 필요할 경우, 결과 무결성이 유용할 수 있다

13. 명세

  • 객체가 특정한 조건을 만족하는지 평가하는 코드는 보통 메서드로 작성된다
  • 평가 조건이 복잡해지면 도메인 엔티티가 리포지토리를 다루게 될 수 있는데, 이것은 좋지 않다
  • 이런 경우 객체의 평가 기준을 만족하는지 판정하기 위한 명세 객체를 따로 둘 수 있다
  • 명세는 객체가 조건을 만족하는 지 확인하는 역할만을 수행하며, 객체 평가 코드를 캡슐화해 원래 객체가 원래 의도를 잘 드러낼 수 있게 한다
  • 명세도 도메인 객체이므로 명세에서 리포지토리 사용을 지양해야 한다는 의견도 있다
    • 이럴 때 일급 컬렉션을 이용할 수 있다
    • 리포지토리에서 객체를 얻어오는 대신 일급 컬렉션을 주입받는 식으로 구현한다
  • 리포지토리의 검색 메서드에 중요한 규칙이 포함되는 경우 도메인 규칙이 리포지토리 구현체로 빠져나가게 된다
    • 이럴 때 중요 규칙을 명세로 정의하고 리포지토리에 명세를 전달하는 방법으로 해결할 수 있다
    • 리포지토리에 명세를 전달하는 방법은 모든 데이터를 받아온 다음 하나하나 명세조건에 부합하는 지 확인해야 하기 때문에 성능이 나쁠 수 있다
      • 성능을 위해 쿼리를 직접 실행하는 서비스를 만들 수 있다

14. 아키텍처

  • 아키텍처는 코드를 구성하는 원칙
  • 개발자는 아키텍처가 제시하는 원칙에 따르면서 ‘어떤 로직을 어디에 구현할 것인지’고민하지 않아도 되며 ‘도메인을 파악하고 잘 표현하는’것에 집중할 수 있게 해준다
  • 도메인 주도 설계와 함께 자주 언급되는 아키텍처
    • 계층형 아키텍처
      • 프레젠테이션, 애플리케이션, 도메인, 인프라스트럭처 4개의 계층으로 구성
      • 상위 계층은 자신보다 하위 계층에 의존할 수 있다. 이 방향을 거스르는 의존은 허용되지 않는다
    • 헥사고날 아키텍처
      • 애플리케이션과 그 외 인터페이스나 저장매체를 자유롭게 탈착하는 컨셉
      • 포트 앤 어댑터(ports-and-adapters)라고 부르기도 한다
      • 애플리케이션에 대한 입력을 받는 포트 및 어댑터를 각각 프라이머리 포트, 프라이머리 어댑터라고 하며
      • 반대로 애플리케이션이 외부와 상호작용하는 포트를 세컨더리 포트라고 하며, 이를 구현한 객체를 세컨더리 어댑터라고 한다
    • 클린 아키텍처
      • 4개의 동심원이 있는 특징적인 그림으로 설명되는 아키텍처
      • 세부사항을 가장자리로 밀어내고, 의존관계의 방향을 안쪽으로 향하게 함으로써 세부사항이 추상에 의존하는 의존관계 역전 원칙을 달성한다
      • 헥사고날 아키텍처와 다르게 클린 아키텍처는 컨셉을 실현하기 위한 구체적인 방식이 명시된다