posts

Hexagonal Architecture 정리

May 11, 2025 updated May 11, 2025 architecturehttp

이 글은 헥사고날 아키텍처를 다시 볼 때 핵심 개념부터 포트/어댑터 흐름까지 한 번에 훑기 좋게 정리한 메모입니다.. 실무에서 바로 구현할 때 어디를 도메인으로 두고 어디를 어댑터로 빼야 하는지 감이 안 올 때 다시 보는 용도에 가깝습니다.

1. 구조

헥사고날 아키텍처(포트와 어댑터 아키텍처)의 주요 구성 요소:

  • 도메인: 핵심 비즈니스 로직
  • 포트: 인터페이스
  • 어댑터: 외부 시스템과의 연결
      ┌─────────────────────┐
      │        외부         │
      │   ┌───────────┐     │
      │   │  어댑터   │     │
      │   └─────┬─────┘     │
      │         │           │
┌─────────────────────────────┐
│     │    포트    │         │
│     └─────┬─────┐│         │
│           │     ││         │
│       ┌───▼───┐ ││         │
│       │도메인 │ ││         │
│       └───┬───┘ ││         │
│           │     ││         │
│     ┌─────▼─────┘│         │
│     │    포트    │         │
└─────────────────────────────┘
      │         │           │
      │   ┌─────▼─────┐     │
      │   │  어댑터   │     │
      │   └───────────┘     │
      │        외부         │
      └─────────────────────┘

2. MVC와의 차이

MVC (Model-View-Controller)

  • 프레젠테이션 계층 중심
  • 비즈니스 로직과 데이터 접근 로직 혼재 가능
  • 구현이 간단함

헥사고날 아키텍처

  • 비즈니스 로직 중심 설계
  • 외부 의존성으로부터 코어 로직 보호
  • 복잡하지만 유지보수와 테스트 용이

3. 장단점

장점

  • 높은 모듈성과 유연성
  • 비즈니스 로직의 독립성 보장
  • 테스트 용이성
  • 기술 스택 변경 용이

단점

  • 초기 설계와 구현이 복잡
  • 소규모 프로젝트에는 과도할 수 있음
  • 높은 러닝 커브

4. 자바-스프링 구현

  • Domain: 순수 자바 객체 (POJO)
  • Ports: 자바 인터페이스
  • Adapters:
    • @Controller
    • @Repository
    • 외부 API 클라이언트 등
  • 의존성 주입(DI)으로 포트와 어댑터 연결

5. 학습 수준

  1. 초급: 기본 개념 이해
  2. 중급: 실제 프로젝트 적용, 포트와 어댑터 설계
  3. 고급: 복잡한 비즈니스 로직 구현, 성능 최적화

6. 구축 과제 적용 시 고려사항

  • 프로젝트 규모와 복잡성 평가
  • 팀의 기술 수준 고려
  • 점진적 도입 전략 수립
  • 기존 코드베이스와의 통합 방안 마련
  • 테스트 전략 수립 (단위 테스트, 통합 테스트)

ports and adapters

1. 포트 (Ports)

포트는 애플리케이션의 외부와 내부를 연결하는 인터페이스입니다.

1.1 인바운드 포트 (Inbound Ports)

  • 정의: 애플리케이션이 외부에서 호출되는 방식을 정의
  • 예시: MemberLookupUseCase, MemberResisterUseCase
  • 특징:
    • 주로 유스케이스(use case) 인터페이스로 구현
    • 애플리케이션의 기능을 추상화
public interface MemberLookupUseCase {
    Member findMemberById(Long id);
}

1.2 아웃바운드 포트 (Outbound Ports)

  • 정의: 애플리케이션이 외부 시스템을 호출하는 방식을 정의
  • 예시: MemberRepositoryAdapter
  • 특징:
    • 데이터 영속성, 외부 API 호출 등을 추상화
    • 구체적인 구현은 어댑터에서 담당
public interface MemberRepositoryAdapter {
    Member save(Member member);
    Optional<Member> findById(Long id);
}

2. 어댑터 (Adapters)

어댑터는 포트 인터페이스의 실제 구현체입니다.

2.1 인바운드 어댑터 (Inbound Adapters)

  • 정의: 외부 요청을 애플리케이션의 포트로 변환
  • 예시: MemberController, MemberControllerAdvice
  • 특징:
    • 주로 웹 컨트롤러, 메시지 리스너 등으로 구현
    • 요청을 받아 적절한 포트(유스케이스)를 호출
@RestController
public class MemberController {
    private final MemberLookupUseCase memberLookupUseCase;

    @GetMapping("/members/{id}")
    public ResponseEntity<Member> getMember(@PathVariable Long id) {
        return ResponseEntity.ok(memberLookupUseCase.findMemberById(id));
    }
}

