뒤로가기
장애에서 배우는 것들

April 12, 2022

frontendcareer

목요일 오후 2시 13분. 슬랙의 #alert 채널에 메시지가 올라왔다.

🚨 [CRITICAL] Checkout conversion rate dropped to 0% (threshold: < 5%)

결제 전환율이 0%. 장바구니에서 결제로 넘어가는 사람이 0명이라는 뜻이다. 처음엔 모니터링 오류인 줄 알았다. 0%는 너무 극단적이니까. 5분 뒤 CS팀에서 메시지가 왔다. "고객 문의가 폭주하고 있어요. 결제 버튼이 안 눌린다는 문의가 벌써 40건입니다."

모니터링 오류가 아니었다.

47분#

47분. 결제가 완전히 멈춰 있던 시간이다.

나중에 분석한 원인은 이랬다. 오후 1시 50분에 배포가 나갔다. 결제 플로우와는 관계없는, 마이페이지의 UI를 수정하는 작은 PR이었다. 근데 그 PR에 포함된 변경 중 하나가, 공통 유틸 함수가 있는 파일을 건드렸다. formatPrice라는 함수의 import 경로를 변경한 거였다.

문제는 이 formatPrice 함수가 결제 확인 화면에서도 쓰이고 있었다는 거다. import 경로 변경 과정에서 tree-shaking이 달라졌고, 프로덕션 빌드에서 이 함수가 undefined가 됐다. 결제 확인 화면이 렌더링될 때 formatPrice(totalAmount)를 호출하면 TypeError가 터지고, Error Boundary가 fallback UI를 보여주는데, 그 fallback에 결제 버튼이 없었다.

로컬에서는 잘 됐다. 스테이징에서도 잘 됐다. 프로덕션 빌드에서만 터졌다. 빌드 최적화 과정에서 import 구조가 달라지면서 발생한 프로덕션 전용 버그.

타임라인을 정리하면:

  • 2:13 PM - 모니터링 알림 발생
  • 2:18 PM - CS팀 보고로 실제 장애 확인
  • 2:25 PM - 프론트엔드 팀 긴급 소집, 원인 조사 시작
  • 2:35 PM - 최근 배포와의 연관성 의심, 롤백 결정
  • 2:42 PM - 롤백 배포 시작
  • 2:48 PM - 롤백 완료, CDN 캐시 퍼지
  • 2:55 PM - 일부 사용자에서 정상 동작 확인
  • 3:00 PM - 전체 정상화 확인

발견부터 정상화까지 47분. 그 47분 동안 결제가 0건이었다.

비난이 시작되는 순간#

장애가 복구되고 나면, 자연스럽게 "누구 때문에"라는 질문이 나온다. 이번에도 그랬다.

PR을 올린 건 주니어 개발자였다. 마이페이지 UI 수정이라는 단순한 작업이었고, 리뷰어도 2명이 승인했다. 근데 결과적으로 결제를 47분간 멈추게 만든 PR이었다.

장애 직후 긴급 미팅에서, 공기가 무거웠다. 그 주니어 개발자 표정이 하얗게 질려 있었다. 팀 리드가 먼저 입을 열었다. "원인은 파악됐고, 일단 복구됐으니까 다행이야. 내일 포스트모템 하자. 오늘은 다들 쉬어."

그리고 덧붙였다. "이건 개인의 실수가 아니야. 시스템의 문제야."

이 한 마디가 분위기를 완전히 바꿨다.

비난 없는 포스트모템#

다음 날 포스트모템을 진행했다. Netflix에서 유명한 "blameless postmortem"(비난 없는 포스트모템) 방식을 참고했다. 핵심 원칙은 간단하다. 원인을 사람에게서 찾지 않고, 시스템에서 찾는다.

