archive · 13 incidents

incidents

사고와 그것으로부터 만든 영구 룰. 같은 사고를 두 번 내는 것은 학습의 실패입니다.
결과만 적은 portfolio 보다 무엇을 몰랐고 무엇을 배웠는지가 더 정직한 기록이라고 믿습니다.

핵심 코드 · API key · 회사명 · IP 전부 마스킹. 작업 맥락만 노출

결제01 incident

결제 후 리포트 안 뜨는 prod 사고

결제 후 리포트 안 뜨는 prod 사고
몰랐던 것

결제 SDK 를 표준대로 박아도, 결제사 *버전에 맞는 검증 방식*을 안 쓰면 '결제됐는데 권한 미부여' 가 난다는 것. SDK 연동 = 끝이 아니라는 것.

무슨 일이 있었나

사용자가 결제 완료 후 redirect 됐는데 권한이 안 풀렸습니다. DB 의 payment row 는 정상인데 접근 미반영. 근본 원인 = 결제사 v2 결제인데 검증 로직이 구버전(v1) 방식으로 연결돼 webhook 검증이 매번 실패 → 반영 누락. 검증 실패 누적 62건 (버전 불일치).

배운 것

결제는 단일 신뢰원(webhook 하나)에 의존하면 안 됩니다. 그리고 사고는 단일 축으로 진단하면 못 잡습니다 — DB row + UX flow(사용자 시점) + webhook 검증·반영, 3축을 동시에 봐야 진짜 root cause 가 나옵니다.

영구 룰

결제 사고 진단 표준 3축: (1) DB row 정상 여부, (2) UX flow 사용자 시점 재현, (3) webhook 도착·검증·반영 경로 일치. 단일 축으로 'DB 정상이니 OK' 결론 X. 동기 확인 + 사후 대조 다경로로 유실 흡수.

원리 04 · 조용한 사고가 진짜 사고
DB03 incident

DB OOM 6번째 — 다축 sweep 전환

DB OOM 6번째 — 다축 sweep 전환
몰랐던 것

OOM 이 단일 원인이 아니라 여러 원인 누적이라는 것. 매번 다른 곳에서 메모리가 새고 있었다는 것.

무슨 일이 있었나

DB 가 새벽에 죽는 사고가 6번째였습니다. 한 가지를 fix 하면 다음 새벽엔 다른 원인이 같은 증상을 냈습니다 — long query / 누적 connection / 스왑 미설정 / 로그 무한 증식 / DB 튜닝 부재. '단일 원인' 착각이라 6번 반복됐습니다.

배운 것

반복되는 운영 사고는 한 패턴 fix 가 아니라 sweep — 가능한 원인을 동시에 정리해야 합니다. '메모리 부족이니 자원 더 사자' 같은 잘못된 신호에 휘둘리면 근본은 그대로 둔 채 시간만 날립니다.

영구 룰

OOM 재발 방지 다축 sweep: 메모리 튜닝 + 비운영 환경 정리 + 스왑 정책 + 로그 사이즈 cap 을 *한 번에* 적용하고 관측 기간을 둠. 단일 fix 후 'OK' 결론 X.

원리 05 · 반복되면 원인은 하나가 아니다

SQL USE 절 hijack — staging 명시했는데 prod 로 redirect

몰랐던 것

마이그레이션 SQL 파일 첫 줄의 `USE <db>;` 절이 *셸 명령 인자로 명시한 DB* 보다 우선한다는 것. CLI 에서 db 이름 명시 = SSoT 가 아니라는 것.

무슨 일이 있었나

DB 마이그를 staging 에 박으려고 `mysql ... staging_db < migration.sql` 셸 명령을 썼는데, SQL 파일 첫 줄에 prod DB 를 가리키는 `USE` 절이 박혀 있어 prod 로 redirect 됐습니다. 7개 마이그 파일이 같은 패턴이었습니다. 다행히 멱등 안전 (CREATE IF NOT EXISTS 류) 이라 실 사고는 없었지만 *DB 분리 신뢰* 가 깨졌습니다.

배운 것

셸 인자보다 SQL 파일 안 절(USE)이 우선합니다. *DB 분리는 셸 명령 의지로 보장되지 않습니다* — SQL 파일이 db 를 명시하면 그게 진실입니다. 멱등이 아닌 마이그였으면 prod 가 부분 적용됐을 수 있습니다 (조용한 사고 직전).

영구 룰

