비즈니스 로직을 외부 기술로부터 보호하기 위한 아키텍처

헥사고날 아키텍처는 2005년 Alistair Cockburn이 제안한 설계 방식으로,
애플리케이션의 핵심 도메인을 외부 의존성으로부터 분리하는 것을 목표로 한다.

다른 이름으로는:

  • Ports and Adapters Architecture
  • Onion Architecture
  • Clean Architecture (유사 개념)

라고도 불린다.


왜 등장했는가?

기존의 레이어드 아키텍처는 이런 구조였다.

 
 
Controller

Service

Repository

DB
 

문제는 무엇이었을까?

  • Service가 Repository에 강하게 의존
  • DB가 바뀌면 비즈니스 로직도 수정
  • 테스트 시 DB 필요
  • 외부 API 교체 시 서비스 코드 수정

즉, 비즈니스 로직이 인프라에 종속되는 구조였다.

헥사고날은 이 의존성을 뒤집는다.


핵심 개념

1️⃣ Port (포트)

비즈니스 로직이 외부와 통신하기 위한 인터페이스.

In Port

  • 외부 요청이 들어오는 인터페이스
  • UseCase

Out Port

  • 외부 시스템(DB, API 등)에 접근하기 위한 인터페이스

2️⃣ Adapter (어댑터)

Port를 실제로 구현하는 기술 코드.

예:

  • Controller
  • JPA Repository
  • Redis Client
  • Kafka Producer

3️⃣ Domain (핵심)

비즈니스 규칙이 존재하는 영역.

여기에는 다음만 존재해야 한다:

  • Entity
  • Value Object
  • Domain Service
  • Business Rule

여기에는 절대 없어야 하는 것:

  • JPA
  • Spring
  • HTTP
  • DB
  • 외부 API

구조 예시

 
 
user
├─ adapter
│ ├─ in (web)
│ └─ out (persistence)
├─ application
│ ├─ port
│ │ ├─ in
│ │ └─ out
│ └─ service
└─ domain
 

의존성 방향은 항상 안쪽을 향한다.

 
 
Adapter → Application → Domain
 

Domain은 아무것도 모른다.


코드 예시

In Port

 
 
public interface CreateUserUseCase {
void createUser(CreateUserCommand command);
}
 

Application Service

 
 
@Service
@RequiredArgsConstructor
public class CreateUserService implements CreateUserUseCase {

private final UserRepositoryPort userRepositoryPort;

@Override
public void createUser(CreateUserCommand command) {
User user = User.create(command);
userRepositoryPort.save(user);
}
}
 

Out Port

 
 
public interface UserRepositoryPort {
void save(User user);
}
 

Adapter (JPA 구현)

 
 
@Repository
@RequiredArgsConstructor
public class UserRepositoryAdapter implements UserRepositoryPort {

private final SpringDataUserRepository repository;

@Override
public void save(User user) {
repository.save(UserJpaEntity.from(user));
}
}
 

Application은 JPA를 모른다.
Domain은 Spring을 모른다.


헥사고날의 장점

1️⃣ 테스트가 쉬워진다

Out Port를 Mock으로 대체하면 된다.

DB 없이 테스트 가능.


2️⃣ 기술 교체가 자유롭다

  • JPA → MyBatis
  • MySQL → MongoDB
  • REST → Kafka

비즈니스 로직 수정 없이 교체 가능.


3️⃣ MSA 전환에 유리

각 도메인을 독립 모듈로 분리하기 쉬움.


4️⃣ 의존성 방향이 명확하다

항상 안쪽(도메인)을 향한다.

외부 기술은 플러그인처럼 붙는다.


단점

1️⃣ 초기 러닝커브

  • Port
  • Adapter
  • UseCase 분리

개념 이해가 필요하다.


2️⃣ 파일 수 증가

CRUD만 있는 작은 프로젝트에는 과할 수 있다.


언제 쓰는 것이 좋은가?

✔ 도메인 복잡도가 높은 시스템
✔ 결제, 정산, 금융 시스템
✔ 장기 유지보수 시스템
✔ 테스트 중요 시스템
✔ MSA 고려 시스템

❌ 단순 게시판 CRUD


레이어드 vs 헥사고날 비교

구분레이어드헥사고날
의존성 위→아래 외부→내부
테스트 DB 필요 Mock 가능
기술 교체 어려움 쉬움
구조 명확성 보통 매우 명확

결론

헥사고날 아키텍처는 단순히 폴더 구조가 아니다.

“비즈니스 로직을 보호하기 위한 설계 철학”이다.

