← 홈으로

손찬양 · 백엔드 문제 해결 사례

기업용 플랫폼의 운영 문제를 상태와 데이터 흐름에서 추적하고 서버 로직을 개선했습니다.

회사에서 해결한 문제를 먼저 소개합니다. 개인 프로젝트는 아래에서 코드와 테스트를 함께 볼 수 있습니다.

다른 실무 사례 · 구매 트랜잭션 · RabbitMQ 요청·결과 분리 · 복합 식별자

경력·이력서 · 경력기술서 PDF · 전체 개인 프로젝트

실무에서 해결한 문제

근거 안내 · 실무 사례는 당시 코드·테스트 정의·저장된 실행 기록을 기준으로 작성했습니다. 회사 테스트를 현재 환경에서 다시 실행한 결과는 아닙니다. 운영 개선 수치와 실행 결과가 없는 항목은 각 사례에 표시했습니다.

이엠캐스트 · B2B 리테일 교육 플랫폼 · 백엔드 개발·운영

B2B 리테일 교육 플랫폼의 백엔드 개발·운영 중 발생한 문제와 수정 내용을 정리했습니다. 회사 코드와 고객 정보는 공개하지 않습니다.

저장 직전에 바뀐 상태를 다시 검증하기

설정 화면을 연 뒤 관련 작업이 시작되면, 이전 상태를 기준으로 저장하면서 기존 데이터와 변경된 처리 기준이 달라질 수 있었습니다.

담당 범위
서버 재검증·변경 차단·후속 갱신 조건 수정과 회귀 테스트.
문제·원인
화면 진입 시점에만 확인한 상태를 저장 단계가 그대로 사용했습니다. 변경 적용 전의 서버 조회와 후속 데이터 갱신 조건을 함께 확인했습니다.
판단·수정
저장 직전 현재 이력을 다시 조회하고, 화면 진입 후 작업이 시작됐다면 변경을 차단했습니다. 입력된 이전 상태가 없으면 요청을 거절하고 후속 데이터 갱신도 같은 조회 기준으로 통일했습니다.
테스트·확인 내용
상태가 유지된 경우, 저장 전 상태가 바뀐 경우, 기존 이력이 있는 경우, 필수 입력이 누락된 경우를 단위 테스트로 확인했습니다.
테스트 범위
저장 전 상태를 다시 확인하는 수정입니다. 상태 조회와 저장 사이의 동시 변경까지 DB에서 원자적으로 차단하는 방식은 아닙니다.

구매 API의 락 경합과 커밋 경계 조정

동시 구매 요청에서 주문 저장 중 DB 데드락이 발생했습니다. 결제 처리를 별도 트랜잭션으로 옮긴 뒤에는 앞 단계의 미커밋 상태를 읽어 결제가 실패하는 문제도 드러났습니다.

담당 범위
구매 API의 분산락 적용과 트랜잭션 범위 조정에 참여했습니다.
문제·원인
주문 저장·컬렉션 갱신의 락 경합과, 결제 요청 상태를 바꾸는 트랜잭션의 커밋 시점을 함께 확인했습니다. 별도 트랜잭션 도입만으로 호출 전체의 정합성이 보장되지는 않았습니다.
판단·수정
주문 단위 분산락과 별도 결제 트랜잭션을 적용했습니다. 결제 요청 상태를 먼저 커밋하도록 호출 경계를 조정하고, 락 관련 오류만 제한적으로 재시도했습니다. 복구 시 이미 결제된 주문은 완료 상태를 확인해 반환했습니다.
테스트·확인 내용
동시 요청·락 재시도·결제 완료 주문의 재처리 테스트가 있습니다. 초기 QA에서 결제 요청 상태를 읽지 못하는 문제가 드러나 커밋 순서를 수정했습니다.
테스트 범위
최종 테스트 실행 결과와 운영 데드락 재발률은 현재 자료에서 확인되지 않았습니다.

여러 서버에서 같은 배치가 실행되는 문제

Spring 스케줄러가 서버마다 실행되면서 동일 배치가 두 번 처리됐습니다. 업무 기록에는 수정 전 API 서버를 한 대로 제한한 임시 대응이 남아 있습니다.

