본문 바로가기
관리자

Programming-[Base]/Design, Architecture

[작성중][헥사고날 아키텍처] - 1. 기본 개념, 도메인 헥사곤

728x90
반응형

 

아키텍처의 중요성

보통 서비스가 고도화됨에 따라 누적 기능이 증가하는 속도가 느려진다. 도메인의 로직이 고도화되고 상호 작용이 늘어날수록 기능을 추가하는게 어려워지기 때문이다. 선행 설계에 드는 비용 대비 기능 추가 비용의 조화가 필요한 것이 원칙이지만, 아키텍처도 이런 tradeoff를 줄여주는 중요한 요소이다.

 

 

1. 헥사고날 아키텍처의 기본 개념

 

내부에서부터 도메인 헥사곤, 애플리케이션 헥사곤, 프레임워크 헥사곤으로 구성된다.

 

https://engineering.linecorp.com/ko/blog/port-and-adapter-architecture

 

 

필요성 ⭐️

내부에서 사용하는 Framework, 외부에서 붙이는 MYSQL 등에 의존하지 않는 프로젝트를 만들기 위함. 보통 많이 사용하는 Layered Architecture(repository- service - controller) 구조만으로는 각 레이어를 인터페이스화(어댑터화)하여 다양한 framework나 vendor사의 기술을 대체하기 쉬운 구조로 만들기가 어렵다. 헥사고날 아키텍처를 적용하면 이런 구조를 모두 인터페이스화하기 때문에 다양한 기술을 적용할 수 있고 갈아끼우기가 쉽게 된다.

 

 

 

1-1. 도메인 헥사곤

엔티티(entity)와 값 객체(value object)로 이루어진다. 엔티티는 도메인을 표현하는 속성값들의 집합? 정도이고, 값 객체는 프로젝트에서 임의로 변경할 수 없지만 Type으로 정규화하는 enum 같은 값이라고 한다. long, int 등 대신 값 객체로 표현하면 불변이고, 의미를 설명할 수 있는 값을 엔티티의 속성 값으로 표시할 수 있게 된다.

 

1-2. 애플리케이션 헥사곤

 

유스케이스(UseCase)

엔티티가 할 수 있는, 엔티티와 관련된 기능 등을 interface로 추상화한 것이다.

 

입력포트(InputPort)

유스케이스의 구현체이다.

 

출력포트(outputPort)

외부 리소스에서 기술에 관계없이 데이터를 가져올 수 있도록 만든 인터페이스이다.

 

 

1-3. 프레임워크 헥사곤

도메인, 애플리케이션 헥사곤 단에서는 어떤 기술을 채택하느냐에 대한 결정사항과는 완전히 분리되어 있었다. 각종 기술들의 종류를 프레임워크 헥사곤에서 결정한다.

 

드라이빙 오퍼레이션

 

 

...

 

 

2장

도메인 헥사곤

 

Entitiy 메서드 관리

순수하게 Entity와 관련된 메서드만 Entity에 포함되어야한다. 만약 Router라는 도메인을 조건(Predicate)에 따라 목록으로 가져오는 메서드를 만든다면, 이것은 순수 메서드가 아니므로 service/RouterSearch 클래스에 포함시킨다.

 

Entity의 식별자(ID) 정의하기

확장성이 크지 않거나 내부에서만 사용하는 시스템의 경우 보통 GeneratedValue를 AUTO INCREMENT로 설정한다. 데이터베이스의 시퀀스 스펙을 따라서 처리하는 것이다. 하지만 헥사고날 아키텍처의 사상에서는 Entity는 정말 순수한 기능만 하는 것으로 상정한다. 그래서 DB와의 의존성도 두지 않기 위해서 UUID로 식별자를 정의한다. 아래 사진과 같은 양식이 될 수 있겠다. 다만 uuid는 용량도 크고 정렬도 되지 않아서 TSID를 대신 사용한다고 한다.

 

ref.) TSID 참고

https://ssdragon.tistory.com/162

 

그리고 Id 값도 Enum Type 값처럼 따로 클래스로 분리해준다.

 

 

그리고나서 아래처럼 withId, withoutId 메서드를 만들어두면 id 값이 있는 경우 withId 메서드를 사용해서 엔티티의 재구성을 허용하고, id가 없는 경우에는 새로운 엔티티를 만들 수 있도록 해준다.

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());
    }
    
    //toString method 생략
}

 

 

애그리게잇(aggregate)으로 처리하기

 

교재에서 Router가 루트 엔티티이고, 그 하위에 스위치, 네트워크, IP가 있는 구조라면 위 도식처럼 각각을 Entity, VO(Value Object) 들로 만들고 루트 엔티티가 되는 Router를 aggregate root가 되도록 합쳐서 구성할 수 있다. 참고로 Switch의 경우는 Id, Type 등의 변할 수 있는 값을 가지므로 엔티티로 처리하고, Network, IP는 불변 객체임을 보장하는 형태이므로 VO로 처리한다. 이렇게 처리하면 엔티티와 VO상 관계를 자연스럽게 표현하고 응집력있게 처리할 수 있다. 다시 말해 Router, Network 관련 로직이 Router에 있는지, Network에 있는지 일일이 찾아볼 필요없이 루트가 되는 Router에서 확인이 가능하다는 것이다.

 

그리고 Network, IP등이 애그리게이트 엔티티에서 사용되어 일관성있게 표현될 수 있게한다. 예를 들어 아래 메서드는 Router 엔티티 클래스에 포함시킬 수 있으며 이 메서드가 Network, IP를 모두 포함한다.

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

 

JPA로 엔티티를 만드는 경우, Switch-Network 관계는 아래처럼 @Embedded 어노테이션으로 내부 VO가 처리되고 Switch 테이블상의 network는 여러 컬럼으로 나뉘어 표시된다.

//-엔티티
@Entity
public class Switch {
    @Id
    private Long switchId;

    @Embedded
    private Network network;
}

//-테이블
| switch_id | network_address | network_name | network_cidr |
-------------------------------------------------------------
| 1         | 192.168.1.1     | network1     | 24           |
| 2         | 192.168.1.2     | network2     | 24           |

 

 

 

도메인 서비스

 

구성 원칙

서비스 레이어는 도메인에 속하므로 바깥의 애플리케이션, 프레임워크 헥사곤의 객체들을 가져다 쓰면 안된다. 애플리케이션, 프레임워크 헥사곤들이 도메인 서비스를 호출하는 클라이언트가 되도록 구성해야한다. 달리 말해 도메인의 관심사 영역을 넘어가는 부분을 도메인 서비스 부분에 작성하면 안된다.

 

그리고 서비스에 사용되는 메서드들은 각각의 도메인에서 순수하게 정의하기는 힘든 정교한 조건이나 로직이 필요한 경우에 작성한다.

 

 

 


 

참조

[서적] 만들면서 배우는 헥사고날 아키텍처 설계와 구현

- 워키북스 | 다비 비에이라 지음, 김영기 옮김

728x90
반응형