[시스템 사고] 헥사고날 아키텍처의 이해와 구현

배현서·2024년 12월 4일

시스템 사고

목록 보기
8/10
post-thumbnail

이 글은 헥사고날 아키텍쳐의 이해를 높이려는 목표로 작성했다.
이전 헥사고날 포스트
이전 헥사고날 포스트2

1. 헥사고날 아키텍처 소개

1.1 헥사고날 아키텍처란?

헥사고날 아키텍처(Hexagonal Architecture)는 포트와 어댑터 아키텍처(Ports and Adapters Architecture)라고도 불리며, 애플리케이션의 핵심 비즈니스 로직을 외부 의존성으로부터 격리하는 아키텍처 패턴입니다.

1.2 도입 이유

1. 기술적 부채 감소
   - 외부 의존성 변경이 용이
   - 비즈니스 로직 보호

2. 테스트 용이성
   - 도메인 로직 단독 테스트 가능
   - 외부 시스템 모킹 용이

3. 유지보수성 향상
   - 관심사 분리
   - 코드 재사용성 증가

1.3 주요 장점

  1. 기술 독립성

    • 프레임워크나 데이터베이스 변경이 용이
    • 비즈니스 로직이 특정 기술에 종속되지 않음
  2. 테스트 용이성

    • 각 계층별 독립적인 테스트 가능
    • 목(Mock) 객체를 통한 테스트 단순화
  3. 유연성과 확장성

    • 새로운 기능 추가가 용이
    • 외부 시스템 통합이 단순화

1.4 잠재적 단점

  1. 초기 개발 비용

    • 더 많은 인터페이스와 클래스 필요
    • 설계 복잡도 증가
  2. 학습 곡선

    • 팀원들의 아키텍처 이해도 필요
    • 새로운 개발자 온보딩 시간 증가

헥사고날아키텍쳐의 layer

1. Application Layer (application/)

  • 애플리케이션의 유스케이스를 구현하는 핵심 계층
  • 주요 구성
    application/
    ├── auth/
    ├── image/
    ├── keyword/
    ├── novel/
    └── user/

세부 역할

  • port/in: 외부에서 애플리케이션을 사용하기 위한 인터페이스 정의

    • UseCase 인터페이스들 (예: CreateNovelUseCase, SocialLoginUsecase)
    • 각 기능의 진입점 역할
  • port/out: 애플리케이션이 외부 시스템과 통신하기 위한 인터페이스

    • 저장소, 외부 서비스와의 통신을 위한 포트 정의
    • 예: NovelPort, AIOutputPort, ImageManagementPort
  • service: 실제 비즈니스 로직 구현

    • UseCase 구현체들이 위치
    • 도메인 객체들을 조작하고 비즈니스 규칙 적용

2. Domain Layer (domain/)

  • 비즈니스 핵심 규칙과 엔티티를 포함하는 계층
domain/
├── common/
├── novel/
├── reaction/
└── user/

세부 역할

  • model: 핵심 비즈니스 엔티티 정의

    • 예: Novel, User, NovelContent
    • 비즈니스 규칙과 검증 로직 포함
  • enums: 도메인 관련 열거형 정의

    • 예: Genre, Status, Step
  • exception: 도메인 특화 예외 정의

3. Infrastructure Layer (infra/)

  • 외부 시스템과의 통합을 담당하는 계층
infra/
├── client/
├── persistence/
└── security/

세부 역할

  • client: 외부 서비스 통합

    • AI 서비스, OAuth, S3 등과의 통신 담당
    • 각 클라이언트의 구체적인 구현 제공
  • persistence: 데이터 영속성 처리

    • JPA 엔티티 정의
    • Repository 구현
    • 도메인 모델과 데이터베이스 간의 매핑 처리
  • security: 보안 관련 구성

    • JWT 인증/인가 처리
    • 보안 필터 및 설정

4. Bootstrap Layer (bootstrap/)

  • 애플리케이션의 진입점과 설정을 담당하는 계층
bootstrap/
├── auth/
├── common/
├── image/
├── keyword/
├── novel/
└── user/

세부 역할

  • api: API 스펙 정의

    • Swagger 문서화
    • API 버전 관리
  • controller: HTTP 요청 처리

    • 요청 검증
    • UseCase 호출
    • 응답 변환
  • dto: 데이터 전송 객체 정의

    • 요청/응답 데이터 구조 정의
  • common: 공통 설정 및 유틸리티

    • 예외 처리
    • 웹 설정
    • Swagger 설정

각 레이어의 의존성 방향:

Bootstrap → Application → Domain ← Infrastructure

헥사고날 아키텍처 분석

1. 레이어별 구현 분석