배치 실행 제어와 예정 목록 기반 모니터링
  1. 서버별 스케줄 실행
  2. ShedLock·Redis 락 획득
  3. 배치 실행·발행 기록
  4. 처리 결과·상태 기록
  1. 예정 고객사·배치 목록
  2. 예정 시각 1시간 뒤 기록 대조
  3. 성공·실패·락 미획득·발행 누락·결과 미수신 구분
  4. Slack 요약

분산락 도입과 모니터링은 별도 시기에 개선했습니다. 정상 결과도 요약에 포함합니다.

담당 범위
분산락 적용과 배치 모니터링을 담당했습니다. 실행 상태·처리 건수·실행 시간을 기록하고 어떤 고객사의 어떤 작업에 문제가 있는지 식별하도록 했습니다.
문제·원인
각 서버의 스케줄러가 별도로 실행돼 같은 배치를 중복 처리했습니다. 서버 간 공유 락이 필요했습니다.
판단·수정
ShedLock과 Redis 기반 락 공급자를 적용했습니다. 배치별 락 이름과 유지 시간을 설정했습니다. 이후 별도 개선으로 고객사·배치별 성공·실패·락 미획득·발행 누락·처리 결과 없음을 집계하고, 예정 시각 1시간 뒤 Slack 요약을 보내도록 구현했습니다.
테스트·확인 내용
서버 로그와 DB 기록으로 배치 실행 결과를 확인했습니다. 모니터링 코드에는 예정된 발행 횟수와 실제 기록을 비교하는 로직, 시간대별 집계와 관련 테스트가 있습니다.
테스트 범위
락 유지 시간을 넘긴 작업과 장애 복구 중의 중복 실행까지 보장하지 않습니다. 전후 중복 횟수는 당시 로그 원본이 없어 수치로 제시하지 않습니다.

같은 사용자라도 서로 다른 행으로 처리하기

같은 사용자에게 여러 그룹 데이터가 있는데 사용자 ID만으로 구분해, 선택한 삭제 항목 일부가 누락됐습니다. 조회 제외 조건과 페이지 합계도 실제 행 수와 어긋났습니다.

담당 범위
복합 필터 모델, 서버 조회·삭제와 동등성 수정, 페이지 경계·다중 그룹 테스트.
문제·원인
사용자 ID만 비교하는 컬렉션 동등성이 다른 그룹을 같은 항목으로 취급했습니다. DB 조회 후의 필터링과 근사 합계가 페이지 경계에서 또 다른 불일치를 만들었습니다.
판단·수정
사용자 ID와 그룹 유형을 조합한 식별자를 조회·삭제·동등성에 적용했습니다. 제외 조건을 QueryDSL WHERE 절로 옮겨 조회와 집계 기준을 맞췄습니다. 많은 제외 항목은 POST 본문으로 전달하고 기존 단일 유형 조회는 유지했습니다.
테스트·확인 내용
같은 사용자의 서로 다른 그룹이 Set에 남는지, 선택한 조합만 삭제되는지, 그룹 제외 조건과 페이지 총건수가 맞는지 테스트했습니다.
테스트 범위
중복 요청 제한의 추가 통합 테스트와 운영 성능 측정은 포함하지 않았습니다.

발송은 됐는데 실패로 기록되는 문제

대량 발송이 동기 RPC의 응답 제한을 넘으면서 실제 수신과 관리자 화면의 실패 이력이 어긋났습니다. 발송 요청 접수와 최종 결과를 구분해야 했습니다.

담당 범위
메시징 타임아웃 대응에서 동기 응답 대기를 없애고 요청 접수와 결과 처리를 분리하는 작업에 참여했습니다.
문제·원인
기존 발송 호출은 RPC 응답을 기다리고 응답이 없으면 실패로 처리했습니다. 업무 기록의 대량 발송 지연과 당시 5초 응답 제한을 대조했습니다.
판단·수정
발송 요청을 RabbitMQ 큐에 비동기로 보내고 대량 요청은 HTTP 202로 접수 응답을 반환하도록 바꿨습니다. 별도 결과 큐 리스너가 최종 이력 갱신 큐로 전달하며, 처리 예외는 제한 재시도 후 DLQ로 분리합니다.
테스트·확인 내용
변경 코드와 업무 완료 기록에서 RPC 호출 제거, 비동기 발행, HTTP 202 응답, 결과 리스너와 재시도·DLQ 설정을 확인했습니다.
테스트 범위
HTTP 202는 요청 접수 응답입니다. 채널별 최종 발송·DB 저장, 중복 처리 방지와 DLQ 재처리의 실행 결과는 현재 자료에서 확인되지 않았습니다.

