https://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4e

도메인 모델과 경계

  • 처음 도메인 모델을 만들때 빠지기 쉬운 함정이 도메인을 완벽하게 표현하는 단일 모델을 만드는 시도를 하는 것이다.
  • 1장에서 말한거처럼 한 도메인은 다시 여러 하위 도메인으로 구분되기 떄문에 한개의 모델로 여러 하위 도메인을 모두 표현하려고 시도하게 되면 모든 하위 도메인에 맞지 않는 모델을 만들게 된다.

 

  • 상품이라는 모델을 생각해보면 카탈로그에서의 상품, 재고 관리에서의 상품, 주문에서의 상품, 배송에서의 상품은 이름만 같지 실제로 의미하는것은 다르다.
  • 카탈로그에서의 상품은 상품 이미지, 상품명, 상품 가격, 옵션목록과 같은 상품정보 위주라면 재고 관리에서의 상품은 실존하는 개별 객체를 추적하기 위한 목적으로 상품을 사용한다.
  • 즉 카탈로그는 물리적으로 한 개인의 상품이 재고 관리에서는 여러 개 존재할 수 있다.

 

  • 논리적으로 같은 존재처럼 보이지만 하위 도메인에 따라 다른 용어를 사용하는 경우도 있다.
  • 카탈로그 도메인에서 상품이 검색 도메인에서는 문서로 불리기도 한다.
  • 비슷하게 시스템을 사용하는 사람을 회원 도메인에서 회원이라고 부르지만 주문 도메인에서는 주문자 라고 부르고 배송 도메인에서는 보내는 사람이라 부르기도 한다.
  • 이렇게 하위 도메인마다 같은 용어라도 의미가 다르고 같은 대상이라도 지칭하는 용어가 다를수 있기 떄문에 한개의 모델로 모든 하위 도메인을 표현하려는 시도는 올바른 방법이 아니며 표현할 수도 없다.

 

  • 하위 도메인마다 사용하는 용어가 다르기 떄문에 올바른 도메인 모델을 개발하기 위해서라면 하위 도메인마다 모델을 만들어야 한다.
  • 각 모델은 명시적으로 구분되는 경계를 가져서 섞이지 않도록 해야 한다.
  • 여러 하위도메인의 모델이 섞이기 시작하면 모델의 의미가 약해질뿐 아니라 여러 도메인의 모델이 서로 얽혀 있기 떄문에 각 하위 도메인 별로 다르게 발전하는 요구사항을 모델에 반영하기 어려워 진다.

 

  • 모델은 특정한 컨텍스트(문맥) 하에 완전한 의미를 갖는다.
  • 같은 제품이라도 카탈로그 컨텍스트와 재고 컨텍스트에서 의미가 서로 다르다.
  • 이렇게 구분되는 경계를 갖는 컨텍스트를 DDD에서는 바운디드 컨텍스트 라고 부른다.

 

Bounded Context

  • Bounded Context는 모델의 경계를 결정하며
  • 한 개의 Bounded Context는 논리적으로 한개의 모델을 갖는다.

 

  • Bounded Context는 용어를 기준으로 구분 한다.
  • 카탈로그 컨텍스트와 재고 컨텍스트는 서로 다른 용어를 사용하므로 이 용어를 기준으로 컨텍스트를 분리할수 있다.
  • Bounded Context는 실제로 사용자에게 기능을 제공하는 물리적 시스템으로 도메인 모델은 이 Context안에서 도메인을 구현한다.

 

  • 이상적으로 하위 도메인과 Bounded Context가 1대1 관계를 가지면 좋겠지만 현실은 그렇지 않을 때가 많다.
  • Bounded Context가 기업의 팀 조직 구조에 따라 결정되기도 한다.
  • 예를 들어, 주문 하위 도메인이라도 주문을 처리하는 팀과 복잡한 결제 금액 계산 로직을 구현하는 팀이 따로 있다고 해보자. 이 경우 주문 하위도메인에 주문 Bounded Context와 결제 금액 계산 Bounded Context가 존재하게 된다.
  • 아직 용어를 명확하게 하지 못해 두 하위 도메인을 한 Bounded Context에서 구현하기도 한다.
  • 카탈로그와 재고 관리가 아직 명확하게 구분되지 않은 경우 두 하위 도메인을 한 Bounded Context에서 구현하기도 한다.

 

  • 규모가 작은 기업은 전체 시스템을 한 개 팀에서 구현할 떄도 있다.
  • 예를 들어 소규모 쇼핑몰을 운영할 경우 한개의 웹 애플리케이션으로 온라인 쇼핑을 서비스한다.
  • 이 경우 하나의 시스템에서 회원, 카탈로그, 재고, 구매, 결제와 관련된 기능을 제공 한다. 즉 여러 하위도메인을 한 개의 Bounded Context에서 구현한다

 

  • 여러 하위 도메인을 하나의 Bounded Context에서 개발할때 주의할 점은 하위 도메인의 모델이 뒤섞이지 않도록 하는것이다.
  • 한개의 이클립스 프로젝트에 각 하위 도메인의 모델이 위치하면 아무래도 전체 하위 도메인을 위한 단일 모델을 만들고 싶은 유혹에 빠지기 쉽다.
  • 이런 유혹에 걸려들면 결과적으로 도메인 모델이 개별 하위 도메인을 제대로 반영하지 못해서 하위 도메인별 기능 확장이 어렵게 되고 이는 서비스의 경쟁력을 떨어뜨리는 원인이 된다.
  • 따라서 비록 한개의 Bounded Context에서 여러 하위 도메인을 포함하더라도 하위 도메인마다 구분되는 패키지를 갖도록 구현해야 하위 도메인을 위한 모델이 서로 뒤섞이지 않아서 하위 도메인마다 Bounded Context를 갖는 효과를 낼 수 있다.

 

  • Bounded Context는 도메인 모델을 구분하는 경계 가 되기 때문에 Bounded Context는 구현하는 하위 도메인에 알맞은 모델을 포함한다.
  • 같은 사용자라 하더라도 주문 Bounded Context와 회원 Bounded Context가 갖는 모델이 달라진다.
  • 또한 같은 상품이라도 카탈로그 Bounded Context의 Product와 재고 Bounded Context의 Product는 각 컨텍스트에 맞는 모델을 갖는다.
  • 따라서 회원의 Member는 애그리것 루트이지만 주문의 Orderer는 밸류가 되고 카탈로그의 Product는 상품이 속할 Catergory와 연관을 갖지만 재고의 Product는 카탈로그의 Category와 연관을 맺지 않는다.

 

