Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

공hannah부

[도메인 주도 개발 시작하기] - 2장 아키텍처 개요 본문

공부/백엔드

[도메인 주도 개발 시작하기] - 2장 아키텍처 개요

Hannah0226 2024. 3. 11. 00:57

2.1 네 개의 영역

표현, 응용, 도메인, 인프라스트럭처는 아키텍처를 설계할 때 출현하는 전형적인 네 가지 영역이다.

  1. 표현 (or UI)영역
    • 사용자의 요청을 받아 응용 영역에 전달하고 응용 영역의 처리 결과를 다시 사용자에게 보여주는 역할을 함
    • 웹 애플리케이션을 개발할 때 많이 사용하는 스프링 MVC 프레임워크가 표현 영역을 위한 기술에 해당함
    • 웹 애플리에이션에서 표현영역의 사용자는 웹 브라우저를 사용하는 사람일 수도 있고, REST API를 호출하는 외부 시스템일 수도 있음
    • 표현 영역은 HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환해서 응용 영역에 전달하고 응용 영역의 응답을 HTTP 응답으로 변환하여 전송함
  2. 응용(Application)영역
    • 표현 영역을 통해 사용자의 요청을 전달받는 응용영역은 시스템이 사용자에게 제공해야 할 기능을 구현함
      ex) 주문 등록, 주문 취소, 상품 상세 조회와 같은 기능
    • 응용 영역은 기능을 구현하기 위해 도메인 영역의 도메인 모델을 사용
      → 응용 서비스는 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임함
  3. 도메인 영역
    • 도메인 모델을 구현
      ex) 1장에서 봤던 Order, OrderLine, ShippingInfo와 같은 도메인 모델
    • 도메인의 핵심 로직을 구현
      ex) 주문 도메인에서의 배송지 변경, 결제 완료, 주문 총액 계산과 같은 핵심 로직
  4. 인프라스트럭처 영역
    • 구현 기술에 대한 것을 다룸
      ex) RDBMS 연동 처리, 메시징 큐에 메시지 전송 or 수신기능 구현, 몽고 DB나 레디스와의 데이터 연동 처리 등
      ex) SMTP를 이용한 메일 발송 기능 구현, HTTP 클라이언트를 이용해 REST API 호출 처리 등
    • 논리적인 개념을 표현하기보다는 실제 구현을 다룸
       
    • 표현, 응용, 도메인 영역은 구현 기술을 사용한 코드를 직접 만들지 않음
      → 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해 필요한 기능을 개발
      ex) 응용 영역에서 DB에 보관된 데이터가 필요하다면 인스타스트럭처 영역의 DB 모듈을 사용해여 데이터를 읽어옴

 

2.2 계층 구조 아키텍처

계층 구조 아키텍처 특징

  • 계층구조는 그 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않음
    ex) 표현 계층은 응용 계층에 의존하고, 응용 계층이 도메인 계층에 의존하지만, 반대로 인프라스트럭처 계층이 도메인 계층에 의존하거나 도메인이 응용계층에 의존하지는 않음
  • 계층 구조를 엄격하게 적용한다면 상위 계층은 바로 아래 계층에만 의존을 가져야 하지만 구현의 편리함을 위해 계층 구조를 유연하게 적용하기도 함
    ex) 응용 계층은 바로 아래 계층인 도메인 계층에 의존하지만 외부 시스템과의 연동을 위해 인프라스트럭처 계층에 의존하기도 함

주의할 점

  • 응용, 도메인 계층은 상세한 구현 기술을 다루는 인프라스트럭처 계층에 종속됨
    ex) 도메인과 응용영역에서의 가격 계산 66p~68p 참고
  • 인프라스트럭처에 의존하면 '테스트 어려움'과 '기능 확장의 어려움'이라는 두가지 문제 발생
    DIP가 해결

 

2.3 DIP

고수준 모듈

  • 의미있는 단일 기능을 제공하는 모듈
    ex) CalculateDiscountService는 가격할인 계산이라는 기능을 구현함
  • 고수준 모듈의 기능을 구현하려면 여러 하위 기능이 필요
    ex) 고객의 정보 구하기, 룰 실행

저수준 모듈

  • 하위 기능을 실제로 구현한 것
    ex) JPA를 이용해 고객 정보를 읽어오는 모듈, Drools로 룰을 실행하는 모듈

→ 고수준 모듈이 제대로 동작하려면 저수준 모델을 사용해야하지만 이를 사용하면서 구현 변경과 테스트가 어렵다는 문제가 발생함

 

