헥사고날 아키텍처

밀크야살빼자·2023년 9월 13일
0
post-thumbnail

멋쟁이사자처럼 연합 프로젝트를 진행하면서 헥사고날 아키텍처에 대해 처음 접하게 되었습니다. 프로젝트를 원활히 진행하기 위해서는 이해도가 중요하다고 판단하여 헥사고날 아키텍처에 대해 알아보았습니다.

핵심 개념

  • 의존성은 외부에서 내부로 향합니다.
  • 변경 가능한 지점, 외부 환경에 의존하는 부분을 port(인터페이스)-adpater(구현체) 패턴으로 의존성을 역전시킵니다.

    객체를 직접 참조하지 않고, 대신 선언된 인터페이스를 통해 객체에 접근하면 의존성을 끊을 수 있습니다. 이렇게 하면 인터페이스를 구현한 다른 객체를 연결할 수 있기 때문입니다.

계층형 아키텍처

  • 프레젠테이션 계층이 도메인 계층을 참조하고 업무로직은 영속성 계층을 참조하는데 이렇게 되면은 프레젠테이션 계층이 영속성 계층을 직접 참조하는 것과 같습니다.

    • 영속성 계층에서 변경이 일어나면 도메인 계층과 프레젠테이션 계층에 영향을 미칩니다. => 의존성을 가짐
  • 연쇄 참조로 의존성을 갖으면 한 곳에서 변경이 일어나면 참조된 다른 곳에서도 변경 영향을 가집니다.

  • 테스트가 쉽지 않습니다.

    • 업무 로직을 테스트 하려면 데이터가 필요합니다.
    • UI를 테스트하려면 업무 로직과 데이터를 함께 해야합니다.
  • UI와 데이터 변경이 빈번하게 일어나는데 이는 업무로직에 영향을 줍니다.

    • 데이터 변경 하나만으로 업무로직에 영향을 줍니다.

계층형 아키텍처의 문제점

1. 데이터베이스(+영속성)에 대한 의존성이 퍼지게 됩니다.

전통적인 계층형 아키텍처에서 도메인 계층은 영속성에 의존합니다. 도메인 계층이 데이터베이스에 의존하게 되면, 데이터베이스에 변화가 발생하면 도메인 계층도 이에 영향을 받게 됩니다.

해당 도메인을 사용하는 서비스 계층에서도 즉시 로딩, 지연 로딩, 트랜잭션, 플러시 등을 고려해야 하며, 이러한 과정에서 영속성에 대한 의존이 프로젝트 전반으로 확산되어 변경에 취약해질 수 있습니다.

2. 아키텍처 경계를 강제할 수 없습니다.

  • 전통적인 계층형 아키텍처에서는 상위 계층에 있는 컴포넌트롤 접근할 목적으로 해당 컴포넌트를 하위 계층으로 내려버릴 수 있습니다.

행위가 계속해서 쌓이면, 경계가 점점 모호해지고 결국에는 허물어져 버릴 수 있습니다. 이로 인해 모든 계층에서 헬퍼나 유틸리티에 의존하게 될 가능성이 있습니다.

3. 계층을 Skip 할 수 있습니다.

계층형 구조에서는 종종 계층을 건너뛰는 것이 가능합니다. 즉, 구현이 간단한 경우 컨트롤러에서 바로 도메인을 참조하여 로직을 작성할 수 있습니다.

  • 두 가지 문제점
    • 기능이 확장 된다.
    • 테스트가 복잡해진다.

4. 유스케이스를 숨깁니다.

개발자가 해당 유스케이스의 존재 여부를 파악하기 어려워서 동일한 로직을 다른 위치에서 새롭게 구현할 수 있으며, 이는 코드를 더럽힐 수 있습니다.

5. 서비스의 크기를 강제할 수 없다.

계층형 구조에서는 서비스의 크기를 강제하지 않습니다. 서비스에는 수십 개의 서비스 로직을 작성할 수 있습니다. 그러나 서비스는 너무 많은 의존성을 가지며, 수많은 웹 계층이 해당 서비스를 의존하게 됩니다. 이로 인해 서비스를 테스트하기 어려워지고 수행해야 할 use case를 찾기도 어려워집니다.