Bounded Context의 구현

출처: https://minkukjo.github.io/dev/2020/11/11/DDD-09/

 

  • Bounded Context는 도메인 모델 뿐만 아니라 도메인의 기능을 사용자에게 제공하는 데 필요한 표현 영역, 응용 서비스, 인프라 영역등을 모두 포함한다.
  • 도메인 모델의 데이터 구조가 바뀌면 DB 테이블 스키마도 함께 변경해야 하므로 해당 테이블도 Bounded Context에 포함된다.

 

 

  • 표현영역은 인간 사용자를 위하 HTML 페이지를 생성할 수도 있고 다른 Bounded Context를 위해 Rest API를 제공할 수도 있다.

 

출처: https://minkukjo.github.io/dev/2020/11/11/DDD-09/

 

  • 모든 Bounded Context가 반드시 DDD로 개발할 필요는 없다.
  • 상품의 리뷰는 복잡한 도메인 로직을 갖지 않기 때문에 CRUD 방식으로 구현해도 된다.
  • 즉, DAO와 데이터 중심의 밸류 객체를 이용해서 리뷰기능을 구현해도 기능을 유지보수 하는데 큰 문제가 없다.

 

  • 한 Bounded Context에서 두 방식을 혼합해서 사용할 수도 있다.
  • 대표적인 예가 CQRS 패턴이다.
    출처: https://leejaedoo.github.io/bounded_context/
    • CQRS는 상태를 변경하는 명령과, 조회하는 쿼리 기닝을 위한 모델을 구분하는 패턴이다.
    • 이 패턴을 단일 Bounded Context에 적용하면 상태를 변경하는 기능은 도메인 모델 기반으로 구현하고 조회 기능은 서비스-DAO를 이용해서 구현할 수 있다.
  •  
  • 각 Bounded Context는 서로 다른 구현기술을 사용할 수도 있다.
  • 웹 MVC는 스프링 MVC를 사용하고 리포지터리 구현 기술로는 JPA/하이버네이트를 사용하는 Bounded Context가 존재하고
  • Netty를 사용해서 RestAPi를 제공하고 Mybatis를 리포지터리 구현 기술로 사용하는 Bounded Context가 존재할수도 있다.
  • 어떤 Bounded Context는 RDBMS대신 HBase나 몽고 DB와 같은 NoSQL을 사용할 수도 있을 것이다.

 

  • 예를들어 상품상세 정보를 보여주는 페이지를 생각해보면
  • 웹 브라우저는 카탈로그 Bounded Context를 통해 상세 정보를 읽어온 뒤 리뷰 Bounded Context의 Rest API를 직접 호출해서 로딩한 JSON 데이터를 알맞게 가공해서 리뷰 목록을 보여줄 수도 있다.
  • 아니면 UI 를 처리하는 서버를 따루 두고 UI 서버에서 Bounded Context와 통신해서 사용자의 요청을 처리하는 방법도 있다.

 

Bounded Context 간 통합

  • 두팀이 관련된 Bounded Context를 개발하면 자연스럽게 Bounded Context간의 통합이 발생한다.
    • 사용자가 제품 상세 페이지를 볼때 보고있는 상품과 유사한 상품 목록을 하단에 보여준다
    • 위와 같은 조건의 기능이 필요할때 Bounded Context간의 통합이 필요하다.
    • 카탈로그 Bounded Context와 추천 Bounded Context의 통합

 

  • 카탈로그 컨텍스트와 추천 컨텍스트의 도메인 모델은 서로 다르다.
  • 카탈로그는 제품 중심으로 도메인 모델을 구현
  • 추천은 추천 연산을 위한 모델을 구현한다.
  • 예를 들어 추천 시스템은 상품의 상세정보를 포함하지 않으며 상품 번호 대신 아이템 ID라는 용어를 사용해서 식별자를 표현하고 추천 순위와 같은 데이터를 담게 된다.
  • 카탈로그 시스템은 추천 시스템으로부터 추천 데이터를 받아오지만 카탈로그 시스템에서는 추천의 도메인 모델을 사용하기 보다는 카탈로그 도메인을 사용해서 추천 상품을 표현해야 한다.
  • 즉 다음과 같이 카탈로그 모델을 기반으로 하는 도메인 서비스를 이용해서 상품 추천 기능을 표현해야 한다.

 

// 상품 추천 기능을 표현하는 카탈로그 도메인 서비스
public interface ProductRecommendationService {
    public List<Product> getRecommendationsOf(ProductId id);
}

 

  • 도메인 서비스를 구현한 클래스는 인프라스트럭처 영역에 위치한다.
  • 이 클래스는 외부 시스템과의 연동을 처리하고 외부 시스템의 모델과 현재 도메인 모델간의 변환을 책임진다.
  • ( 이거 완전 안티커럽션 레이언데 안티커럽션 레이어란 말을 안한다. )

 

출처: https://leejaedoo.github.io/bounded_context/

 

  • RecSystemClient는 외부 추천 시스템이 제공하는 Rest API를 이용해서 특정 상품을 위한 추천 상품 목록을 로딩한다.
  • 이 Rest API가 제공하는 데이터는 추천 시스템의 모델을 기반으로 하고 있기 떄문에 API 응답은 다음과 같이 상품 도메인 모델과 일치하지 않는 데이터를 제공할 것이다.

 

  • RecSystemClient는 Rest API로부터 데이터를 읽어와 카탈로그 도메인에 맞는 상품 모델로 변환한다.
public class RecSystemClient implements ProductRecommendationService {
    private ProductRepository productRepository;
    