기술은 바뀐다.
DB는 바뀐다.
프레임워크는 바뀐다.

하지만 도메인은 바뀌지 않는다.

핵심은 이것이다:

도메인을 중심에 두고, 외부 기술을 주변부로 밀어내는 것.

LIST

 1. 왜 개발자가 고객 행동을 알아야 하는가?

  • 기능이 아니라 “전환”을 설계해야 하기 때문
  • 트래픽 ≠ 매출
  • 로그 ≠ 인사이트

2. 고객 행동 분석 → 이벤트 로그 설계

🔹 이론

  • 고객은 검색 → 탐색 → 비교 → 구매 → 재구매 흐름을 가진다

🔹 개발자 번역

고객 행동 = 상태 변화

 
VISIT → VIEW → ADD_TO_CART → CHECKOUT → PAYMENT → COMPLETE

👉 설계 포인트

  • 모든 행동은 이벤트로 남겨야 함
  • timestamp + userId + productId 필수
  • 단순 access log로는 부족

간단 사례

나쁜 설계:

 
order 테이블만 존재

좋은 설계:

 
event_log - event_type - user_id - session_id - product_id - occurred_at

3. 고객구매모델 → 전환 퍼널 구조 설계

🔹 이론

AIDA 모델 (Attention → Interest → Desire → Action)

🔹 개발자 번역

퍼널은 단순 통계가 아니라
DB/로그 설계 단계에서 결정됨

 
상품조회수 → 장바구니 전환율 → 결제 시도율 → 결제 완료율

설계 포인트

  • Checkout 진입 로그 따로 남겨야 함
  • 실패 이벤트도 반드시 기록
 
PAYMENT_FAILED PAYMENT_RETRY

이게 없으면 KPI 계산 불가


4. 고객 세분화 → 도메인 모델 확장

🔹 이론

고객 세분화 = 타겟 그룹 분류

🔹 개발자 번역

User Aggregate에 이런 속성 필요:

 
- 가입경로 - 구매횟수 - 평균 구매금액 - 최근 방문일 - 마케팅 동의 여부

세분화는 BI 영역이 아니라
데이터 모델 설계 단계에서 결정됨


5. 가치사슬 분석 → 서비스 모듈 경계 설정

🔹 이론

가치사슬 = 기업 활동 흐름

🔹 개발자 번역

 
상품관리 → 재고 → 주문 → 결제 → 배송 → 정산

이건 단순 기능 나열이 아니라

👉 마이크로서비스 경계 후보

Settlement 분리 이유도 여기서 나옴


6. KPI → 설계의 최종 검증 지표

🔹 이론

KPI는 전략의 결과 지표

🔹 개발자 번역

KPI가 정해지면 설계가 바뀜

예:

KPI = 재구매율

그러면 필요한 것:

  • 주문 히스토리 조회 최적화
  • 쿠폰 이벤트 로그
  • 재방문 트리거 이벤트

🎯 핵심 메시지 정리

마케팅 이론은 개발자에게 “비즈니스 요구사항”이다.
고객 행동을 모르면 설계는 기술적 완성도만 높고, 매출과는 무관해진다.


💡 블로그 마무리 한 줄

“전환은 기획이 만든다.
하지만 전환을 측정 가능하게 만드는 것은 설계다.”

LIST

1️⃣ 전통적 Executor 모델

자바에서 우리가 쓰던 방식입니다.

 
ExecutorService executor = Executors.newFixedThreadPool(10); executor.submit(() -> processSettlement());

구조

  • OS Thread 기반
  • ThreadPool 크기 고정
  • Blocking I/O면 스레드가 멈춰 있음

문제점

  • 스레드 하나당 메모리 큼 (~1MB)
  • 1,000개 요청 처리하려면 1,000 스레드 필요
  • 대기 I/O에서 리소스 낭비

그래서 풀 크기 튜닝, 큐 사이즈 조정, Rejection 정책 고민합니다.

배치 돌릴 때:

  • maxPoolSize
  • queueCapacity
  • CallerRunsPolicy

이런 거 고민하셨을 겁니다.


2️⃣ Virtual Thread 

 
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); executor.submit(() -> processSettlement());

구조

  • JVM이 관리하는 경량 스레드
  • OS Thread에 매핑됨
  • Blocking해도 다른 작업 수행 가능

차이 핵심

구분Executor (Platform Thread)                                                                       Virtual Thread
기반 OS Thread JVM 관리
메모리 매우 작음
생성 비용 비쌈 거의 없음
동시성 수 수백~수천 수십만 가능
코드 스타일 비동기/풀 튜닝 동기 코드 그대로

