Hexagonal Architecture

세모네모동굴배이·2025년 7월 2일

공지사항 시스템 흐름도 - 헥사고날 아키텍처

🏗️ 전체 아키텍처 구조

Web Adapter → UseCase Port → Service(Application) → Out Port → Persistence Adapter

• Controller가 Service 구현체를 전혀 모르고
• 오직 UseCase 인터페이스만 의존
• 도메인은 Spring / JPA / Web 을 전혀 모름

공지사항 시스템 아키텍처 분석

1. 폴더 구조 (헥사고날 아키텍처)

패키지 구조는 기술적인 계층이 아닌 아키텍처의 의도(In/Out, Port/Adapter)가 명확히 드러나도록 구성했습니다.

demo.hexagonal
├── 📂 adapter                 # [Infra] 외부 세계와 소통하는 어댑터
│   ├── 📂 in                  # Driving Adapter (요청을 받아들이는 곳)
│   │   └── 📂 web             # Web Controller, Web DTO
│   └── 📂 out                 # Driven Adapter (요청을 내보내는 곳)
│       └── 📂 persistence     # JPA Entity, Repository Impl, Mapper
│
├── 📂 application             # [App] 도메인과 어댑터를 연결하는 오케스트레이션
│   ├── 📂 port                # 인터페이스 (Port) 정의
│   │   ├── 📂 in              # UseCase Interface (Input Port)
│   │   └── 📂 out             # Repository Interface (Output Port)
│   └── 📂 service             # UseCase 구현체 (트랜잭션 관리, 흐름 제어)
│
└── 📂 domain                  # [Core] 외부 의존성이 전혀 없는 순수 비즈니스 로직
    ├── 📂 exception           # 도메인 비즈니스 예외 (BoardException)
    └── 📂 model               # 핵심 도메인 모델 (Pure Kotlin Class)

2. 각 레이어별 역할

🧩 3. 각 레이어의 역할 상세 분석
헥사고날 아키텍처는 각 구역의 역할이 매우 엄격하게 나뉩니다.

🟢 Adapter Layer (바깥 고리)
애플리케이션과 외부 세계를 연결하는 관문입니다.

  • In Adapter (Web): HTTP 요청을 받아 자바/코틀린 객체로 파싱하고, 유효성을 검증한 뒤 UseCase를 호출합니다.

  • Out Adapter (Persistence): Port 인터페이스를 구현(implements)하여 실제 DB(JPA, MyBatis)에 접근합니다. 도메인 객체를 JPA 엔티티로 변환(Mapping)해서 저장하는 역할을 수행합니다.

🟠 Application Layer (중간 고리)
도메인과 어댑터를 연결하는 조정자(Orchestrator) 역할입니다.

  • Port (In/Out): 어댑터와 애플리케이션이 소통하는 명세(Interface)입니다.

  • Service: In-Port를 구현합니다. 트랜잭션(@Transactional)을 관리하고, 도메인 객체의 메서드를 호출하여 비즈니스 흐름을 만듭니다.

🔴 Domain Layer (핵심 코어)
소프트웨어의 존재 이유이자 핵심 가치입니다.

  • Model: 프레임워크(Spring)나 기술(DB)에 의존하지 않는 순수한 POJO/POKO 객체입니다.

  • Logic: "게시글 제목은 비어있을 수 없다", "수정 시 내용이 변경되어야 한다" 같은 순수 비즈니스 규칙이 이곳에 위치합니다.

3. 데이터 흐름도 (Data Flow)

[Client]
    ↓ 1. POST /api/boards (JSON 요청)
[BoardController] (Web Adapter)
    ↓ 2. CreateBoardRequest → Command 변환 및 UseCase 호출
[BoardService] (Application)
    ↓ 3. 순수 도메인 객체(Board) 생성
    ↓ 4. 저장 인터페이스(Port) 호출
[BoardPersistenceAdapter] (Persistence Adapter)
    ↓ 5. 도메인(Board) → 엔티티(JpaEntity) 변환 (Mapper)
    ↓ 6. JPA Repository save() 호출
[BoardJpaRepository] (Hibernate)
    ↓ 7. INSERT SQL 실행
[Database] (MySQL/H2)
    ↑ 8. 저장된 데이터 반환 (Auto Increment ID 포함)
[BoardPersistenceAdapter]
    ↓ 9. 엔티티 → 도메인 변환 후 반환
[BoardService]
    ↓ 10. 생성된 도메인 객체 반환
[BoardController]
    ↓ 11. 도메인 → BoardResponse DTO 변환
[Client]
    ← 12. 201 Created 응답 (JSON)

💡 흐름 요약

  1. Request: 웹 요청(DTO)이 들어오면 Command 객체로 변환됩니다. (이 단계에서 입력값 검증 수행)

  2. Input Port: Controller는 Service 구체 클래스 대신 UseCase 인터페이스를 호출합니다.

  3. Domain Logic: Service는 Board 도메인 객체를 생성하거나 도메인 메서드를 호출하여 비즈니스 로직을 수행합니다.

  4. Output Port: Service는 저장을 위해 RepositoryPort 인터페이스를 호출합니다.

  5. Persistence: 구현체인 Adapter가 Domain을 Entity로 변환(Mapping)하여 DB에 저장합니다.

  6. Response: 역순으로 돌아가며, 최종적으로 Response DTO로 변환되어 클라이언트에게 반환됩니다.

4. 아키텍처의 장점

1. 의존성 규칙 (Dependency Rule)

모든 의존성은 바깥쪽(Adapter)에서 안쪽(Domain) 으로만 향합니다.

  • DomainApplication, Adapter에 대해 전혀 알지 못합니다.
  • ApplicationAdapter에 대해 알지 못합니다 (Port 인터페이스를 통해서만 소통).
  • JPA Entity(@Entity)Domain Model(Pure Class) 은 철저히 분리되어 있으며, Mapper를 통해 변환됩니다.

2. 도메인 중심 설계 (Rich Domain Model)

  • Service는 단순히 로직의 흐름(Orchestration)만 제어합니다.
  • 실제 상태 변경과 비즈니스 규칙 검증은 Domain Model 내부의 메서드가 책임집니다.

3. 포트와 어댑터 (Ports and Adapters)

  • In-Port (UseCase): 클라이언트가 애플리케이션에 무엇을 요청할 수 있는지 정의합니다.
  • Out-Port (Repository Port): 애플리케이션이 데이터를 저장/조회하기 위해 무엇이 필요한지 정의합니다.

0개의 댓글