클린 아키텍처

  • 로버트 C. 마틴
  • 로버트 C. 마틴은 클린 아키텍처에서 비즈니스 규칙이 외부인 데이터베이스, 프레임워크, UI, 외부 시스템으로부터 독립적으로 설계되어야 한다고 강조합니다. 이렇게 설계된 아키텍처는 비즈니스 규칙이 외부의 영향을 받지 않고, 테스트하기 쉽도록 만듭니다.
  • 도메인 코드는 외부로 향하는 어떤 의존성도 가져서는 안 됩니다.
    • 의존성은 안쪽으로 향함
  • 의존성 역전 원칙(Dependency Inversion Principle, DIP)을 이용하여 모든 의존성이 내부(도메인)으로 향하도록 합니다.
  • 도메인은 사용되는 영속성 프레임워크에 대해 알지 못해야 합니다.
  • 특정 프레임워크에서 분리된 안정적인 도메인 코드를 작성할 수 있습니다.
  • 영속성 계층이 도메인 계층에 의존하며, 영속성 엔티티를 도메인 엔티티로 변환하는 과정이 필요합니다.
  • 도메인 밖에는 UseCase가 존재하며, 단일 책임을 갖기 위해 조금 더 세분화되어 있습니다.(UseCase와 entity가 가장 중요함)
    • 비즈니스 규칙이 정해져 있기 때문입니다.
    • Entity는 업무 규칙, UseCase는 어떻게 사용할지를 정의합니다.
  • 파란색과 초록색은 어떻게 보여지는지를 정의합니다.

헥사고날 아키텍처

  • 알레스테어 콕번이 만든 용어로 클린 아키텍처를 일반화한 구조 중 하나인 육각형 아키텍처입니다.
  • 사전적 의미 : "육각형 건축물"
  • 전통적인 계층형 아키텍처의 단점을 보완하기 위해 생겼습니다.
  • 소프트웨어 설계에 사용되는 아키텍처 패턴 중 하나로, 여러 소프트웨어 환경에 쉽게 연결할 수 있도록 하며, 느슨하게 결합된 애플리케이션 구성요소를 만드는 것을 목표로 합니다.

  • 헥사고날 아키텍처는 내부(도메인)와 외부(인프라)로 구분됩니다.

    • 내부 영역 : 순수한 비즈니스 로직을 표현하며 캡슐화된 영역이고 기능적 요구사항에 따라 먼저 설계합니다.
    • 외부 영역 : 내부 영역에서 기술을 분리하여 구성한 영역이고 내부 영역 설계 이후 설계합니다.
  • 도메인 모델

    • DDD의 도메인 모델

      • 모든 엔티티에 대한 변경(비즈니스 로직)은 여기서만 실행합니다. 외부에서 발생하면 안된다.
      • 이런 엔티티에 대한 변경을 일반적으로 비즈니스 로직이라고 부릅니다.
    • 어떠한 의존성도 없어야 하는 것이 원칙입니다.

    • 예외 상황 : 레포지토리의 경우에는 포트를 이용해 어댑터를 주입받아서 사용합니다.

      • 왜냐하면 엔티티를 만들고 싶은데 DB에 저장되어 있는 데이터를 기준으로 해서 만들어야 한다면? 도메인 모델에서 데이터베이스에 포트를 통해서 가지고 와서 엔티티를 만듭니다.
  • 헥사고날 아키텍처는 사용자 인터페이스나 데이터베이스 모두 비즈니스 로직으로부터 분리해야 하는 외부 요소로 봅니다.

    • 비즈니스 로직은 외부 요소에 의존하지 않아야 합니다. 대신 프레젠테이션 계층과 데이터 소스 계층은 도메인 계층에 의존해야 합니다. 이는 외부와의 상호작용을 인터페이스로 추상화하여 비즈니스 로직 내에서 외부 코드나 로직의 주입을 막는 것을 의미합니다. 또한, 외부 라이브러리나 프레임워크로부터 도메인 로직을 분리시키는 것이 아키텍처의 핵심입니다.
    • 어떤 레이어에 어떤 로직을 작성할지를 명확하고 더 작은 책임을 가질 수 있도록 분리할 수 있다.
  • 모든 의존성은 코어를 향합니다.

  • 도메인의 비즈니스 로직을 외부 라이브러리 및 도구로부터 분리할 때 사용하는 인터페이스를 포트와 어댑터라고 합니다. 이러한 구조로 인해 이 아키텍처는 포트와 어댑터 아키텍처로도 알려져 있습니다.

    • 육각형의 외부에는 애플리케이션과 상호작용하는 어댑터가 있으며, 일부 어댑터는 외부 시스템과 상호작용하고, 다른 어댑터는 데이터베이스와 상호작용합니다.
    • 하위 계층에서 사용되는 기술이 상위 계층에 노출되지 않도록 해야합니다.
    • 인바운드 포트 : 내부 영역 사용을 위해 노출된 API
    • 아웃바운드 포트 : 내부 영역이 외부 영역을 사용하기 위한 API
  • Primary(Driving) Adapters(왼쪽)

    • 애플리케이션 코어를 호출하는 어댑터
    • 사용자의 요청을 받아들일 때 사용. 데이터가 진입할 때
    • 람다로 치면 핸들러, 일반적인 프레임워크로 치면 컨트롤러에 해당
    • adapter, application을 동작시키는(Driving) 역할
    • UI 쪽에 해당됨
    • 인바운드 포트
    • 유스케이스에 의해 구현되고 호출되는 인터페이스
  • Secondary(Driven) Adapters(오른쪽)

    • 애플리케이션 코어에 의해 호출되는 어댑터
    • 도메인 모델 처리에 사용
    • adpater, application에 의해 동작되는(Driven) 역할
    • 주로 인프라와 연결되는 부분
    • 아웃바운드 포트
    • 어댑터에 의해 구현되고 코어에 의해 호출되는 인터페이스

