프로그래밍

헥사고날 아키텍쳐 - 프레임워크 헥사곤

일태우 2024. 8. 4. 21:40

프레임워크 헥사곤

소프트웨어와 통신할 수 있는 기술을 결정한다. 통신은 두 가지 형태로 발생할 수 있다.

  • 드라이빙 방식(Driving): 입력 어댑터(Input Adapter)를 사용한다.
  • 드리븐 방식(Driven): 출력 어댑터(Output Adapter)를 사용한다.

드라이빙 오퍼레이션과 입력 어댑터

드라이빙 오퍼레이션은 소프트웨어에 동작을 요청하는 것이다.

  • 웹 애플리케이션에서 사용자가 버튼 눌러 폼을 제출하는 경우
  • 외부 시스템에서 REST API로 애플리케이션에 데이터를 요청하는 경우
  • 외부 시스템에서 MQ로 메세지를 보내는 경우

이러한 API는 외부 엔티티가 시스템과 상호작용하고, 외부 엔티티의 요청을 도메인 애플리케이션으로 변환하는 방법을 정의한다.

드라이빙이라는 단어는 외부 엔티티들이 시스템의 동작을 유도하기 때문에 쓰인다.

다음은 입력 포트 오퍼레이션 중 stdin을 이용하여 호출 하는 입력 어댑터이다.

public class RouterViewCLIAdapter {

    private RouterViewUseCase routerViewUseCase;

    public RouterViewCLIAdapter() {
        setAdapters();
    }

    public List<Router> obtainRelatedRouters(String type) {
        return routerViewUseCase.getRouters(Router.filterRouterByType(RouterType.valueOf(type)));
    }

    private void setAdapters() {
        this.routerViewUseCase = new RouterViewInputPort(RouterViewFileAdapter.getInstance());
    }
}

유스케이스 인터페이스를 통해 입력 포트를 사용하는 것이 중요한 포인트이다.

  • 명령어를 통해 obtainRelatedRouters를 호출하면 애플리케이션 유스케이스를 호출한다.
  • 입력 데이터 캡슐화: 문자열 형태의 type을 RouterType 열거형으로 변환하여 도메인 엔티티로 캡슐화 한다.
  • 도메인 로직 호출: 변환된 RouterType을 통해 도메인 제약사항을 다룬다.
  • 유스케이스 실행: 필터링된 결과를 routerViewUseCase.getRouters를 통해 비즈니스 로직을 수행한다.

REST 와 같은 다른 통신 형식을 활성화하려면 REST 통신 엔드포인트 노출을 위한 의존성을 포함하는 새로운 REST 어댑터를 생성하면 된다.

드리븐 오퍼레이션과 출력 어댑터

애플리케이션에서 트리거되며, 외부에서 소프트웨어 요구사항을 충족시키는 데 필요한 데이터를 가져온다. 일반적으로 드리븐 오퍼레이션은 일부 드라이빙 오퍼레이션에 응답해 발생한다. 출력 어댑터를 통해서 정의되며 이 어댑터는 그것들을 구현하는 출력 포트와 일치해야 한다.

출력 포트는 애플리케이션 비즈니스를 수행하는 데 필요한 데이터의 종류를 알려주며, 데이터를 어떻게 가져올지 설명하는 것이 출력 어댑터의 역할이다.

  • 도메인 로직에서 처리된 데이터를 데이터베이스에 저장하는 경우
  • 애플리케이션이 외부 API를 호출하여 데이터를 전송하거나 요청하는 경우
  • 도메인 로직이 완료된 후 MQ에 메세지를 보내는 경우

예를 들어 애플리케이션이 오라클 기반 DB로 구성되었다가 Mongo DB로 변경한다고 해보자. 초기에는 오라클 DB와 지속성을 허용하는 출력 어댑터만 가지고 있었다. Mongo와 통신하기 위해 애플리케이션과 도메인 헥사곤은 건들면 안되고, 프레임워크 헥사곤에 출력 어댑터를 생성해야 한다. 입력 어댑터와 출력 어댑터 모두 헥사곤 내부를 가리키고 있기 때문에 이것들을 애플리케이션 및 도메인 헥사곤에 종속되게 만들어 의존성을 역전 시킨다.

드리븐이라는 단어는 헥사고날 애플리케이션 자체에 의해 오퍼레이션이 유도되고 통제되며, 다른 외부 시스템에서 동작을 트리거 하기 때문이다.

public class RouterViewFileAdapter implements RouterViewOutputPort {

    private static RouterViewFileAdapter instance;

    @Override
    public List<Router> fetchRouters() {
        return readFileAsString();
    }

    private static List<Router> readFileAsString() {
        List<Router> routers = new ArrayList<>();
        try (Stream<String> stream = new BufferedReader(
                new InputStreamReader(RouterViewFileAdapter.class.getClassLoader().getResourceAsStream("routers.txt"))).lines()) {
            stream.forEach(line -> {
                String[] routerEntry = line.split(";");
                var id = routerEntry[0];
                var type = routerEntry[1];
                Router router = new Router(RouterType.valueOf(type), RouterId.of(id));
                routers.add(router);
            });
        } catch (Exception e) {
            e.printStackTrace();
        }

        return routers;
    }

    private RouterViewFileAdapter() {

    }

    public static RouterViewFileAdapter getInstance() {
        if (instance == null) {
            instance = new RouterViewFileAdapter();
        }
        return instance;
    }
}

애플리케이션이 외부 데이터를 얻는 방법으로 파일에서 얻는 방법을 출력 어댑터가 출력 포트 인터페이스를 구현하여 정의하는 코드다

이로써 외부 입력 → 입력 어댑터 → 입력 포트 → 유스케이스 → 출력 포트 → 출력 어댑터 → 외부 시스템의 흐름을 살펴 보았다.