이번 글에서는 Spring Boot와 Kotlin을 활용하여 주식 관련 서비스를 설계하는 과정을 설명하겠습니다. 특히, Hexagonal Architecture를 기반으로 프로젝트를 구성한 예제를 중심으로 살펴보겠습니다.
물론 초기 설계 이므로, 고도화가 필요합니다. 고도화 하는 과정의 일부도 추가적으로 작성할 예정 입니다.
Hexagonal Architecture의 장점은 의존성의 방향을 명확히 하여 비즈니스 로직이 외부 기술에 의해 영향을 받지 않도록 하는 데 있습니다. 이를 통해 테스트 용이성과 유지보수성을 높일 수 있습니다. 핵심은 의존성으로 의존성, 그리고 의존성의 방향을 관리하는것이 제가 설계한 아키텍쳐의 핵심 설계라고 볼 수 있습니다.
Hexagonal Architecture, 또한 "Ports and Adapters Architecture"로 알려진 이 아키텍처는 소프트웨어 시스템을 독립적인 비즈니스 로직과 그 비즈니스 로직을 호출하는 외부 요소로 나누어 설계합니다. 이 아키텍처의 주요 목표는 시스템의 유연성과 확장성을 높이는 것입니다.
Application Core
- Use Case: 시스템의 주요 비즈니스 로직을 구현합니다. 유스 케이스는 특정 작업을 수행하는 데 필요한 모든 비즈니스 규칙을 포함합니다.
- Domain: 비즈니스 엔티티와 규칙을 포함합니다. 도메인은 비즈니스 로직의 중심이며, 시스템의 상태를 나타냅니다.
- Input Ports: 외부에서 Application Core로의 입력을 처리합니다. 예를 들어, 웹 요청을 처리하는 포트입니다.
- Output Ports: Application Core에서 외부 시스템으로의 출력을 처리합니다. 예를 들어, 데이터베이스에 저장하는 작업을 처리하는 포트입니다.
Adapters
- Driving Adapters: 시스템의 유스 케이스를 트리거하는 외부 요소입니다. 예를 들어, 웹 어댑터는 사용자 요청을 받아 유스 케이스를 실행합니다.
- Driven Adapters: 유스 케이스가 필요로 하는 외부 서비스나 시스템과의 상호작용을 처리합니다. 예를 들어, 데이터베이스 어댑터는 데이터를 저장하고 검색하는 역할을 합니다.
유연성: 각 구성 요소가 명확하게 분리되어 있어 쉽게 교체하거나 확장할 수 있습니다. 예를 들어, 데이터베이스를 변경하더라도 Application Core는 영향을 받지 않습니다.
테스트 용이성: 비즈니스 로직이 외부 시스템과 분리되어 있어 독립적으로 테스트할 수 있습니다.
확장성: 새로운 기능을 추가할 때 기존 시스템을 최소한으로 수정하여 추가할 수 있습니다.
이 프로젝트는 주식 관련 데이터를 처리하고 제공하는 stock-service 모듈입니다. 멀티 모듈 구조 또는 MSA(Microservices Architecture)로 구성되어 있으며, 주요 기술 스택으로는 Spring Boot, Kotlin, 그리고 Hibernate Reactive를 사용하고 있습니다.
kotlin
└── adapter
└── in
└── web
└── AssembleStockInfoController.kt
└── out
└── persistence
└── AssembleStockInfoAdapter.kt
└── AssembleStockInfoMapper.kt
└── StockJpaEntity.kt
└── StockJpaRepository.kt
└── application
└── port
└── in
└── AssembleStockInfoCommand.kt
└── AssembleStockInfoUseCase.kt
└── out
└── dto
└── AssembleStockInfoMapper.kt
└── AssembleStockInfoPort.kt
└── service
└── AssembleStockInfoService.kt
└── common
└── PersistenceAdapter.kt
└── UseCase.kt
└── UseCaseImpl.kt
└── WebAdapter.kt
└── domain
└── Stock.kt
└── Application.kt
- SketchController.kt: 웹 요청을 처리하는 컨트롤러입니다.
- AssembleStockInfoAdapter.kt: 데이터베이스와 상호작용하는 어댑터입니다.
application: 비즈니스 로직을 담당하는 계층입니다. port 패키지는 입력과 출력을 정의하며, service 패키지는 비즈니스 로직을 구현합니다.
- AssembleStockInfoCommand.kt: 입력 명령을 정의합니다.
- AssembleStockInfoUseCase.kt: 유스케이스를 정의합니다.
- AssembleStockInfoService.kt: 서비스 로직을 구현합니다.
common: 공통으로 사용되는 유틸리티나 기반 클래스들을 모아둔 패키지입니다.
- PersistenceAdapter.kt: 퍼시스턴스 어댑터 관련 클래스입니다.
- UseCase.kt, UseCaseImpl.kt: 유스케이스 관련 클래스들입니다.
- WebAdapter.kt: 웹 어댑터 관련 클래스입니다.
domain: 도메인 모델을 정의하는 계층입니다.
- Stock.kt: 주식 관련 도메인 모델을 정의합니다.
의존성의 방향 예시를 보여 드리려고 합니다.
해당 파일은 adapter.out.persistence 패키지 입니다.
package com.example.stock.adapter.out.persistence
import com.example.stock.application.port.out.AssembleStockInfoPort
import com.example.stock.application.port.out.dto.StockDTO
import com.example.stock.common.PersistenceAdapter
@PersistenceAdapter
class AssembleStockInfoAdapter(
private val repository: StockJpaRepository,
) : AssembleStockInfoPort {
override fun assembleStockInfo(
stockId: Int,
stockName: String,
stockDerivative: Double,
stockPrice: Int,
): StockDTO {
val jpaEntity = repository.save() // TODO: 예제라서 save 구현이 없음
return jpaEntity.toDTO()
}
}
여기서 jpaEntity자체를 반환 하도록 할 수 있습니다. 하지만, 그렇게 되면 AssembleStockInfoPort는 StockJpaEntity에 대한 의존성을 가지게 됩니다.
그렇기에 반환 값이 StockDTO가 되는 것이며, 이 StockDTO는 package com.example.stock.application.port.out.dto 에 존재 하게 됩니다.
이런식으로 의존성의 방향을 엄격하게 제한하는것이 제 아키텍쳐의 핵심 원칙 입니다. 다음 포스팅에서는 이 규칙을 어떻게 강화 할 수 있는지에 대해 포스팅 하려고 합니다