인생은 고통의 연속

도메인 주도 설계 - 아키텍처 본문

아키텍쳐/도메인 주도 설계

도메인 주도 설계 - 아키텍처

gnidoc 2019. 3. 23. 02:21
    반응형

    아키텍처 설계시 필요한 영역

    • 표현(UI) 영역 : 사용자의 요청을 받아 응용 영역에 전달하거나 응용 영역의 처리 결과를 사용자에게 보여줌. 대표적으로 스프링 MVC 프레임워크가 해당함.
    • 응용 영역 : 도메인 모델을 이용해서 사용자에게 제공할 기능을 구현. 실제 도메인 로직 구현은 도메인 모델에 위임한다.
    • 도메인 영역 : 도메인 모델을 구현. 도메인 모델은 도메인의 핵심 로직을 구현.

    • 인프라스트럭처 영역 : 논리적인 개념을 표현하기보다는 실제 구현 기술에 대한 것을 다룬다.

    ※ 표현/도메인/응용 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다. 대신 인프라 스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.
    (응용 영역에서 DB의 쿼리를 직접 사용하거나 도메인 영역에서 메일을 보내기 위한 함수를 구현하지 않는다는 뜻)

     

    계층 구조 아키텍처

    [네 영역을 구성할 때 많이 사용하는 아키텍처]

    계층 구조는 상위 계층에서 하위 계층으로의 의존만 존재하고 반대는 성립하지 않는다. 하지만 구현의 편리함을 위해 아래의 그림처럼 계층 구조를 유연하게 적용할 수 있다.

    [유연하게 적용한 계층 구조]

    하지만 인프라스트럭처에 의존하면 '테스트 어려움'과 '기능 확장의 어려움' 문제 2가지가 발생한다. 이런 문제는 DIP를 적용하여 해결할 수 있다.

     

    DIP

    DIP는 Dependency Inversion Principle으로 번역하자면 의존 역전 원칙이다. DIP에서는 고수준, 저수준 모듈 2가지로 분류한다.

    • 고수준 모듈 : 의미 있는 단일 기능을 제공하는 모듈.
    • 저수준 모듈 : 하위 기능을 실제로 구현한 것.

    고수준 모듈이 제대로 동작하려면 저수준 모듈을 사용해야된다. 하지만 이럴 경우 앞에서 언급된 '구현 변경과 테스트 어려움' 문제가 발생한다. DIP는 이 문제를 해결하기 위해서 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다. 정확히는 추상화한 인터페이스를 사용하여 해결한다.

    [책에 나온 DIP 예시]

    원래라면 고수준 모듈이 저수준 모듈을 사용하려면 고수준 모듈이 저수준 모듈에 의존해야 하는데, DIP를 적용하면 저수준 모듈이 고수준 모듈에 의존하게 된다. DIP를 적용하면 다른 영역이 인프라스트럭처 영역에 의존할 때 발생했던 문제들을 해결할 수 있다.

    의존 주입을 지원하는 스프링과 같은 프레임워크를 사용하면 쉽게 구현체를 변경할 수 있다. 그리고 Mock 프레임워크를 이용해서 대용 객체를 생성하여 테스트에 필요한 저수준 모듈의 객체 생성을 할 수 있다.(즉, 실제 구현 대신 stub이나 mock과 같은 대용 객체로 모든 상황을 테스트할 수 있다.) 이렇게 실제 구현없이 테스트할 수 있는 이유는 DIP를 적용해서 고수준 모듈이 저수준 모듈에 의존하지 않도록 했기 때문이다.

     

    DIP 주의사항

    DIP는 단순히 인터페이스와 구현 클래스를 분리하는게 아니다. DIP의 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함인데 대충 저수준 모듈에서 인터페이스만 추출한다고 DIP가 아니다.

    [잘못된 예시]

    위의 그림이 잘못된 구조이다. 도메인 영역이 구현 기술을 다루는 인프라에 의존하기 떄문이다.(즉, 고수준 모듈인 도메인이 저수준 모듈인 인프라에 의존하고 있다) 제대로 DIP를 이해했다면 하위 기능을 추상화한 인터페이스는 고수준 모듈에 위치해야 된다.

    [제대로 이해한 DIP]

     

    DIP와 아키텍처

    DIP를 적용하면 인프라 영역이 응용 영역과 도메인 영역에 의존(상속)하는 구조가 된다. 그러므로 도메인과 응용 영역에 대한 영향을 최소화하여 구현 기술(인프라)을 변경하는 것이 가능하다.

     

    도메인 영역의 주요 구성 요소

    • 엔티티(Entity) : 고유의 식별자를 갖는 개체로 자신의 라이프사이클을 갖는다. 도메인의 고유한 개념을 표현하고 도메인 모델의 데이터를 포함하여 해당 데이터와 관련된 기능을 함께 제공한다.
    • 밸류(Value) : 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 도메인 객체의 속성을 표현할 때 사용한다. 엔티티의 속성으로 사용될 뿐만 아니라 다른 밸류 타입의 속성으로 사용될 수 있다.
    • 애그리거트(Aggregate) : 관련된 엔티티와 밸류 객체를 개념적으로 묶은 것.
    • 리포지터리(Repository) : 도메인 모델의 영속성을 처리.(엔티티 객체를 로딩하거나 저장하는 기능 같은 것)
    • 도메인 서비스 : 특정 엔티티에 속하지 않은 도메인 로직을 제공한다.

     

    엔티티와 밸류

    도메인 모델의 엔티티와 DB 테이블의 엔티티와는 다르다. 가장 큰 차이점은 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다는 점.(DB 테이블과 다르게 클래스에서 멤버 함수를 만들 수 있다.)

    그리고 도메인 모델의 엔티티는 2개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있다.(DB 테이블은 관계형으로 표현하는게 엔티티의 개념을 이해하는데 도움이 되기 때문이다)

    밸류는 불변으로 구현하는 것을 권장하는데 이는 엔티티의 밸류 타입 데이터를 변경할 때 객체 자체를 완전히 교체한다는 것을 뜻한다.

     

    애그리거트

    애그리거트 : 관련 객체를 하나로 묶은 군집

    도메인 모델도 개별 객체뿐만 아니라 상위 수준에서 모델을 볼 수 있어야 전체 모델의 관계와 개별 모델을 이해하는데 도움이 된다. 그래서 도메인 개념은 하위 개념을 표현한 모델을 하나로 묶은 상위 개념으로 표현할 수 있다. 이처럼 애그리거트를 사용하면 관련 객체를 묶어서 객체 군집 단위로 모델을 바라볼 수 있게 한다.

    애그리거트는 군집에 속한 객체들을 관리하는 루트 엔티티를 갖는다. 루트 엔티티는 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리거트가 구현해야할 기능을 제공한다. 이때 애그리거트 루트를 통해서 간접적으로 애그리거트 내의 다른 엔티티나 밸류 객체에 접근할 수 있는데 이는 애그리거트의 내부 구현을 숨겨서 애그리거트 단위로 구현을 캡슐화할 수 있도록 한 것이다.

     

    리포지터리

    리포지터리 : 물리적인 저장소에 도메인 객체를 보관하기 위한 도메인 모델. 엔티티, 밸류가 요구사항에서 도출되는 도메인 모델이라면 리포지터리는 구현을 위한 도메인 모델.

    리포지터리는 애그리거트 단위로 도메인 객체를 저장/조회하는 기능을 정의한다. 응용 서비스는 의존 주입과 같은 방식을 사용해서 실제 리포지터리 구현 객체에 접근한다.

    응용 서비스와 리포지터리는 밀접한 연관이 있는데 이유는 다음과 같다.

    • 응용 서비스는 필요한 도메인 객체를 구하거나 저장할 때 리포지터리를 사용한다.
    • 응용 서비스는 트랜잭션을 관리하는데, 트랜잭션 처리는 리포지터리 구현 기술에 영향을 받는다.

    리포지터리의 사용 주체가 응용 서비스이기 때문에 리포지터리는 응용 서비스가 필요로 하는 메서드를 제공한다. 가장 기본이 되는 메서드는 다음과 같다.

    • 애그리거트를 저장하는 메서드
    • 애그리거트 루트 식별자로 애그리거트를 조회하는 메서드

     

    요청 처리 흐름

    • 사용자가 애플리케이션에 기능 실행을 요청하면 그 요청을 처음 받는 영역은 표현 계층
      (스프링 MVC라면 컨트롤러가 요청을 받아 처리)
    • 표현 영역은 사용자가 전송한 데이터 형식이 올바른지 검사하고 문제가 없다면 데이터를 이용해서 응용 서비스에 기능 실행을 위임한다. 이때 표현 영역은 사용자가 전송한 데이터를 응용 서비스가 요구하는 형식으로 변환해서 전달한다.
      (웹브라우저를 이용한다면 HTTP 요청 파라미터를 응용 서비스가 필요로하는 데이터로 변환해서 응용 서비스를 실행할 때 파라미터로 전달한다.)
    • 응용 서비스는 도메인 모델을 이요해서 기능을 구현한다. 도메인 객체를 리포지터리에서 가져오거나 신규 도메인 객체를 생성해서 리포지터리에 저장한다.

     

    인프라스트럭처

    인프라스트럭처는 표현, 응용, 도메인 영역을 지원한다. 다른 영역에서 필요로 하는 프레임워크, 구현 기술, 보조 기능을 지원한다.

    DIP를 통해 인터페이스를 사용하는게 시스템을 유연하고 테스트하기 쉽게 만든다는 것을 알 수 있지만 무조건 인프라에 대한 의존을 없애는 것이 좋은 것은 아니다. 왜냐하면 의존을 완전히 갖지 않도록 하다가 구현을 더 복잡하고 어렵게 만들 수 있기 때문이다.(대표적으로 스프링의 @Transactional 애노테이션)

     

    모듈 구성

    아키텍처의 각 영역은 별도 패키지에 위치한다. 도메인이 크면 하위 도메인으로 나누고 각 하위 도메인마다 별도 패키지를 구성한다. 도메인 모듈은 도메인에 속한 애그리거트를 기준으로 다시 패키지를 구성한다. 그리고 각 애그리거트와 모델, 리포지터리는 같은 패키지에 위치시킨다.(예를 들어 주문과 관련된 것들은 com.myshop.order.domain에 위치시킨다.)

    3개의 하위 도메인(카탈로그, 주문, 멤버)으로 나누고
    카탈로그 도메인 모듈은 상품, 카테고리 애그리거트로 구성됨

    도메인이 복잡하면 도메인 모델과 도메인 서비스를 다음과 같이 별도 패키지에 위치시킬 수도 있다.(응용 서비스도 마찬가지로 구분할 수 있다.)

    • com.myshop.order.domain.order : 애그리거트 위치
    • com.myshop.order.domain.service : 도메인 서비스 위치

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    반응형
    Comments