BE-commerce: 결제 실패를 정합하게 되돌리는 모듈형 모놀리스
BE-commerce는 주문부터 결제, 원장, 정산·대사, 추천까지를 하나의 Spring Modulith 애플리케이션으로 구현한 이커머스 백엔드입니다. 이 시리즈에서 다루는 질문은 “기능을 어떻게 만들었나”보다 실패했을 때 무엇을 확정하고, 무엇을 모르는 상태로 남기며, 어떤 숫자를 측정한 뒤 선택했는가에 가깝습니다.
예를 들어 결제 타임아웃을 실패로 확정하면 이중 결제가 생길 수 있고, 요청 제한을 초당 100건으로 둔다고 해서 서버 용량이 100건/초가 되지는 않습니다. 그래서 각 글은 문제를 정의하고, 가설을 세우고, 실험 결과와 적용 범위를 분리해 적습니다.
프로젝트 소개와 실행 방법은 저장소 README에 있습니다. 아래 지도에서 영역을 고르면 해당 영역의 글만 볼 수 있습니다. 처음 읽는다면 유입·인증에서 인증 비용과 요청 제한을 읽은 뒤, 결제와 정산·대사, 개인화 순서로 이동하면 시스템의 경계가 자연스럽게 이어집니다.
영역별 글
핵심 ERD 펼치기 핵심 ERD 접기
유입·인증
로그인, 사용자별·전역 요청 제한, 대기열로 몰려드는 요청을 먼저 받습니다. 비밀번호 해시 강도를 올릴 때 로그인 한 건의 메모리 비용이 얼마나 커지는지도 여기서 쟀습니다.
저장소 문서: ADR-009 비밀번호 해시와 이관 경로
비밀번호 해시를 Argon2id로 바꿨더니 로그인 메모리가 문제가 됐다
Argon2id는 BCrypt보다 메모리를 많이 써 동시 로그인 100건에 약 2GB를 요구했다. 512MB 힙에서는 OOM이 발생했고, 인코더 테스트를 통과한 뒤 해시 컬럼 길이 문제도 발견했다. 비밀번호 해시 교체에는 보안 강도뿐 아니라 동시성 예산과 이관 종료 조건이 필요했다.
초당 100건은 서버 용량이 아니다: 결제 API 요청 제한을 다시 측정한 이유
결제 API 앞 Redis 필터에 사용자별 5건/초, 전역 100건/초 제한을 두었다. 하지만 단일 계정과 닫힌 루프 부하는 전역 용량을 측정하지 못했다. 개방 루프에서 100~120건/초의 성능 꺾임을 확인하고, 요청률 제한과 과부하 보호를 구분한 과정을 정리한다.
재고가 0인데도 대기열을 닫지 못했다: 매진 판정의 조건을 다시 세운 이유
재고 100개 상품에 300명이 몰린 부하에서 대기열은 판매 성공률을 높였지만, 매진 뒤 결제까지 갔다가 거절되는 헛걸음은 140건으로 줄지 않았다. 재고 0과 결제 결과 미확정 예약을 나눠 측정한 결과를 정리한다.
스토어프론트
홈, 카테고리, 검색, 상품 상세, 장바구니, 주문 화면이 결제와 같은 API 위에 있습니다. 결제 응답이 불확실하면 화면에도 그 상태를 그대로 드러내고 주문 조회로 확정합니다.
저장소 문서: 스토어프론트 설계
검색 엔진을 넣을지 지연으로 먼저 재 봤다: 지연 축은 답했고 품질 축은 남았다
상품 목록 페이지 지연의 82.5%가 패싯 집계였다. 검색 엔진 도입 기준을 측정 전에 정해 두고 쟀더니 300ms를 넘긴 원인은 집계가 아니라 포화였고 패싯을 60초 캐시하자 패싯 p95가 90.9ms에서 5.7ms로 줄어 엔진을 검토할 조건이 사라졌다. 그런데 이커머스가 검색 엔진을 들이는 큰 이유는 지연이 아니라 품질이고 그 축은 아직 재지 않았다.
재고 차감 락을 인메모리 DB로 고르면 틀린다: 실 MySQL에서 낙관적 락이 가장 느렸다
재고 20개에 스레드 30개가 동시에 차감하는 실험에서 세 락 전략 모두 초과판매는 0이었고 소요 시간만 갈렸다. H2 인메모리에서는 낙관적 락이 비관적 락보다 빨랐는데 실 MySQL 8.4에서는 조건부 UPDATE 40ms, 비관적 락 75ms, 낙관적 락 151ms로 순서가 뒤집혔다. 고경합 150스레드에서는 조건부 대 낙관적이 79ms 대 429ms였다.
재고 동시성은 조건부 UPDATE를 고른 뒤에도 끝나지 않았다
재고 20개에 스레드 30개를 동시에 앞세워 세 가지 차감 전략을 실 MySQL에서 쟀습니다. 인메모리에서 앞섰던 낙관적 락의 순위가 실 DB에서 뒤집혀 조건부 UPDATE를 골랐습니다. 그 뒤 재고를 잡는 시점이 다시 바뀌었고 한 행이 버티는 처리량 상한과 느슨한 카운터와의 교환도 다시 쟀습니다.
검색 품질을 쟀더니 MySQL 이 따라오지 못했다: Lucene, Elasticsearch, OpenSearch 중 앱 안 Lucene 을 고른 이유
오타와 어형 변화가 섞인 검색어에서 nDCG@10이 지금 검색(LIKE) 0.071, MySQL FULLTEXT 0.324, Lucene 0.808, Elasticsearch 0.814, OpenSearch 0.798이었다. 측정 전에 정한 기준대로 엔진을 넣었고 세 엔진의 품질 차이가 0.02 안이라 운영 비용으로 앱 안 Lucene을 골랐다. 1차에서는 오타 허용이 blazer를 lazer에 밀리게 했다.
검색어와 필터를 함께 걸고 앱 안 Lucene이 몇 건까지 버티는지 쟀다
검색어가 있으면 카테고리·가격·재고 필터가 모두 무시되던 검색에 필터와 패싯을 붙였다. 엔진 안에서 걸면 결과 수가 정답과 171개 쿼리 모두 같았고 상위 500개를 받아 DB에서 거르면 95.5%를 잃었다. 복제 카탈로그로 규모를 늘리자 앱 안 Lucene은 300만 건에서 p95 232ms로 지연 예산을 넘었다.
결제
승인과 취소, 원장, 지갑과 포인트, 에스크로를 처리합니다. PG 응답이 오지 않은 결제는 성공이나 실패로 확정하지 않고 UNKNOWN으로 남긴 뒤 조회로 복구합니다. 이 저장소의 코어입니다.
저장소 문서: 결제 도메인 핵심 개념
결제 API는 타임아웃됐는데, 카드 승인은 이미 됐다면?
결제의 실패와 모름을 구분하고 조회·보상·웹훅으로 정합성을 복구한 과정. 타임아웃은 UNKNOWN으로 남겨 복구 배치가 확정한다. 승인 뒤 내부 실패는 보상으로 되돌리고, failover는 요청이 PG에 닿지 못한 경우로 한정한다.
멱등키 15일을 24시간으로 줄이려다 멈춘 이유: 외부 문서보다 호출하는 PG의 계약이 먼저였다
Stripe 문서만 보고 결제 멱등키 보관 기간을 15일에서 24시간으로 줄이려다, 실제로 사용하는 토스페이먼츠의 계약이 15일이라는 사실을 확인했다. 반대로 PG 호출을 트랜잭션 밖으로 빼는 판단은 장애 사례와 커넥션 점유를 다시 확인한 뒤 사가로 전환했다.
승인 응답보다 웹훅이 먼저 오면 결제 상태를 어떻게 보존할까
자체 목 PG가 승인 응답 뒤에만 웹훅을 보내도록 만들어 선행 웹훅 경로를 놓쳤다. 결제 행이 아직 없을 때 예외를 던지면 보류 상태까지 롤백되는 문제를 재현하고, 먼저 온 웹훅을 보류 후 재처리하는 구조로 바꾼 과정을 정리한다.
가상계좌에서 결제 완료를 최종 상태로 믿으면 안 되는 이유
토스 문서에 따르면 가상계좌는 입금 기한이 지나도 웹훅이 오지 않고 일부 은행은 입금 완료를 보낸 뒤 최대 2분 뒤에 되돌립니다. BE-commerce는 그 두 자리를 상태머신과 만료 배치로 막았지만 입금 웹훅을 그 상태머신에 잇는 연결은 아직 없습니다. 만든 것과 이어지지 않은 것을 코드 기준으로 나눴습니다.
결제 재시도는 가용성을 높이는가, 장애를 증폭하는가
승인 재시도는 이중 결제를 만들 수 있고, 조회 재시도는 retry storm을 만들 수 있다. BE-commerce에서 결과 확정 여부를 기준으로 재시도 경계를 나누고, 재시도 횟수보다 장애 중 호출량과 정상 API 영향을 측정하는 이유를 정리했다.
결제 오케스트레이션은 장애를 줄이는가, 복잡성을 옮기는가
배민은 카드 결제에 PG 세 곳을 붙이고 Adyen은 오케스트레이션이 복잡성을 없애지 않는다고 씁니다. BE-commerce에는 failover 라우팅 코드와 규칙 테스트가 있지만 기본값은 꺼져 있고 경로는 가짜 PG 둘입니다. 무엇을 코드로 강제했고 무엇을 검증하지 못했는지 나눠 적었습니다.
결제 원장의 Hot Entity: 정합성을 지키면서 처리량을 얻는 법
원장에 강한 일관성을 적용해도 특정 계정과 판매자에 쓰기가 몰리면 처리량이 떨어진다. append-only 원장, 잔액 스냅샷, 직렬화 배치 사이에서 무엇을 측정하고 언제 선택해야 하는지 정리했다.
PG가 느려지면 웹훅까지 줄을 선다: 동시 호출 상한의 비용은 식으로 나온다
PG 응답이 5초로 느려지자 PG를 부르지도 않는 웹훅 수신 API의 p95가 13초가 됐고 26.1%가 토스의 10초 규약을 넘겼다. PG 동시 호출 상한 40은 이것을 9ms로 되돌리는 대신 결제의 84%를 거절한다. 그 84%는 실험 전에 리틀의 법칙으로 계산되는 값이었다.
쌓인 미확정 결제는 언제 풀리는가: 복구 시간의 하한과 상한, 그리고 알림을 나이에 거는 이유
PG 타임아웃으로 쌓인 미확정 결제 887건이 전부 확정되기까지의 시간은 복구 배치의 속도가 아니라 코드 상수 1분이 정했다. 청크를 줄이자 그때부터는 청크 ÷ 주기가 정했다. 두 식으로 기본 설정에서 1,351건은 약 3.7분, 1만 건은 약 20분이 걸린다고 미리 계산된다. 같은 이유로 적체 알림은 건수가 아니라 가장 오래된 건의 나이에 건다.
재전송을 켰더니 느린 PG에서 결제 실패가 늘었다
PG 응답이 끊긴 승인을 같은 멱등키로 한 번 더 보내게 했다. 응답 일부를 잃는 PG에서는 확정 실패가 14.29%에서 1.90%로 줄었지만 동시 호출 상한이 찬 느린 PG에서는 31.56%에서 55.23%로 늘었다. 상한에 빈 자리가 4개 이상일 때만 다시 보내게 하자 느린 PG에서는 31.40%로 돌아왔고 손실에서는 2.82%로 이득이 남았다. 그 결정이 운영 기본값에 빠져 있던 것은 뒤늦게 찾아 고쳤다.
떠난 고객의 결제를 빼도 PG 호출은 8.4%만 줄었다
PG가 느리면 고객이 이미 화면을 떠난 결제도 PG 동시 호출 상한의 자리를 끝까지 차지한다. 요청에 남은 시간을 싣고 다 된 결제는 PG를 부르지 않게 했다. 요청의 30%를 떠난 결제로 보내면 PG 호출도 30% 줄 거라 예측했지만 8.4%만 줄었다. 비운 자리를 남은 결제가 바로 채웠고 상한 거절이 609건에서 169건으로 줄었다. 처음에는 클라이언트 시계의 절대 시각을 받았다가 남은 시간으로 바꿨다. 결제 화면과 똑같이 움직이는 고객으로 다시 재자 뺀 결제는 0건이었다. 기다림을 멈춘 고객도 조회로 결과를 기다리고 있어 화면이 보내는 남은 시간을 27초로 고쳤다.
짧은 PG 타임아웃은 우리 쪽 대기만 줄였다
PG 읽기 타임아웃 5초는 토스 공식 권장 60초보다 12배 짧았다. 가짜 PG가 끊긴 호출도 끝까지 처리하게 고친 뒤 두 값을 쟀다. 평소 느린 꼬리에서 5초는 결제의 10.2%를 결과 모름으로 만들었고 확정까지 p95 92.7초가 걸렸다. PG가 전부 느린 브라운아웃에서 5초는 1.5배 더 팔았지만 모두 결과 모름을 거쳤고 PG 승인 호출은 16배였다. 측정 전에 적은 기준을 넘어 60초로 바꾸고 결제 화면이 그 시간까지 결과를 기다리게 했다.
정산·대사
판매자별 정산금을 집계하고 PG 정산 파일과 내부 기록을 대조합니다. 규칙으로 가르지 못한 불일치는 사람이 확인하는 큐로 보내고, 큐가 방치되면 알림을 보냅니다.
저장소 문서: 지급 대사 엔진과 외부 reference
API 서버를 늘렸더니 정산 배치가 두 번 돌았다: 실행권과 배포 단위를 분리한 이유
API 인스턴스를 둘로 늘리자 같은 정산 배치가 중복 실행됐고, 한 대로 유지하자 배포 중 결제 요청이 실패했다. 유니크 제약만으로는 실행권을 정할 수 없어서, MSA 분리 대신 API와 워커의 실행·배포 단위를 나눈 과정을 정리한다.
대사 담당자가 결제 한 건의 진실을 찾는 데 13번 확인해야 했다: 근거를 한 화면에 모은 이유
결제 대사 담당자가 한 주문의 원인을 판단하려면 API 7회와 목록 검색 6회를 거쳐야 했다. 여러 도메인의 근거를 읽기 전용 타임라인으로 모으고, 확정 가능한 원인만 근거와 함께 제안하도록 바꾼 과정을 정리한다.
정산 배치에서 처리량과 가맹점 공정성 중 무엇을 우선할 것인가
정산 배치는 틱마다 읽는 양과 순서(id 오름차순)가 정해져 있다. 큰 가맹점 하나가 틱을 다 채우면 뒤 가맹점은 다음 틱으로 밀린다. 정책을 만들어 실측하고 기본값은 꺼 두었다.
현재 잔액과 거래 사건을 분리해야 하는 이유
1만 원 승인으로 대사를 MATCHED로 확정한 뒤 3천 원 부분취소가 왔다. 내부 스냅샷은 7,000원이 됐는데 확정된 판정은 10,000원 그대로였다. 승인 행을 덮어쓰던 것을 사건별 행으로 쌓게 바꾼 기록이다.
결제 시스템의 정합성을 어떻게 운영 지표로 증명할 것인가
Stripe는 원장 품질을 clearing·timeliness·completeness 세 축으로 재고 Adyen은 경로마다 우선순위를 나눕니다. 그 틀을 BE-commerce 규모의 질문 넷으로 줄이고 지금 지표로 있는 것과 목록에만 있는 것을 코드 기준으로 나눴습니다.
대사 결과가 맞아도 확정하면 안 되는 경우: Pending과 Posted의 경계
Modern Treasury는 ACH 입금을 pending 단계에서 잠정 대사해 하루를 앞당기고 posted가 오면 다시 대사합니다. BE-commerce 지급 대사는 금액이 맞아도 posted가 아니면 MATCHED로 올리지 않고, 그 결과가 지급 확정의 유일한 입구입니다. 코드로 강제한 것과 합성 report로만 확인한 것을 나눠 적었습니다.
정산을 떼어 낼 근거 하나를 기각했다: 그 실험이 대신 포인트 적립의 동시성 결함을 찾았다
"정산 배치가 결제를 느리게 한다"는 가설은 배치를 7번 연달아 돌려도 결제 p95가 변하지 않아 기각했다. 커넥션 풀 20개 중 최대 15개를 썼고 대기는 0이었다. 대신 그 실험의 1차 실행에서 결제 승인 70%가 500으로 무너졌고 원인은 포인트 적립의 낙관적 락 충돌이었다. 적립을 DB 원자 증가로 바꿔 같은 최악 조건의 승인 성공률을 39.6%에서 100%로 올렸다.
개인화
클릭과 조회, 구매 로그를 모아 홈 추천을 구성합니다. 추론이 늦으면 인기 상품으로 물러섭니다. 생성 모델 GenPage는 혼자서는 순위 모델에 졌습니다. 순위 모델 점수와 합친 줄 구성은 오프라인과 가상 사용자 짝 A/B에서 규칙을 넘었고 홈 2쪽 줄 구성의 기본값이 됐습니다.
저장소 문서: 개인화 아키텍처
추천 모델을 학습해 보고 넣지 않았다: 넘을 선을 먼저 정했고 생성 중 차단도 재 보고 껐다
학습한 ALS 모델의 최고 점수(MAP@12 0.0076)는 측정 전에 정한 선(마지막 구매 재추천 0.0234)의 3분의 1이었고 인기 상품보다도 낮았다. 품절 상품을 생성 중에 막는 방식은 막으려던 문제가 실제 카탈로그에 없었고 서빙 중앙값을 62ms에서 369ms로 늘려 개인화를 못 받는 요청을 만들었다. 둘 다 코드는 남기고 켜지 않았다.
CDC가 139배 느리게 나왔다: 느렸던 것은 적지 않은 기본값이었다
사용자 활동 로그를 Debezium CDC로 나르게 바꾸고 재 보니 lag p95가 아웃박스의 139배(1,944ms 대 14ms)였다. 그대로 적었으면 CDC를 기각했을 것이다. 커넥터의 지연 설정 셋을 명시하자 13ms로 같아졌다. 그 전에 반영률 0%·오류 로그 0건인 측정이 있었고 원인은 컨슈머 빈이 아예 없던 것이었다.
캐시 값을 몇 바이트부터 압축할까: 교차점은 쟀고, 임계값은 거기에 두지 않았다
Redis 캐시 값을 압축하면 187B에서는 오히려 7% 커지고 1KB에서는 66% 줄었다. 손익이 갈리는 교차점은 187~279B였지만 임계값은 1KB로 정했다. 코덱은 통념과 달리 SNAPPY가 LZ4보다 CPU를 덜 썼고 이유는 확인하지 못했다. 그리고 값은 정했지만 켜지 않았다. 적용할 캐시가 없었다.
과부하에서 무엇을 포기하나: 정책은 지연을 정하고 개인화 비율은 모델 용량이 정한다
추천 모델이 감당하는 양보다 요청이 많을 때 넘친 요청을 어떻게 포기할지 세 정책을 비교했다. 개인화를 받은 요청 비율(coverage)은 세 정책이 ±0.3%p 안에서 같았고 갈린 것은 p95(160ms·320ms·454ms)였다. 아무것도 거절하지 않는 정책은 4배 부하에서 coverage가 31.4%로 더 좋아 보였는데 요청을 받지도 못해 분모에서 빠진 생존자 편향이었다.
늦게 도착한 이벤트를 버리고 있었다: 순서를 사는 대신 병합을 순서에 무관하게 만들었다
같은 활동 로그로 만든 온라인 컨텍스트와 오프라인 재계산이 순서가 뒤집힌 조건에서 20명 전원 갈라졌다(일치율 0%). 병합 규칙이 늦게 온 이벤트를 버리고 있었다. 사용자별 순서 보장을 사는 대신 병합을 순서에 무관하게 바꿔 100%로 맞췄다. 목록 길이로 세던 창 집계는 60%만 맞았고 재는 도구도 같은 잘림을 물려받고 있었다.
홈을 여러 쪽으로 나눠 만들었다: 커서가 보여 준 것을 들고 다니고, 다음 쪽 순서는 방금 본 상품이 정한다
GenPage처럼 홈을 몇 행씩 나눠 만들고 다음 쪽이 앞 쪽과 그 사이의 세션 활동을 입력으로 받게 했다. 쪽 상태는 서버에 두지 않고 커서에 실었다. 사용자 30명 실측에서 쪽 사이 중복은 0(커서가 없으면 2쪽 평균 8.6개)이었고 세션 반영은 96.7~100%였다. 1차에서는 인기 표에 아동복이 0개라 세션 신호가 조용히 버려지고 있었다.
GenPage를 실제 홈에 붙여 보니 모델보다 입력과 과부하 계약이 먼저 흔들렸다
사용자 이력으로 홈 2쪽을 생성하는 작은 GenPage 모델을 만들고 실제 서빙 경로까지 연결했다. 모델 품질은 MAP@12 0.0209로 기준선 0.0234를 넘지 못했지만, 더 큰 문제는 빈 HTTP 본문·학습과 다른 입력·게이트 밖 2쪽 호출이었다. 네 실험을 한 흐름으로 묶어 무엇을 확인하고 기본값을 켜지 않았는지 정리한다.
캐시 압축은 동시 요청이 몰릴수록 이득이 커졌다
한 스레드로 정한 캐시 압축 임계값(1KB)과 코덱(Snappy)을 스레드 8개, 32개로 다시 쟀다. 압축은 CPU를 쓰니 동시 요청에서 불리할 것으로 봤는데 50KB 값에서 32스레드 p95가 압축 없이 5.89ms, Snappy로 2.24ms였다. 앱이 Redis 연결 하나를 나눠 써서 바이트 수가 줄을 세우기 때문이다. 두 결정은 그대로 둔다.
GenPage는 순위 모델에 다섯 번 지고 비교 규칙을 맞춘 뒤 순위 점수와 합쳐 이기고 기본값이 됐다
Netflix GenPage 구성을 H&M 구매 데이터로 옮겨 업계 표준(후보 생성 + LightGBM 순위 모델)과 겨뤘다. 상품 고르기, 줄 구성, Taobao 노출 데이터, 통계 곁채널, 행 점수 결합까지 다섯 번 모두 졌다. 비교 규칙을 두 쪽에 같게 걸고 순위 모델 점수 위에서 GenPage가 줄만 고르게 하자 홀드아웃 68,984명에서 규칙 줄 구성을 페이지 nDCG +2.0%로 이겼다. 그 방식을 모델 서버와 앱 홈 2쪽 기능 플래그로 붙였고 지연은 지금 방식보다 짧았다. 표본이 작아 판정을 내리지 못한 가상 사용자 A/B를 같은 사용자 68,984명 짝 설계로 다시 재자 구간이 16분의 1로 줄며 이겼다. 앱이 실제로 보여 주는 구성으로 재도 이겨서 홈 2쪽 줄 구성의 기본값을 결합 방식으로 켰다.
수요를 가정으로 세우자 쌓아 둔 실측이 용량 모델로 읽혔다
커머스 백엔드의 공급 실측(모델 서버 처리량, DB 읽기·쓰기 무릎, 앱 홈 경로)은 문서마다 쌓여 있었지만 수요 모델이 없어 몇 배 모자란지 말할 수 없었다. MAU 10만 · 30만 · 100만 가정에서 peak RPS를 유도해 대조하자 첫 병목이 모델 서버로 드러났다. 1대 9.34/s는 MAU 약 7.5만에서 홈 수요가 넘어선다. 잠금 직렬 상한과 Little's Law 가 실측과 2% 안에서 맞는지 확인한 뒤 수평 확장 예측을 측정 전에 커밋했다. 2대 실측 15.15/s는 예측 범위 밖이었다. 법칙은 맞았고 서비스 시간을 상수로 둔 가정이 틀렸다. AWS 서울 온디맨드 환산 월 비용은 $903에서 $1,792이고 성장할수록 모델 서버 대수가 비용을 정한다.
검증·CI
PR마다 실제 MySQL·Redis로 통합 테스트를 돌리고, 건너뛴 테스트는 통과로 치지 않습니다. 측정용 테스트는 매일 한 번 돌리고, PR은 바뀐 모듈의 테스트만 고릅니다.
저장소 문서: CI 워크플로
문서와 테스트가 맞아도 배포를 믿을 수 없었다: 실행하지 않은 경로의 비용
배포 단위 분리, 원장 잔액 조회, 아웃박스 복구, 데이터 이관처럼 문서와 테스트만으로 통과시킨 경로를 실제로 실행했다. 설계가 있어도 실행권·잔액 조회·커밋 후 발행·이관 중 변경에서 무엇이 빠지는지 확인한 과정을 정리한다.
PR마다 18~35분 걸리던 통합 테스트를 전부 돌아도 3분으로 줄였다
실 DB 통합 테스트 잡을 로그로 쪼개 가며 줄였다. 측정용 테스트를 매일 도는 워크플로로 옮기고 MySQL을 한 번만 띄우고 Spring Modulith로 바뀐 모듈의 테스트만 골랐다. 그 과정에서 내가 넣은 종료 훅이 60초를 쓰는 것과 리포트에 안 잡히던 데이터 적재 2.5분을 찾았다. 전부 돌아도 3.2분, 골라 돌면 1.7분이다. 효과가 편차보다 작았던 개선 하나는 닫았다.
제재 명단과 이름이 비슷하면 지급을 막아야 할까: 누락과 오탐 사이의 기준
정산금을 판매자 계좌로 보내기 전 제재 명단과 대조하는 장치를 실제 지급 경로에 연결했다. UN 명단을 넣어 보니 파서가 세 군데 틀렸고 한글 이름은 한 건도 없었다. 이름만으로 자동 통과하지 않고, 무엇을 놓치고 무엇을 사람에게 넘길지 정한 과정을 정리한다.
FDS 차단을 승인 경로에 바로 연결하지 않은 이유
Stripe Radar는 결제마다 100ms 안에 판정합니다. BE-commerce는 FDS 판정을 결제가 끝난 뒤에만 돌리며 그 이유를 지연이라고 적어 두었지만 재 보니 판정은 p99 3~5ms였습니다. 지연을 지운 뒤에도 켜지 못한 세 가지와 그중 하나를 잘못 짚었던 이야기입니다.
AI 운영
장애 분석, 대사 원인 후보, 운영 타임라인 서술에 모델을 붙였습니다. 모델보다 검증기를 먼저 만들고, 검증기가 허락하지 않는 자리에서는 모델을 껐습니다.
저장소 문서: AI 운영 자동화 검토
AI 운영 자동화에서 모델보다 검증기를 먼저 만들어야 하는 이유
모델을 붙이기 전에 포트와 숫자 검증기부터 만들었다. 실제 데이터를 넣자 검증기가 분류기가 계산한 진짜 270원을 출처에 없다며 반려했고, 고친 뒤에도 반복 실행에서만 드러나는 결함이 둘 더 나왔다.
AI 평가 점수와 실제 사용 가능성은 왜 다른가
결제 상담 초안을 자동 채점하는 루브릭이 평균 4.97/5점을 줬지만, 사람이 블라인드로 쓴 답과 비교하니 편집 거리 중앙값이 45.6%였다. 루브릭을 조이면 정상 문장을 놓치고, 고객 영향을 요구하면 모델이 모르는 것까지 단언했고, 다른 계열 심판도 틀렸다.
대사 원인 분류에 AI를 붙였지만 다시 껐다: 규칙이 이미 같은 답을 내고 있었다
규칙이 설명하지 못한 대사 예외에 모델을 붙였지만, 실제 잔여 분포에서 규칙 분류기와 같은 답을 냈다. 개발 표본의 91~100%만 보고 자동화를 켜지 않고, 홀드아웃에서 개선이 0임을 확인한 뒤 기본값을 규칙으로 유지한 이유를 정리한다.
AI를 켤 곳과 끌 곳을 가르는 기준
결제 운영 자동화 세 곳에서 모델을 켜고 끈 기준. 장애 로그 진단과 운영자용 요약은 켰고 대사 원인 분류는 껐다. 그 과정에서 평가 장치도 세 번 틀렸고, 마지막 한 건은 모델이 아니라 제 라벨이 틀렸을 가능성으로 남겼다.
댓글
댓글 수정/삭제는 GitHub Discussions에서 가능합니다.