3️⃣ 배치/정산 관점에서 비교

예: 10,000건 정산 처리

❌ 기존 방식

  • 20개 스레드 풀
  • DB I/O 대기
  • 처리 시간 늘어남
  • 풀 크기 고민해야 함

✅ Virtual Thread

  • 10,000개 작업 그냥 던짐
  • 각 작업은 동기 코드
  • 대기 I/O 동안 다른 작업 실행
  • 풀 튜닝 거의 불필요

4️⃣ 언제 Executor가 낫나?

  • CPU 연산 위주
  • 병렬 계산
  • ForkJoinPool 사용 시
  • 라이브러리 호환성 문제 있을 때

5️⃣ 상황 적용

정산/배치 특징:

  • DB I/O 많음
  • 외부 API 호출 있음
  • 부분환불, 멱등성 체크

이건 전형적인 I/O bound

👉 Virtual Thread 궁합 좋음


6️⃣ 코루틴이랑 뭐가 다르냐

코틀린 코루틴은:

  • 완전 다른 스케줄링 모델
  • suspend 기반
  • non-blocking 설계 필요

Virtual Thread는:

  • 기존 동기 코드 그대로 사용
  • 학습 비용 거의 없음

7️⃣ 설계자 관점 핵심

- 이 작업은 CPU-bound인가?

- I/O-bound인가?

- 스레드 수를 내가 튜닝해야 하나?

- 코드 단순성이 중요한가?

LIST

1. 왜 정산은 배치 설계가 중요한가

정산은 다음 특성을 가진다.

  • 대량 데이터
  • 장시간 실행
  • 부분 실패 허용
  • 재실행이 일상

따라서 핵심은 정확성 + 복구 가능성이다.


2. 도메인 경계

Settlement Context

  • SettlementJobRun (정산 실행 단위)
  • SettlementItem (정산 항목)
  • SettlementResult (처리 결과)

외부 지급/전표 시스템은 의존성 분리한다.


3. 배치 플로우 설계

 
1. JobRun 생성 2. 정산 대상 조회 3. SettlementItem 생성 4. Item 단위 처리 5. 결과 집계

JobRun은 실행 1회를 의미하고
Item은 재처리 가능한 최소 단위다.


4. 트랜잭션 경계

  • JobRun 생성: 단일 트랜잭션
  • Item 생성: 단일 트랜잭션
  • Item 처리: Item당 트랜잭션

❌ 전체 배치를 하나의 트랜잭션으로 묶지 않는다.


5. 멱등성 & 재처리 설계

Item 자연키

 
merchantId + settlementPeriod + sourceDocId
  • 유니크 인덱스로 중복 생성 방지
  • 동일 Item은 재처리 시 결과 덮어쓰기 or 유지

재처리 정책

  • 시스템 오류 → 재시도 가능
  • 데이터 오류 → 수동 보정 후 재실행

6. 장애 전파 차단

  • 계산 단계와 지급 단계 분리
  • 외부 시스템 장애 시:
    • Item 상태만 FAILED
    • JobRun은 계속 진행

👉 전체 배치 실패 없음


7. 워커 구조

  • Item을 상태 기준으로 조회
  • PENDING → PROCESSING → SUCCESS / FAILED

재시도는 상태 기반으로 제어

 


8. 이 설계로 증명하는 것

  • 배치 트랜잭션 분해 능력
  • 멱등/재처리 설계
  • 장애 격리
  • 정합성 vs 가용성 판단
LIST

1. 왜 이 주제를 골랐는가

결재 시스템은 단순 CRUD가 아니라

  • 상태 전이
  • 동시성
  • 중복 요청
  • 조직/권한 규칙
    이 한꺼번에 얽히는 설계형 도메인이다.

“결재를 기능이 아니라 상태 머신으로 설계할 수 있는가”
를 증명하기 위해 만들었다.


2. 도메인 경계(Bounded Context)

Approval Context

  • Approval (결재건)
  • ApprovalStep (단계)
  • Approver (결재자)
  • ApprovalAction (승인/반려 이력)

외부 도메인(정산/회계/알림)은 직접 참조하지 않고 이벤트로만 연결한다.


3. 상태머신 설계

Approval은 명확한 상태 전이를 가진다.

 
DRAFT → IN_PROGRESS → APPROVED → REJECTED
  • 각 Step은 단일 승인 주체
  • 다음 단계 활성화는 현재 단계 승인 완료 시점에만 발생
  • 상태 전이는 서버에서만 결정 (클라이언트 신뢰 X)

4. 트랜잭션 경계