    @Override
    public List<Product> getRecommendationOf(ProductId id) {
        List<RecommendationItem> items = getRecItems(id.getValue());
        return toProducts(items);
    }

    private List<RecommendationItem> getRecItems(String itemId) {
        // externalRecClient는 외부 추천 시스템을 위한 클라이언트라고 가정
        return externalRecClient.getRecs(itemId);
    }

    private List<Product> toProducts(List<RecommendationItem> items) {
        return items.stream()
                    .map(item -> toProductId(item.getItemId()))
                    .map(prodId -> productRepository.findById(prodId))
                    .collect(toList());
    }

    private ProductId toProductId(String itemId) {
        return new ProductId(itemId);
    }
    //...
}

 

  • getRecItems는 exeternalRecClient는 외부 추천 시스템에 연결할 때 사용하는 클라이언트로서 추천 시스템을 관리하는 팀에서 배포하는 모듈이라고 가정하자. ( 어댑터 ? )
  • 이 모듈이 제공하는 RecommendationItem은 추천 시스템의 모델을 따를 것이다.
  • RecSystemClient는 추천 시스템의 모델을 받아와 toProducts() 메서드를 이용해서 카탈로그 도메인의 Product 모델로 변환하는 작업을 처리한다. ( 번역기 )

 

  • 두 모델간의 변환 과정이 복잡하면 번역기만 따로 두기도 한다.
  • Rest API를 통해 직접 두 Bounded Context를 직접 통합 하는 방법이다.
  • 대표적인 간접 통합 방법은 메세지큐를 이용하는것이다.

 

 

  • 카탈로그 Bounded Context는 추천 시스템이 필요로 하는 사용자 활동 이력을 메시지 큐에 추가한다. ( 카탈로그 Bounded Context는 상류 같음 )
  • 메시지 큐는 보통 비동기로 메시지를 처리하기 때문에 카탈로그 Bounded Context 는 메시지를 큐에 추가한 뒤에 추천 Bounded Context가 메시지를 처리할 때까지 기다리지 않고 바로 이어서 자신의 처리를 계속한다.

 

  • 추천 Bounded Context는 큐에서 이력 메시지를 읽어와 추천을 계산하는데 사용할 것이다.
  • 이는 두 Bounded Context가 사용할 메시지의 데이터 구조를 맞춰야 함을 의미.
  • 각각의 Bounded Context를 담당하는 팀은 서로 만나서 주고받을 데이터 형식에 대해 협의해야 한다. ( 협조적인 상류팀 )
  • 메시지 시스템을 카탈로그 측에서 관리하고 있다면 위의 그림과 같이 카탈로그 도메인을 따르는 데이터를 담을것이다.

 

  • 어떤 도메인 관점에서 모델을 사용하느냐에 따라 두 Bounded Context의 구현 코드가 달라지게 된다.
  • 카탈로그 도메인 관점에서 큐에 저장할 메시지를 생성하면 카탈로그 시스템의 연동 코드는 카탈로그 기준의 데이터를 그대로 메싲 큐에 저장한다. ( 이러면 카탈로그가 상류, 추천이 하류 ? )
  • 카탈로그 도메인 모델을 기준으로 메시지를 전송하므로 추천 시스템에서는 자신의 모델에 맞게 메시지를 변환해서 처리해야 한다. ( 카탈로그가 상류, 추천이 하류 )

 

  • 카탈로그 도메인을 메시징 큐에 카탈로그와 관련된 메시지를 저장하고 다른 Bounded Context는 이 큐로부터 필요한 메시지를 수신하는 방식
  • 즉, 한쪽에서 메시지를 출판하고 다른 쪽에서 메시지를 구독하는 출판/구독 (publish subscribe)모델을 따른다.

 

마이크로 서비스와 Bounded Context

  • 마이크로서비스는 애플리케이션을 작은 서비스로 나누어 개발하는 아키텍처 스타일이다.
  • 개별서비스를 독립된 프로세스로 실행하고 각 서비스가 REST API나 메시징을 이용해서 통신하는 구조를 갖는다.
  • 이런 특징은 Bounded Context와 잘 어울린다.
  • 각 Bounded Context는 모델의 경계를 형성하는데 Bounded Context를 마이크로 서비스로 구현하면 자연스럽게 컨텍스트별로 모델이 분리된다.
  • 별도 프로세스로 개발한 Bounded Context는 독립적으로 배포하고 모니터링하고 확장하게 되는데 이 역시 마이크로서비스의 특징이다.

https://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4e

https://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4ehttps://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4ehttps://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4ehttps://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4ehttps://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4e

https://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4e

https://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4e

https://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4e

https://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4ehttps://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4e

https://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4e

https://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4e

https://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4e

https://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4e

https://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4e

https://www.notion.so/9-1-fcb6b44a0067439a933d23aa8280ca4e

여러 애그리것이 필요한 기능

  • 도메인 영역 코드를 작성하다 보면 하나의 애그리것으로 기능을 구현할 수 없을 때가 있다.
  • 대표적인 예가 결제 금액 계산 로직
    • 상품 애그리거트 : 구매하는 상품의 가격이 필요하다. 또한 상품에 따라 배송비가 추가되기도 한다.
    • 주문 애그리거트 : 상품별로 구매 개수가 필요하다.
    • 할인 쿠폰 애그리거트 : 쿠폰별로 지정한 할인 금액이나 비율에 따라 주문 총금액을 할인한다.
    • 회원 애그리거트 : 회원 등급에 따라 추가 할인이 가능하다
    • 위 상황에서 실제 결제 금액을 계산하는 주체는 어떤 애그리것일까 ?
      • 총 주문 금액을 계산하는 것은 주문 애그리것이 할수 있지만 총 주문 금액에서 할인 금액을 계산해야 하는데 이 할인금액을 구하는 것은 누구의 책임일까 ?
      • 할인 쿠폰이 규칙을 갖고 있으니 할인 쿠폰의 책임인가 ?
      • 그런데 할인 쿠폰을 두개 이상 적용할 수 있다면 단일 할인 쿠폰 애그리것으로는 총 결제 금액을 계산할 수 없다.
    • 만약 주문 애그리것이 필요한 애그리것이나 필요 데이터를 모두 가지도록 한뒤 할인금액계산을 주문 애그리것에 할당한다면 어떻게 될까 ?
      • 만약 할인정책이 변경되거나 추가된다면 주문애그리것은 할인 애그리것과 관련이 1도 없는데도 불구하고 수정을 해야 한다.
    • 이렇게 한 애그리것에 넣기에 애매한 도메인 기능을 특정 애그리것에서 억지로 구현하면 안된다.
    • 왜냐 이경우 애그리것은 자신의 책임 범위를 넘어서는 구현을 하기 떄문에 코드가 길어지고 외부에 대한 의존이 높아지게 된다.
    • 이는 코드를 복잡하게 만들어 수정을 어렵게 만드는 요인이 된다.
    • 게다가 애그리것의 범위를 넘어서는 도메인 개념이 애그리것에 숨어들어서 명시적으로 드러나지 않게 된다.
    • 이런 경우엔 도메인 서비스 를 사용하자.