외부로부터의 도메인 로직의 결합을 제거하며, 변경할 이유가 적을수록 유지보수성이 높은 코드입니다.

패키지 구조

payment-system
        ㄴ account
            ㄴ adapter
                ㄴ in
                    ㄴ web
                        ㄴ AccountController
                ㄴ out
                    ㄴ persistence
                        ㄴ AccountPersistenceAdapter
                        ㄴ SpringDataAccountRepository
            ㄴ domain
                ㄴ Account
                    ㄴ Activity
            ㄴ application
                ㄴ SendMoneyService
                ㄴ port
                    ㄴ in
                        ㄴ SendMoneyUseCase
                    ㄴ out
                        ㄴ LoadAccountPort
                        ㄴ UpdateAccountStatePort

in 패키지(usecase)

  • 시스템 외부에서 내부로 들어오는 요청을 처리하는 유스케이스가 위치합니다.
  • 비즈니스 로직을 캡슐화하여 유스케이스를 통해서 비즈니스 로직을 호출 할 수 있도록 구현되어 외부에서 발생한 요청을 처리하고, 필요에 따라 데이터를 읽거나 쓰는 등의 작업을 수행합니다.
  • 사용자가 애플리케이션의 기능을 호출하거나 외부 시스템이 애플리케이션의 유스케이스를 트리거할 때 사용됩니다.

out 패키지(port)

  • 외부 시스템, 데이터 저장소 등과의 상호작용을 추상화하는 포트입니다.
  • 애플리케이션의 비즈니스 로직이 외부 시스템과 상호작용하기 위해 사용되는 인터페이스입니다. 데이터베이스 연결, 외부 api 호출 등과 관련된 코드들이 위치합니다.
    비즈니스 로직이 처리한 결과나 시스템 내부에서 수정된 데이터를 외부 시스템이나 데이터 저장소에 업데이트하는 책임을 가집니다.

내부에는 persistencePort를 선언하고 외부에는 persistenceAdapter를 구현합니다. 이는 core에는 DB와 관련된 의존성을 제거하기 위함입니다. 도메인이 JPA Entity가 아니기 때문에 변경 감지를 사용하지 못하고 아웃포트를 통해 DB에 update 쿼리가 실행되도록 해주어야 합니다.

// 기존 레이어드 아키텍처 - Member는 JPA Entity
@Service
public class MemberService {

    private final MemberRepository memberRepository;

    // ...

    @Transactional
    public void updateNickname(Long memberId, NicknameUpdateRequest nicknameUpdateRequest) {
        Member member = findMember(memberId);
        member.updateNickname(nicknameUpdateRequest.getNewNickname());
    }
    // ...
}
// 변경된 헥사고날 아키텍처 - Member는 POJO
@Service
public class MemberService implements MemberUseCase {

    private final MemberPersistencePort memberPersistencePort;

    // ...