개인 프로젝트 · 공개 구현

직접 설계·구현한 프로젝트입니다. 상세에서 기술 선택, 코드와 테스트 조건을 확인할 수 있습니다.

StockRush

주문 상태 전이와 Transactional Outbox, 실패 복구

AI Gateway

LLM 요청 정책, 캐시와 외부 모델 장애 처리

테스트 결과와 환경은 각 프로젝트 상세에 표시했습니다.

결제 지연·이벤트 중복을 처리하는 주문 서비스

한정 재고 주문에서 결제 지연·이벤트 중복·Kafka 중단에 대응하도록 Saga, Outbox와 중복 처리를 막는 소비자를 구현했습니다.

시스템 흐름 처리 순서와 오류 대응
Client 01
Gateway 02
Order Saga 03
Inventory / Payment 04
Outbox / Kafka 05
Read Model 06
멱등 소비 동일 이벤트 2회 → Outbox 1건

처리 완료 이벤트 키로 중복 차단

종료 상태 조건 늦은 승인 → 무시

취소 주문 상태 유지

발행 재시도 5회 실패 → FAILED

실패 상태와 재처리 경로 보존

해결한 문제

재시도 뒤 주문·재고·결제·조회 상태가 서로 어긋남

동시 주문, 결제 실패와 지연, Kafka 중단, 이벤트 재처리 상황에서는 주문 상태와 재고 수량, 결제 결과, 조회 모델이 서로 다른 속도로 바뀝니다. 서비스별 상태 변경과 실패 처리 로직을 구현하고 주문의 최종 상태를 테스트했습니다.

주요 설계

Saga 상태 조율

재고·결제·쿠폰·출고 상태 전이를 주문 서비스에서 조율

신뢰할 수 있는 이벤트 발행

DB 변경과 Outbox 행을 함께 저장해 발행 실패를 재처리

주요 코드

PersistentCreateOrderService.create 파일services/order-service/src/main/java/com/stockrush/order/application/PersistentCreateOrderService.java
boolean saved = orderRepository.saveIfAbsent(result.order(), command.idempotencyKey());
if (!saved) {
    return CreateOrderResult.replayed(replayOrder(command.idempotencyKey(), command.memberId()));
}
outboxEventRepository.save(result.outboxEvent());

처리 조건주문 저장과 Outbox 기록을 같은 트랜잭션에 두고 같은 요청이 다시 들어오면 기존 주문을 조회해 반환합니다.

관련 테스트PersistentCreateOrderServiceIntegrationTest.persists_order_items_and_pending_outbox_event_together

테스트 결과

  • OrderSagaEventHandlerIntegrationTest가 동일 이벤트를 두 번 처리해도 Outbox가 1건만 남는지 확인합니다.
  • 취소 완료 뒤 늦게 도착한 결제 승인 이벤트가 주문 상태를 바꾸지 않는지 테스트했습니다.
테스트 환경·제한

코드와 통합 테스트로 중복 이벤트 처리, 취소 상태 유지와 발행 재시도를 확인했습니다. 실제 결제 연동, 다중 리전, 운영 규모 부하와 발행기의 고가용성은 테스트하지 않았습니다.

보상 중복 지급·쿠폰 초과 발급·포인트 중복 차감을 막는 백엔드

첫 로그인 보상 중복 지급, 쿠폰 초과 발급과 포인트 음수 잔액을 PostgreSQL 제약·잠금·조건부 갱신으로 막았습니다. Redis 락과 RabbitMQ 단일 소비자 처리 방식은 동시 요청 테스트로 비교했습니다.

전략 비교 처리 순서와 오류 대응
Scenario API 01
DB Baseline 02
Redis + DB 03
RabbitMQ Lane 04
Outbox 05
Infra Tests 06
최초 보상 동시 요청 8건 → 보상 1건

7건 거절 · 중복 0건

포인트 차감 100 - (2 × 60) → 40

1건 성공 · 음수 잔액 0건

쿠폰 용량 요청 8건 / 정원 3건

3건 발급 · 5건 거절

해결한 문제

중복 보상, 쿠폰 초과 발급, 포인트 음수 잔액