Netflix의 철학을 빌리면 이렇다. "We look for causes in the system, not causes in people." 사람은 실수를 한다. 그건 바꿀 수 없다. 바꿀 수 있는 건 시스템이다. 실수를 해도 장애가 발생하지 않는 시스템, 장애가 발생해도 빠르게 복구되는 시스템을 만드는 게 포스트모템의 목적이다.

포스트모템 문서의 구조는 이랬다:

1. 타임라인#

분 단위로 뭐가 일어났는지. 이건 사실의 기록이다. 해석이 아니라.

2. 영향#

  • 결제 불가 시간: 47분
  • 영향 받은 사용자 수: 약 1,200명 (해당 시간대 결제 시도 기준)
  • 예상 매출 손실: 약 X만 원
  • CS 문의 건수: 87건

숫자로 정리하는 게 중요하다. "심각했다"가 아니라 "47분, 1,200명"으로.

3. 근본 원인 (Root Cause)#

여기서 5 Whys를 적용했다.

Why 1: 결제 확인 화면이 렌더링 에러를 일으켰다. → formatPriceundefined였다. Why 2: formatPriceundefined가 된 이유는? → 프로덕션 빌드에서 tree-shaking이 import 경로 변경으로 달라졌다. Why 3: 왜 이걸 배포 전에 못 잡았나? → 프로덕션 빌드 환경에서의 E2E 테스트가 결제 플로우를 커버하지 않았다. Why 4: 왜 결제 플로우 E2E 테스트가 없었나? → 결제 플로우는 외부 PG 연동이 있어서 테스트 환경 구성이 복잡했고, "나중에 하자"고 미뤄뒀다. Why 5: 왜 미뤄졌나? → 다른 기능 개발 일정에 밀려서 우선순위가 계속 내려갔다.

5 Whys의 끝에 도달하면, "주니어가 실수했다"가 아니라 **"결제 플로우의 E2E 테스트가 없었다"**와 **"프로덕션 빌드 환경에서의 검증 프로세스가 부족했다"**가 나온다. 시스템의 문제다.

4. 잘한 것#

이 섹션이 중요하다. 포스트모템이 반성문이 되면 안 된다. 잘한 것도 기록해야 한다.

  • 모니터링 알림이 5분 이내에 발생했다. (모니터링 시스템이 잘 동작했다)
  • 롤백 결정이 10분 만에 내려졌다. (원인 분석에 시간을 쓰지 않고 빠르게 롤백한 판단이 좋았다)
  • 롤백 파이프라인이 정상 동작해서 6분 만에 배포가 완료됐다.
  • CS팀이 빠르게 상황을 공유해줘서 모니터링과 크로스체크가 됐다.

5. 액션 아이템#

각 항목에 담당자와 기한을 붙인다. "좋은 아이디어"가 아니라 "누가 언제까지 한다"여야 한다.

  • [프론트엔드] 결제 플로우 E2E 테스트 작성 (담당: OO, 기한: 2주)
  • [인프라] 프로덕션 빌드 후 주요 페이지 스모크 테스트 자동화 (담당: OO, 기한: 3주)
  • [프론트엔드] Error Boundary fallback에 최소한의 주요 기능(결제 버튼) 포함 (담당: OO, 기한: 1주)
  • [공통] 공통 유틸 파일 변경 시 영향 범위 분석 도구 도입 검토 (담당: OO, 기한: 4주)

포스트모템 이후 바뀐 것들#

포스트모템 자체는 1시간짜리 회의였다. 근데 그 이후 팀에서 바뀐 것들이 상당했다.

첫째, 결제 플로우 E2E 테스트가 생겼다. 진작 했어야 하는 건데, 장애가 터지고 나서야 우선순위가 올라갔다. PG 연동 부분은 mock 서버로 처리하고, 결제 버튼 클릭부터 주문 완료 화면까지의 전체 플로우를 테스트하는 시나리오가 만들어졌다. 이 테스트 하나가 이후 3개월 동안 비슷한 종류의 문제를 2번 사전에 잡아냈다.

