본문 바로가기
Kotlin Spring/Kotlin Spring 강의 내용

6) 개발 architecture (2) 헥사고날 아키텍처(Hexagonal Architecture)

by Bill Lab 2025. 8. 31.
728x90

1. 레이어드 아키텍처 개념

      : 애플리케이션의 비즈니스 로직(도메인)과 외부 시스템을 철저히 분리

2. 계층(Layer) 구조

       1) 포트(Port)

           : 외부와 통신하기 위한 인터페이스

       2) 어댑터(Adapter)

           : 포트를 구현하는 외부 기술 구체화(포트의 구현체이자, Controller 역할 수)

       3) 핵심 도메인(Core Domain)

           : 순수 비즈니스 로직, 외부 의존 없음 (레이어드 아키텍처의 도메인과 동일)

 

(출처: Hexagonal Architecture with Go and Google Wire )

com.example
 ├─ user
 │   ├─ domain                
 │   │    ├─ User.kt
 │   │    └─ UserDomainService.kt
 │   ├─ Port                     // Interface 역할 수행
 │   │    └─ UserRegistrationPort.kt
 │   ├─ adapter
 │   │    ├─ in                  // 입력 어댑터 (Controller, REST API)
 │   │    │    └─ UserController.kt
 │   │    └─ out                 // 출력 어댑터 (DB, Kafka 등)
 │   │         └─ UserRepositoryAdapter.kt
 │   └─ config
 └─ common

 

3. 예제 코드

     : 헥사고날 아키텍처 기반으로 “회원 가입 & 쿠폰 발급”로직을 가볍게 구현단하면?

 

    1) 도메인 서비스

package com.example.user.domain

class UserDomainService {
    fun canRegister(email: String): Boolean {
        return !email.endsWith("@banned.com")
    }
}

 

    2) Port(Interface)

package com.example.user.application

import com.example.user.domain.User

interface UserRegistrationPort {
    fun saveUser(user: User): User
}

 

    3) Adapter(Out - DB)

package com.example.user.adapter.out

import com.example.user.application.UserRegistrationPort
import com.example.user.domain.User
import org.springframework.stereotype.Component

@Component
class UserRepositoryAdapter(
    private val userRepository: UserJpaRepository
): UserRegistrationPort {
    override fun saveUser(user: User): User {
        return userRepository.save(user.toEntity()).toDomain()
    }
}

 

    3) Adapter(In - Controller)

package com.example.user.adapter.`in`

import com.example.user.application.UserRegistrationPort
import com.example.user.domain.User
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/users")
class UserController(
    private val userRegistrationPort: UserRegistrationPort
) {
    @PostMapping
    fun register(@RequestBody request: UserRequest): User {
        val user = User(request.email, request.name)
        return userRegistrationPort.saveUser(user)
    }
}

 

4. 주요 장점

"도메인 순수성 보장"
: Core Domain은 외부 의존이 없음

"유연한 외부 교체"
: DB → Mongo, REST API → Kafka 등 쉽게 교체 가능

"테스트 용이성"
: Domain은 순수 Kotlin으로 단위 테스트 가능, 외부 의존을 mocking으로 처리

"유스케이스 흐름 명확화"
: Application Service(Port)에서 흐름 관리

 

 

5. 주요 단점

"구조 복잡도 증가"
 - Layered Architecture보다 패키지와 클래스가 늘어남
 - Port/Adapter/Domain/Controller 등 계층이 많아지고, DI 구조도 복잡
 - 작은 프로젝트나 단순 CRUD에서는 과한 추상화로 느껴질 수 있음

"학습 곡선"
 - 개발자가 Port, Adapter, Domain의 역할을 직관적으로 이해해야 함
   (특히 “이게 Application Service인지 Port인지” 헷갈리기 쉬움)
 - Layered만 쓰던 개발자가 처음 접하면 혼란 발생

"Boilerplate 코드 증가"
 - Interface/Port 작성, Adapter 구현, DTO 매핑 등 반복 코드가 늘어남
 - 작은 프로젝트에서 초기 생산성이 떨어질 수 있음

"유스케이스 흐름 표현 제한"
 - Port는 Interface이므로 복잡한 유스케이스 흐름을 표현하려면 여러 Port 호출 + Adapter 구현 필요
 - Layered에서처럼 Application Service 한 곳에서 흐름 조율하는 것보다 코드가 분산됨

"DI/설정 부담"
 - Adapter 주입, Port 구현체 설정 등 스프링 DI 설정이 많아짐
 - 잘못 설계하면 순환 참조나 잘못된 의존성 주입 발생 가능
728x90