모든 회원 이벤트를 하나의 사용자 ID 잠금으로 묶으면 특정 키에 요청이 몰리고, 아무 제어 없이 처리하면 중복 보상, 쿠폰 초과 발급, 포인트 음수 잔액과 후속 이벤트 누락이 발생합니다.

주요 설계

DB에서 중복·초과 처리 차단

유니크·CHECK 제약, 조건부 갱신과 행 잠금 적용

보상·쿠폰·포인트별 잠금

회원·캠페인별 Redis 잠금을 적용하고 DB 보호 조건 유지

주요 코드

SqlCouponCampaignRepository.issueWithCapacityGuard 파일backend/src/main/java/com/example/consistency/coupon/SqlCouponCampaignRepository.java
where id = ?
  and status = 'ACTIVE'
  and issued_count < capacity
for update

처리 조건캠페인 행 잠금과 용량 조건을 한 SQL 흐름에서 판정해 동시 요청의 초과 발급을 차단합니다.

관련 테스트SqlCouponCampaignRepositoryTest.issueWithCapacityGuardUsesSingleStatementWithCampaignRowLock

테스트 결과

  • FirstLoginRewardDbConcurrencyIT는 동시 요청 8건 중 1건 지급·7건 거절을 테스트합니다.
  • PointSpendDbConcurrencyIT는 잔액 100에서 60 차감 2건 중 1건만 성공해 잔액 40을 유지합니다.
  • 정원 3건을 초과한 쿠폰은 발급되지 않았습니다.
테스트 환경·제한

PostgreSQL·Redis·RabbitMQ Testcontainers를 사용했습니다. RabbitMQ는 Spring 인스턴스 1개·소비자 1개로 테스트했습니다. Redis 락 장애, 장시간 Redis·RabbitMQ 장애 복구, 운영 규모 부하·SLO와 방식별 성능 비교는 포함하지 않았습니다.

문서 접근 권한을 확인하고 근거가 없으면 답변을 거절하는 RAG

권한 조건을 적용한 검색 결과로 모델 입력과 출처를 구성했습니다. 허용된 근거가 없으면 모델을 호출하지 않고 거절 응답을 반환합니다.

시스템 흐름 처리 순서와 오류 대응
React Console 01
FastAPI 02
Policy Guard 03
Vector Search 04
Answer Policy 05
Eval Trace 06
권한 검색 문서 5건 → 허용 3건

워크스페이스·소유자·부서 권한 선필터

답변 거절 근거 없음 → 답변 생성 안 함

빈 인용과 거절 사유 반환

출처 범위 권한 밖 인용 0건

권한 확인을 거친 검색 결과로 구성

해결한 문제

비인가 문서 노출과 근거 없는 답변 생성

기업 내부 정책 RAG는 검색 품질만으로 부족합니다. 사용자가 볼 수 없는 문서가 검색 후보에 섞이지 않아야 하고, 답변에는 근거가 남아야 하며, 근거가 부족한 경우 거절 응답을 반환해야 합니다.

주요 설계

권한 검사 우선

답변 생성 전에 접근 가능한 문단만 검색 후보로 제한

근거 기반 거절

인용 근거가 없으면 답변과 인용을 비운 거절 응답을 반환

주요 코드

RetrievalService.retrieve 파일app/retrieval.py
access_reason = _access_reason(chunk, query)
if access_reason is None:
    continue

처리 조건저장소 필터 뒤에도 워크스페이스·소유자·부서 가시성을 다시 판정해 비인가 결과를 제거합니다.

관련 테스트test_retrieval_allows_public_owner_and_department_documents_only

테스트 결과

  • 권한 검색 테스트에서 문서 5개 중 허용 범위 3개만 반환하고 다른 workspace의 문서는 반환하지 않는지 확인합니다.
  • 근거가 없으면 answer=null, citations=[]와 insufficient_evidence 사유를 반환합니다.
테스트 환경·제한

기본 테스트는 메모리 저장소와 테스트 모델을 사용합니다. PostgreSQL 권한 검색은 별도 통합 테스트로 구성했습니다. 공개 데모는 고정 데이터 화면입니다. 사내 인증·운영 OIDC, 관리형 PostgreSQL·pgvector, 실제 OpenAI 호출, 배포 확인과 답변 사실성 자동 검증은 포함하지 않았습니다.

요청 예산을 확인하고 외부 모델 오류를 처리하는 LLM 게이트웨이