둘째, 프로덕션 빌드 스모크 테스트가 배포 파이프라인에 들어갔다. 배포 전에 프로덕션 빌드를 올린 뒤, 주요 페이지(메인, 검색, 상세, 장바구니, 결제)가 정상 렌더링되는지 자동으로 확인한다. 렌더링 에러가 있으면 배포가 멈춘다. 이것도 장애 전에는 "있으면 좋겠다" 수준이었는데, 장애 후에는 필수가 됐다.

셋째, Error Boundary 전략이 바뀌었다. 기존에는 에러가 나면 "문제가 발생했습니다" 같은 범용 fallback을 보여줬다. 이제는 페이지별로 다른 fallback을 제공한다. 결제 화면의 fallback에는 최소한 "다시 시도" 버튼과 CS 연락처가 포함되어 있다. 에러가 나도 사용자가 완전히 막히지 않도록.

넷째, 배포 자신감이 올라갔다. 아이러니하게도, 장애를 겪고 나서 배포에 대한 두려움이 줄었다. 테스트가 추가되고, 스모크 테스트가 있고, 롤백 프로세스가 검증됐으니까. "배포하면 뭔가 터질 수 있다"는 막연한 불안이 "터지더라도 빠르게 잡을 수 있다"는 자신감으로 바뀌었다.

장애가 가르쳐 준 것들#

이 경험에서 배운 게 몇 가지 있다.

장애의 직접 원인과 근본 원인은 다르다. 직접 원인은 "import 경로가 변경돼서 프로덕션 빌드에서 함수가 누락됐다"이다. 이걸 고치면 이 특정 장애는 재발하지 않는다. 근데 근본 원인은 "프로덕션 환경에서의 검증이 부족하다"이다. 이걸 고치지 않으면, 비슷한 종류의 다른 장애가 언젠가 또 터진다. 직접 원인을 고치는 건 소방이고, 근본 원인을 고치는 건 방화다. 둘 다 필요하다.

"나중에 하자"는 리스크의 다른 이름이다. 결제 플로우 E2E 테스트는 6개월 전부터 "해야 하는데"라고 모두가 알고 있었다. 근데 항상 더 급한 기능 개발에 밀렸다. 이건 의식적인 결정이 아니라 관성이었다. 장애가 터지고 나서야 그 "나중"의 비용이 얼마인지 체감했다. E2E 테스트를 6개월 전에 만들었으면, 47분의 장애와 X만 원의 매출 손실을 막을 수 있었다.

장애 대응은 근육이다. Netflix가 Chaos Engineering과 GameDay를 하는 이유가 이거다. 장애 대응을 평소에 연습해둬야 실전에서 제대로 동작한다. Netflix에서는 팀들이 의도적으로 장애를 일으키고 복구하는 훈련을 한다. 우리 팀이 47분 만에 복구할 수 있었던 건, 롤백 프로세스를 이전에 몇 번 써본 적이 있었기 때문이다. 한 번도 롤백을 해본 적 없는 팀이었으면, 현장에서 롤백 방법을 찾느라 2시간은 더 걸렸을 거다.

비난 문화 vs 학습 문화#

장애 이후 팀의 반응은 크게 두 갈래로 나뉜다.

비난 문화: "누가 이 PR을 올렸어?" "리뷰어가 왜 이걸 못 잡았어?" "배포 전에 확인 안 했어?" 이런 반응이 반복되면, 사람들은 변화를 두려워하게 된다. 배포를 미루고, PR을 작게 쪼개는 게 아니라 아예 안 올리려 하고, "내가 안 건드리면 내 책임이 아니다"는 방어적 태도가 생긴다.

학습 문화: "어떤 시스템적 원인이 있었지?" "같은 종류의 문제를 앞으로 어떻게 방지하지?" "이번 대응에서 잘한 건 뭐고 개선할 건 뭐지?" 이런 반응이 반복되면, 장애가 무서운 게 아니라 배움의 기회가 된다. 사람들이 더 적극적으로 문제를 공유하고, 잠재적 리스크를 미리 말하게 된다.