DIP

  • 위와 같은 문제를 해결하기 위해 저수준 모듈이 고수준 모듈에 의존하도록 바꿈
    추상화 인터페이스를 사용하여 구현
  • CalculateDiscountService 입장에서 봤을 때 룰 적용을 Drools로 구현했는지 자바로 구현했는지는 중요X
      '고객 정보와 구매 정보에 룰을 적용해서 할인 금액을 구한다' 만 중요함
    추상화 인터페이스 적용 71p 참고
    → 
    CalculateDiscountService에는 Drools에 의존하는 코드가 없고 단지 RuleDiscounter가 룰을 적용한다는 사실만 있게 됨

  • 구조를 보면 CalculateDiscountService는 더이상 구현 기술인 Drools에 의존X
    '룰을 이용한 할인 금액 예산'을 추상화한 RuleDiscounter 인터페이스에 의존
  • '룰을 이용한 할인 금액 계산'은 고수준 모듈의 개념이므로 RuleDiscounter 인터페이스는 고수준 모듈에 속함
  • DroolsRuleDiscounter는 고수준 하위 기능인 RuleDiscounter를 구현한 것이므로 저수준 모듈에 속함

  • DIP를 적용하면 위 그림과 같이 저수준 모듈이 고수준 모둘에 의존하게 됨
  • 이처럼 고수준 모듈이 저수준 모듈을 사용하려면 고수준 모듈이 저수준 모듈을 의존해야하는데, 반대로 저수준 모듈이 고수준 모듈에 의존한다고 해서 이를 DIP(Dependency Inversion Principle) 의존 역전 원칙이라고 부름

DIP로 다른 영역이 인프랏스트럭처 영역에 의존할 때 발생했던 문제 해결

  1. 구현 기술 교체 문제
    고수준 모듈은 더 이상 저수준 모듈에 의존하지 않고 구현을 추상화한 인터페이스에 의존함
    //사용할 저수준 객체 생성
    RuleDiscounter ruleDiscounter = new DroolsRuleDiscounter();
    
    //생성자 방식으로 주입
    CalculateDiscountService disService = new CalculateDiscountService(ruleDiscounter);
      구현 기술을 변경하더라도 CalculateDiscountService를 수정할 필요가 없게 됨
    //사용할 저수준 객체 생성
    RuleDiscounter ruleDiscounter = new SimpleRuleDiscounter();
    
    //생성자 방식으로 주입
    CalculateDiscountService disService = new CalculateDiscountService(ruleDiscounter);
     → 사용할 저수준 구현 객체를 생성하는 코드(SimpleRuleDiscounter)만 변경하면 됨
    1. 테스트가 어려웠던 문제
      CalculateDiscountService가 제대로 동작하는지 테스트하려면 CustomerRepository와 RuleDiscounter를 구현한 객체가 필요
      → 만약 CalculateDiscountService가 저수준 모듈에 직접 의존했다면 저수준 모듈이 만들어지기 전까지 테스트를 할 수 없었겠지만 CustomerRepository와 RuleDiscounter가 인터페이스이므로 대역 객체를 사용해서 테스트 진행 가능
      → 이때 대역 객체는 Mokito라는 Mock 프레임워크를 이용해 생성할 수 있음

2.3.1 DIP 주의사항

  • DIP를 잘못 생각하면 단순히 인터페이스와 구현 클래스를 분리하는 정도로 받아들일 수 있음
    왼쪽 구조에서 도메인 영역은 구현 기술을 다루는 인프라스트럭처 영역에 의존하고 있음
    → 이는 여전히 고수준 모듈이 저수준 모듈에 의존하고 있는 것
    → 오른쪽 구조처럼 추상화한 인터페이스는 저수준 모듈이 아닌 고수준 모듈에 위치해야 함

 

2.3.2 DIP와 아키텍처

  • 인프라스트럭처 계층이 가장 하단에 위치하는 계층형 구조와 달리 아키텍처에 DIP를 적용하면 인프라스트럭처 영역이 응용 영역과 도메인 영역에 의존(상속)하는 구조가 됨
  • 인프라스트럭처에 위치한 클래스가 도메인이나 응용 영역에 정의한 인터페이스를 상속받아 구현하는 구조가 되므로 도메인과 응용 영역에 대한 영향을 주지 않거나 최소화하면서 구현 기술을 변경하는 것이 가능함

 

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

  • 도메인 영역의 모델은 도메인의 주요 개념을 표현하며 핵심 로직을 구현함

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