서비스마다 흩어진 LLM 인증·예산·캐시·라우팅·복구 정책을 OpenAI 호환 WebFlux 게이트웨이의 한 요청 처리 순서로 통합했습니다.

사내 문서 검색, 상담 챗봇, 요약 기능처럼 여러 애플리케이션에서 LLM을 호출하는 사용 시나리오를 가정할 수 있으며, 이는 실제 운영 사례가 아닙니다.

요청 흐름: Auth → Quota → Guardrail → Cache → Router → Dispatch → Fallback → Guardrail → Record

시스템 흐름 처리 순서와 오류 대응
App API 01
Router 02
Cache 03
Fallback 04
Provider Port 05
Trace 06
정책 적용 순서 호출 전 할당량 검사

인증→할당량→검사→캐시→라우팅→복구→기록

라우팅 정책 캐시와 대체 모델 정책

응답 방식과 대상 선택 정책을 분리

응답 인터페이스 JSON + SSE

일괄·스트리밍·도구 호출

해결한 문제

애플리케이션마다 중복되는 LLM 정책과 외부 모델 장애 처리

LLM 기능을 여러 애플리케이션에 붙이면 외부 모델 SDK, 장애 대응, 토큰·비용 예산, 캐시, 처리 조건과 요청 기록이 서비스마다 따로 구현됩니다. 게이트웨이가 공통 정책과 요청 기록을 처리하도록 구성했습니다.

주요 설계

공통 호환 API

OpenAI 호환 /v1/chat/completions로 클라이언트 변경 최소화

고정된 정책 순서

인증·할당량·입출력 검사·캐시·라우팅·복구·기록 순서 고정

주요 코드

GatewayPipeline.execute 파일backend/src/main/java/com/example/gateway/api/GatewayPipeline.java
QuotaOutcome quota = quotaGuard.evaluate(request, estimatedTokens);
if (quota != QuotaOutcome.ALLOWED) {
    return rejected(request, mode, quota, elapsed(start));
}

처리 조건예상 토큰으로 사용자 조직의 할당량을 먼저 검사해 초과 요청을 외부 모델 호출 전에 종료합니다.

관련 테스트RequestLogStoreTest.quotaRejectPathCreatesRowWithNullProviderAndOutcome

테스트 결과

  • ChatCompletionController 테스트가 JSON 일괄 응답과 SSE 스트리밍 경로를 분리해 검증합니다.
  • 인증 누락 401, 잘못된 요청 400과 할당량·예산 제한을 API 테스트로 확인합니다.
  • 재시도 예산이 소진되면 다음 모델 호출을 차단하고, 할당량 초과 요청은 모델을 호출하지 않고 거절했습니다.
테스트 환경·제한

고정 응답 모델과 메모리 저장소로 테스트했습니다. 실제 OpenAI·Anthropic 호출, 운영 Redis·PostgreSQL, 클라우드 배포와 부하 테스트는 포함하지 않았습니다.

CDC 이벤트 수집·저장·재처리를 구현한 프로토타입

CDC 실행 환경, 제어 API와 레이크하우스를 각각 구현했습니다. 같은 DB 변경 이벤트의 중복 저장을 막고, 실패 원인과 재처리 상태를 원본 LSN·오프셋과 함께 기록했습니다.

시스템 흐름 처리 순서와 오류 대응
PostgreSQL 01
Debezium 02
Kafka Raw 03
Object Sink 04
Iceberg 05
Lineage 06
중복 억제 동일 이벤트 → 이력 1건

PostgreSQL 처리 이력 테스트

원천 정보 보존 LSN · 오프셋 · 이벤트 ID

로컬 수집과 변환 단계별 확인

실패 재처리 DLQ · 재처리 · 재시도

복구 상태와 원천 정보 추적

해결한 문제

중복 CDC, 복구 이력 손실과 자동 연결되지 않은 구간의 종단 전달

CDC 이벤트는 커넥터 재시작과 재전달로 중복될 수 있고 적재 실패 뒤 복구 이력이 사라질 수 있습니다. 수집·제어·적재 구간이 자동 연결되지 않아 종단 전달과 재처리를 보장할 수 없는 범위도 명시했습니다.

주요 설계

원천 메타데이터 기반 이벤트 ID

수집기·스키마·테이블·기본 키·LSN·변경 종류로 ID 생성, 오프셋 별도 보존

복구 상태 분리