하나의 승인 요청 = 하나의 로컬 트랜잭션

트랜잭션 안에서 처리하는 것:

  • 승인자 권한 검증
  • 현재 Step 상태 검증
  • 승인 처리
  • 다음 Step 활성화
  • Outbox 이벤트 저장

트랜잭션 밖:

  • 알림 발송
  • 외부 시스템 반영

👉 결재의 정합성은 트랜잭션 안에서, 확장은 비동기로


5. 멱등성 설계

결재는 중복 요청이 반드시 발생한다.

멱등 키 전략

 
approve:{approvalId}:{stepId}:{approverId}:{clientRequestId}
  • DB 유니크 인덱스로 중복 차단
  • 이미 처리된 요청이면 이전 결과 반환
  • 상태 전이는 낙관적 락(version) 으로 보호

6. Outbox 이벤트

승인 완료 시 다음 이벤트를 저장한다.

 
ApprovalCompletedEvent - approvalId - finalStep - approvedAt

이벤트는 비동기로:

  • 알림
  • 정산 대상 등록
  • 감사 로그 처리

👉 결재 도메인은 외부 실패에 영향받지 않는다


7. 핵심 코드 (요약)

  • POST /approvals/{id}/approve
  • ApprovalService.approve(...)
  • ApprovalStateMachine.transition(...)
  • OutboxEventRepository.save(...)

8. 이 설계로 증명하는 것

  • 상태 기반 설계
  • 트랜잭션 경계 분리
  • 멱등성/동시성 대응
  • 장애 전파 차단
LIST

IT 설계는 종종 “기술적으로 가장 올바른 구조”를 찾는 일로 오해된다.
하지만 실제 현장에서 시스템이 무너지는 이유는 기술 부족이 아니라 현실을 잘못 가정했기 때문인 경우가 훨씬 많다.

경영학과 경제학은 애초에 한 가지 사실을 전제로 출발한다.

정보는 항상 불완전하고,
사람은 항상 합리적이지 않다.

이 전제는 IT 설계에도 그대로 적용된다.
완벽한 요구사항, 합리적인 사용자, 규칙을 잘 지키는 운영자는 존재하지 않는다.

그렇다면 IT 설계의 목적은 무엇이어야 할까.


1. IT 설계는 ‘정답’을 찾는 일이 아니다

경영학은 정답을 가르치지 않는다.
대신 망하지 않을 선택을 반복하는 방법을 다룬다.

IT 설계도 같다.

  • 요구사항은 바뀐다
  • 일정은 압축된다
  • 인력은 교체된다
  • 운영 환경은 예측 불가하다

이 상황에서 “가장 이상적인 아키텍처”를 찾는 것은 의미가 없다.
중요한 질문은 이것이다.

이 선택은 나중에 되돌릴 수 있는가?

좋은 설계란,

  • 변경 비용이 낮고
  • 실패했을 때 피해가 국소화되며
  • 일부가 망가져도 전체가 죽지 않는 구조다

즉, 정답 설계가 아니라 생존 설계다.


2. 잘하는 개발자가 아니라, 깨지지 않는 시스템

경영학은 ‘유능한 개인’보다 ‘지속 가능한 조직’을 중시한다.
IT도 동일하다.

에이스 개발자 한 명에 의존하는 시스템은 이미 실패한 구조다.

  • 그 사람이 휴가를 가면 멈춘다
  • 그 사람이 퇴사하면 공백이 생긴다
  • 그 사람만 이해하는 로직이 늘어난다

반대로 좋은 설계는 이렇게 말한다.

“이 시스템은 누가 와도 운영할 수 있다.”

그래서 설계에는 반드시 다음이 포함된다.

  • 명확한 책임 경계
  • 표준화된 인터페이스
  • 로그와 모니터링
  • 자동화된 배포와 복구
  • 문서와 규칙

이는 기술적 미학이 아니라 조직 리스크 관리다.
경영학적 사고가 없는 설계는 결국 사람 문제로 무너진다.


3. 사람은 합리적이지 않다 — 그래서 시스템이 중요하다

경제학은 인간을 “항상 합리적인 존재”로 보지 않는다.
오히려 비합리적 행동까지 포함해 설명한다.

이 관점은 IT 설계에 매우 중요하다.

  • 사용자는 매뉴얼을 읽지 않는다
  • 운영자는 귀찮으면 우회한다
  • 개발자는 평가 기준에 맞춰 움직인다

이건 도덕의 문제가 아니라 인센티브의 문제다.

  • 보안 설정이 복잡하면 → 보안은 꺼진다
  • 배포가 번거로우면 → 수동 배포가 된다
  • 장애 보고 시 불이익이 있으면 → 장애는 숨겨진다