마이그레이션 SQL 파일 안 `USE <db>;` 절 영구 금지. db 이름 SSoT = mysql 명령 인자 (`-D <db>` 또는 stdin 직접). 마이그 PR 머지 전 grep `^USE` 자동 검증 추가.

원리 04 · 조용한 사고가 진짜 사고

초록불 허수 테스트 — 안전장치를 빼도 통과

몰랐던 것

통과하는 테스트가 아무것도 검증하지 않을 수 있다는 것. 검증 조건을 넓게 잡으면 무관한 것에 걸려 '항상 초록불' 이 된다는 것 — 초록불은 '검증됨' 이 아니라 '실패 안 함' 일 뿐이라는 것.

무슨 일이 있었나

전자서명 백엔드의 동시성 안전장치(잠금·경합 방어) 검증 테스트가 전부 통과 중이었습니다. 통과 자체를 의심해 안전장치를 일부러 제거하고 재실행했더니 → 여전히 통과. 검증 범위를 전역으로 넓게 잡은 탓에 무관한 상태에 걸려 통과하던 허수 테스트였습니다. 장치가 실제로 작동하는지는 한 번도 검증된 적이 없었던 셈입니다.

배운 것

안전장치 테스트는 장치를 뺐을 때 반드시 실패해야 진짜입니다. 음성 대조(negative control) 없는 안전장치 테스트는 그럴듯한 거짓입니다 — 법적 증거를 다루는 제품에서 이게 조용히 살아 있었다는 게 핵심 교훈입니다.

영구 룰

모든 안전장치 테스트에 음성 대조 필수: (1) 블록 대상 문장을 정밀 지정, (2) 장치 제거 시 RED 확인 후에만 테스트 신뢰, (3) 신규 안전장치 = 음성 대조 페어로만 머지.

원리 04 · 조용한 사고가 진짜 사고
인프라02 incident

공용 호스팅 해외접속 차단 (DB IP 오진 정정)

공용 호스팅 해외접속 차단 (DB IP 오진 정정)
몰랐던 것

공용 호스팅이 특정 지역 외 IP 를 자동 차단할 수 있다는 것. CI 가 원격 DB 접속에 실패하면 'DB IP 가 바뀐 듯' 으로 오진하기 쉽다는 것.

무슨 일이 있었나

CI 가 호스팅 DB 에 접속하지 못했습니다. 확인 안 된 첫 추측 'DB IP 바뀐 듯' 으로 재발급까지 달렸고 → 계속 실패. 진짜 원인은 호스팅의 해외 접속 차단 정책이었습니다. 추측을 먼저 검증했으면 빨랐을 일입니다.

배운 것

원격 인프라 사고에서 첫 추측이 맞을 확률은 낮습니다. 확인 안 된 추측으로 달리면 시간만 날립니다 — provider 정책/방화벽/차단 룰을 사실로 먼저 확인해야 합니다.

영구 룰

CI/CD 인프라 사고 진단 표준: (1) 직접 접속 가능 여부 확인, (2) provider 차단 정책/방화벽 확인, (3) 마지막에 DB/앱 진단. 추측 X — 사실 우선.

진단 원칙 · 확인 안 된 첫 추측으로 달리지 마

Bridge req body 이중 등록 hang — CSP report 504

몰랐던 것

Node http 서버에서 `req.on('data')` 가 *이미 처리된 body* 위에 새로 등록되면 event 가 발화하지 않는다는 것. 라우터·미들웨어가 body 를 한 번 읽으면 그 stream 은 닫힌다는 것.

무슨 일이 있었나

csp-report 라우트가 504 로 누적됐습니다. 코드를 보면 라우터가 `req.on('data', ...)` 로 body 를 새로 읽으려는데, 상위 미들웨어가 이미 `req._body` 로 흡수한 상태였습니다. data event 가 발화하지 않으니 요청이 30초 hang → gateway timeout 504. 운영자에겐 *알 수 없는 504* 로만 보였습니다.

배운 것

Node http body stream 은 *한 번만 읽을 수 있습니다*. 미들웨어가 흡수했으면 라우터는 `req._body` 또는 `req._rawBody` 를 써야 합니다 — 새로 `req.on('data')` 등록 X. *조용한 hang* 이 사용자에겐 *조용한 사고* 입니다.

영구 룰

bridge 라우트는 `req._body` / `req._rawBody` 만 사용. `req.on('data')` 새 등록 영구 금지. 새 body-consuming 미들웨어 추가 시 라우터 호환성 검토 표준.

원리 04 · 조용한 사고가 진짜 사고
보안02 incident