1.1 Domain Hexagon (내부 영역)

// domain/novel/model/Novel.kt
data class Novel (
    val id: String = DomainId.generate().id,
    val genre: String,
    val status: String = Status.ONGOING.stringStatus,
    // ...
) {
    // 개선 필요: 비즈니스 로직이 부족함
    // 추가 필요한 예시:
    fun canAddContent(): Boolean
    fun validateContent(content: String)
    fun changeStatus(newStatus: Status)
}

잘된 점

  • 순수한 도메인 모델 구현
  • 의존성 없는 POJO 구조
  • DomainId를 통한 식별자 생성 추상화

개선 필요

  • 도메인 모델에 비즈니스 로직 부족
  • Value Object 활용 미흡 (예: genre를 String 대신 Genre 클래스로)
  • 도메인 이벤트 부재

1.2 Application Hexagon (유스케이스 영역)

@Service
class NovelService(
    private val novelPort: NovelPort,
    private val novelContentPort: NovelContentPort,
    // ...
) : CreateNovelUseCase, RetrieveNovelUseCae {
    @Transactional
    override fun createNovelWithContent(userId: String, command: CreateNovelUseCase.NovelWithContentCommand): CreateNovelUseCase.Response {
        // ...
    }
}

잘된 점

  • 포트 인터페이스를 통한 의존성 역전
  • 트랜잭션 관리
  • 명확한 유스케이스 정의

개선 필요

  • CQRS 패턴 미적용 (조회와 명령 분리 필요)
  • 서비스 로직이 너무 복잡함
  • 도메인 이벤트 처리 부재

1.3 Infrastructure Hexagon (어댑터 영역)

@Component
class NovelAdapter(
    private val novelRepository: NovelRepository
) : NovelPort {
    override fun retrieveById(novelId: String): Novel? {
        // 개선 필요: Optional 처리
        return novelRepository.findById(novelId).get().toDomain();
    }
}

잘된 점

  • 포트 인터페이스 구현
  • 청크 처리를 통한 성능 최적화
  • 적절한 예외 처리

개선 필요

  • Null 안전성 향상 필요
  • 도메인 변환 로직 개선 필요
  • 캐싱 전략 부재

1.4 Bootstrap Hexagon (진입점 영역)

@RestController
class NovelController(
    private val createNovelUseCase: CreateNovelUseCase,
    private val retrieveNovelUseCae: RetrieveNovelUseCae
) : NovelApi {
    override fun createNovel(userId: String, request: CreateNovelRequest): CreateNovelResponse {
        // ...
    }
}

잘된 점

  • API 스펙 명확한 정의
  • UseCase 기반 구현
  • DTO 변환 패턴 일관성

개선 필요

  • 입력 값 검증 부족
  • API 버저닝 전략 부재
  • 예외 처리 체계화 필요

2. 전반적인 개선 방안

2.1 도메인 모델 강화

data class Novel(
    // 현재
    val status: String = Status.ONGOING.stringStatus,
    
    // 개선
    val status: Status = Status.ONGOING,
    
    // 비즈니스 로직 추가
    fun validateNovelCreation() {
        require(title.length in 1..100) { "제목은 1-100자 사이여야 합니다" }
        // ...
    }
)

2.2 CQRS 패턴 도입

interface NovelQueryService {
    fun findNovelsByWriter(writerId: String): List<NovelReadModel>
}

interface NovelCommandService {
    fun createNovel(command: CreateNovelCommand): NovelId
}

2.3 도메인 이벤트 시스템

data class NovelCreatedEvent(
    val novelId: String,
    val writerId: String,
    val timestamp: LocalDateTime
)

interface DomainEventPublisher {
    fun publish(event: DomainEvent)
}

3. 우수 구현 사례

  1. 포트와 어댑터 패턴

    • 인터페이스를 통한 의존성 역전
    • 외부 시스템과의 결합도 낮춤
  2. 모듈화

    • 도메인별 명확한 패키지 구조
    • 관심사의 분리가 잘 되어있음
  3. 성능 최적화

    • 대량 데이터 처리를 위한 청크 처리
    • 효율적인 조회 로직

4. 결론

현재 구현은 헥사고날 아키텍처의 기본 원칙을 따르고 있으나, 도메인 주도 설계의 핵심 개념들을 더 적극적으로 도입할 필요가 있다고 느껴졌다. 특히 도메인 모델의 강화와 CQRS 패턴 도입을 통해 더 견고한 아키텍처로 발전시킬 수 있을 것 같다.

이러한 개선을 통해 비즈니스 로직의 응집도를 높이고, 시스템의 확장성과 유지보수성을 더욱 향상시킬 예정이다.

0개의 댓글