    @Override
    @Transactional
    public void updateNickname(Long memberId, NicknameUpdateCommand nicknameUpdateCommand) {
        Member member = findMember(memberId);
        memberPersistencePort.updateNickname(member.getId(), nicknameUpdateCommand.getNickname());
    }
    // ...
}


  1. 주문번호와 조리시간 설정 후 adapter로 넘깁니다.
  2. 인터페이스에서 약속된 유스케이스에서 주문을 접수하다를 호출해 접수를 시도합니다.
  3. 인터페이스를 통해 사전 정의 된 주문을 접수하다라는 것을 조회합니다.
  4. order 엔티티를 통해 주문 접수 처리 상태로 변경합니다.
  5. 인터페이스를 통해 정의된 주문 저장을 호출해 저장합니다.
  6. 서버를 통해 변경하고 로컬 DB에 저장합니다.

헥사고날 아키텍처의 장점

  • 도메인 비즈니스 모델에 집중
    DIP(Dependency Inversion Principle)를 통해 의존성이 도메인에서 외부로 향하지 않으므로 외부 요소에 대해 신경 쓰지 않고 개발할 수 있습니다.
    세부사항 변경 시 도메인 계층의 업무 규칙에 영향을 받지 않습니다. 예를 들어, 화면을 변경하거나 api가 변경되어도 도메인 계층에 영향을 받지 않습니다.
  • 모듈 일부를 배포하는게 용이
    기술적인 부분과 실제 비즈니스 로직을 분리함으로써, 각 도메인 별로 비즈니스 로직을 분리함으로써, 느슨한 결합을 유지할 수 있습니다.
  • 기능 확장이 용이
    원하는 기능에 대한 포트와 해당 포트를 사용할 어댑터를 추가함으로써 이를 구현할 수 있습니다. 이는 바깥쪽 세부사항을 독립적으로 손쉽게 변경 가능합니다.
  • 쉬운 테스트 구성
    모든 외부 기술들은 포트를 통해 비즈니스 로직과 연결되기 때문에, 각각의 역할을 수행하기 위해 필요한 포트만 사용하여 모킹 어댑터를 통해 테스트를 쉽게 수행할 수 있습니다. 또한, 내부 비즈니스 로직을 테스트할 때 외부에 의존성이 없기 때문에 모킹이 필요한 경우가 적어집니다.
    다시말해, 도메인 계층의 유스케이스와 엔티티는 참조하는 것이 없고, 테스트 데이터가 필요할 경우 레파지토리 어댑터 대신 주문정보 조회 및 저장이라는 인터페이스를 맞추면 테스트 객체로 변경하여 테스트 데이터를 제공할 수 있기 때문입니다.
  • 개발비용이 감소
    모든 의존성이 도메인을 향하게 되므로 계층 간 의존성이 낮아지고 유연성이 향상됩니다. 이는 요구사항 변경에 빠르게 대응할 수 있고, 테스트도 쉽게 적용할 수 있음을 의미합니다.
  • SoC(관심사 분리)의 장점
    외부와의 연결에 문제가 발생하면 Adapter를 확인하면 되고, 인터페이스의 정의를 변경하고자 한다면 Port를, 마지막으로 비즈니스 로직이 올바르게 동작하지 않는다면 도메인 로직만 확인하면 됩니다. 이는 쉬운 테스트를 가능하게 해줍니다.
    • 외부와의 연결에 문제가 생기면? 어댑터
    • 인터페이스는? 포트
    • 처리 중간에 EventBridge에 이벤트를 보내거나 트레이스 로그를 심고 싶다면? 서비스
    • 비즈니스 로직이 제대로 동작하지 않으면? 도메인 모델

헥사고날 아키텍처의 단점

  • 코드가 많아진다.
    도메인 계층이 영속성, UI와 같은 외부 계층과 철저히 분리되어야 하기 때문에, 각 계층에서 엔티티에 대한 모델을 유지보수해야 합니다.
  • 불필요한 오버헤드
    아키텍처를 도입하기 전에는 포트, 어댑터 등의 새로운 개념을 이해해야 합니다. 아키텍처를 구현하기 위해 포트(인터페이스)를 생성하고, 도메인 모델의 여러 표현 사이를 매핑하는 객체를 만들어야 합니다.

📜 참고 자료

profile
기록기록기록기록기록

0개의 댓글