도메인 서비스

  • 위처럼 한 애그리것에 넣기 애매한 도메인 개념을 구현하려면 애그리것에 억지로 넣기 보단 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러내면 된다.
  • 응용 영역의 서비스가 응용 로직을 다룬다면 도메인 서비스는 도메인 로직을 다룬다.
  • 도메인 서비스는 상태 없이 로직만 구현한다
  • 필요한 상태는 애그리것이나 다른 방법으로 전달받는다.
  • 도메인 서비스는 도메인의 의미가 드러나는 용어를 타입과 메서드 이름으로 짓는다.
public class DiscountCalculationService {
	public Money calculateDiscountAmounts(
			List<OrderLIne> orderLines,
			List<Coupon> coupons,
			MemberGrade grade) {
		Money couponDiscount = coupons.stream()
																		.map(coupon -> calculateDiscount(coupon))
																		.reduce(Money(0), (v1, v2) -> v1.add(v2));

		Money membershipDiscount = calculateDiscount(orderer.getMember().getGrade());

		return couponDiscount.add(membershipDiscount);
	}

	...
}

 

  • 할인 계산 서비스를 사용하는 주체는 애그리것이 될 수 있고 응용 서비스가 될수도 있다.
  • 다음과 같이 기능을 전달하면 사용 주체는 애그리것이 된다.
public class Order {
	public void calculateAmounts(
			DiscountCalculationService disCalSvc, MemberGrade grade) {
		Money totalAmounts = getTotalAmounts();
		Money discountAmounts = disCalSvc.calculateDiscountAmounts(this.orderLInes, this.coupons, greade);
		this.paymentAmounts = totalAmounts.minus(discountAmounts);
	}
	...

 

  • 애그리것 객체에 도메인 서비스를 전달하는 것은 응용 서비스 책임이다.
public class OrderService {
	private DiscountCalculationService discountCalculationService;

	@Transactional
	public OrderNo placeOrder(OrderRequest orderRequest) {
		OrderNo orderno = orderRepository.nextId();
		Order order = createOrder(orderNo, orderRequest);
		orderRepository.save(order);
		// 응용 서비스 실행 후 표현 영역에서 필요한 값 리턴

		return orderNo;
	}

	private Order createOrder(OrderNo orderNo, OrderRequest orderReq) {
		Member member =findMember(orderReq.getOrdererId());
		Order order = new Order(orderNo, orderReq.gerOrderLines(),
							orderReq.getCoupons(), createOrderer(member),
							orderReq.getShippingInfo());
		order.calculateAmounts(this.discountCalculationService, member.getGrade());
		return order;
	}
	...
}

 

도메인 서비스 객체를 애그리것에 주입하지 않기.

  • 애그리것의 메소드를 실행할때 도메인 서비스 객체를 파라미터로 전달하는 것은 애그리것이 도메인 서비스에 의존한다는 것을 의미
  • 스프링이 제공하는 의존주입을 사용하여 도메인 서비스를 애그리것에 주입해야 기술적으로 나은것 같은 착각이 들 수 있다.
  • 하지만 개인적 (작가의견) 으론 좋은 방법이 아니라고 한다.
  • 도메인 객체는 필드로 구성된 데이터와 메서드를 이용한 기능을 이용해서 개념적으로 하나인 모델을 표현한다.
  • 모델의 데이터를 담는 필드는 모델에서 중요한 구성요소이다. 그런데 도메인 서비스 필드는 데이터 자체와는 관련이 없다.
  • order 객체를 DB에 보관할때 다른 필드와 달리 저장 대상도 아니다.
  • 또 Order이 제공하는 모든 기능에서 도메인 서비스 ( 할인정책 ) 을 필요로 하는 것도 아니다.
  • 일부 기능만 필요로 하는 도메인서비스 객체를 애그리것에 의존 주입할 필요는 없다.
  • 이는 프레임워크의 기능을 사용하고 싶은 개발자의 욕심을 채우는것에 불과하다.

 

 

  • 도메인 서비스를 인자로 전달하지 않고 반대로 도메인 서비스의 기능을 실행할때 애그리것을 전달하기도 한다.
public class TransgerService {
	public void transfer(Account fromAcc, Account toAcc, Money amounts) {
		fromAcc.withdraw(amounts);
		toAcc.credit(amounts);
	}
}

 

  • 특정기능이 도메인 서비스인지 응용서비스인지 감이 안오는 경우엔 해당 로직이 애그리것의 상태를 변경하거나 애그리것의 상태값을 변경하는지 검사해보면 된다.
  • 도메인 로직이면서 (위와 같이) 한 애그리것에 넣기 적합하지 않은 것은 도메인 서비스로 구현하게 된다.

 

도메인 서비스의 패키지 위치

  • 도메인 서비스는 도메인 로직을 실행하므로 도메인 서비스의 위치는 다른 도메인 구성요소와 동일한 패키지에 위치한다.
  • 예를 들어 주문금액계산 도메인 서비스는 주문 애그리것과 동일 패키지에 위치한다.
  • 도메인 서비스의 개수가 많거나 엔티티나 밸류와 같은 다른 구성요소와 명시적으로 구분하고 싶다면 domain 패키지 밑에 domain.model, domain.service, domain.repository와 같이 하위 패키지를 구분해서 위치시켜도 된다.

 

 

도메인 서비스의 인터페이스와 클래스