2.2 아웃바운드 어댑터 (Outbound Adapters)

  • 정의: 애플리케이션의 요청을 외부 시스템에 맞게 변환
  • 예시: MemberRepositoryAdapterImpl, ExternalApiAdapter
  • 특징:
    • 데이터베이스 연동, 외부 API 호출 등을 구현
    • 아웃바운드 포트 인터페이스를 구현
@Repository
public class MemberRepositoryAdapterImpl implements MemberRepositoryAdapter {
    private final JpaMemberRepository jpaMemberRepository;

    @Override
    public Member save(Member member) {
        MemberEntity entity = MemberMapper.toEntity(member);
        return MemberMapper.toDomain(jpaMemberRepository.save(entity));
    }
}

3. 주요 개념 관계

  1. 도메인 중심: 모든 포트와 어댑터는 도메인 모델을 중심으로 동작합니다.
  2. 의존성 방향: 어댑터 → 포트 → 도메인 (의존성은 항상 안쪽으로)
  3. 교체 가능성: 어댑터는 쉽게 교체 가능해야 합니다 (예: JPA → MongoDB)
  4. 테스트 용이성: 포트를 통해 모의 객체(mock)를 쉽게 만들 수 있습니다.

4. 구현 시 주의사항

  • 포트는 도메인 언어로 정의해야 합니다.
  • 어댑터는 특정 기술에 종속적일 수 있지만, 포트는 기술 중립적이어야 합니다.
  • 동일한 포트에 대해 여러 어댑터를 구현할 수 있어야 합니다.
  • 도메인 로직은 포트와 어댑터에 의존하지 않아야 합니다.

1. 전체 구조

kr.co.visibleray.prop
├── application
│   ├── port
│   │   ├── inbound
│   │   └── outbound
│   └── usecase
├── domain
│   ├── enumeration
│   ├── exception
│   └── model
├── infrastructure
│   └── adapter
│       ├── inbound
│       └── outbound
└── util

2. 주요 패키지 설명

2.1 도메인 계층 (Domain Layer)

위치: kr.co.visibleray.prop.domain

  • enumeration: 도메인 관련 열거형
    • ResultCode.java: 결과 코드 정의
  • exception: 도메인 특화 예외
    • ApplicationException.java: 애플리케이션 레벨 예외
    • InvalidEmailFormatException.java: 이메일 형식 관련 예외
  • model: 도메인 모델 클래스들
    • Email.java, FileInfo.java, RecodedDateTime.java 등: 값 객체
    • member/Member.java: 회원 도메인 모델
    • organization/Organization.java: 조직 도메인 모델

2.2 애플리케이션 계층 (Application Layer)

위치: kr.co.visibleray.prop.application

  • port
    • inbound: 인바운드 포트 (유스케이스 인터페이스)
      • MemberLookupUseCase.java
      • MemberResisterUseCase.java
    • outbound: 아웃바운드 포트
      • MemberPersistenceAdapter.java
  • usecase: 유스케이스 구현
    • MemberLookupUseCaseImpl.java
    • MemberResisterUseCaseImpl.java

2.3 인프라스트럭처 계층 (Infrastructure Layer)

위치: kr.co.visibleray.prop.infrastructure

  • adapter
    • inbound
      • rest: REST API 관련 구성
        • aop: 예외 처리 AOP
        • config: 보안 및 Swagger 설정
        • constants: API 상수
        • controller: REST 컨트롤러
        • dto: 데이터 전송 객체
        • filter: 요청 필터
    • outbound
      • persistence: 데이터 영속성 관련
        • config: QueryDSL 설정
        • member: 회원 관련 영속성 구현
        • organization: 조직 관련 영속성 구현

2.4 유틸리티 (Utility)

위치: kr.co.visibleray.prop.util

  • CollectionUtils.java: 컬렉션 관련 유틸리티
  • StringUtils.java: 문자열 관련 유틸리티

3. 주요 컴포넌트 설명

3.1 인바운드 포트 (Inbound Ports)

  • MemberLookupUseCase.java: 회원 조회 유스케이스
  • MemberResisterUseCase.java: 회원 등록 유스케이스

이들은 애플리케이션의 주요 기능을 정의하는 인터페이스입니다.

3.2 아웃바운드 포트 (Outbound Ports)

  • MemberPersistenceAdapter.java: 회원 정보 영속성 인터페이스

외부 시스템(예: 데이터베이스)과의 상호작용을 추상화합니다.

3.3 유스케이스 구현 (Use Case Implementations)

  • MemberLookupUseCaseImpl.java
  • MemberResisterUseCaseImpl.java

인바운드 포트의 실제 구현체로, 비즈니스 로직을 포함합니다.

3.4 인바운드 어댑터 (Inbound Adapters)

  • MemberController.java: 회원 관련 REST API 컨트롤러

외부 요청을 받아 적절한 유스케이스로 전달합니다.

3.5 아웃바운드 어댑터 (Outbound Adapters)

  • MemberPersistenceAdapterImpl.java: 회원 정보 영속성 구현