VPS 죽음 — 원격 접속 설정

VPS 죽음 — 원격 접속 설정
몰랐던 것

원격 접속 설정 한 줄 잘못 박으면 VPS 가 영구히 접근 불가가 된다는 것. out-of-band 접근 없으면 복구 불가라는 것.

무슨 일이 있었나

보안 강화 중 원격 접속 설정을 동시에 여러 줄 수정했습니다. 데몬 재시작 후 즉시 disconnect → 새 연결 거부. out-of-band 경로가 없어 영구 lock out 됐습니다.

배운 것

'연결 자체' 의 root layer 변경은 한 번 잘못 박으면 끝입니다. 되돌릴 수 없는 변경엔 안전장치(병행 연결·확인 경로)를 *먼저* 깔고 손대야 합니다.

영구 룰

원격 접속 설정 변경 시: (1) out-of-band 접근 먼저 확보, (2) 기존 연결 열어둔 채 변경, (3) 변경 후 즉시 새 연결 검증, (4) 일정 시간 두 연결 동시 유지.

원칙 · 되돌릴 수 없는 변경엔 안전장치 먼저

멀티테넌트 RLS 누수 audit sweep

멀티테넌트 RLS 누수 audit sweep
몰랐던 것

RLS 가 모든 테이블에 일관 적용된 줄 알았던 것. 일부 join · 일부 view 가 tenant 격리 우회 가능하다는 것. cross-tenant 자동 회귀 테스트 0 이면 누수 발견 불가라는 것.

무슨 일이 있었나

테스트 시점 super_admin 권한으로 통과 → tenant 일반 사용자 권한이면 다른 tenant 데이터를 read 가능한 케이스 35건을 발견했습니다. RLS 정책은 박혀있는데 일부 query path 가 우회하고 있었습니다. 한 번에 sweep 으로 전 파일을 audit 했습니다.

배운 것

단일 룰 박는 것 (RLS) 만으로는 부족합니다. 룰을 검증하는 *자동 회귀 테스트* 가 같이 박혀있어야 누수가 안 생깁니다. 룰 + 검증 = pair.

영구 룰

멀티테넌트 라우트 신설 시 자동 강제 체크: (1) RLS 정책 정의, (2) cross-tenant 자동 회귀 테스트 추가, (3) super_admin 통과 + tenant 일반 사용자 격리 동시 검증, (4) PR 머지 전 회귀 PASS 확인.

원칙 · 룰은 검증과 pair
디자인02 incident

signup 흰화면 사고

몰랐던 것

자동 코드 변환(codemod) 이 의존성(import) 라인까지 같이 지우면 페이지가 *에러도 없이* 백지로 죽을 수 있다는 것. 자동화의 silent 위험.

무슨 일이 있었나

i18n 일괄 변환 codemod 가 무관한 import 라인까지 함께 제거했습니다. 새로 가입하는 사용자만 보는 signup 페이지가 백지로 떴고, 운영자에겐 에러가 안 떠 30분간 인지하지 못했습니다 (조용한 실패).

배운 것

일괄 자동변환이 import/의존성을 건드리면 silent fail 이 납니다. 게다가 사람이 안 보는 경로(신규 가입)에서 나면 사용자가 알릴 때까지 모릅니다 — 안 보는 경로일수록 시스템이 자동으로 잡게 강제해야 합니다.

영구 룰

codemod 자동 import 제거 영구 금지. 일괄 변환은 'imports 안 건드린다' 원칙 + 변경 직후 자동 빌드 검증(눈으로 나중에 X).

원리 04 · 조용한 사고가 진짜 사고

광고 ↔ 랜딩 톤 불일치 — 전환 0

광고 ↔ 랜딩 톤 불일치 — 전환 0
몰랐던 것

광고 크리에이티브와 랜딩 톤이 *분위기 수준에서* 어긋나면 전환이 *경고 없이* 0 으로 죽는다는 것. 기능·결제 다 정상이라 *코드 사고가 아닌 사고*.

무슨 일이 있었나

광고 캠페인 톤은 dramatic·hook 결인데 랜딩은 매거진·tranquil 결이었습니다. 사용자가 광고 클릭 후 *다른 사이트에 들어온 듯한 brand mismatch* 를 경험했고, 결제·기능이 다 정상인데 전환이 0 이었습니다. *조용한 매출 사고*.

배운 것

