1화
블레임리스

1
새벽 한 시 사십 분, 회사 뒷골목의 24시간 술집에는 손님이 두 테이블뿐이었다.
회식은 열한 시에 끝났다. 사람들이 택시를 잡는 동안 서지안은 “한 잔 더 할 사람” 하고 물었고, 손을 든 건 하필 유도현 하나였다. 둘은 잠깐 서로를 쳐다봤다. 지안은 도현이 손을 내리길 바랐고, 도현은 지안이 말을 취소하길 바랐을 것이다. 어느 쪽도 그러지 않았다.
지안은 SRE 팀장이었고 도현은 결제팀 백엔드 개발자였다. 두 사람 사이가 안 좋다는 건 회사에서 굳이 설명이 필요 없는 사실이었다. 지난 분기 회고에서 도현은 SRE의 배포 동결 정책을 “게이트키핑”이라고 불렀고, 지안은 결제팀의 배포 체크리스트를 “장식”이라고 불렀다. 둘 다 회의록에 남았다.
그래도 술은 술이었다. 둘 다 소주를 맥주에 타지 않고 마시는 부류였고, 안주를 별로 시키지 않았고, 취해도 말이 많아지지 않았다. 그 정도 공통점이면 같은 테이블에 두 시간쯤 앉아 있을 수는 있었다.
“팀장님은 왜 그렇게 배포를 싫어하세요.”
“배포를 싫어하는 게 아니라 새벽에 깨는 걸 싫어해.”
“그럼 지금은 왜 안 주무시고.”
“이건 내가 골라서 깨 있는 거고.”
“온콜 아닌 주라서 마시는 거죠. 규정 있잖아요. 음주 상태로 프로덕션 접근 금지, 인시던트 지휘 금지.”
“그 규정 내가 썼어. 그러니까 온콜 아닌 주에 마시는 거야.”
도현이 잔을 채웠다. 지안의 잔이 먼저, 그다음 자기 잔. 그 순서는 지켰다. 벽에 걸린 TV에서는 소리를 죽인 야구 재방송이 나오고 있었고, 사장님은 카운터에서 졸고 있었다. 지안의 가방에는 늘 그렇듯 노트북이 들어 있었다. 온콜이 아닌 주에도 가방에서 빼지 않는 건 습관이었다.
두 시 십사 분, 테이블 위의 휴대폰 두 대가 동시에 울렸다.
2
PagerDuty 알림음은 어떤 상황에서도 사람을 일으켜 세운다. 지안은 잔을 내려놓고 휴대폰을 집었다.
[SEV1] payments-api 5xx rate 43% (threshold 5%) Service: payments-api / Region: kr-1 / Triggered 02:14:07
“결제야.” 지안이 말했다. 도현은 이미 자기 휴대폰을 뒤집어 보고 있었다.
지안은 테이블 위의 빈 소주병 두 개를 봤다. 규정은 알고 있었다. 지금 브리지 콜에 들어갈 수 있는 SRE는 온콜인 유리 하나였고, 온콜 혼자 SEV1을 지휘하게 두는 건 규정보다 나쁜 일이었다. 지안은 그렇게 결정했고, 그 결정이 나중에 어떻게 읽힐지도 알았다.
지안은 가방에서 노트북을 꺼내 테이블 위의 접시를 밀어냈다. 도현은 이어폰을 꽂고 밖으로 나갔다. 술집 유리문 너머로 그가 통화하듯 서성이는 게 보였다. 팀장과 팀원이 새벽 두 시에 같은 테이블에서 브리지 콜에 들어가는 그림을 굳이 만들 필요는 없었다.
브리지 콜 참가자 목록에 이름이 하나둘 올라왔다. 그 주 온콜이었던 SRE 한유리는 이미 들어와 있었다. 알림이 울린 지 41초 만에 페이지를 ack했다고 타임라인에 찍혀 있었다. 지난달에도 유리는 잠결에 알림을 받고 1분 안에 들어왔다. 지안은 이어폰을 고쳐 꽂았다. 적어도 첫 대응은 늦지 않았다.
“인시던트 커맨더 제가 잡겠습니다.” 지안이 말했다. “유리 씨, 지금 보이는 거 뭐예요?”
“payments-db 연결 수가 상한 500에 붙었어요. API 쪽 풀도 새 연결을 못 만들고 대기 큐가 쌓이고 있고요. 파드는 다 살아 있는데 DB를 못 잡아서 타임아웃 나는 거예요.”
“뭔가 커넥션을 먹고 있다는 건데. 배포 있었어요?”
“결제팀 마지막 배포는 어제 오후 네 시예요.” 도현의 목소리에 골목 바람 소리가 섞여 들어왔다.
“배포가 아니면 플래그거나 배치거나.” 지안은 스위치보드 콘솔 주소를 눌렀다. 스위치보드는 회사가 쓰는 피처 플래그 시스템이었다. 다시 로그인하라는 창이 떴다. 휴대폰에서 인증을 누르는 동안 유리가 말했다.
“reconcile-batch-v2 확인해볼게요. 그거 샤드마다 커넥션 따로 여는 거라 풀 안 타요.”
유리는 그 배치의 부하 테스트에도 참여했다. 지안은 인증 창을 닫고 프로덕션 변경 이력을 열었다.
reconcile-batch-v2 · OFF → ON · 02:11:52 · actor: 유도현
지안은 그 줄을 두 번 읽었다. 유리문 밖에서 도현이 걸음을 멈췄다. 그도 같은 화면을 휴대폰으로 보고 있는 모양이었다.
“02:11에 켜졌네요.” 유리가 말했다.
지안은 배치 이름 옆의 붉은 ‘운영 보류’ 표시를 봤다. 환불은 원장에 행만 도로 넣으면 끝나는 게 아니었다. 이미 계산된 가맹점별 잔액까지 다시 맞춰야 했다. 그 일을 한꺼번에 하는 v2는 부하 제한을 붙이지 못해 출시가 밀려 있었다.
“도현 씨, 이거 켰어요?”
기록을 위해 묻는 질문이었다. 02:11에 도현은 지안의 맞은편에 앉아 있었다. 휴대폰은 테이블 위에 엎어져 있었고, 두 손은 소주잔과 젓가락에 가 있었다. 지안은 그걸 봤다.
“아니요.” 잠깐 뜸을 들이고, 다시. “제가 아닙니다.”
지안은 적어도 도현이 그 시각에 직접 화면을 만지지 않았다는 건 알았다. 그리고 그걸 말하는 순간 무슨 일이 생기는지도 알았다. “02:11에 유도현은 내 맞은편에서 소주를 마시고 있었다”는 문장은 인시던트 채널에 적히는 즉시 다른 문장을 하나 더 만든다. 서지안은 소주 두 병을 나눠 마신 상태로 SEV1 인시던트 커맨더를 잡았다. 그 규정을 쓴 사람이 서지안이었다.
“누가 켰는지는 나중에. 지금은 끕니다. 유리 씨, 플래그 오프 부탁해요.”
“네, 하고 있어요. 콘솔이 좀 느려서요.”
플래그는 02:31에 꺼졌다. 커넥션 풀은 02:36에 정상으로 돌아왔고, 5xx 비율은 02:40에 임계치 아래로 내려갔다. 장애 시간 26분. 결제 실패 건수는 대략 1만 4천 건.
브리지 콜이 끝났을 때는 세 시 반이었다. 도현이 유리문을 열고 들어왔다. 바깥 공기 때문인지 술이 다 깬 얼굴이었다.
“저 아니에요.”
“알아.”
“아시죠. 팀장님은 보셨잖아요.”
“봤어.” 지안은 노트북을 덮지 않았다. “근데 내가 봤다고 말하려면 어디서 봤는지도 말해야 돼.”
도현은 테이블 위의 빈 병 두 개를 내려다봤다. 그도 규정은 알고 있었다. 잠시 서 있다가 계산서를 집어 들었다. 지안은 말리지 않았다.
3
토요일 아침, 지안은 커피를 내리고 타임라인을 다시 처음부터 썼다. 인시던트 커맨더의 습관이었다. 새벽 두 시의 기억을 믿기 어려울 때는 기록을 맞춰봤다. 같은 사건도 어떤 로그를 여느냐에 따라 다르게 보였다.
첫 번째 로그는 스위치보드 감사 로그였다. 콘솔 화면에는 “actor: 유도현”으로만 보였지만, API로 원본을 뽑으면 필드가 더 있었다.
{
"flag": "reconcile-batch-v2",
"env": "production",
"from": false, "to": true,
"at": "2026-09-11T02:11:52+09:00",
"actor": { "type": "api_token", "name": "ci-deploy-payments", "owner": "yoo.dohyun" },
"source_ip": "10.40.0.7"
}
UI 세션이 아니라 API 토큰이었다. 토큰 이름은 ci-deploy-payments. 결제팀 CI 파이프라인이 배포 후 플래그를 자동으로 조정할 때 쓰는 토큰이었고, 소유자가 도현으로 잡힌 건 그가 3주 전 그 파이프라인을 만들었기 때문이었다. 콘솔은 토큰 소유자를 actor로 표시한다. 그래서 “유도현”이었다.
그러니까 도현의 계정이 아니라 도현 이름이 붙은 토큰이었다. 그리고 그 토큰은 CI 러너의 비밀 저장소에 들어 있었다.
두 번째 로그는 비밀 저장소 접근 기록이었다. ci-deploy-payments 토큰 값을 마지막으로 읽은 건 CI 러너가 아니었다. 9월 8일 오후 두 시, sre-admin 권한을 가진 사람의 세션이 토큰을 읽었다. sre-admin 권한을 가진 사람은 회사에 둘이었다. 서지안과 한유리.
읽기 요청의 세션 ID를 로그인 기록에 대조했다. 유리의 사내 노트북에서 인증한 세션이었다. 지안은 자기 기록도 확인했다. 그날 오후 두 시에는 발표 자료를 띄운 채였고, 비밀 저장소 접근은 없었다.
세 번째는 IP였다. 10.40.0.7은 사무실 VPN 게이트웨이의 내부 주소였다. CI 러너가 쓰는 경로가 아니었다. VPN 접속 로그를 열자 그 시각에 연결되어 있던 사람은 셋이었다. 유리, 야근 중이던 데이터팀 인턴, 그리고 지안 자신. 도현의 이름은 없었다. 세 사람을 같은 주소로 내보내는 게이트웨이의 연결 기록을 더 내려받았다. 스위치보드가 받은 호출의 시각과 포트를 맞추자 유리의 VPN 세션이 남았다. 그 세션에 연결된 기기도 같은 사내 노트북이었다.
그건 알리바이보다 나은 것이었다. 적어도 지안에게는.
네 번째는 인시던트 채널의 메시지 타임스탬프였다.
02:17:03 한유리 reconcile-batch-v2 플래그 확인해볼게요. 그거 샤드마다 커넥션 따로 여는 거라 풀 안 타요.
지안이 스위치보드 변경 이력을 연 건 02:17:40이었다. 브리지 콜 녹취를 돌려보니 지안이 “배포가 아니면 플래그거나 배치거나”라고 말한 게 02:16:50이었다. 유리는 13초 뒤에 플래그 이름과 원인 메커니즘을 말했다. 그것만으로는 이상할 게 없었다. 부하 테스트를 했던 사람이었고, 첫 대응도 늘 빨랐다. 지안은 타임라인의 ‘원인을 미리 알고 있었음’을 지우고 ‘원인 후보를 빠르게 지목’이라고 고쳤다.
다섯 번째는 플래그를 끄는 데 걸린 시간이었다. 유리가 “하고 있어요”라고 말한 게 02:18. 실제로 꺼진 게 02:31. 13분. 스위치보드 콘솔 응답 시간 대시보드에서 그 시각의 p99는 400밀리초였다. 지안 자신도 로그인에서 시간을 썼다. 전체 응답 시간이 짧다고 유리 화면도 정상이었다고 할 수는 없었다.
지안은 유리 세션의 요청 기록을 따로 뽑았다. 02:18부터 상태 조회는 몇 초 간격으로 성공하고 있었다. 플래그를 끄는 요청은 02:31에 처음 들어왔고, 183밀리초 만에 성공했다. 실패한 변경 요청은 없었다. 그사이 유리가 반복해서 열어본 건 배치 진행률 화면이었다.
지안은 배치 워커 로그를 열었다. reconcile-batch-v2는 02:12:04에 시작해서 02:29:47에 완료 상태로 끝났다. 플래그가 꺼지기 1분 13초 전이었다. OFF를 받으면 다음 청크에서 중단하는 배치였는데, 중간에 끊긴 게 아니라 끝까지 돈 것이다. 그 배치가 하는 일은 원본 결제 이벤트에서 정산 원장과 가맹점별 잔액을 다시 계산해서 덮어쓰는 것이었다. 누락된 환불이 있었다면 원장과 잔액을 함께 맞춰놓았을 것이다.
지안은 커피가 식는 것도 잊고 정산 원장의 스냅샷을 뒤졌다. 목요일 밤 백업과 금요일 새벽 배치 이후 상태를 비교했다. 차이는 정확히 412건이었다. 8월 28일에 처리된 환불 건들이 목요일 백업에는 없었고, 배치 이후에는 있었다.
8월 28일. 지안은 그 날짜를 알고 있었다. 정산 DB의 파티션을 정리하는 수동 마이그레이션이 있었던 날이었다. 작업자는 한유리였고, 작업 후 검증 체크리스트에는 “이상 없음”이라고 적혀 있었다.
그리고 월요일에는 분기 회계 감사가 잡혀 있었다.
4
월요일 오후 두 시, 포스트모템 회의실.
플랫폼 본부장 강민석이 먼저 결론을 말하는 스타일이라는 걸 지안은 알고 있었다.
“자, 우리 문화가 뭐예요. 블레임리스. 누가 잘못했냐가 아니라 시스템이 왜 그걸 허용했냐. 결제팀 CI 토큰이 프로덕션 플래그를 만질 수 있었다, 이게 문제죠. 액션 아이템은 토큰 권한 축소, 플래그 변경 승인 절차. 도현 씨, 부담 갖지 말고. 개인 실수는 여기서 안 따집니다.”
“실수가 아니었으니까 따져야 합니다.” 지안이 말했다.
회의실이 조용해졌다. 도현이 지안을 봤다. 유리는 노트북 화면을 보고 있었다.
지안은 타임라인을 화면에 띄웠다. 토큰을 읽은 세션과 플래그를 켠 연결 기록. 배치 진행률 조회와 첫 OFF 요청. 마지막 화면에는 배치 전후의 원장을 나란히 놓았다. 412건. 8월 28일.
“블레임리스는 실수를 숨기지 않게 하려고 쓰는 원칙이에요. 그런데 여기서는 복구하겠다는 말과 실제 행동이 달라요. 끄겠다고 한 뒤에 배치 진행률만 보고 있었어요. 그 배치가 끝나자 감사에서 문제가 될 환불 412건이 돌아왔고요.”
“지안 씨.” 민석이 말했다. “그건 추정이잖아요.”
“토큰을 읽은 세션과 플래그를 켠 VPN 연결이 모두 유리 씨 노트북으로 이어져요. OFF 요청은 배치가 끝난 뒤에 처음 들어왔고요.” 지안은 유리를 봤다. “유리 씨, 이 기록에서 틀린 부분이 있어요?”
유리가 고개를 들었다. 눈이 빨갛지도, 떨리지도 않았다. 지안이 예상한 것보다 훨씬 지친 얼굴이었다.
“412건이요.” 유리가 말했다. “제가 지웠어요. 파티션 드롭할 때 날짜 조건을 잘못 걸었고, 확인은 건수만 했는데, 그 파티션 건수가 원래 적어서 눈에 안 띄었어요. 다음 날 알았어요. 환불이 원장에 없으면 감사에서 뭐가 되는지 아시죠. 제가 그 자리에서 얘기했으면 됐는데, 못 했어요. 그때 얘기했으면 실수였을 거예요. 근데 2주를 들고 있었더니 실수가 아니게 되더라고요.”
“복구 요청을 올릴 수 있었잖아요.”
“환불 복구는 재무 확인을 받아야 해요. 왜 잔액이 틀렸는지 쓰고요. v2는 원장하고 잔액을 한 번에 맞추니까, 그걸 먼저 돌리면 요청서를 안 써도 될 줄 알았어요.”
“장애는요.” 지안이 물었다. “커넥션 풀 터질 거 알았어요?”
“알았어요. 새벽 두 시면 결제가 제일 적을 때니까, 20분이면 괜찮을 거라고 생각했어요.” 유리는 잠깐 말을 멈췄다. “1만 4천 건은 생각 못 했어요. 그건 정말로.”
“도현 씨 이름으로 남게 한 건요.”
“그건.” 유리가 처음으로 도현을 봤다. “그건 토큰이 그거밖에 없어서요. 근데 그 이름이 찍힌다는 것도 알고 있었으니까, 몰랐다고 하면 거짓말이에요. 죄송합니다.”
도현은 아무 말도 하지 않았다. 지안은 그가 뭐라고 할지 알 것 같았고, 그가 아무 말도 하지 않는 편이 낫다는 것도 알았다.
민석이 안경을 벗었다. “포스트모템 문서는 지안 씨가 다시 써 주세요. 이건 제가 위로 가져가야 하는 건이고요.”
5
회의가 끝나고, 지안은 옥상으로 올라갔다. 도현이 5분쯤 뒤에 따라 올라왔다. 우연히 올라온 사람처럼 반대편 난간에 섰다.
“금요일 새벽에 그냥 말하셨어도 됐잖아요. 같이 있었다고.”
“그 말 하면 그다음 문장이 뭔지 알잖아. 두 병 마시고 SEV1 잡았다. 규정 쓴 사람이.”
“그래도요.”
“토요일 아침에 로그 뽑으면서 계속 생각했어. 이게 안 나오면 어떡하지.” 지안은 난간에 팔을 얹었다. “안 나왔으면 말했을 거야. 그러고 나서 규정에 내 이름도 적었겠지.”
“규정 고치실 거예요?”
“아니. 규정은 맞아. 내가 틀린 거지.”
지안은 휴대폰을 꺼내 도현에게 보여줬다. 회의가 끝나고 민석과 보안팀에 보낸 메일이었다. 음주 후 프로덕션 접근과 인시던트 지휘. 브리지 콜 시작 시각까지 적혀 있었다.
“네가 밖에서 콜 들어온 것도 적었어. 네가 한 일은 네가 확인해서 보내.”
도현이 짧게 웃었다. 웃음이라기보다 숨을 내쉰 것에 가까웠다.
“토큰 권한은 제가 줄일게요. 이번 주 안에.”
“체크리스트에 넣어.”
“장식이라면서요.”
“장식이라도 있으면 뭘 안 했는지는 보이잖아.”
바람이 불었다. 둘은 한동안 각자의 난간에서 각자의 방향을 보고 서 있었다. 내려가려던 도현이 계단 앞에서 걸음을 멈췄다.
“다음 회식 끝나고요. 그 집 또 가세요?”
“싫어.”
지안은 잠깐 있다가 덧붙였다.
“열한 시 반. 늦으면 먼저 시작해.”