요소 설명
엔티티
ENTITY
고유의 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다. 주문(Order), 회원(Member), 상품(Product)과 같이 도메인의 고유한 개념을 표현한다. 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능을 함께 제공한다.
밸류
VALUE
고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 값을 표현하는데 사용된다. 배송지 주소를 표현하기 위한 주소(Address)나 구매 금액을 위한 금액(Money)와 같은 타입이 밸류 타입이다. 엔티티의 속성으로 사용할 뿐만 아니라 다른 밸류 타입의 속성으로 사용할 수 있다.
애그리거트
AGGREGATE
애그리거트는 관련된 엔티티와 밸류 객체를 개념적으로 하나로 묶는 것이다. 예를 들어 주문과 관련된 Order 엔티티, OrderLine 밸류, Orderer 밸류 객체를 '주문' 애그리거트로 묶을 수 있다.
리포지터리
REPOSITORY
도메인 모델의 영속성을 처리한다. 예를 들어 DBMS 테이블에서 엔티티 객체를 로딩하거나 저장하는 기능을 제공한다.
도메인 서비스
DOMAIN SERVICE
특정 엔티티에 속하지 않은 도메인 로직을 제공한다. '할인 금액 계산'은 상품, 쿠폰, 회원 등급, 구매 금액 등 다양한 조건을 이용해서 구현하게 되는데, 이렇게 도메인 로직이 여러 엔티티와 밸류를 필요로 하면 도메인 서비스에서 로직을 구현한다.

 

2.4.1 엔티티와 밸류

실제 도메인 모델의 엔티티와 DB관계형 모델의 엔티티는 서로 다름

  • 도메인 모델의 엔티티는 데이터와 함께 도메인 기능도 함께 제공함
    ex) 주문을 표현하는 엔티티는 주문과 관련된 데이터뿐만 아니라 배송지 주소 변경을 위한 기능을 함께 제공
    public class Order{
        //주문 도메인 모델의 데이터
        private OrderNo number;
        private Orderer orderer;
        private ShippingInfo shippingInfo;
        ...
        
        //도메인 기능
        public void changeShippingInfo(ShippingInfo new ShippingInfo){
            ...
        }
    }​​
  • 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있음
    → RDBMS와 같은 관계형 데이터베이스는 밸류 타입을 제대로 표현하기 힘듦

2.4.2 애그리거트

애그리거트

  • 배경: 도메인 모델을 볼 때 개별 객체뿐만 아니라 상위 수준에서 모델을 볼 수 있어야 전체 모델의 관계와 개별 모델을 이해할 수 있음. 이때 전체 구조를 이해하는데 도움이 되는 것이 바로 애그리커트
  • 정의: 관련 객체를 하나로 묶는 군집
  • 예시) 주문이라는 도메인 개념은 '주문', '배송지 정보', '주문자', '주문 목록', '총 결제 금액'의 하위 모델로 구성됨. 이 하위 개념을 표현한 모델을 하나로 묶어서 '주문'이라는 상위개념으로 표현할 수 있음
  • 장점: 개별 객체가 아닌 관련 객체를 묶어서 객체 군집 단위로 모델을 바라볼 수 있게 됨. 이를 통해 큰 틀에서 도메인 모델을 관리할 수 있음
  • 루트 엔티티: 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리커트가 구현해야 할 기능을 제공
    → 애그리거트를 사용하는 코드는 애그리거트 루트가 제공하는 기능을 실행하고 애그리거트 루트를 통해서 간접적으로 애그리커트 내의 다른 엔티티나 밸류 객체에 접근함
    → 애그리거트의 내부 구현을 숨겨서 애그리거트 단위로 구현을 캡슐화할 수 있음
  • 주문 애그리거트는 Order를 통하지 않고 ShippingInfo를 변경할 수 있는 방법 제공 X
    → 배송지를 변경하려면 루트 엔티티인 Order를 사용해야 하므로 배송지 정보를 변경할 때에는 Order가 구현한 도메인 로직을 항상 따르게 됨

2.4.3 리포지터리

리포지터리

  • 배경: 도메인 객체를 지속적으로 사용하려면 RDBMS, NoSQL, 로컬 파일과 같은 물리적인 저장소에 도메인 객체를 보관해야 함. 이를 위한 도메인 모델이 바로 리포지터리
  • 기능: 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의함
    ex) 주문 애그리거트를 위한 리포지터리
    public interface OrderRepository{
        Order findByName(OrderNumber number);
        void save(Order order);
        void delete(Order order);
    }​
  • 도메인 모델을 사용해야 하는 코드는 리포지터리를 통해 도메인 객체를 구현한 뒤 도메인 객체의 기능을 실행함
    ex) 주문 취소 기능을 제공하는 응용 서비스는 OrderRepository를 이용해 Order 객체를 구하고 해당 기능을 실행 87p 코드 참고
  • 도메인 모델 관점에서 OrderRepository는 도메인 객체를 영속화하는데 필요한 기능을 추상화 한 것으로 고수준 모듈에 속하고, 기반 기술을 이용해서 OrderRepository를 구현한 클래스는 저수준 모듈로 인프라스트럭처 영역에 속함

