기술 서적 정리/만들면서 배우는 헥사고날 아키텍처

02. 도메인 헥사곤으로 비즈니스 규칙 감싸기

wampy 2023. 5. 1.
  • 도메인 헥사곤은 가장 내부에 있는 헥사곤이므로 그 위에 있는 어떤 헥사곤에도 의존하지 않는다

엔티티를 활용한 문제 영역 모델링

  • 엔티티를 간주하기 위해선 엔티티가 식별자를 가져야 함

도메인 엔티티의 순수성

  • 문제 영역 모델링의 핵심 -> 엔티티를 만드는 것
    • 엔티티는 비즈니스 요구사항과 밀접한 관계를 가져야 한다
    • 기술적인 요구사항으로부터 보호해야 함
  • -> 비즈니스 관련 코드와 기술 관련 코드가 혼동되는 것을 방지하자

관련 엔티티

  • Router 클래스에 있는 라우터들을 필터링하고 나열하는 retriveRouter 메서드...
public static List<Router> retrieveRouter(List<Router> routers, Predicate<Router> predicate){
    return routers.stream()
        .filter(predicate)
        .collect(Collectors.<Router>toList());
}
  • 이 동작은 라우터의 본질적인 특성인가?
  • 리스트에 라우터를 추가하기 전에 라우터 타입을 확인하는 데 사용한 제약사항은?
    • 이 제약사항을 엔티티 클래스에 직접 포함시킨다
    • 해당 제약사항을 어써션 처리하기 위한 명세를 작성한다
// 라우터 타입을 확인하는 제약사항을 확인하는 메서드
public static Predicate<Router> filterRouterByType(RouterType routerType){
    return routerType.equals(RouterType.CORE)
        ? Router.isCore() :
    Router.isEdge();
}

public static Predicate<Router> isCore(){
    return p -> p.getRouterType() == RouterType.CORE;
}

public static Predicate<Router> isEdge(){
    return p -> p.getRouterType() == RouterType.EDGE;
}

도메인 서비스 메서드를 수용하기 위한... RouterSearch 도메인 서비스 클래스

Router 클래스에서 이 클래스로 retriveve 메서드를 옮긴다

public class RouterSearch {

    public static List<Router> retrieveRouter(List<Router> routers, Predicate<Router> predicate){
        return routers.stream()
                .filter(predicate)
                .collect(Collectors.<Router>toList());
    }
}

UUID를 이용한 식별자 정의

public class RouterId {

    private final UUID id;

    private RouterId(UUID id){
        this.id = id;
    }

    public static RouterId withId(String id){
        return new RouterId(UUID.fromString(id));
    }

    public static RouterId withoutId(){
        return new RouterId(UUID.randomUUID());
    }

    @Override
    public String toString() {
        return "RouterId{" +
                "id='" + id + '\'' +
                '}';
    }
}

값 객체를 통한 서술형 향상

  • 값 객체는 다음과 같은 특성을 기반으로 함
    • 값 객체는 불변이다
    • 값 객체는 식별자를 갖지 않는다
  • long이나 int형 값 대신 값 객체를 통해 속성을 기술 속성을 좀 더 명확하게 표현할 수 있다
  • ex. 값 객체를 사용하지 않았을 때
public class Event implements Comparable<Event> {
    private EventId id;
    private OffsetDateTime timestamp;
    private String protocol;
    private String activity;
    ...
}

로그 파싱할 때...

` casanova.578781 > com.study.domain: `

`var srcHost = event.getActivity().split(">")[0]`

public class Activity {

    private String description;
    private final String srcHost;
    private final String dstHost;

    public Activity (String description, String srcHost, String dstHost){
        this.description = description
        this.srcHost = description.split(">")[0];
        this.dstHost = description.split(">"[1]);
    }
    
    public String retrieveSrcHost() {
        return this.srcHost;
    }

    @Override
    public String toString() {
        return "Activity{" +
                "srcHost='" + srcHost + '\'' +
                ", dstHost='" + dstHost + '\'' +
                '}';
    }
}