실제 데이터베이스 작업을 수행하는 구현체입니다.

4. 특징 및 장점

  1. 관심사의 분리: 각 계층이 명확히 구분되어 있어 유지보수가 용이합니다.
  2. 도메인 중심 설계: domain 패키지에 비즈니스 핵심 로직이 집중되어 있습니다.
  3. 의존성 역전: 애플리케이션 코어(domain, application)가 외부 계층에 의존하지 않습니다.
  4. 유연성: 인터페이스를 통한 느슨한 결합으로 구현체 교체가 용이합니다.
  5. 테스트 용이성: 각 계층과 컴포넌트를 독립적으로 테스트할 수 있습니다.

5. 구현 시 주의사항

  1. 도메인 모델의 순수성 유지: domain 패키지의 클래스들은 외부 의존성이 없어야 합니다.
  2. 포트 인터페이스 설계: 비즈니스 요구사항을 잘 반영하도록 설계해야 합니다.
  3. 어댑터 구현: 특정 기술에 종속적일 수 있지만, 교체 가능성을 고려해야 합니다.
  4. 의존성 주입: 구체적인 구현체 대신 인터페이스에 의존하도록 합니다.

Dev flow

1. 도메인 모델 정의 (domain 패키지)

  • model 폴더:
    • Member.java
    • Organization.java
    • Notice.java
  • enumeration 폴더:
    • FileType.java
    • ResultCode.java
    • SubscriptionType.java
  • exception 폴더:
    • ApplicationException.java
    • InvalidEmailFormatException.java

2. 유스케이스 정의 (application.port.inbound 패키지)

  • MemberLookupUseCase.java
  • MemberResisterUseCase.java

3. 영속성 어댑터 인터페이스 정의 (application.port.outbound 패키지)

  • MemberPersistenceAdapter.java

4. 유스케이스 구현 (application.usecase 패키지)

  • MemberLookupUseCaseImpl.java
  • MemberResisterUseCaseImpl.java

5. 영속성 계층 구현 (infrastructure.adapter.outbound.persistence 패키지)

  • member 폴더:
    • MemberEntity.java
    • MemberRepository.java
    • MemberRepositoryCustom.java
    • MemberRepositoryImpl.java
    • MemberPersistenceAdapterImpl.java
    • MemberFactory.java

6. REST API 컨트롤러 구현 (infrastructure.adapter.inbound.rest.controller 패키지)

  • MemberController.java
  • OrganizationController.java
  • NoticeController.java
  • FileController.java
  • ImageController.java
  • SubscriptionController.java

7. DTO 클래스 작성 (infrastructure.adapter.inbound.rest.controller.dto 패키지)

  • request 폴더:
    • MemberSaveRequest.java
    • MemberModifyRequest.java
  • response 폴더:
    • MemberInfo.java
    • PageResult.java

8. 설정 및 보안 (infrastructure.adapter.inbound.rest.config 패키지)

  • SecurityConfig.java
  • SwaggerConfig.java

9. 예외 처리 (infrastructure.adapter.inbound.rest.aop 패키지)

  • ApplicationExceptionHandler.java
  • DefaultExceptionHandler.java

10. 유틸리티 클래스 작성 (util 패키지)

  • CollectionUtils.java
  • StringUtils.java

11. 애플리케이션 설정 (resources 폴더)

  • application.yml
  • application-local.yml

12. 테스트 코드 작성

  • 도메인 모델 테스트
  • 유스케이스 테스트
  • 영속성 계층 테스트
  • 컨트롤러 테스트
  • 통합 테스트

개발 순서

  1. 핵심 도메인 모델 (예: Member) 부터 시작
  2. 해당 도메인 관련 유스케이스 정의 및 구현
  3. 영속성 계층 구현
  4. REST API 컨트롤러 구현
  5. 다른 도메인 모델에 대해 1-4 과정 반복
  6. 전역 설정, 보안, 예외 처리 구현

┌──────────────────────────────────────────────────────┐ │ 개발 흐름 다이어그램
└──────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ 1. 도메인 모델 정의 (domain 패키지)
└──────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ 2. 유스케이스 정의 (application.port.inbound)
└──────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ 3. 영속성 어댑터 인터페이스 (application.port.outbound) └──────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ 4. 유스케이스 구현 (application.usecase)
└──────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ 5. 영속성 계층 구현 (infrastructure.outbound)
└──────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ 6. REST API 컨트롤러 구현 (infrastructure.inbound)
└──────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ 7. DTO 클래스 작성
│ 8. 설정 및 보안 구현
│ 9. 예외 처리 구현
│ 10. 유틸리티 클래스 작성
│ 11. 애플리케이션 설정
└──────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ 12. 테스트 코드 작성 (각 단계마다 병행)
└──────────────────────────────────────────────────────┘


https://git.hnine.com/HNINE-Ex/prop/prop-api