지금까지 레거시 아키텍쳐의 문제점을 분석하고, 이를 개선하기 위한 헥사고날 아키텍쳐의 도입과 설계에 대해 다루었습니다.
이번 마지막 글에서는 실제로 새 아키텍쳐가 어떤 효과를 가져왔는지를 구체적인 예시와 함께 살펴보겠습니다.
1. 풍부한 도메인 모델을 통한 비즈니스 로직의 응집도 향상
레거시 코드
// TransactionCoreService 클래스(서비스 레이어)에서 일일이 총액을 계산
fun tranSaleHeaderGenerate(
transactionInformation: PaymentEndRequestDTO,
paymentData: PaymentsReqResData
): TranSaleHeader {
val cardAmt = BigDecimal(credit.sumOf { it.request?.totalAmount ?: 0L })
val giftAmt = paymentData.giftReqResData.sumOf { it.useMoney ?: BigDecimal.ZERO }
val pntAmt = paymentData.crmReqResData.pointRequestData?.paymentAmt ?: BigDecimal.ZERO
// ... 더 많은 계산 로직들
}
신규 코드
data class PaymentHeader(
val cardPayments: List<CardPayment> = emptyList(),
val cardRefunds: List<CardRefund> = emptyList(),
val cashPayment: CashPayment? = null,
val cashRefund: CashRefund? = null,
) {
//...
// 도메인 객체가 자신의 비즈니스 로직에 대한 책임을 가짐
val availableCardPayments = cardPayments.filter { it.status.isApproved() }
val cardSaleAmount: BigDecimal get() = availableCardPayments.sumOf { it.saleAmount }
//...
val totalSaleAmount: BigDecimal get() = cardSaleAmount + cashSaleAmount + giftSaleAmount +
kakaoSaleAmount + naverSaleAmount + paycoSaleAmount
//...
// 상태 변경도 도메인 객체가 책임짐
fun complete(billNo: String): PaymentHeader = copy(
status = PaymentHeaderStatus.PAID,
cardPayments = cardPayments.map { it.copy(isNew = false) },
billNo = billNo
)
//...
}
2. 명확한 의존성 방향과 책임 분리
레거시 코드
// Service레이어 끼리의 의존관계가 어지럽게 얽혀 있고, core 모듈에서 외부세계(ex. van사 결제 모듈, 프린터 모듈)에 직접 의존하고 있음
@Service
class PaymentCoreService @Inject constructor(
//...
// 이름만 Service이고 실제로는 특정 van사의 결제 모듈(외부 세계)을 직접 호출하는 서비스
private val creditCardService: CreditCardService,
private val cashReceiptService: CashReceiptService,
private val commonCoreService: CommonCoreService,
private val session: Session,
//...
)
신규 코드
// 서비스는 UseCase 구현에만 집중
class PaymentService(
//...
private val paymentExecutor: PaymentPort,
private val paymentHeaderRepository: PaymentHeaderRepositoryPort,
private val orderRepository: OrderRepositoryPort,
private val postOrderCompleteProcessor: PostOrderCompleteProcessor,
//...
) : PaymentUseCase {
override suspend fun processCardPayment(command: CreditPaymentCommand): PaymentHeader {
// 비즈니스 규칙 검증
require(paymentHeader.status.canProceedPayment()) {
throw InvalidPaymentStatusException(status = paymentHeader.status)
}
// 외부 시스템과의 통신은 port를 통해 추상화
val newCardPayment = paymentExecutor.processCardPayment(command.toPaymentRequestDto())
.getOrThrow()
.toDomain()
// 액터에 의한 UseCase가 아닌 비즈니스 로직은 별도 프로세서로 분리
if (paymentUpdatedOrder.isCompletedPayment) {
return paymentUpdatedOrder
.also { completedOrder ->
postOrderCompleteProcessor.process(completedOrder)
}
.paymentHeader
}
return newPaymentHeader
}
}
3. 외부 세계와의 명확한 경계
레거시 코드
// 서비스라는 이름만 가진 단순 API 호출 래퍼
@Service
class AspApiService(
private val environment: Environment,
) {
private val aspApiService: AspApi =
ApiServiceContext(baseUrl, AspApi::class.java).getApiService() as AspApi
fun transactionUpload(transactionUploadRequestDTO: TransactionUploadRequestDTO): AspApiCommonResponse? {
return try {
val call = aspApiService.transactionUpload(transactionUploadRequestDTO)
val response = call.execute()
response.body()
} catch (e: Exception) {
throw e
}
}
}
신규 코드
// Port를 통한 외부 세계와의 격리
interface AspApiClientPort {
//...
fun transactionUpload(transactionData: TransactionDataReqDto): Result<Unit>
//...
}
interface PaymentPort {
//...
fun processCardPayment(request: PaymentRequestDto): Result<PaymentResponse>
fun processCardRefund(request: RefundRequestDto): Result<RefundResponse>
//...
}
// 액터에 의한 UseCase가 아닌 비즈니스 로직은 별도 프로세서로 분리
class PostOrderCompleteProcessor(
private val localPrinter: LocalPrinterPort,
private val salesDataGenerator: SalesDataGenerator,
private val aspApiClient: AspApiClientPort,
) {
fun process(order: Order) {
// 영수증 출력
localPrinter.printReceipt(order)
// 매출 데이터 생성 및 업로드
val salesData = salesDataGenerator.generate(order)
aspApiClient.upload(salesData.toAspReqDto())
}
}
4. 주요 개선 효과
- 비즈니스 로직의 응집도 향상
- 도메인 객체가 자신의 책임을 가짐
- 상태와 행위가 한 곳에서 관리됨
- 비즈니스 룰의 중앙화
- 테스트 용이성
- 외부 의존성이 Port로 격리되어 있어 Mocking이 용이
- 비즈니스 로직 테스트가 외부 시스템과 분리됨
- 유지보수성 향상
- 명확한 책임 분리로 코드 이해도 증가
- 변경의 영향 범위가 명확함
- 새로운 기능 추가가 용이
- 재사용성 확보
- POS, KIOSK, 급식 입장관리 등 다양한 오프라인 결제 솔루션에서 핵심 비즈니스 로직 재사용 가능
- 플랫폼 독립적인 설계로 크로스플랫폼 지원이 용이
5. 향후 개선점
도메인 이벤트 도입
- 현재는 PostProcessor를 통해 부가 기능을 처리하지만, 도메인 이벤트를 도입하면 더 낮은 결합도 달성 가능
- 예를 들어
OrderCompletedEvent
,PaymentRefundedEvent
등의 이벤트를 발행하고 구독하는 방식으로 변경
이번 프로젝트를 통해 신입 개발자로서 아키텍처의 중요성을 몸소 체험할 수 있었습니다.
특히 기존의 레거시 코드와 새로운 헥사고날 아키텍처를 비교하면서, 깔끔한 아키텍처가 코드의 품질과 유지보수성에 얼마나 큰 영향을 미치는지 실감했습니다.
물론 아직 개선의 여지가 많이 남아있지만, 이번 경험을 토대로 앞으로도 더 나은 설계를 위해 지속적으로 고민하고 개선해나갈 예정입니다.
끝으로 이런 도전 기회를 준 회사와 관리자 분들께 감사드립니다.
앞으로도 더 좋은 코드를 위해 끊임없이 공부하고 성장하는 개발자가 되도록 하겠습니다.
728x90
'아키텍쳐 설계 경험 > 헥사고날 아키텍쳐 설계 도전기' 카테고리의 다른 글
[신입 개발자의 첫 번째 아키텍쳐 설계 도전기 - #2] 헥사고날 아키텍처 도입과 설계 (0) | 2025.01.05 |
---|---|
[신입 개발자의 첫 번째 아키텍쳐 설계 도전기 - #1] 레거시 아키텍처의 문제점 분석 (1) | 2024.12.16 |
[신입 개발자의 첫 번째 아키텍쳐 설계 도전기 - 프롤로그] 입사 1년차 선물로.. 중요한 자사 프로젝트의 설계를 맡게 되었다!? (0) | 2024.12.16 |