`var srcHost = evetn.getActivity().retrieveSrcHost()`

에그리게잇을 통한 일관성 보장

  • 관련 엔티티와 값 객체의 그룹의 전체적인 개념을 설명
  • 객체의 데이터와 동작을 조정한다
  • 애그리게잇 영역와 상호작용할 진입점(entry point)를 정의해야 함
    • 애그리게잇 루트(aggregate root)
    • 애그리게잇의 일부인 엔티티와 값 객체들에 대한 참조를 유지
  • 바운더리를 통해 바운더리 내부의 객체가 수행하는 오퍼레이션에서 더 나은 일관성을 보장
  • 낙관적 잠금, 비관적 잠금, JTA 기법 통합하는데 유연해짐
    • 낙관적 잠금? (ref. https://reiphiel.tistory.com/entry/understanding-jpa-lock)
      • 낙관적 잠금은 현실적으로 데이터 갱신시 경합이 발생하지 않을 것이라고 낙관적으로 보고 잠금을 거는 기법입니다.
      • 일동의 충돌 감지
      • Hibernate에서 사용
        • `@Version` 버전 필드를 생성해서 버전 관리 하는 식으로 동시성 제어
        • `@OptimisticLocking`
          • Hibernate에서 제공하는 낙관적 잠금을 사용하는 방법
          • NONE, VERSION, DIRTY, ALL
            • DIRTY, ALL : 버전 필드 없이도 낙관적 잠금 사용
    • 비관점 잠금 (선점 잠금)
      • 동일한 데이터를 동시에 수정할 가능성이 높다는 비관적인 전제로 잠금을 거는 방식
      • 이 경우 충돌감지를 통해서 잠금을 발생시키면 충돌발생에 의한 예외가 자주 발생하게 됩니다. 이럴경우 비관적 잠금을 통해서 예외를 발생시키지 않고 정합성을 보장하는 것이 가능합니다. 다만 성능적인 측면은 손실을 감수해야 합니다. 주로 데이터베이스에서 제공하는 배타잠금(Exclusive Lock)을 사용합니다.
  • 성능과 확장성 관점에서는 항상 애그리게잇을 가능한 작게 유지하기 위해 노력해야 함

도메인 서비스 활용

  • MVC 아키텍처에서의 서비스...
    • 애플리케이션의 서로 다른 측면을 연결하고
    • 데이터를 처리하며
    • 시스템의 내부와 외부에서 호출을 조정하는 다리 역할
  • 서비스는
    • 무언가 가치 있는 활동을 수행하는 능력
    • 훌륭한 아키텍처는.. -> 관심사의 분리(SoC: Separation of Concerns), 모듈화, 디커플링
  • 도메인 서비스
    • 다른 서비스와 마찬가지로 가치 있는 작업을 수행하지만
    • 문제 영역의 제한된 범위 내에서만 수행
public class Router {
	...
        
	public void addNetworkToSwitch(Network network){
        this.networkSwitch = networkSwitch.addNetwork(network);
    }

    public Network createNetwork(IP address, String name, int cidr){
        return new Network(address, name, cidr);
    }
}
public class NetworkOperation {

	final private int MINIMUM_ALLOWED_CIDR = 8;
    
    public void createNewNetwork(Router router, IP address, String name, int cidr) {
        if (cidr < MINIMUM_ALLOWED_CIDR)
            throw new IllegalArgumentException("CIDR is below " + MINIMUM_ALLOWED_CIDR);
        
        if (isNetworkAvailable(router, address))
            throw new IllegalArgumentException("Address already exist");
        
        Network network = router.createNetwork(address, name, cidr);
        router.addNetworkToWitch(network);
    }
    
    private boolean isNetworkAvailable(ROuter router, IP address) {
        var availability = true;
        for (Network network : router.retreieveNetworks()) {
            if (network.getAddress().eqauls(address) && network.getCidr() == cidr) {
                availability = false;
                break;
            }
        }
        
        return availability;
    }
}
  • 엔티티와 값 객체에 잘 어울리지 않는 작업을 처리하는 서비스 클래스에 책임을 위임

정책 및 명세 패턴을 활용한 비즈니스 규칙 처리

  • 비즈니스 지식을 동작하는 소프트웨어로 변환하는 데 활용할 수 있는 기법들...
  • 정책 (Policy)
    • 전략(startegy)
    • 코드 블록으로 문제 영역의 일부를 캡슐화
    • 제공된 데이터에 대해 어떤 작업이나 처리를 함
    • 커플링을 피하기 위해 의도적으로 엔티티와 값 객체를 분리해 유지
  • 명세 (Specification)
    • 객체의 특성을 보장하는데 사용되는 조건이나 프레디케이트
    • 단순한 논리적인 연산자보다는 더 표현적인 방법으로 프레디게이트를 캡슐화 함
package dev.davivieira.domain.specification.shared;

public interface Specification<T> {

    boolean isSatisfiedBy(T t);

    Specification<T> and(Specification<T> specification);
}
package dev.davivieira.domain.specification.shared;

public abstract class AbstractSpecification<T> implements Specification<T> {

    public abstract boolean isSatisfiedBy(T t);

    public Specification<T> and(final Specification<T> specification) {
        return new AndSpecification<T>(this, specification);
    }
}
public class AndSpecification<T> extends AbstractSpecification<T> {

    private Specification<T> spec1;
    private Specification<T> spec2;

    public AndSpecification(final Specification<T> spec1, final Specification<T> spec2) {
        this.spec1 = spec1;
        this.spec2 = spec2;
    }

    public boolean isSatisfiedBy(final T t) {
        return spec1.isSatisfiedBy(t) && spec2.isSatisfiedBy(t);
    }
}
public class CIDRSpecification extends AbstractSpecification<Integer> {

    final static public int MINIMUM_ALLOWED_CIDR = 8;
	// 1. 새로운 네트워크 생성에 허용되는 최소 CIDR을 제한하는 비즈니스 규칙
    @Override
    public boolean isSatisfiedBy(Integer cidr) {
        return cidr > MINIMUM_ALLOWED_CIDR;
    }
}
public class NetworkAvailabilitySpecification extends AbstractSpecification<Router> {

    private IP address;
    private String name;
    private int cidr;

    public NetworkAvailabilitySpecification(IP address, String name, int cidr) {
        this.address = address;
        this.name = name;
        this.cidr = cidr;
    }

    @Override
    public boolean isSatisfiedBy(Router router) {
        return router!=null && isNetworkAvailable(router);
    }
	
    // 2. 네트워크 주소가 이미 사용되고 있는지 검사하는 비즈니스 규칙
    private boolean isNetworkAvailable(Router router){
        var availability = true;
        for (Network network : router.retrieveNetworks()) {
            if(network.getAddress().equals(address) && network.getName().equals(name) && network.getCidr() == cidr)
                availability = false;
            break;
        }
        return availability;
    }
}
public class NetworkOperation {

    public static Router createNewNetwork(Router router, Network network) {
        var availabilitySpec = new NetworkAvailabilitySpecification(network.getAddress(), network.getName(), network.getCidr());
        var cidrSpec = new CIDRSpecification();
        var routerTypeSpec = new RouterTypeSpecification();
        var amountSpec = new NetworkAmountSpecification();

        if(cidrSpec.isSatisfiedBy(network.getCidr()))
            throw new IllegalArgumentException("CIDR is below "+CIDRSpecification.MINIMUM_ALLOWED_CIDR);

        if(!availabilitySpec.isSatisfiedBy(router))
            throw new IllegalArgumentException("Address already exist");

        if(amountSpec.and(routerTypeSpec).isSatisfiedBy(router)) {
            Network newNetwork = router.createNetwork(network.getAddress(), network.getName(), network.getCidr());
            router.addNetworkToSwitch(newNetwork);
        }
        return router;
    }
}

댓글