지예환Ewan Jee

TROUBLESHOOTING

  • 무신사: Retention 팀

    API Gateway 전환 중 스케일아웃 502 약 800건

    70% 전환 단계에서 파드가 늘 때마다 난 502 약 800건을 '새 파드가 준비를 마치기 전에 요청을 받는 준비 시점 문제'로 정의하고, 파드가 Ready가 되는 시점을 약 15초에서 약 35초로 늦춰 100% 구간 약 1,600만 요청을 서버 오류 1건으로 마쳤습니다.

    자세히
    증상
    트래픽을 API Gateway로 10→30→70→100% 네 단계로 옮기던 중 70% 단계에서, 파드가 늘어날 때마다 502(게이트웨이가 뒤쪽 서버에서 정상 응답을 받지 못했다는 오류)가 났고 모두 약 800건이었습니다.
    문제 정의
    502가 파드가 늘어날 때마다 났다는 점에서, 문제를 새 파드가 요청을 받을 준비를 마치기 전에 트래픽이 들어오는 준비 시점 문제로 좁혔습니다.
    접근
    전환 비율을 10→30→70→100%로 나눠 올렸고, 70% 단계에서 502가 나오자 앱·nginx 준비 확인과 메모리 설정을 고친 뒤 100%로 넘어갔습니다.
    원인
    기존 설정에서는 앱 준비 확인이 15초 시점에 통과해 파드가 약 15초 만에 Ready(요청을 받을 수 있다고 표시되는 상태)가 됐지만, 그때 앱은 아직 요청을 처리할 준비를 마치지 못했고 nginx에는 준비 확인이 아예 없었습니다.
    해결
    앱의 준비 확인(readiness probe, 요청을 받을 준비가 됐는지 확인하는 검사)을 15초에서 30초로 늦추고, nginx 준비 확인을 35초로 새로 두고, 메모리를 2.0GiB에서 3.6GiB로 맞췄습니다.
    검증
    파드가 Ready가 되는 시점은 약 15초에서 약 35초로 늦춰졌고, 그 뒤 공개 API 23개(라우트 규칙 26개)를 7일 만에 100% 전환한 구간의 약 1,600만 요청 중 서버 오류는 1건이었습니다.
    재발 방지
    이제 파드는 앱과 nginx가 모두 준비 확인을 통과한 뒤에만 트래픽을 받습니다.
    문서화
    Kubernetes 설정 변경은 ADR(설계 결정 기록)로 남겨 합의를 거친 뒤 반영했습니다.
  • 무신사: Retention 팀

    Redis master를 놓친 연결

    새로 뜬 파드 2개에만 몰린 쓰기 오류를 '클라이언트가 master를 잃은 상태로 고정돼 재시작해야만 회복되는 문제'로 정의하고, 9개 시나리오 로컬 재현으로 원인과 회복 방법(연결을 새로 만들기)을 확인한 뒤, 그 방식으로 연결을 스스로 다시 만드는 자가 복구를 개념 검증(PoC)으로 만들었습니다.

    자세히
    증상
    약 30분 동안 이벤트 응모 요청이 'Master is currently unknown' 오류와 함께 HTTP 500으로 실패했고(약 100건), 장애 시작 약 5분 뒤 자동 알림이 울렸습니다. 오류는 오토스케일로 새로 뜬 파드 2개에만 몰렸습니다.
    문제 정의
    Redis 서버는 정상인데 오류가 특정 파드에만 몰렸으므로, 문제를 서버 장애가 아니라 그 파드의 Redis 클라이언트가 master를 잃은 상태로 고정돼 재시작해야만 회복되는 문제로 정의했습니다. 목표는 파드를 다시 띄우지 않아도 연결이 스스로 회복하게 만드는 것이었습니다.
    접근
    로컬 Redis로 장애를 시나리오 9개와 대조군 1개로 재현해 운영 현상과 구조가 같은지 맞춰 봤습니다. 이어서 master가 돌아온 뒤 프로세스 안에서 쓸 수 있는 복구 방법(연결 검사, 연결 공유 끄기, 연결 초기화)과 새 연결 팩토리(연결을 만드는 객체) 생성을 하나씩 시험했습니다.
    원인
    Lettuce 6.3.2(자바 Redis 클라이언트)의 master-replica(쓰기는 master 한 대가 맡고 복제본이 읽기를 나누는 구성) 정적 구성에서는 구성을 읽는 순간 master에 닿지 못하면 master가 노드 목록에서 영구히 빠져, master에만 써야 하는 분산 락(서버 여러 대가 같은 작업을 동시에 하지 않게 거는 잠금) 요청이 계속 실패했습니다. 재현 시나리오 9개가 모두 운영에서 본 현상과 구조가 같았고, 락 처리 코드에 예외 처리가 없어 락 실패 응답(409) 대신 500이 나간 것도 확인했습니다.
    해결
    오류를 감지하면 연결을 새로 만드는 자가 복구를 개념 검증(PoC)으로 만들었습니다. 여러 요청이 동시에 오류를 봐도 연결은 한 번만 다시 만들고(single-flight), 재시도 간격은 점점 늘립니다(지수 backoff).
    검증
    재현에서 master가 돌아온 뒤에도 프로세스 안 복구 방법은 모두 회복에 실패했고 새 연결 팩토리를 만든 경우만 즉시 회복돼, 연결을 새로 만드는 방식이 맞다는 근거가 됐습니다. 운영에서도 문제 파드 2개를 지운 뒤 오류가 0건이 됐습니다.
    재발 방지
    원인 보고서는 이 오류를 감지하면 파드를 자동으로 교체(준비·생존 확인과 연동, 재현에서 효과 확인)하고, DNS TTL(주소 정보를 캐시에 두는 시간)을 60초에서 5~10초로 줄이자고(효과는 추정) 권고했습니다. 자가 복구는 사람이 파드를 다시 띄우지 않아도 연결이 스스로 돌아오게 하는 것이 목표입니다.
    문서화
    원인 보고서, 증거 문서, 시나리오 9개를 다시 돌릴 수 있는 재현 하네스(재현용 테스트 코드), 다른 가설을 반증한 기록을 남겼습니다. 자가 복구 작업은 Jira 유지보수(KTLO, 운영 유지보수) 작업 1건으로 추적했고, 착수와 목표는 격주 회고에 적었습니다.
  • 무신사: Retention 팀

    출시 전 혜택 총액 오류 약 5만 원 차단

    시안(디자인 초안)의 혜택 총액 41,030원이 실제 최대 총액 91,020원의 절반에도 못 미친(약 5만 원 차이) 것을 출시 전에 찾아 총액 산출 정책의 문제로 정의하고, 정책을 고친 뒤 출시했습니다.

    자세히
    증상
    시안에 적힌 혜택 총액은 41,030원이었지만 실제로 받을 수 있는 최대 총액은 91,020원으로, 약 5만 원 차이가 났고 실제 최대치가 시안의 약 2.2배였습니다.
    문제 정의
    혜택홈 개편이 풀려던 문제가 '받을 금액이 보이지 않는 것'이었으므로, 이 차이를 계산 실수가 아니라 총액 산출 정책이 실제 혜택 구성과 맞지 않는 문제로 정의했습니다.
    접근
    시안 총액을 실제로 받을 수 있는 최대 총액과 대조해 차이를 찾은 뒤, 총액을 계산하는 산출 방식 5가지를 나란히 놓고 비교했습니다.
    원인
    총액 산출 정책이 실제 혜택 구성과 맞지 않아, 시안 총액은 실제 최대치의 약 45%에 그쳤습니다.
    해결
    산출 방식 5가지를 비교해 총액 산출 정책을 고쳤습니다.
    검증
    고친 정책은 출시 전에 반영돼, 실제 최대치보다 약 5만 원 적게 보일 뻔한 총액이 사용자에게 나가지 않았습니다.
    문서화
    이 건이 속한 혜택홈 개편의 판 구조와 정책 결정은 ADR, HLD, 인수인계 문서로 남겼습니다.
  • 무신사: Global 팀

    증설로는 못 막았을 가격 API 장애

    '증설로 막을 수 있었나'라는 질문을 자원 부족 여부를 가리는 문제로 바꿔, 가격 API를 부르는 프런트 서버에 자원 여유가 있었는데(CPU 3.2% → 3.9%, 메모리 74% → 41%) 들어온 요청 오류만 약 300배 늘었음을 보이고 원인이 느린 하위 API를 동기로 기다린 구조임을 밝혔습니다.

    자세히
    증상
    세일이 끝난 심야에 가격 조회 API가 타임아웃으로 실패하기 시작해 다른 서비스의 호출 실패와 서킷 브레이커(실패가 이어지면 호출을 잠시 끊는 장치) 열림으로 이어졌고, P1 등급 장애로 자동 보고된 뒤 약 10분 안에 서킷 자동 복구로 해소됐습니다.
    문제 정의
    장애 대응 책임자가 '증설 계획은 없었나, 세일 종료는 늘 있던 일인데 왜 이번에만 났나'를 물었고, 이를 '우리 쪽 호출이 원인이었나'와 '자원이 모자랐나' 두 질문으로 나눠 각각 데이터로 답했습니다.
    접근
    먼저 장애 구간과, 같은 규모의 대량 가격 변경이 있었지만 장애가 없던 같은 세일의 시작일을 추적 데이터(트레이스)로 나란히 비교했습니다. 이어서 표본 추출(샘플링)을 거치지 않은 자원·오류 지표로 프런트 서버의 장애 구간을 평소와 비교했습니다.
    원인
    같은 세일의 시작일에는 우리 배치 서버의 가격 호출(캐시 우회)이 장애 때보다 오히려 많았는데도 장애가 없어(추적 표본 기준), 우리 쪽 설정(캐시 우회, 타임아웃 1초 + 재시도 3회)은 지연을 키운 요인일 뿐이었습니다. 실제 원인은 하위 세일 API가 최근 변경으로 백프레셔(부하 조절)가 풀려 느려지자, 가격 API가 이를 동기로 기다리며 DB 연결을 점유해 다른 API까지 느려진 것이었습니다.
    해결
    우선순위 순으로 가격 호출 재시도를 3회에서 0회 또는 1회 + 지터(무작위 대기)로 줄이고, 사내 다른 호출에 이미 쓰던 서킷 브레이커를 가격 호출에도 적용하자고 권고했습니다. 이어서 상품 화면의 고정 캐시 우회를 설정값으로 바꾸되 먼저 캐시 적중률을 재고, 배치 일정이 자정 변경 시간대와 겹치는지 점검하자고 했습니다.
    검증
    프런트 서버의 자원 지표는 평소 대비 CPU 3.2% → 3.9%, 메모리 74% → 41%, 메모리 부족 종료·재시작 0건이었고, 배치 서버의 CPU 스로틀(한도 때문에 CPU를 못 쓴 시간)도 평소보다 오히려 적었습니다. 반면 같은 프런트 서버의 비샘플링 오류는 나가는 호출이 약 60배, 들어온 요청이 약 300배로 늘어, 자원이 아니라 느린 호출을 기다린 것이 문제였음을 보여 줬습니다.
    재발 방지
    배치 서버 메모리가 상시 약 95%인 원인, 오토스케일 미설정 가능성, CPU 한도 미설정 가능성을 따로 보는 용량 점검 트랙을 제안했습니다. 'SLA(약속한 응답 수준)를 넘긴 호출이 난 팀이 장애 대응을, 그 API를 제공한 팀이 근본 원인을 맡는다'는 책임 원칙도 합의됐습니다.
    문서화
    장애 티켓 1건으로 관리된 이 장애에 대해 원인 분석 문서(RCA) 2건(가격 API 급증 원인 분석, 증설 여부 후속 분석)을 썼습니다. 문서에는 조치를 우리 팀 몫과 원인 팀 몫으로 나눠 우선순위와 함께 적었습니다.
  • 무신사: Global 팀

    크롤러 몰림으로 GC 폭주

    처리량이 약 8분의 1로 떨어진 GC 폭주를 '부하를 만든 요청이 어느 화면에 몰렸나'의 문제로 좁혀, 20초 간격 지표로 약 8분마다 몰린 크롤러 요청을 찾고 원인 화면을 장애 인지 11분 만에 특정했습니다.

    자세히
    증상
    프런트 서버 처리량이 약 8분의 1로 떨어지면서 메이저 GC(오래 살아남은 객체 영역까지 정리하는 GC)가 약 5분간 이어졌고, GC 시간은 초당 최대 1,089ms까지 올랐습니다. 일부 파드는 CPU가 평소 0.4코어에서 5.5~6.0코어로 오른 채 고정됐고, 오토스케일로 파드 수는 4배가 됐습니다.
    문제 정의
    GC 폭주를 원인이 아니라 결과로 보고, 그 부하를 만든 요청이 어느 화면으로 몰렸는지를 찾는 문제로 좁혔습니다.
    접근
    전날 만들어 둔 봇 감지 대시보드와, AI 도구에 연결한 Datadog·Grafana에서 뽑은 20초 간격 지표로 요청을 봇 여부별로 나눠 봤습니다. 장애를 인지하고 11분 뒤에 원인 화면을 장애 대응 채널에 처음 특정해 공유했습니다.
    원인
    몰린 요청 가운데 검색 엔진 크롤러(Googlebot)가 42%, 다른 크롤러가 18%였고, 이 요청이 캐시 미스(캐시에 없어 원본을 다시 읽는 요청)를 키웠습니다.
    해결
    원인 화면을 특정한 뒤, 프런트 서버에서 BFF(화면 전용 API 계층) 연동과 그 HTTP 호출 설정을 제거하는 대응 PR 1건을 머지했습니다.
    검증
    20초 간격 지표에서 이 크롤러 요청이 약 8분마다 한꺼번에 몰리는 주기가 보인 것이 크롤러 몰림을 원인으로 판단한 근거였습니다.
    재발 방지
    이후 엣지(CDN 앞단)에서 봇 판별 확인 절차, 엣지 캐시, 요청 속도 제한을 모든 국가와 상품 카탈로그 경로 전체에 적용하는 방어 계획을 세웠습니다.
    문서화
    격주 회고에 장애 수치와 원인 화면을 찾기까지의 타임라인을 정리했고, 사내 위키에는 이 장애의 원인 분석 문서(RCA)가 있습니다. 봇이 몰릴 때 브랜드 경로의 분당 요청이 평소의 약 50배까지 오른 사례를 근거로 한 봇 방어 플레이북(대응 절차 문서)도 적대적 검토(일부러 허점을 찾는 검토) 2회를 거쳐 정리됐습니다.
  • 무신사: Global 팀

    LLM 번역 타임아웃 8.3%

    번역 타임아웃의 실제 규모를 15일 전수 지표로 다시 재서 8.3%로 확정하고(처음 표본 기준 26~80%), 호출 대상 응답 p99 약 7초와 기본 타임아웃 2초를 대조해 원인을 새 클라이언트의 타임아웃 설정 누락으로 좁혔습니다.

    자세히
    증상
    LLM 기반 번역 호출이 읽기 타임아웃으로 반복해서 실패했습니다. 번역을 요청한 서버가 오류를 처리해 상위 서비스에 HTTP 오류로 전달되지는 않았지만, 캐시에 없는 항목은 번역되지 않고 원문 그대로 나갔습니다.
    문제 정의
    장애가 아니라 오류 표시 없이 번역만 빠지는 품질 문제였으므로, 실제 실패율부터 정확히 재는 문제로 정의했습니다.
    접근
    APM(요청 추적 도구)의 스팬(요청 하나를 처리한 기록) 개수로 오류율을 셌더니 26~80%가 나왔지만, 오류 스팬은 모두 남기고 정상 스팬만 일부 남기는 표본 추출 때문임을 알아채고 표본 추출 전 지표로 바꿔 다시 셌습니다. 이어서 호출 대상의 응답 시간 분포, 기존 번역 클라이언트 설정, 다른 번역 경로의 오류율, 코드의 재시도 설정을 차례로 대조했습니다.
    원인
    새 번역 클라이언트에 개별 타임아웃이 없어 전역 기본값 2초(연결·읽기)를 물려받았는데, LLM 번역 서버의 응답 시간은 p95 약 3.5초, p99 약 7초(p95·p99는 느린 쪽 5%·1% 경계의 응답 시간), 배치가 도는 날 최대 90~112초라 약 8%는 구조적으로 2초를 넘을 수밖에 없었습니다. 기존 번역 클라이언트는 읽기 타임아웃이 15~20초로 맞춰져 있었고, 새 클라이언트만 이 설정이 빠져 있었습니다.
    해결
    즉시 조치로 새 클라이언트의 읽기 타임아웃을 기존 수준인 약 20초로 두고, 근본 조치로 캐시 적중률을 높이고 미리 번역해 두며, LLM 호출 자체에도 마감 시간을 둬 비용 낭비를 막자고 권고했습니다. 재시도는 부하를 키울 수 있어 새로 넣지 말자고 했습니다.
    검증
    표본 추출 전 지표로 15일 동안 센 번역 호출 약 280만 건 중 약 23만 건이 타임아웃이어서, 실패율은 8.3%였습니다. 성공한 호출도 p99가 1.91초로 2초에 가까웠고, 다른 번역 경로의 오류율은 약 0%여서 문제가 이 LLM 경로에만 있음을 확인했습니다.
    재발 방지
    새 HTTP 호출 클라이언트를 만들 때 타임아웃 설정이 빠졌는지를 점검 항목으로 남겼습니다. 코드에서 재시도가 꺼져 있음(재시도 안 함 설정)도 확인해, '재시도가 실패를 키웠다'는 잘못된 추정을 배제했습니다.
    문서화
    표본 기준과 전수 기준 수치가 달라진 이유, 원인, 권고를 원인 분석 문서(RCA)로 남겼고, 사내 위키에도 이 건의 원인 분석 페이지가 있습니다. 같은 문서에 호출 쪽 번역 캐시(DB)가 설계 문서의 '상태를 두지 않는다'는 전제와 어긋난다는 설계-코드 불일치도 적었습니다.
  • 무신사: Global 팀

    데이터 계약 오류 7건

    지표 오류를 설계가 가정한 데이터 계약과 실제 데이터의 불일치 문제로 보고, 설계 문서를 v0.1부터 v0.6까지 고칠 때마다 실제 데이터로 다시 검사해 배포 전에 계약 8개 중 7개를 고쳤습니다.

    자세히
    증상
    설계한 지표를 일본 데이터에 돌려 보니 가입 후 7일 안 첫 구매율이 82~88%로 비현실적으로 높게 나왔고, 1회 구매자 약 49만 명이 잘못 분류됐습니다.
    문제 정의
    설계 단계부터, 문서가 가정한 데이터 계약(어떤 컬럼·필터 조건·사용자 키·구매자 분류 규칙을 쓰는지)이 실제 데이터와 맞는지 검증하는 문제로 정의했습니다.
    접근
    설계 문서(LLD, 구현 수준 설계)를 v0.1부터 v0.6까지 6번 고칠 때마다 실제 데이터로 스모크 테스트(설계대로 빠르게 돌려 보는 검사)를 돌렸고, 1차와 2차 검사에서 불일치를 찾았습니다. 검사는 AI 세션에서 실제 테이블 구조와 값을 직접 조회해 설계와 대조하는 방식이었습니다.
    원인
    검사에서 국가 구분 값 누락, 주문 상태 필터 오류, 사용자 식별 키 오류, 구매자 분류 규칙 오류, 7일 지표 분모의 편향 등이 나왔고, 데이터 계약 8개 중 7개가 실제 데이터와 맞지 않았습니다. 82~88%는 분모가 전체 가입자가 아닌 다른 기준으로 잡혀 부푼 값이었고(자기 선택 편향, 분모가 전체를 대표하지 못하는 편향), 약 49만 명 오분류는 구매자 분류 규칙 오류 때문이었습니다.
    해결
    분모를 전체 가입자 기준으로 바로잡고 구매자 분류 규칙을 고치는 등 불일치 7개를 모두 고쳤습니다.
    검증
    프로토타입을 v1부터 v5까지 고쳐 운영에 적재할 때까지, 이미 고친 오류가 다시 생긴 회귀는 0건이었습니다.
    재발 방지
    계산 결과 하나를 그대로 믿지 않고, 별도 AI 검토 도구로 보정값·예측 모델·계산식을 교차 검증하는 절차를 상시로 둡니다.
    문서화
    실제 데이터와 맞춘 계약을 설계 문서(LLD) v0.1~v0.6에 차례로 반영했고, 검사에서 찾은 결함과 회귀 0건은 2주 단위 자체 회고에 기록했습니다. 이 작업이 속한 성장 지표 분석 프로젝트는 Jira 이니셔티브 1건으로 관리돼 출시 완료까지 추적됐습니다.
  • 무신사: Retention 팀

    KAPT 빌드 시간 55~71% 단축

    빌드와 테스트 기동이 느린 이유를 'Kotlin 애노테이션 처리(KAPT)가 필요 없는 모듈에까지 걸린 문제'로 좁히고, 적용 모듈을 15개에서 5개로 줄여 처리 시간을 55~71%, 테스트 컨텍스트 기동을 62~67% 줄였습니다(테스트 9,164개 통과).

    자세히
    증상
    Kotlin 애노테이션 처리(KAPT, 컴파일 때 애노테이션을 읽어 코드를 만드는 단계) 때문에 빌드와 테스트 기동이 느렸습니다.
    문제 정의
    느린 원인을 애노테이션 처리가 실제로 필요한 모듈에만 걸려 있는가의 문제로 정의했습니다.
    접근
    애노테이션 처리가 실제로 필요한 모듈만 남긴다는 기준으로 적용 범위를 줄였고, 줄이기 전후의 KAPT 처리 시간과 테스트 컨텍스트 기동 시간을 비교한 뒤 전체 테스트로 동작이 같은지 확인했습니다.
    원인
    KAPT가 처리가 필요 없는 모듈까지 15개 모듈 전체에 적용돼 있었고, 실제로 필요한 모듈은 5개였습니다.
    해결
    KAPT 적용을 15개 모듈에서 5개 모듈로 줄여, 나머지 모듈에서 돌던 애노테이션 처리를 없앴습니다.
    검증
    KAPT 처리 시간은 55~71% 줄었고, 테스트 컨텍스트(테스트 전에 애플리케이션을 띄우는 단계) 기동 시간은 두 모듈에서 각각 62%, 67% 줄었습니다. 테스트 9,164개가 모두 통과해 동작이 바뀌지 않았음을 확인했습니다.
  • 무신사: Retention 팀

    API 보안 취약점 개선