Netflix가 인시던트의 정의를 확장한 것도 같은 맥락이다. 큰 장애만 인시던트가 아니라, "서비스를 저하시키거나 방해하는 모든 현상"을 인시던트로 다루겠다는 거다. 작은 문제도 기록하고, 분석하고, 배우겠다는 뜻이다. 이게 가능하려면 비난이 없어야 한다. "이 정도도 인시던트로 올려?"라는 반응이 있으면, 아무도 작은 문제를 공유하지 않게 된다.

그 주니어 개발자#

장애를 일으킨(것처럼 보인) 주니어 개발자. 포스트모템 이후 그 개발자가 DM을 보내왔다. "솔직히 많이 무서웠는데, 포스트모템이 시스템 문제에 집중한 게 다행이었어요. 근데 여전히 죄책감이 있어요."

이렇게 답했다. "그 PR을 리뷰한 사람이 2명 있고, 그 리뷰어들도 이 문제를 못 잡았어. 프로덕션 빌드에서만 터지는 문제를 코드 리뷰로 잡는 건 현실적으로 불가능해. 네가 아니라 다른 누가 그 PR을 올렸어도 같은 결과였을 거야. 이건 진짜로 시스템 문제야."

그리고 한 가지 더. "이번에 네가 결제 플로우 E2E 테스트를 작성하기로 했잖아. 그 테스트가 앞으로 같은 종류의 장애를 막을 거야. 장애를 경험한 사람이 방어 코드를 만드는 게 가장 효과적이거든. 동기 부여가 다르니까."

2주 뒤, 그 개발자가 만든 E2E 테스트가 머지됐다. 꽤 꼼꼼했다. 결제 플로우의 모든 분기를 커버하고 있었고, 에러 시나리오까지 포함되어 있었다. 장애를 겪은 사람의 테스트 케이스는 확실히 다르다. 어떤 부분이 깨질 수 있는지를 체감으로 아니까.

장애는 가장 비싼 수업료다. 근데 가장 효과적이기도 하다#

이상적으로는 장애 없이 배우면 좋겠다. 다른 회사의 포스트모템을 읽고, 기술 블로그의 사례를 보고, 미리 대비하면 좋겠다. 근데 현실은, 직접 경험한 장애만큼 강렬한 학습은 없다.

서버 상태 캐싱의 중요성을 글로 100번 읽는 것보다, 캐시가 꼬여서 사용자에게 다른 사람의 데이터가 보인 경험 1번이 더 강렬하다. 배포 파이프라인에 테스트를 넣어야 한다는 걸 이론으로 아는 것보다, 테스트 없이 배포했다가 장애를 겪는 게 더 확실한 동기 부여가 된다.

다만, 장애에서 배우려면 조건이 있다. 비난이 아니라 분석이 있어야 하고, 분석에서 끝나는 게 아니라 구체적인 액션이 나와야 하고, 그 액션이 실제로 실행되어야 한다. 포스트모템 문서를 쓰고 서랍에 넣으면 아무 의미가 없다.

결제가 47분간 멈춘 그날 이후, 한 가지 확실해진 게 있다. 장애는 실패가 아니라 피드백이다. 시스템이 "여기가 약해"라고 알려주는 신호다. 그 신호를 무시하면 같은 자리에서 다시 터진다. 그 신호를 분석하고 대응하면, 시스템이 더 강해진다.

그리고 솔직히, 그날의 긴장감은 아직도 생생하다. 슬랙 알림이 울리고, 회의실에 모이고, 화면을 같이 보면서 원인을 추적하던 그 47분. 무서웠지만, 그때만큼 팀이 하나로 뭉친 적도 없었다. 장애는 팀을 시험하고, 동시에 팀을 단단하게 만든다.