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

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

by Bill Lab 2025. 8. 31.
반응형

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

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

2. 계층(Layer) 구조

       1) 포트(Port)

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

           - Inbound Port 는 usecase 의 interface 역할 

           - Outbound Port 는 repository interface 역할

 

       2) 어댑터(Adapter)

           - 포트의 구현체 역할

           - Inbound Adapter는 Controller, Kafka Listener 역할 수행

           - Outbound Adapter는 외부 통신, DB, Kafka Producer 구현 등의 기술에 대한 구현체 코드 작성 

      

       3) 핵심 도메인(Core Domain)

           - Application(usecase 조율)과 Domain(순수 비지니스 규칙) 모두 여기 소속

           - 외부 기술 의존 없음 

 

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

com.example
 ├─ user
 │   ├─ domain
 │   │    ├─ User.kt
 │   │    └─ UserDomainService.kt
 │   │
 │   ├─ port
 │   │    ├─ in
 │   │    │    └─ RegisterUserUseCase.kt
 │   │    └─ out
 │   │         └─ UserRepositoryPort.kt
 │   │
 │   ├─ application
 │   │    └─ RegisterUserService.kt
 │   │
 │   ├─ adapter
 │   │    ├─ in
 │   │    │    └─ UserController.kt (입력 adapter)
 │   │    └─ out
 │   │         └─ UserRepositoryAdapter.kt(출력 adapter)
 │   │
 │   └─ config
 │
 └─ common

 

"호출 흐름"
1. Controller (Inbound Adapter) 
2. Usecase (Inbound Port)
2. Application Service (Implements Usecase)
3. Domain Service (Domain)
4. Repository Port (Outbound Port)
5. Repository Adapter (Outbound Adapter)

 

3. 예제 코드

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

 

     1) Inbound Adapter(Controller)

import com.example.user.port.in.RegisterUserUseCase
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/users")
class UserController(
    private val registerUserUseCase: RegisterUserUseCase
) {

    @PostMapping
    fun register(@RequestBody request: RegisterUserRequest): UserResponse {
        return registerUserUseCase.register(request.email, request.name)
    }
}

 

    2) Inbound Port(usecase interface) 

interface RegisterUserUseCase {
    fun register(email: String, name: String): UserResponse
}

 

    3) Application Service

package com.example.user.application

import com.example.user.domain.User
import com.example.user.domain.UserDomainService
import com.example.user.port.`in`.RegisterUserUseCase
import com.example.user.port.out.UserRepositoryPort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional
class RegisterUserService(
    private val userRepositoryPort: UserRepositoryPort
) : RegisterUserUseCase {

    private val domainService = UserDomainService()

    override fun register(email: String, name: String): UserResponse {
        domainService.validateEmail(email)
        val user = User(email, name)
        val savedUser = userRepositoryPort.save(user)
        return UserResponse(savedUser.email, savedUser.name)
    }
}

 

    4) Domain Service

package com.example.user.domain

class UserDomainService {

    fun validateEmail(email: String) {
        require(!email.endsWith("@banned.com")) {
            "차단된 이메일입니다."
        }
    }
}

 

    5) Outbound Port (Repository 추상체)

package com.example.user.port.out

import com.example.user.domain.User

interface UserRepositoryPort {
    fun save(user: User): User
}

 

  6) Outbound Adapter(Repository 구현체)

package com.example.user.adapter.out

import com.example.user.domain.User
import com.example.user.port.out.UserRepositoryPort
import org.springframework.stereotype.Component

@Component
class UserRepositoryAdapter(
    private val userJpaRepository: UserJpaRepository
) : UserRepositoryPort {

    override fun save(user: User): User {
        val entity = UserEntity(
            email = user.email,
            name = user.name
        )

        val saved = userJpaRepository.save(entity)

        return User(saved.email, saved.name)
    }
}

   

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 설정이 많아짐
 - 잘못 설계하면 순환 참조나 잘못된 의존성 주입 발생 가능
반응형