응용서비스

  • 응용 서비스와 리포지토리는 밀접한 연관이 있음
    • 응용 서비스는 필요한 도메인 객체를 구하거나 저장할 때 리포지터리 사용
    • 응용 서비스는 트랜잭션을 관리하는데, 트랜젝션 처리는 리포지터리 구현 기술의 영향을 받음
  • 리포지터리가 응용서비스에게 제공하는 메서드의 기본
    • 애그리거트를 저장하는 메서드
      → void save(Some some);
    • 애그리거트 루트 식별자로 애그리커트를 조회하는 메서드
      → Some findById(SomeId id);
    • 이외에도 delete(id)나 counts() 등도 있음

2.5 요청 처리 흐름

  1. 사용자가 애플리케이션에 기능 실행을 요청하면 표현영역이 이를 받음
    (스프링 MVC를 사용해서 웹 애플리케이션을 구현했다면) 컨트롤러가 사용자의 요청을 받아 처리하게 됨
  2. 표현 영역은 사용자가 전송한 데이터 형식이 올바른지 검사하고 문제가 없다면 데이터를 이용해 응용 서비스에 기능 실행을 위임함(이때 표현 영역은 사용자가 전송한 데이터를 응용 서비스가 요구하는 형식으로 변환해서 전달)
  3. 응용서비스는 도메인 모델을 이용해서 기능을 구현
  4. 기능 구현에 필요한 도메인 객체를 리포지토리에서 가져와 실행하거나 신규 도메인 객체를 생성해서 리포지토리에 저장

 

2.6 인프라스트럭처 개요

 인프라스트럭처는 표현영역, 응용영역, 도메인영역을 지원함

  • 도메인 객체의 영속성 처리, 트랜잭션, SMTP 클라이언트, REST 클라이언트 등 다른 영역에서 필요로 하는 프레임워크, 구현 기술, 보조 기능을 지원함
  • DIP에서 언급했던 것과 같이 인프라스트럭처의 기능을 직접 사용하는 것보다 도메인 또는 응용 영역에 정의한 인터페이스를 인프라스트럭처 영역에서 구현하는 것이 시스템을 더 유연하고 테스트하기 쉽게 만들어줌
  • BUT 무조건 인프라스트럭처에 대한 의존을 없앨 필요는 X
    ex) @Transactional 애너테이션: @Transactional을 사용하면 한 줄로 트랜잭션을 처리할 수 있는데 스프링에 대한 의존을 없애버리면 복잡한 스프링 설정을 사용해야 함

 

2.7 모듈 구성

아키텍쳐의 각 영역은 별도 패키지에 위치함

  • 패키지 구성 예시


  • 도메인이 클 경우에는 하위 도메인으로 나누고 각 하위 도메인마다 별도 패키지를 구성하고, 도메인 모듈은 도메인에 속한 애그리거트를 기준으로 다시 패키지를 구성함 (애그리거트, 모델, 리포지토리는 같은 패키지에 위치시킴)

 

  • 모듈 구조를 얼마나 세분화해야 하는지에 대해 정해진 규칙은 없지만 한 패키지에 10~15개 미만의 타입 개수가 적절

 

2장을 마치며...

애플리케이션 계층 아키텍처에 대해 정확히 알게되었다. 지금까지 백엔드 개발을 하며 컨트롤러, 서비스, 엔티티 등을 왜 나누는지조차 모르고 그냥 배운대로 코드를 작성했었다. 하지만 오늘 아키텍처에 대해 상세히 공부하며 컨트롤러는 표현의 영역, 서비스는 응용의 영역, 엔티티는 도메인의 영역, DB모듈은 인프라스트럭처의 영역임을 학습할 수 있었다. 또한 DIP가 원래의 의존 방향을 역전시켜 테스트와 기능 확장의 어려움을 해결한다는 사실을 알게되었고, 이때 추상화 인터페이스를 고수준 영역에 생성하여 저수준 모듈이 고수준 모듈을 상속해 의존관계를 역전시킬 수 있음을 알게되었다.

평소 이유를 모른 채 사용하던 것들의 개념과 이유를 학습할 수 있었던 유익한 시간이었다!