광고와 랜딩은 *같은 universe* 로 묶여야 합니다. 톤 mismatch = 사용자가 '잘못 들어왔다' 고 인식하고 즉시 이탈합니다 — 분위기는 cosmetic 이 아니라 conversion 의 본체입니다.

영구 룰

사이트와이드 톤 통일 (매거진 톤 등) + 광고/사이트 룰 분리 + 광고 launch 전 *랜딩 도착 시점 톤 매칭* 점검 표준. 톤 일관성은 코드 회귀처럼 매번 점검.

원리 04 · 조용한 사고가 진짜 사고
워크플로03 incident

단일 파일 900라인 누적 사고

단일 파일 900라인 누적 사고
몰랐던 것

기능을 같은 파일에 계속 추가하면 *어느 순간* 디버깅·리뷰·수정이 동시에 망가진다는 것. '나중에 분리해야지' 는 사람 의지로 안 막힌다는 것.

무슨 일이 있었나

워크스페이스 핵심 라우트 파일이 누적 900라인이 됐습니다. 한 파일에 인증·게시·승인·결제·알림·첨부가 다 박혀, 한 줄 수정에 무관한 기능 회귀가 자꾸 났습니다. 16시간 세션을 밀어붙이며 *기능 속도*만 보고 *구조 부채*를 무시한 결과였습니다.

배운 것

코드 비대화는 *기능 추가 속도*가 아니라 *룰·자동 분리* 강제만 막을 수 있습니다. 사람 의지로 '나중에 분리' 는 절대 안 됩니다 — 누적 임계를 넘으면 분리 비용이 fix 비용보다 커집니다.

영구 룰

*파일 500라인 초과 금지* + *새 기능 = 새 파일* + *한 task = 한 파일/한 함수/한 라우트* 영구 룰. 편집 중 500라인 넘어가면 즉시 알림 + 리팩토링 우선. 누적 후 분리 X.

원리 05 · 반복되면 원인은 하나가 아니다

스펙 미확정 후 실행 — sweep 4회 재작업

스펙 미확정 후 실행 — sweep 4회 재작업
몰랐던 것

3건+ 작업을 *대화 컨텍스트만 믿고* 진입하면 sweep 4회 재작업이 *기본값*이라는 것. 실행 속도 욕심이 *결과적으로 가장 느린* 패턴이라는 것.

무슨 일이 있었나

여러 변경을 PRD·승인 없이 *맥락 추측*으로 진입했습니다. 1차 작업 → 검토 → 어긋남 발견 → 재작업 → 또 어긋남 → 4회 sweep 으로 끝났습니다. 1회면 끝났을 작업이 4회로 늘어났습니다.

배운 것

'추측 즉시 행동' 은 *빠른* 게 아니라 *재작업 누적* 입니다. 큰 방향 재정의는 *task 재설계 먼저 → OK → 작업* 이 결과적으로 빠릅니다. 검증 안 한 가속은 음수 가속입니다.

영구 룰

3건+ 작업·큰 방향 변경·신규 라우트·결제·인가·외부 API 진입 시 *1페이지 PRD + 승인 후* 실행 표준. 컨텍스트 추측 X, 인용 + 해석 1줄 확인 우선.

원리 04 · 조용한 사고가 진짜 사고

'다 고쳤다' — AI 완료 보고의 그럴듯함

몰랐던 것

AI 의 '전체 코드를 훑고 전부 수정 완료' 보고가 실측과 다를 수 있다는 것. 틀린 보고보다 그럴듯한 보고가 위험하다는 것 — 겉보기 완결이라 리뷰로도 안 걸린다는 것.

무슨 일이 있었나

라이브 채팅 앱의 인프라 전면 이전 중 AI 가 '전체 수정 완료' 를 보고했습니다. 실측하니 수정 누락이 다수 — 놓친 호출 경로 하나가 곧 '어떤 사용자의 채팅이 조용히 죽는' 사고가 되는 조건이었습니다. 보고는 완결형인데 산출물은 아니었습니다.

배운 것

완료는 보고가 아니라 목록 대조로만 인정해야 합니다. 수정 대상 전수를 항목 단위 추적 문서로 먼저 뽑고, 하나씩 대조해야 누락 가능성을 줄일 수 있습니다. AI 산출물 검증의 기본기입니다.

영구 룰

이전·일괄 수정(sweep) 작업 표준: (1) 대상 전수 추적 문서 선작성, (2) 목록 대조로만 완료 판정, (3) '보고 신뢰' 영구 금지 — 완료 주장은 실측 후에만 인정.

원칙 · 완료는 보고가 아니라 목록 대조로