이커머스 결제 이벤트 스트림에서 실시간으로 피처를 뽑아 이상 거래를 스코어링하고, 임계 초과 건을 검토/보상 흐름으로 넘기는 플랫폼.
백엔드 강점(이벤트 파이프라인, Transactional Outbox, 멱등성, 보상 트랜잭션) 위에 ML 서빙 레이어를 얹은 구조다. 규칙 기반 탐지가 못 잡는 패턴을 학습 기반으로 보완한다.
이 레포는 학습·포트폴리오 목적이다. 모든 데이터는 합성이며 실제 결제 정보를 포함하지 않는다.
flowchart LR
subgraph pipeline["pipeline (Spring Boot)"]
K[결제 이벤트 Kafka] --> FC[실시간 피처 계산]
FC --> CALL[스코어 요청]
CALL --> DEC{스코어 >= 임계값?}
DEC -->|No| PASS[정상 통과]
DEC -->|Yes| OB[(Transactional Outbox)]
OB --> RELAY[Outbox Relay]
RELAY --> REVIEW[검토/보상 큐]
end
subgraph scorer["scorer (Python FastAPI)"]
API[POST /score] --> MODEL[이상 탐지 모델]
end
CALL --> API
MODEL --> CALL
두 서비스로 분리한다. 언어 경계는 모델 파일이다.
| 서비스 | 스택 | 책임 | 대응 트랙 |
|---|---|---|---|
pipeline/ |
Spring Boot, Kafka | 이벤트 수집, 피처 계산, Outbox, 스코어 요청, 트리거 | 백엔드 · 스트리밍 |
scorer/ |
Python, FastAPI, scikit-learn | 모델 학습·평가·서빙·드리프트 감시 | ML serving · MLOps |
각 결정의 배경과 트레이드오프는 docs/decisions/에 있다.
- 001 규칙이 아니라 이상 탐지를 쓰는 이유
- 002 피처를 Kafka 스트림에서 뽑는 이유
- 003 온라인 스코어링과 배치 스코어링의 경계
- 004 스코어를 보상 트랜잭션과 연결하는 경계
- 005 모델 드리프트 감시
- 006 pipeline 첫 슬라이스 범위
# 1. 인프라 (kafka, redis, mysql)
docker compose up -d
# 2. scorer: 합성 데이터 -> 학습 -> 서빙
cd scorer
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python -m src.features --generate
python -m src.train
uvicorn src.serve:app --reload --port 8001
# 3. pipeline: Spring Boot 기동
cd ../pipeline
./gradlew bootRun-
scorer완성: features → train → evaluate → serve → batch → monitor. 실행·테스트·HTTP 계약 검증 완료 -
pipeline슬라이스 1: Kafka consumer → 피처 계산 → scorer 호출 → 판정 → Outbox. EmbeddedKafka 통합 테스트 2건 통과 (JDK 21, Gradle 8.10.2, BUILD SUCCESSFUL) -
pipeline슬라이스 2: 인메모리 seam을 실구현으로 교체. JPA Transactional Outbox +SELECT FOR UPDATE SKIP LOCKED(실제 MySQL 8, READ COMMITTED로 갭 락 회피), Resilience4j CircuitBreaker, Redis 피처 상태(실제 Redis). Testcontainers로 검증, 9 tests passed - Outbox Relay 실제 Kafka 발행 + @Scheduled 주기 배출(READY→PUBLISHED/FAILED 상태 기계), 컨슈머 poison pill 방어(DefaultErrorHandler+DLT), 피처 누적 eventId 멱등. Testcontainers/EmbeddedKafka로 검증
- 관측성: actuator + micrometer로
/actuator/prometheus노출. 판정 분포·이상 스코어 분포·scorer 호출 지연·Outbox 발행 결과·DLT park를 도메인 메트릭으로 심고, resilience4j 서킷브레이커 상태를 함께 노출. 실제 Testcontainers/EmbeddedKafka 흐름에서 카운터·타이머 증가를 단언 - 각 단계
docs/decisions/결정 기록 → 블로그 초안 2편(정확도 함정 · SKIP LOCKED 검증)
프로파일: 기본(운영)은 JPA + Redis 구현, inmemory 프로파일은 인메모리 구현(배선 테스트용).
검증 절차와 실측 결과는 VERIFICATION.md 참고.
- 평가: PR-AUC 0.829, 임계값을 올릴수록 precision↑ recall↓ 트레이드오프 확인
- 배치 스코어링: 5000건 → PASS 4714 / REVIEW 128 / HOLD 158
- 드리프트: 동일 분포는 오탐 0, drift=1.0 데이터는 6개 중 5개 피처를 DRIFT로 감지 후 재학습 권장
- 테스트: 6 passed
Java · Spring Boot · Spring Kafka · Spring Data JPA · MySQL · Redis · Resilience4j · Micrometer/Prometheus Python · FastAPI · scikit-learn · pandas
pipeline은 spring-boot-starter-actuator + micrometer-registry-prometheus로 /actuator/prometheus를 스크레이프 포맷으로 연다. 심은 도메인 메트릭은 다음과 같다(태그는 전부 유한 집합만 쓴다. eventId 같은 고유값은 카디널리티 폭발을 막기 위해 태그로 넣지 않는다).
| 메트릭 | 타입 | 태그 | 의미 |
|---|---|---|---|
payment_decision_total |
Counter | decision=PASS/REVIEW/HOLD |
판정 분포(트리거 건수) |
payment_anomaly_score |
DistributionSummary | (SLO 버킷 0.5·0.8) | 이상 스코어 분포. 판정 임계에 버킷을 맞춰 검토·홀드 구간 비율을 바로 읽는다. NaN(scorer 불가)은 제외 |
payment_scorer_call_seconds |
Timer | outcome=success/error |
scorer 호출 지연·오류율. SLO 버킷을 RestClient 타임아웃(800ms)에 맞춤 |
payment_outbox_publish_total |
Counter | result=success/retry/failed |
Outbox 발행 상태 기계 결과 |
payment_dlt_park_total |
Counter | reason=deserialization/listener |
컨슈머 poison pill DLT park |
resilience4j_circuitbreaker_state |
Gauge | name=scorer |
서킷 상태(resilience4j-micrometer 자동 노출) |
알려진 태그 조합은 시작 시 0으로 선등록해, 대시보드가 "데이터 없음"과 "값이 0"을 구분할 수 있게 한다. Kafka · Docker · GitHub Actions