  • 도메인 서비스의 로직이 고정되어 있지 않은 경우 도메인 서비스 자체를 인터페이스로 구현하고 이를 구현한 클래스를 둘 수도 있다.
  • 특히 도메인 로직을 외부 시스템이나 별도 엔진을 이용해서 구현해야 할 경우에 인터페이스와 클래스를 분리하게 된다.

 

출처:https://minkukjo.github.io/dev/2020/11/19/DDD-07/

 

  • 도메인 서비스의 구현이 특정 구현 기술에 의존적이거나 외부 시스템 API를 실행한다면 도메인서비스를 인터페이스로 추상화 해야한다. 이를 통해 도메인 영역이 특정 구현에 종속되는 것을 방지할 수 있고 도메인 영역에 대한 테스트가 수월해진다.

표현 영역과 응용 영역

사용자에게 기능을 잘 제공하려면 도메인과 사용자를 연결시킬 표현영역과 응용영역이 필요하다.

  • 표현영역은 응용서비스가 요구하는 형식으로 사용자 요청을 변환한다.
  • 응용서비스가 요구하는 객체를 생성한뒤 응용서비스의 메서드를 호출한다.
public ModelAndView join(HttpServletRequest request) {
	String email = request.getParameter("email");
	String password = request.getParameter("password");

	//사용자 요청을 응용서비스에 맞게 변환
  JoinRequest joinReq = new JoinRequest(email, password);
  joinService.join(joinReq);
}

  • 응용서비스를 실행한뒤에 표현영역은 실행결과를 사용자에 알맞은 형식으로 응답한다.
  • 응용서비스는 표현영역이 뭘하던 의존하지 않는다. 사용자가 무엇을 사용하던지 알 필요가 없다.

응용 서비스의 역할

  • 응용서비스는 사용자의 요청을 처리하기 위해 리포지터리로 부터 도메인 객체를 구하고 도메인 객체를 사용한다.
  • 응용서비스의 주요 역할은 도메인 객체를 사용해서 사용자의 요청을 처리하는것이므로 표현영역에서 봤을때 응용 서비스는 도메인 영역과 표현영역을 연결해주는 창구인 파사드 역할을 한다.
public Result doSomeFunc(SomeReq req) {
	//1. 리포지터리에서 애그리것을 구한다
	SomeAgg agg = someAggRepository.findByid(req.getId());
	checkNull(agg);

	//2. 애그리것의 도메인 기능을 실행한다.
	agg.doFunc(req.getValue());

	// 3. 결과를 리턴한다.
  return createSuccessResult(agg);
}

  • 응용서비스가 복잡하다면 응용서비스에서 도메인 로직의 일부를 구현하고 있는지 확인해보자.
  • 트랜잭션은 응용서비스에서 처리한다.

도메인 로직 넣지 않기

  • 도메인로직을 도메인 영역과 응용 서비스에 분산해서 구현하면 코드품질에 문제가 발생한다.
  1. 코드의 응집도가 떨어진다.
  1. 여러 응용서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.
    1. 보조클래스를 만들어서 사용할수 있지만 애초에 도메인 영역에 구현하면 간단하다.

응용서비스의 구현

  • 응용서비스는 표현 영역과 도메인 영역을 연결하는 매개채 역할을 하는데 디자인 패턴에서 파사드 패턴과 같은 역할을 한다.
  • 응용 서비스는 복잡한 로직이 없기 때문에 구현이 쉽다.

응용 서비스의 크기

  • 보통 응용서비스의 크기는 다음 두가지 방법중 한가지 방식으로 구현한다.
  1. 한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기.
    • 한 도메인과 관련된 기능을 구현한 코드가 한 클래스에 위치하므로 각 기능에서 동일로직에 대한 코드 중복을 제거할수 있다.
    • 하지만 단점으로는 서비스 클래스의 크기가 커진다.
    • 코드가 커진다는 것은 연관성이 적은 코드가 한 클래스에 함께 위치할 가능성이 높아짐을 의미하는데 이는 결과적으로 관련없는 코드가 뒤섞여서 코드를 이해하는데 방해가 될 수 있다.
  1. 구분되는 기능별로 응용 서비스 클래스를 따로 구현하기.
    • 구분되는 기능별로 서비스 클래스를 구현하는 방식은 서비스 클래스에서 한개 내지 2~3개의 기능을 구현한다.
    • 이방식을 사용하면 클래스 갯수는 많아지지만 한 클래스에 관련 기능을 모두 구현하는것과 비교해서 코드 품질을 일정수준으로 유지하는데 도움이 된다.
    • 각 기능마다 동일한 로직을 구현할 경우 여러 클래스에 중복해서 동일한 코드를 구현할 가능성이 있다.
    • 이런 경우 별도 클래스에 로직을 구현해서 코드가 중복되는것을 방지할수 있다.
    public final class MemberServiceHelper {
    	//구현
    }

  • 범균이 형님은 한 클래스가 여러 역할을 갖는 것보다 각 클래스마다 구분되는 역할을 갖는것을 선호하신다. ( 2번 선호 )

응용 서비스의 인터페이스와 클래스(흥미로운 주제)

  • 응용 서비스를 구현할때 논쟁이 될만한 것이 인터페이스가 필요한지 여부이다
    • 인터페이스가 필요한 경우는 의존성을 분리할 때 또는 구현 클래스가 여러개일때 등등 여러가지 이유가 있지만 구현 클래스가 다수일때 인터페이스가 유용하게 쓰인다.
    • 그런데 응용서비스는 보통 런타임에 이를 교체하는 경우가 거의 없을뿐만 아니라 한 응용 서비스의 구현 클래스가 두 개인 경우도 매우 드물다.
    • 이런 이유로 인터페이스와 클래스를 따로 구현하면 소스 파일만 많아지고 구현 클래스에 대한 간접참조가 증가해서 전체 구조만 복잡해지는 문제가 발생한다.
    • 따라서 인터페이스가 명확하게 필요하기 전까지는 응용 서비스에 대한 인터페이스를 작성하는것이 좋은 설계는 아니다.
    • 만약 TDD를 하는 경우에는 미리 응용서비스를 구현할수 없으므로 응용서비스의 인터페이스를 작성하게 될것이다.
    • 하지만 mockito와 같은 테스트 도구를 사용하면 테스트용 가짜 객체를 만들기 때문에 인터페이스가 없어도 표현영역을 테스트 할수 있다.
    • 결과적으로 응용서비스에 대한 인터페이스 필요성을 약화시킨다.

메서드 파라미터와 값 리턴

파라미터

