MSA 환경에서 은행 도메인의 트랜잭션 관리와 데이터 일관성을 학습하기 위한 프로젝트입니다.
결제 시스템을 개발할 때마다 실제 은행 API나 PG사 연동이 필요합니다.
테스트를 위해 매번 복잡한 계약 절차를 거치거나, 샌드박스 환경의 제약에 부딪히게 됩니다.
이 프로젝트는 가상의 은행 시스템을 직접 구축하여:
- 결제 시스템 개발 시 자유롭게 테스트할 수 있는 환경 제공
- 은행 도메인의 인프라 구조 이해
- MSA 환경에서 트랜잭션 관리 방법 학습
- 데이터 일관성과 동시성 제어 문제 해결 경험
| 구분 | 기술 | 버전 |
|---|---|---|
| Language | Java | 21 |
| Framework | Spring Boot | 4.0.0 |
| Cloud | Spring Cloud | 2025.1.0 |
| Database | PostgreSQL + pgvector | 17+ |
| Messaging | Apache Kafka (KRaft) | 3.8.0 |
| Query | QueryDSL | 5.1.0 jakarta |
| Container | Docker, Docker Compose | - |
| Monitoring | Prometheus, Grafana, Zipkin | - |
┌─────────────────┐
│ Nginx │
│ (L7 LB) │
└────────┬────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Gateway Server│ │ Gateway Server│
│ -1 :8080 │ │ -2 :8089 │
└───────┬───────┘ └───────┬───────┘
│ │
└───────────────┬───────────────┘
│
┌───────────────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ User Service │ │ Auth Server │ │Account Service│
│ :8087 │ │ :8086 │ │ :8081 │
└───────────────┘ └───────────────┘ └───────────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│Transaction Svc│ │Transfer Service│ │ Card Service │
│ :8082 │ │ :8083 │ │ :8084 │
└───────────────┘ └───────────────┘ └───────────────┘
│ │ │
└───────────────────────────┼───────────────────────────┘
│
▼
┌───────────────┐
│Ledger Service │
│ :8085 │
└───────────────┘
┌─────────────────┐ ┌─────────────────┐
│ Eureka Server-1 │◄───►│ Eureka Server-2 │
│ :8761 │ │ :8762 │
└─────────────────┘ └─────────────────┘
▲
│
┌─────────────────┐
│ Config Server │
│ :8888 │
└─────────────────┘
| 서비스 | 포트 | 설명 |
|---|---|---|
| eureka-server | 8761, 8762 | 서비스 디스커버리 (이중화) |
| config-server | 8888 | 중앙 설정 관리 |
| gateway-server | 8080, 8089 | API Gateway (이중화) |
| 서비스 | 포트 | 설명 | 핵심 학습 |
|---|---|---|---|
| user-service | 8087 | 사용자 관리 | 기본 CRUD, 이벤트 발행 |
| auth-server | 8086 | 인증/인가 | JWT, Spring Security |
| account-service | 8081 | 계좌 관리 | 낙관적/비관적 락, 동시성 |
| transaction-service | 8082 | 입출금 처리 | 멱등성 (Idempotency Key) |
| transfer-service | 8083 | 계좌 이체 | SAGA 패턴, Outbox 패턴 |
| card-service | 8084 | 카드 결제 | Resilience4j, 한도 관리 |
| ledger-service | 8085 | 원장 기록 | 불변 데이터, 복식부기 |
단일 DB에서는 ACID가 보장되지만, MSA에서는 서비스마다 DB가 분리됩니다.
- SAGA 패턴: 보상 트랜잭션을 통한 Eventually Consistent
- Outbox 패턴: DB 트랜잭션과 이벤트 발행의 원자성 보장
같은 계좌에 동시에 여러 거래가 발생하면 잔액이 꼬일 수 있습니다.
- 낙관적 락:
@Version필드를 통한 충돌 감지 - 비관적 락:
@Lock(PESSIMISTIC_WRITE)DB 레벨 잠금
네트워크 장애로 같은 요청이 중복 전송될 수 있습니다.
- Idempotency Key:
X-Idempotency-Key헤더로 중복 요청 방지
일부 서비스가 죽어도 전체 시스템은 동작해야 합니다.
- Circuit Breaker: 장애 전파 차단
- Retry + Backoff: 일시적 장애 복구
- Rate Limiter: 과부하 방지
common-lib/ # 공통 모듈 (Maven Central 배포)
├── event/ # IntegrationEvent (멱등성/순서보장/재시도/TTL)
├── util/ # UuidUtils (도메인별 ID 생성)
└── exception/ # 공통 예외
의존성 추가
implementation 'io.github.jun-bank:common-lib:0.0.1'service/
├── global/ # 전역 설정 레이어
│ ├── config/ # JPA, Kafka, Security, Feign, Swagger, Async
│ ├── infrastructure/ # BaseEntity, AuditorAware
│ ├── security/ # UserPrincipal, HeaderAuthenticationFilter
│ ├── feign/ # FeignErrorDecoder, FeignRequestInterceptor
│ └── aop/ # LoggingAspect
└── domain/
└── {domain}/ # 도메인 단위
├── domain/ # 순수 도메인 (Entity, VO, Enum)
├── application/ # 유스케이스 + Port + DTO
├── infrastructure/ # Adapter (Out) - Repository, Kafka
└── presentation/ # Adapter (In) - Controller
| 패키지 | 파일 | 설명 |
|---|---|---|
config/ |
JpaConfig | JPA Auditing 활성화 |
| QueryDslConfig | JPAQueryFactory 빈 등록 | |
| KafkaProducerConfig | 멱등성 Producer (Spring Kafka 4.0) | |
| KafkaConsumerConfig | 수동 ACK Consumer | |
| SecurityConfig | 헤더 기반 인증, Stateless | |
| FeignConfig | 로깅, 에러 디코더 | |
| SwaggerConfig | OpenAPI 문서화 | |
| AsyncConfig | ThreadPoolTaskExecutor | |
infrastructure/ |
BaseEntity | Audit + Soft Delete |
| AuditorAwareImpl | JPA Auditing 사용자 정보 | |
security/ |
UserPrincipal | UserDetails 구현체 |
| HeaderAuthenticationFilter | Gateway 헤더 → SecurityContext | |
| SecurityContextUtil | 현재 사용자 조회 유틸리티 | |
feign/ |
FeignErrorDecoder | HTTP 에러 → BusinessException |
| FeignRequestInterceptor | 인증 헤더 전파 | |
aop/ |
LoggingAspect | 요청/응답 로깅 |
Client → Gateway → JWT 검증 → 헤더 주입 → 내부 서비스
│
▼
X-User-Id, X-User-Role, X-User-Email
- Gateway: JWT 토큰 검증 후 사용자 정보를 헤더로 전달
- 내부 서비스: 헤더만 신뢰, JWT 검증 없음
- HeaderAuthenticationFilter: 헤더 → SecurityContext 변환
| 토픽 | Producer | Consumer | 설명 |
|---|---|---|---|
user.created |
User | Auth | 회원가입 → 계정 생성 |
user.deleted |
User | All | 회원탈퇴 → 연관 데이터 정리 |
account.balance.changed |
Account | Ledger | 잔액 변경 기록 |
transfer.debit.requested |
Transfer | Account | SAGA 출금 요청 |
transfer.credit.requested |
Transfer | Account | SAGA 입금 요청 |
transfer.completed |
Transfer | Ledger | 이체 완료 기록 |
public record IntegrationEvent(
String eventId, // 멱등성 보장
String eventType, // 이벤트 타입
int sequenceNumber, // 순서 보장
LocalDateTime timestamp, // 발생 시각
LocalDateTime expiresAt, // TTL
int retryCount, // 재시도 횟수
Object payload // 실제 데이터
) {}- Java 21
- Docker & Docker Compose
- Gradle 8.x
# 1. 인프라 실행 (Kafka, PostgreSQL, Zipkin)
cd infrastructure
docker-compose up -d
# 2. Eureka Server 실행 (서비스 디스커버리)
cd eureka-server
./gradlew bootRun
# 3. Config Server 실행 (설정 서버)
cd config-server
./gradlew bootRun
# 4. Gateway Server 실행
cd gateway-server
./gradlew bootRun
# 5. 비즈니스 서비스 실행
cd user-service && ./gradlew bootRun &
cd auth-server && ./gradlew bootRun &
cd account-service && ./gradlew bootRun &
# ... 나머지 서비스- 프로젝트 초기 구조 설정 (10개 서비스)
- common-lib 구현 (IntegrationEvent, UuidUtils, 113개 테스트)
- 인프라 서버 기본 설정 (Eureka, Config, Gateway)
- 비즈니스 서비스 Global 레이어 (110개 파일)
- JPA, Kafka, Security, Feign, Swagger, Async 설정
- BaseEntity (Soft Delete), JPA Auditing
- 헤더 기반 인증 필터
- 로깅 AOP
- 비즈니스 서비스 Domain 레이어 (66개 파일)
- 7개 서비스 전체 도메인 모델 구현 완료
- Exception, Enum, VO, Aggregate Root 포함
- 각 서비스별 핵심 학습 주제 반영
- Auth Server JWT 구현
- Gateway JWT 검증 필터
- Application Layer (UseCase, Port, Service)
- Infrastructure Layer (Entity, Repository, Kafka)
- Presentation Layer (Controller, DTO)
- SAGA 패턴 구현 (Transfer Service)
- Resilience4j 적용 (Card Service)
- 통합 테스트
- Docker Compose 전체 구성
| 서비스 | 파일 수 | Aggregate Root | 주요 VO | 핵심 Enum |
|---|---|---|---|---|
| user-service | 7 | User | UserId, Email, PhoneNumber | UserStatus |
| auth-server | 12 | AuthUser, RefreshToken, LoginHistory | AuthUserId, Password, RefreshTokenId | UserRole, AuthUserStatus |
| account-service | 8 | Account | AccountId, AccountNumber, Money | AccountType, AccountStatus |
| transaction-service | 9 | Transaction, IdempotencyRecord | TransactionId, IdempotencyKey, Money | TransactionType, TransactionStatus |
| transfer-service | 10 | Transfer, OutboxEvent | TransferId, OutboxEventId, Money | TransferStatus, SagaStatus, OutboxStatus |
| card-service | 11 | Card, Payment | CardId, CardNumber, PaymentId, Money | CardType, CardStatus, PaymentStatus |
| ledger-service | 9 | LedgerEntry, AuditLog | LedgerEntryId, AuditLogId, Money | EntryType, TransactionCategory |
domain/user/domain/
├── exception/
│ ├── UserErrorCode.java # 에러 코드 (INVALID_EMAIL, USER_NOT_FOUND 등)
│ └── UserException.java # 도메인 예외 (팩토리 메서드 패턴)
└── model/
├── User.java # Aggregate Root (회원 정보 관리)
├── UserStatus.java # 상태 Enum (ACTIVE, INACTIVE, SUSPENDED, WITHDRAWN)
└── vo/
├── UserId.java # USR-xxxxxxxx (8자리 영숫자)
├── Email.java # 이메일 형식 검증, 정규화
└── PhoneNumber.java # 한국 전화번호 형식 (010-xxxx-xxxx)
domain/auth/domain/
├── exception/
│ ├── AuthErrorCode.java # 인증 에러 코드
│ └── AuthException.java # 인증 예외
└── model/
├── AuthUser.java # Aggregate Root (인증 사용자)
├── RefreshToken.java # Refresh Token 관리
├── LoginHistory.java # 로그인 이력 (감사 로그)
├── UserRole.java # 역할 Enum (USER, ADMIN, SYSTEM)
├── AuthUserStatus.java # 인증 상태 Enum
└── vo/
├── AuthUserId.java # AUTH-xxxxxxxx
├── Email.java # 이메일 VO
├── Password.java # BCrypt 해시, 강도 검증
├── RefreshTokenId.java # RTK-xxxxxxxx
└── LoginHistoryId.java # LHI-xxxxxxxx
domain/account/domain/
├── exception/
│ ├── AccountErrorCode.java # 계좌 에러 코드
│ └── AccountException.java # 계좌 예외
└── model/
├── Account.java # Aggregate Root (계좌, 낙관적/비관적 락)
├── AccountType.java # 유형 Enum (CHECKING, SAVINGS, DEPOSIT)
├── AccountStatus.java # 상태 Enum (ACTIVE, DORMANT, FROZEN, CLOSED)
└── vo/
├── AccountId.java # ACC-xxxxxxxx
├── AccountNumber.java # 은행코드-상품-일련-검증 (110-xxxx-xxxx-xx)
└── Money.java # BigDecimal 래퍼, 통화 연산
domain/transaction/domain/
├── exception/
│ ├── TransactionErrorCode.java # 거래 에러 코드 (멱등성 포함)
│ └── TransactionException.java # 거래 예외
└── model/
├── Transaction.java # Aggregate Root (입출금 거래)
├── IdempotencyRecord.java # 멱등성 레코드 (IN_PROGRESS/COMPLETED/FAILED)
├── TransactionType.java # 유형 Enum (DEPOSIT, WITHDRAWAL, TRANSFER_IN 등)
├── TransactionStatus.java # 상태 Enum (PENDING, SUCCESS, FAILED, CANCELLED)
└── vo/
├── TransactionId.java # TXN-xxxxxxxx
├── IdempotencyKey.java # 클라이언트 제공 키 (8~128자, 24시간 TTL)
└── Money.java # 금액 VO
domain/transfer/domain/
├── exception/
│ ├── TransferErrorCode.java # 이체 에러 코드 (SAGA 관련)
│ └── TransferException.java # 이체 예외
└── model/
├── Transfer.java # Aggregate Root (이체, SAGA 상태 관리)
├── OutboxEvent.java # Outbox 패턴 이벤트 (트랜잭션 원자성)
├── TransferStatus.java # 상태 Enum (PENDING → COMPLETED/FAILED/CANCELLED)
├── SagaStatus.java # SAGA 단계 (DEBIT_PENDING → CREDIT_PENDING → COMPLETED)
├── OutboxStatus.java # Outbox 상태 (PENDING, PUBLISHED, FAILED)
└── vo/
├── TransferId.java # TRF-xxxxxxxx
├── OutboxEventId.java # OBX-xxxxxxxx
└── Money.java # 금액 VO
domain/card/domain/
├── exception/
│ ├── CardErrorCode.java # 카드 에러 코드 (한도 초과 등)
│ └── CardException.java # 카드 예외
└── model/
├── Card.java # Aggregate Root (카드, 한도 관리)
├── Payment.java # 결제 엔티티 (Card의 하위 엔티티)
├── CardType.java # 유형 Enum (DEBIT, CREDIT, PREPAID)
├── CardStatus.java # 상태 Enum (ACTIVE, INACTIVE, EXPIRED, BLOCKED)
├── PaymentStatus.java # 결제 상태 Enum (PENDING, APPROVED, DECLINED 등)
└── vo/
├── CardId.java # CRD-xxxxxxxx
├── CardNumber.java # 16자리 카드번호 (마스킹 지원)
├── PaymentId.java # PAY-xxxxxxxx
└── Money.java # 금액 VO
domain/ledger/domain/
├── exception/
│ ├── LedgerErrorCode.java # 원장 에러 코드
│ └── LedgerException.java # 원장 예외
└── model/
├── LedgerEntry.java # Aggregate Root (원장 기록, Append-only)
├── AuditLog.java # 감사 로그 (불변)
├── EntryType.java # 유형 Enum (DEBIT, CREDIT)
├── TransactionCategory.java # 카테고리 Enum (DEPOSIT, WITHDRAWAL, TRANSFER 등)
└── vo/
├── LedgerEntryId.java # LED-xxxxxxxx
├── AuditLogId.java # AUD-xxxxxxxx
└── Money.java # 금액 VO
모든 VO는 record로 구현하여 불변성을 보장하고, 생성자에서 자가 검증을 수행합니다.
public record AccountId(String value) {
public static final String PREFIX = "ACC";
public AccountId {
if (!isValid(value)) {
throw AccountException.invalidAccountIdFormat(value);
}
}
public static AccountId generate() {
return new AccountId(PREFIX + "-" + UuidUtils.generateShortId());
}
}Aggregate 내부 상태는 반드시 Root를 통해서만 변경되며, 비즈니스 메서드가 도메인 행위를 캡슐화합니다.
public class Account {
public void withdraw(Money amount) {
validateActive();
validateSufficientBalance(amount);
this.balance = this.balance.subtract(amount);
}
private void validateSufficientBalance(Money amount) {
if (this.balance.isLessThan(amount)) {
throw AccountException.insufficientBalance(this.balance, amount);
}
}
}상태 Enum이 비즈니스 규칙과 상태 전이 정책을 직접 관리합니다.
public enum TransferStatus {
PENDING("대기중", false),
COMPLETED("완료", true),
FAILED("실패", true),
CANCELLED("취소", true);
public boolean canTransitionTo(TransferStatus target) {
return switch (this) {
case PENDING -> target != PENDING;
case COMPLETED, FAILED, CANCELLED -> false;
};
}
}ErrorCode Enum과 팩토리 메서드 패턴으로 명확하고 일관된 예외 처리를 구현합니다.
public class UserException extends BusinessException {
public static UserException userNotFound(String userId) {
return new UserException(
UserErrorCode.USER_NOT_FOUND,
Map.of("userId", userId)
);
}
public static UserException emailAlreadyExists(String email) {
return new UserException(
UserErrorCode.EMAIL_ALREADY_EXISTS,
Map.of("email", email)
);
}
}| 저장소 | 설명 |
|---|---|
| config-repo | 설정 파일 저장소 |
| infrastructure | Docker Compose, 인프라 설정 |
MIT License