시스템은 사람을 바꾸지 못한다.
하지만 사람의 행동 방향은 바꿀 수 있다.

그래서 좋은 설계는 이렇게 묻는다.

“이 구조에서 사람들이 가장 쉽게 할 행동은 무엇인가?”

그리고 그 행동이 시스템에 안전한 방향이 되도록 설계한다.


4. 개인의 합리적 선택이 시스템을 망가뜨릴 수 있다

경제학에서 시장은 “의도하지 않은 결과의 집합”이다.
IT 시스템도 마찬가지다.

각 개인은 합리적으로 행동한다.

  • 일정이 급하니 테스트를 생략한다
  • 장애 대응을 빨리 끝내기 위해 임시 조치를 넣는다
  • 다음 배포에 고치자며 기술 부채를 미룬다

하지만 이 선택이 누적되면 결과는 예측 가능하다.

  • 테스트 부재 → 장애 상시화
  • 임시 코드 → 구조 붕괴
  • 기술 부채 → 변경 불가 시스템

대부분의 장애는 악의가 아니라 합리적 판단의 누적 결과다.

그래서 IT 설계는 사람을 통제하는 일이 아니라,
합리적 선택의 결과가 망하지 않도록 구조를 만드는 일이다.


5. 결론: IT 설계의 본질

경영학과 경제학 관점에서 보면,
IT 설계의 본질은 명확해진다.

  • 완벽한 요구사항을 가정하지 않는다
  • 합리적인 인간을 전제하지 않는다
  • 정답 아키텍처를 찾지 않는다

대신,

불완전한 환경에서도 시스템이 버티게 만드는 구조를 만든다.

좋은 IT 설계란,

  • 실패를 전제로 하고
  • 변경을 허용하며
  • 사람이 실수해도 시스템이 보호하는 구조다

이는 기술 역량의 문제가 아니라 사고 수준의 문제다.

그리고 이 관점을 가진 개발자는
단순한 코더가 아니라 설계자, 아키텍트, 시스템 빌더가 된다.

LIST

MSA가 뭐냐 (Microservices Architecture)

 

MSA = 하나의 큰 시스템을, 작고 독립적인 서비스 여러 개로 쪼개는 방식입니다.

  • 기존: 하나의 모놀리식 애플리케이션
  • MSA: 여러 개의 독립 서비스가 네트워크(API)로 통신

핵심 한 줄 요약:

“기능 단위가 아니라 ‘서비스 단위’로 시스템을 쪼갠 구조”

 

 

MSA 아키텍처가 뭐냐

MSA 아키텍처

*“마이크로서비스들을 어떻게 나누고, 어떻게 연결하고, 어떻게 운영할 것인가”*에 대한 전체 설계 원칙과 구조

기본 구성요소 (실무 기준)

  1. Service
    • 하나의 비즈니스 책임만 가짐
    • 예: 회원서비스 / 결제서비스 / 주문서비스
    • 각자 DB를 가짐 (중요)
  2. API 통신
    • REST / gRPC / 메시지 큐
    • 서비스 간 직접 DB 접근 금지
  3. API Gateway
    • 외부 요청의 진입점
    • 인증, 라우팅, 로깅 처리
  4. Service Discovery
    • 서비스 위치 자동 탐색
    • (예: Eureka, Consul)
  5. 독립 배포
    • 서비스 하나만 수정 → 그 서비스만 배포

모놀리식 vs MSA (현실 비교)

항목                                          모놀리식                                                             MSA

 

배포 전체 재배포 서비스 단위
장애 전체 영향 부분 영향
개발 빠름(초기) 느림(초기)
운영 단순 복잡
조직 소규모 팀 분리 전제

👉 중요 포인트

  • MSA는 기술 문제가 아니라 조직/운영 문제
  • 팀 구조가 안 맞으면 100% 실패

많은 SI에서 말하는 “MSA”의 실체

현실적으로 많이 나오는 것:

가짜 MSA

  • 패키지만 나눔
  • DB 공유
  • 배포는 한 번에
  • → 이건 분산 모놀리식

진짜 MSA

  • DB 분리
  • 배포 분리
  • 장애 격리
  • 팀 책임 분리


SI + 공공 + 일정 빡빡 + 운영 인력 적음

풀 MSA는 리스크 큼

현실적 선택:

  • 모듈러 모놀리식
  • 또는 핵심 1~2개 서비스만 MSA
  •  

LIST

+ Recent posts