  • 값 전달을 파라미터로 받을수도 별도의 데이터 클래스 dto를 만들어 전달받을 수도 있다.
  • 스프링 MVC는 웹 요청 파라미터를 자바 객체로 변환해주는 기능 ( Converter ) 를 제공해주므로 파라미터가 두개 이상 존재하면 데이터 전달을 위한 별도의 클래스를 사용하는것이 편리하다.

값 리턴

  • 값리턴시 편하게 애그리것을 return할수도 있지만 애그리것을 리턴할 경우 의존성이 깨진다.
  • 도메인의 로직이 응용서비스에서 소비되는것이 아닌 표현영역까지 가서 소비되면 응집도를 낮추는 원인이 된다.
  • 응용서비스는 표현영역에서 필요한 데이터만 return 하도록 하자.

표현영역에 의존하지 않기.

  • 응용서비스의 파라미터 타입을 결정할때 주의할 점은 표현 영역과 관련된 타입을 사용하면 안된다.
  • 예를 들어 HttpServletRequest 나 HttpSession을 응용서비스에 파라미터로 전달하면 안된다.
  • 응용서비스에서 표현 영역에 대한 의존이 발생하면 응용 서비스만 단독으로 테스트하기가 어려워진다.
  • 만약 httpsession을 응용 서비스까지 가져와 변경해버린다면 표현 영역에서만 사용해야되는 httpsession이 응용서비스까지 내려온것이므로 표현 영역에 대한 응집도가 꺠진다.

트랜잭션 처리

  • 트랜잭션을 관리하는것은 응용 서비스의 중요한 역할이다
  • 스프링에서 제공하는 @Transaction 어노테이션을 사용하자. 매우 편하다
  • 이 어노테이션을 적용하면 RuntimeException이 발생하면 트랜잭션을 롤백하고 그렇지 않으면 커밋한다.

도메인 이벤트 처리

  • 응용서비스는 또한 도메인 영역에서 발생시킨 이벤트를 처리하는것이 주요 역할중 하나다.
  • 도메인 이벤트란 도메인에서 발생한 상태변경을 의미하며 "암호 변경됨", "주문 취소함"과 같은것이 이벤트가 될 수 있다.
public void changePassword(String oldPw, String newPw) {
        if (!password.match(oldPw)) {
            throw new IdPasswordNotMatchingException();
        }
        this.password = new Password(newPw);
        Events.raise(new PasswordChangedEvent(id.getId(), newPw));
    }
  • 도메인에서 이벤트를 발생시키면 그 이벤트를 받아서 처리할 코드가 필요한데 그 역할을 하는것이 바로 응용 서비스다.
  • 암호가 변경되면 암호가 변경됬다는 알림을 보내는 이벤트 핸들러를 등록할수 있을것이다.
  • 이벤트를 사용하면 코드가 다소 복잡해지는 대신 도메인 간의 의존성이나 외부시스템에 대한 의존을 낮춰주는 장점을 얻을 수 있다.
  • 또한 시스템을 확장하는데에 이벤트가 핵심 역할을 수행하게 된다.

표현 영역

  • 표현영역의 책임은 크게 다음과 같다.
    1. 사용자가 시스템을 사용할 수 있는 (화면) 흐름을 제공하고 제어한다.
    1. 사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다.
    1. 사용자의 세션을 관리한다. ( 권한 검사와도 연결이 된다. )

값 검증

  • 값 검증은 표현과 응용 두곳에서 모두 할수 있다.
  • 원칙적으로는 응용서비스에서 처리한다.

  • 표현계층에서 값 검증을 할 경우
    • 스프링 MVC는 에러 메시지를 보여주기 위한 용도로 Errors나 BindingResult를 사용한다.
    • 그렇기에 폼에 에러메시지를 보여주기 위해 다소 번잡한 코드를 작성해야 한다.

  • 응용계층에서 값 검증을 할 경우
    • 각 값이 올바른지 확인할 목적으로 익셉션을 사용할때의 문제점은 사용자에게 좋지않은 경험을 제공한다는 것이다.
    • 사용자는 잘못입력했을경우 어느부분에서 잘못됬는지 알고싶을것이다.
    • 하지만 응용계층에서 값을 검사하는시점에 첫번째값이 잘못되어 익셉션을 발생시키면 나머지 항목에 대해서는 값을 검사하지 않게된다.
    • 그래서 첫번째값의 검증만 알게되고 나머지 항목은 값이 올바른지 여부를 알수 없게되어 사용자가 여러번 입력하게 만든다.

  • 결국 표현계층에서 검증해야 하는데 Spring은 Validator인터페이스를 별도로 제공하므로 위 코드를 사용하면 간결하게 줄일수 있다.
  • 이렇게 표현영역에서 필수값과 값의 형식을 검사하면 실질적으로 응용서비스는 아이디 중복여부와 같은 논리적 오류만 검사하면 된다.
  • 표현영역
    • 필수값, 값의 형식, 범위등을 검증
  • 응용 서비스
    • 데이터의 존재 유무와 같은 논리적 오류를 검증한다.

  • 범균이형님은 두곳에서 다 값검사를 하는것이 아닌 위처럼 구분지어서 하시는 편이라고 하신다.
  • 하지만 응용서비스를 사용하는 주체가 다양하면 응용 서비스에서 반드시 파라미터로 전달받은 값이 올바른지 검사를 해야 된다고 하신다.

권한 검사