원본·표준 이벤트·재시도·DLQ·재처리 상태를 분리해 원인과 복구를 추적

주요 코드

CanonicalIngestService.ingestRawEnvelope 파일backend/src/main/java/com/example/cdcplatform/event/CanonicalIngestService.java
DebeziumEnvelope envelope = DebeziumEnvelope.parse(rawEnvelopeJson);
CanonicalCdcEvent event = CanonicalCdcEvent.fromEnvelope(envelope);
String sourceOffsetJson = sourceOffsetJson(envelope);
boolean created = canonicalEventPublisher.recordCanonicalEvent(event, sourceOffsetJson);

처리 조건Debezium 원본을 표준 이벤트로 정규화하면서 원천 오프셋을 보존하고 처리 장부 결과로 중복을 판정합니다.

관련 테스트CanonicalIngestServiceTest.duplicateRawEnvelopeDoesNotIncreaseCanonicalEventCount

테스트 결과

  • CdcEventLedgerRepositoryTest에서 동일 이벤트를 두 번 등록하면 첫 등록만 성공하고 처리 이력은 1건입니다.
  • CanonicalIngestServiceTest는 테스트용 발행기로 LSN·오프셋 보존과 중복 응답을 확인합니다. 실제 DB 테스트와는 별도입니다.
테스트 환경·제한

CDC 실행 환경, 제어 API와 레이크하우스를 각각 테스트했습니다. 세 구성요소를 연결한 자동 수집·적재는 구현하지 않았습니다. Kafka 토픽 상시 소비, 장기간 Kafka Connect 실행, AWS S3·Athena·Trino, 운영 규모 부하와 다중 리전은 테스트하지 않았습니다.

실패 이벤트를 별도 처리하고 추천 배치 결과를 기록하는 백엔드

중복·실패 행동 이벤트가 사용자 프로필을 오염시키지 않도록 멱등 처리와 DLQ를 적용하고, 추천 점수와 스냅샷 최신성을 함께 기록했습니다.

시스템 흐름 처리 순서와 오류 대응
Catalog 01
Event Processor 02
Profile 03
Ranker 04
Snapshot 05
Admin Report 06
실패 이벤트 3회 실패 → DLQ

새 멱등 키로 재처리

추천 고정 입력 상품 6개

브랜드·사이즈·가격·재고 점수

배치 스냅샷 2개 이상 · 미반영 0개

활성 사용자 최신성 확인

해결한 문제

중복·실패 이벤트가 프로필과 추천 스냅샷을 오염함

패션 커머스 추천은 브랜드, 카테고리, 태그, 사이즈, 가격대와 재고 같은 상품 정보에 사용자 행동 이벤트를 함께 반영해야 합니다. 중복 이벤트나 처리 실패가 프로필을 오염시키지 않도록 이벤트 처리, 배치 스냅샷과 관리자 리포트를 분리했습니다.

주요 설계

외부 의존성 없는 핵심 로직

외부 설치 없이 도메인·서비스 흐름을 검증

실패 이벤트 격리

멱등 키·재시도 예산·DLQ·재처리로 오염 차단

주요 코드

apply_event_to_profile 파일src/fashion_personalization/recommendation.py
selected_size = event.payload.get("size")
if isinstance(selected_size, str) and selected_size in product.sizes:
    _add_weight(profile.size_preferences, selected_size, max(weight, 0.1))

처리 조건선택한 값이 실제 상품 옵션에 있을 때만 사이즈 선호에 반영해 잘못된 프로필 신호를 막습니다.

관련 테스트test_size_preference_contributes_to_ranking

테스트 결과

  • EventProcessor 테스트가 동일 idempotency key를 한 번만 반영하고 3회 실패 뒤 DLQ로 격리합니다.
  • 추천 테스트가 고정 상품 6개로 브랜드·사이즈·가격·재고 조건을 추천 순위에 반영하는지 확인합니다.
  • 배치 스냅샷 2개 이상에서 반영 이벤트 수가 부족한 스냅샷은 0개였습니다.
테스트 환경·제한

단일 프로세스의 메모리 저장소를 사용했습니다. HTTP 이벤트 워커, 외부 브로커, Outbox와 자동 배치 스케줄러는 구현하지 않았습니다. PostgreSQL·SQS·EventBridge·S3 연동, AWS 배포와 실제 ML·A/B 성과는 테스트 범위에 포함하지 않았습니다.