  • 보안 프레임워크의 복잡도를 떠나 보통 다음의 세곳에서 권한 검사를 수행할 수 있다.
    • 표현 영역
    • 응용 서비스
    • 도메인 영역

  • 이 URL을 처리하는 컨트롤러에 웹 요청을 전달하기 전에 인증 여부를 검사해서 인증된 사용자의 웹 요청만 컨트롤러에 전달.
  • 인증된 사용자가 아닐경우 로그인 화면으로 리다이렉트
  • 위와 같은 접근제어를 하기 좋은 위치가 서블릿 필터다.
  • 만약 URL만으로도 불가능한 경우에는 스프링 시큐리티는 AOP를 이용한 어노테이션을 이용해 검사를 할 수 있다.
@PreAuthorize("hasRole('ADMIN')")
    @Transactional
    public void block(String memberId) {
        Member member = memberRepository.findById(new MemberId(memberId));
        if (member == null) throw new NoMemberException();

        member.block();
    }

  • 개별 도메인 단위로 검사해야하는 경우는 구현이 아래와 같이 복잡해진다. ( 게시글 삭제는 본인이 또는 관리자 역할을 가진 사용자만 할수 있는 경우의 검증 )

	public void delete(String userId, Long articleId) {
		Article article = articleRepository.findById(articleId);
		checkArticleExistence(article);
		permissionService.checkDeletePermission(userId, article);
		article.markDeleted();
	}

  • 만약 프레임워크 이해도가 낮아 프레임 워크 확장을 원하는 수준으로 할 수 없다면 프레임워크를 사용하는 대신 도메인에 맞는 권한 검사 기능을 직접 구현하는 것이 코드 유지보수에 유리할 수 있다.

조회 전용 기능과 응용 서비스

public class OrderListService {
	public List<OrderView> getOrderList(String ordererId) {
		return orderViewDao.selectByOrderer(ordererId);
	}
}
  • 위와 같이 아주 간단한 조회기능인 경우 표현영역에서 바로 조회 전용 기능을 사용해도 된다.

@RequestMapping("/myorders")
	public String list(ModelMap model) {
		String ordererId = SecurityContext.getAuthentication().getId();
		List<OrderView> orders = orderViewDao.selectByOrderer(ordererId);
		model.addAttribute("orders", orders);
		return "order/list";
	}

  • 범균이형님은 굳이 응용 서비스를 만들 필요성을 못느낀다면 안만들어도 된다고 생각하신다.
  • 조회를 위한 응용 서비스가 단지 조회 전용 기능을 실행하는 코드밖에 없다면 응용 서비스를 생략한다.

 

Hpa의 동작 방식

 

 

출처: 쿠버네티스 공식 홈페이지

Horizontal Pod Autoscaler 다이어그램

https://kubernetes.io/ko/docs/tasks/run-application/horizontal-pod-autoscale/

자세한 설명은 공식홈페이지에 잘 나와있다.

 

Prometheus를 이용한 Hpa

공식문서에 보면 Hpa는 Api 집합 에서 메트릭을 가져오는데 기본은 metrics.k8s.io고

Prometheus metric을 이용하기 위해선 custom.metrics.k8s.io를 사용한다.

 

출처: https://morioh.com/p/1cc4fd9693fa

 

Custom Metrics은 Prometheus Adapter를 바라보고 Prometheus Adapter는 Prometheus를 바라본다.

실질적으론 서비스를 바라보고 있다.

 

적용해보기

 

  1. Prometheus adapter 설치
  1. Prometheus 와의 연결 확인
  1. Custom Metrics ApiServer 설치
  1. Prometheus Adapter와의 연결 확인

 

  • Prometheus Adapter yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: custom-metrics-apiserver
  name: custom-metrics-apiserver
  namespace: custom-metrics
spec:
  replicas: 1
  selector:
    matchLabels:
      app: custom-metrics-apiserver
  template:
    metadata:
      labels:
        app: custom-metrics-apiserver
      name: custom-metrics-apiserver
    spec:
      serviceAccountName: custom-metrics-apiserver
      containers:
      - name: custom-metrics-apiserver
        image: directxman12/k8s-prometheus-adapter-amd64
        args:
        - --secure-port=6443
        - --tls-cert-file=/var/run/serving-cert/serving.crt
        - --tls-private-key-file=/var/run/serving-cert/serving.key
        - --logtostderr=true
        - --prometheus-url=http://prometheus.prom.svc:9090/
        - --metrics-relist-interval=1m
        - --v=10
        - --config=/etc/adapter/config.yaml
        ports:
        - containerPort: 6443
        volumeMounts:
        - mountPath: /var/run/serving-cert
          name: volume-serving-cert
          readOnly: true
        - mountPath: /etc/adapter/
          name: config
          readOnly: true
        - mountPath: /tmp
          name: tmp-vol
      volumes:
      - name: volume-serving-cert
        secret:
          secretName: cm-adapter-serving-certs
      - name: config
        configMap:
          name: adapter-config
      - name: tmp-vol
        emptyDir: {}
  • pod spec의 args 부분에 prometheus-url에 prometheus k8s dns 를 적어준다.

 

  • Prometheus Service yaml
apiVersion: v1
kind: Service
metadata:
  name: custom-metrics-apiserver
  namespace: custom-metrics
spec:
  ports:
  - port: 443
    targetPort: 6443
  selector:
    app: custom-metrics-apiserver

 

  • Custom Metrics ApiService
apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
  name: v1beta2.custom.metrics.k8s.io
spec:
  service:
    name: custom-metrics-apiserver
    namespace: custom-metrics
  group: custom.metrics.k8s.io
  version: v1beta2
  insecureSkipTLSVerify: true
  groupPriorityMinimum: 100
  versionPriority: 200
  • spec의 service는 prometheus-adapter의 svc를 입력한다.

 


 

 

예제는 아래의 깃주소를 참고

https://github.com/kubernetes-sigs/prometheus-adapter/blob/master/deploy/manifests/custom-metrics-apiservice.yaml

 

 


 

원하는 메트릭을 hpa의 조건으로 정하기

위와 같이 연결을 하면 이제 prometheus의 metric을 사용할 기본 토대가 마련되었다.

 

원하는 메트릭을 가져올 쿼리를 설정하는 곳은 prometheus adapter args로 설정한

/etc/adapter/config.yaml 에서 설정한다.

configmap으로 연결되어있으므로 adapter-config cm에 설정하면 된다.

 

설정하는 방법은 아래의 깃주소를 참고

https://github.com/kubernetes-sigs/prometheus-adapter/blob/master/docs/config.md

 

 

적용이 안되는 경우

  1. custom metric apiserver가 존재하지 않거나 연결이 되지 않은 경우
  1. 프로메테우스에 조건에 맞는 메트릭이 없는 경우
  1. 기타 이유들...

 

 

 

https://www.notion.so/TimescaleDB-a31941c1c7df4837ab4050357c87267c

 

TimescaleDB 테이블 생성 후 관리

TimeScaleDB란

www.notion.so

 

TimeScaleDB란

postgresql 기반으로 나온 시계열 데이터베이스다.

TimeScaleDB Table 생성

CREATE TABLE conditions (
    time        TIMESTAMPTZ       NOT NULL,
    location    TEXT              NOT NULL,
    temperature DOUBLE PRECISION  NULL
);

SELECT create_hypertable('conditions', 'time'); //hypertable 설정

시계열데이터베이스를 사용하기 위해선 hypertable설정을 해줘야 한다.

Chunk

table 생성후 hypertable 적용하면 chunk table이 생성된다.

chunk table을 따로 설정하지 않았다면 기본주기는 7일이며 7일뒤에 새로운 chunk table을 생성한다.

chunk table은 table에 저장된 data들을 가지고 있다.

chunk table 삭제시 data들도 삭제된다.

TimeScaleDB 오래된 데이터 제거

시계열 데이터는 데이터가 많이 쌓이므로 주기적으로 제거할 필요가 있지만

DELETE로 삭제시 많은 부하가 걸리므로 아래와 같은 방식을 공식홈페이지에선 추천한다.

SELECT add_retention_policy('conditions', INTERVAL '24 hours');

  • 24시간이 지난 chunk table 삭제

chunk table이 삭제가 안되는 경우

  1. chunk table이 한개밖에 없는 경우
    • chunk table의 주기를 확인해보자.

Chunk Table의 주기 설정

SELECT set_chunk_time_interval('conditions', INTERVAL '24 hours');

  • 24시간 주기 설정
  • 이 설정은 테이블 생성시 설정한 주기가 끝난 이후에 적용된다.
  • 만약 테이블 생성시 설정하지 않았다면 기본설정인 7일 이후에 적용된다.

마무리

  • 필요한 기능만 살펴보느라 아주 기본적인 기능만 살펴봤는데 기회가 되면 더 깊게 공부해볼 필요가 있는것 같습니다.

https://www.notion.so/K8s-Apiversion-a7c2c8da3e184d659175d191820db1e0

 

K8s Apiversion

왜 api버전이 바뀌어 있을까 ?

www.notion.so

 

왜 api버전이 바뀌어 있을까 ?

머선129

  • hpa autoscaling v2beta2버전으로 배포한 어느날 수정을 위해 k edit hpa -n ns hpa-name 명령어로 조회한 순간 apiversion이 autoscaling/v1으로 변경된것을 확인했다.
  • 어찌된 일일까 ?

출처

https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api_changes.md#operational-overview

 

```java
To demonstrate the general process, here is a (hypothetical) example:

1. A user POSTs a Pod object to /api/v7beta1/...
2. The JSON is unmarshalled into a v7beta1.Pod structure
3. Default values are applied to the v7beta1.Pod
4. The v7beta1.Pod is converted to an api.Pod structure
5. The api.Pod is validated, and any errors are returned to the user
6. The api.Pod is converted to a v6.Pod (because v6 is the latest stable version)
7. The v6.Pod is marshalled into JSON and written to etcd
```
  • 공식문서에 보면 beta버전으로 등록할 경우 베타버전으로 등록되지않고 최신안정화버전으로 변경되어 등록된 후 etcd에 저장된다.
Now that we have the Pod object stored, a user can GET that object in any supported api version. For example:

1. A user GETs the Pod from /api/v5/...
2. The JSON is read from etcd and unmarshalled into a v6.Pod structure
3. Default values are applied to the v6.Pod
4. The v6.Pod is converted to an api.Pod structure
5. The api.Pod is converted to a v5.Pod structure
6. The v5.Pod is marshalled into JSON and sent to the user
  • 호출할 경우 지원되는 모든버전으로 호출할수 있습니다.

이런 이유로 v2beta2로 등록한 autoscaling이 버전없이 조회할경우 etcd에 저장된 v1버전으로 조회가 된것.

'개발 > Kubernetes' 카테고리의 다른 글

컨테이너 런타임  (0) 2021.03.07
Mutating Webhook 를 이용한 pod injection  (0) 2021.02.16
쿠버네티스의 클러스터 구성  (0) 2021.01.28
k8s 에서 spring boot jib image command 수정  (0) 2020.12.24
클러스터 구성  (0) 2020.09.13

https://www.notion.so/5-JPA-d590d2e73f084585b558d194f87d1ef1

'book > DDD start' 카테고리의 다른 글

7장 도메인 서비스  (0) 2021.06.22
6장 응용서비스와 표현 영역  (0) 2021.06.18
4장 리포지터리와 모델구현 ( JPA 중심 )  (0) 2021.06.04
DDD start 3장 - 애그리것  (0) 2021.05.22
D.D.D start 1,2장  (0) 2021.05.19

https://www.notion.so/4-JPA-03842af0e4c841b5bba9d8e89f3ae0b2

'book > DDD start' 카테고리의 다른 글

7장 도메인 서비스  (0) 2021.06.22
6장 응용서비스와 표현 영역  (0) 2021.06.18
5장 리포지토리 조회기능 ( JPA 중심 )  (0) 2021.06.05
DDD start 3장 - 애그리것  (0) 2021.05.22
D.D.D start 1,2장  (0) 2021.05.19

+ Recent posts