Big Project · AI's EYE 42
테스트 성공 후 공개 화면 장애 진단 및 최종 QA
자동 테스트와 build가 통과하면 공개 화면에서 보이는 문제도 새 코드 bug라고 단정하기 쉽다. 배포 환경과 오래 실행된 생산자를 어떻게 분리해 보는지 막막하다.
근거 · 프로젝트 문서 · 코드 · 테스트
1장: 관측과 테스트의 불일치: 프런트엔드부터 보기
솔라의 책상 위 모니터 두 개가 극명한 대비를 이루고 있었다. 왼쪽 모니터의 CI/CD 파이프라인 대시보드는 온통 초록색이었다. Build 단계부터 수백 개의 테스트 케이스를 실행하는 Test 단계를 거쳐 Deploy 단계까지, 모든 것이 완벽하게 성공했음을 알리는 초록색 체크 표시가 선명했다. 문제가 된 코드를 수정하고 배포한 지 10분이 채 지나지 않았다.
하지만 오른쪽 모니터에 떠 있는 실제 서비스 대시보드는 몇 분째 멈춰 있었다. 그래프는 마지막 수치를 그린 채 미동도 없었고, 화면 구석의 ‘마지막 업데이트’ 시각은 15분 전에 멈춰 있었다. 성공의 증거와 실패의 증거가 나란히 놓여있는 셈이었다.
솔라는 마른침을 삼키며 두 화면을 번갈아 보았다. “말도 안 돼. 이번엔 정말 다 고쳤다고 생각했는데. 테스트도 전부 통과했잖아. 그런데 왜 화면은 그대로인 거야?”
솔라의 혼잣말은 좌절감에 가까웠다. 자동화된 테스트가 완벽하게 통과했다는 것은 곧 코드의 논리적 결함이 없다는 뜻으로 믿어왔다. 그렇다면 지금 눈앞의 이 장애는, 테스트가 잡아내지 못하는 또 다른 미지의 버그이거나, 혹은 자신이 수정한 코드가 어딘가에서 새로운 문제를 일으켰다는 뜻이었다. 머릿속이 복잡해졌다.
그때, 조용히 다가온 루나가 솔라의 시선이 머무는 곳을 함께 바라봤다. 루나는 모든 것이 성공했다고 외치는 왼쪽 모니터에는 눈길도 주지 않았다. 그녀의 시선은 오직 멈춰버린 오른쪽 서비스 화면에 고정되어 있었다.
“솔라.”
루나는 파이프라인의 성공 여부나 코드의 잘잘못을 묻지 않았다. 대신 멈춘 화면을 가리키며 물었다.
“우리가 지금 보는 이 화면, 사용자가 보는 화면과 똑같지? 사용자가 지금 뭘 경험하고 있을까?”
“그야… 데이터가 갱신되지 않는 경험이지.” 솔라가 마지못해 대답했다. “화면이 그냥 멈춰있으니까.”
“그럼, 거기서부터 시작하자. ‘화면이 멈춰있다’는 건 우리가 관찰한 사실이야. 테스트가 통과했다는 건 또 다른 사실이고. 두 사실이 충돌할 땐, 일단 눈에 보이는 현상부터 제대로 정의해야 해. 코드를 보기 전에.”
루나의 말은 솔라의 시선을 코드 편집기와 테스트 결과에서 사용자의 화면으로 강제로 돌려놓았다. 늘 하던 대로 브라우저의 개발자 도구를 여는 솔라의 손길이 조금은 망설였다. ‘코드가 문제가 아니라면, 뭘 봐야 하지?’
“화면이 멈춘 것처럼 보이는 이유는 뭘까? 정말로 아무 일도 일어나지 않고 있을까? 아니면 무언가 시도는 하고 있는데 결과가 없을 뿐일까?”
루나의 질문에 솔라는 개발자 도구의 ‘네트워크(Network)’ 탭을 열었다. 화면에는 빼곡한 기록들이 실시간으로 쌓이고 있었다. 솔라의 눈이 커졌다.
“어? 이상하다. 요청은 계속 보내고 있어. 5초에 한 번씩, 대시보드 데이터를 달라고 서버에 계속 요청하고 있네.”
솔라는 스크롤을 내려보았다. /api/dashboard/summary 라는 주소로 나가는 요청들이 하나같이 붉은색으로 실패하거나, 혹은 성공(200 OK) 표시가 떠도 응답 데이터의 크기가 터무니없이 작았다. 응답 내용을 열어보니, 15분 전의 데이터가 캐시된 채 그대로 돌아오고 있었다.
“아…”
짧은 탄식이 터져 나왔다. 프런트엔드 코드는 잘못이 없었다. 정해진 시간마다 꼬박꼬박 서버에 데이터를 요청하는, 자신의 임무를 성실히 수행하고 있었다. 다만 서버가 그 요청에 제대로 된 최신 데이터를 주지 않았을 뿐이다. 화면이 ‘멈춘’ 게 아니라, ‘새로운 그림을 그릴 재료를 받지 못해’ 멈춰 있었던 것이다.
솔라는 고개를 들었다. “프런트엔드 코드는 문제가 아니었어. 데이터를 달라고 계속 요청은 하고 있었으니까. 내가 짠 코드는 정상적으로 작동하고 있었던 거야. 테스트가 통과한 건 당연했네.”
지금까지 솔라는 ‘테스트 통과’와 ‘화면 장애’라는 두 가지 사실 사이에서 길을 잃고 있었다. 둘 중 하나는 거짓이거나, 아니면 자신의 코드에 대한 믿음이 틀렸다고 생각했다. 하지만 이제 두 사실을 분리해서 볼 수 있게 되었다. 자동화된 테스트는 코드 자체의 논리적 무결성을 증명했고, 그 증명은 옳았다. 그리고 공개 화면의 장애는 그 코드와는 다른 계층에서 발생한 문제라는 것 또한 명백해졌다.
‘클라이언트 측 관측 분리.’ 솔라는 속으로 새로운 원칙을 정리했다. 문제가 발생하면, 코드나 테스트 결과를 먼저 의심하는 대신, 사용자가 보는 화면에서 무슨 일이 일어나는지부터 정확하게 관찰하고 분리해야 한다는 것.
그러나 한 가지 의문이 풀리자, 더 큰 질문이 고개를 들었다. 솔라는 다시 루나를 바라보았다.
“언니, 프런트엔드에서는 문제가 없다는 걸 확인했어. 요청은 제대로 가고 있으니까. 그럼 이제… 어디를 봐야 해? 백엔드는 너무 넓어서 어디서부터 시작해야 할지 감도 안 와.”
2장: API 상태 추적: 실시간 백엔드 진단
솔라의 책상 위, 작은 메모장이 이전의 혼란을 정리하고 있었다. 언니 루나가 방금 가져온 것이었다. 메모장의 맨 위에는 ‘진단 경로’라는 제목이 쓰여 있었다. 그리고 그 아래, 첫 번째 항목.
1. 클라이언트 (프런트엔드)
관찰: 화면 멈춤진단: 5초마다 데이터 요청 (Polling) 정상결론: 클라이언트 코드 문제 아님
솔라는 잠시 모니터에서 눈을 떼고 메모장을 바라보았다. 방금 전까지만 해도 ‘테스트 통과’와 ‘화면 장애’라는 두 개의 거대한 사실 사이에서 어쩔 줄 몰라 하던 마음이 조금은 가라앉는 기분이었다. 하나의 가설을 명확하게 지워낸 흔적이었다.
루나는 솔라가 보고 있던 개발자 도구의 네트워크 탭에서, 반복적으로 나타나는 /api/dashboard/summary라는 주소를 손가락으로 가볍게 짚었다. “우리가 확인한 프런트엔드의 유일한 연결고리네. 여기서부터가 백엔드의 시작이야. ‘백엔드가 너무 넓다’고 생각하기 전에, 바로 이 문을 열어보는 거지.”
그 말에 솔라는 정신을 차렸다. 맞다. 백엔드라는 막연한 대상을 상대할 필요는 없었다. 프런트엔드와 대화하는 유일한 창구, 바로 이 API 엔드포인트가 다음 진단 대상이었다.
솔라는 망설임 없이 새 브라우저 탭을 열고 주소창에 직접 API 주소를 입력했다. 화면에는 사람의 눈으로 읽기 어려운 JSON 데이터가 빼곡하게 나타났다. 솔라는 거의 반사적으로 응답 속도를 확인했다. 100밀리초도 채 걸리지 않았다.
“언니, 이것 봐. API는 전혀 문제없어. 호출하니까 바로 응답이 오잖아. 상태 코드도 ‘200 OK’고.”
솔라의 목소리에는 일말의 자신감이 섞여 있었다. 프런트엔드에 이어 API까지 무죄라면, 문제는 더 깊은 곳에 있을 것이고, 자신의 추리가 옳은 방향으로 가고 있다는 신호처럼 느껴졌다. API가 빠르고 정상적으로 응답한다면, 일단 API 서버 자체의 문제는 아니라고 생각하기 쉬웠다. 하지만 루나는 솔라의 결론에 동의하지도, 반박하지도 않았다. 대신 조용한 질문을 던졌다.
“응답이 빠른 건 좋은 신호지. 그럼, 그 빠른 응답 안에 뭐가 들어있어? 우리가 기대하는 데이터가 맞아?”
“어…?”
솔라는 순간 말문이 막혔다. 응답이 왔다는 사실에만 집중했을 뿐, 그 내용이 무엇인지는 자세히 들여다보지 않았다. 마치 택배가 현관 앞에 도착했다는 알림만 보고, 상자 안에 주문한 물건이 제대로 들어있는지는 확인하지 않은 셈이었다.
솔라는 서둘러 JSON 응답 내용을 훑어보기 시작했다. 여러 데이터 필드들 속에서 last_updated_at 이라는 익숙한 필드가 눈에 띄었다. 그 값은 멈춰버린 대시보드 화면에서 봤던 시각과 정확히 일치했다. 20분도 더 지난 시각이었다.
“아…”
두 번째 탄식이 터져 나왔다. API는 ‘응답’은 하고 있었지만, ‘올바른 대답’을 하고 있지는 않았다. 프런트엔드가 매번 새로운 데이터를 달라고 문을 두드리면, 백엔드는 20분 전의 낡은 데이터를 주면서 “여기 있어!”라고 대답하고 있었던 것이다.
솔라는 고개를 저었다. “API가 거짓말을 하고 있었네. 정상인 척하면서 오래된 데이터를 계속 주고 있었어.”
이제 문제의 범위는 훨씬 좁혀졌다. 프런트엔드가 아닌 백엔드 문제라는 것이 명확해졌고, 그중에서도 /api/dashboard/summary 라는 특정 API가 stale(오래된) 데이터를 반환하는 현상까지 특정했다.
‘API 상태 확인.’ 솔라는 속으로 두 번째 원칙을 세웠다. API의 상태를 확인할 때는 단순히 응답이 오는지, 빠른지만 볼 것이 아니라, 그 응답에 담긴 ‘내용의 신선도’까지 확인해야 한다는 것. 이제 솔라의 머릿속 진단 경로에 두 번째 줄이 추가될 차례였다.
2. API (백엔드 게이트웨이)
진단: /api/dashboard/summary 직접 호출관찰: 200 OK, 빠른 응답. 하지만 데이터가 20분 전 상태.결론: API가 최신 상태를 반환하지 못함.
그러나 이 결론은 또 다른 질문으로 이어졌다. 솔라는 다시금 막막함을 느꼈다.
“좋아, API가 범인이라는 건 알겠어. 그런데 왜? 이 API는 어디서 데이터를 가져오는 거지? 자체적으로 데이터를 저장하고 있나? 아니면… 데이터베이스에서 가져오는 건가?”
3장: 데이터 영속성 추적: DB와 생산자 확인
솔라의 ‘진단 경로’ 메모장은 이제 세 번째 항목을 맞이할 준비를 하고 있었다. 하지만 빈 칸 앞에서 솔라의 펜은 길을 잃었다. 앞선 두 단계는 명확했다. 사용자의 화면(클라이언트)에서 시작해, 시스템의 정문 격인 API까지 도달했다. API가 오래된 데이터를 내어준다는 사실까지는 밝혀냈다. 그러나 그 API는 어디서부터 잘못된 것일까?
솔라는 API 서버의 코드를 열어 /api/dashboard/summary 엔드포인트의 로직을 뚫어져라 쳐다봤다. 코드는 생각보다 단순했다. 요청이 들어오면, 캐시를 먼저 확인하고, 캐시에 데이터가 없으면 데이터베이스에서 정보를 조회해 반환하는 구조였다. 캐시 TTL(Time-To-Live)은 30초로, 현재 장애와는 무관해 보였다. 그렇다면 답은 하나였다. 데이터베이스.
루나는 솔라의 시선이 코드와 메모장 사이를 오가는 것을 보더니, 조용히 아키텍처 다이어그램이 그려진 다른 노트를 펼쳐 솔라 앞에 놓았다. 복잡한 시스템의 흐름 속에서, 루나는 API Server와 PostgreSQL DB 부분을 손가락으로 이었다.
“API는 문지기 같은 거야. 창고에 가서 물건을 가져다줄 뿐이지. 문지기가 낡은 물건을 준다면, 창고 자체에 새 물건이 없는 건 아닐까?”
그 말에 솔라는 정신을 차리고 터미널 창을 열었다. 익숙한 명령어로 운영 데이터베이스에 접속했다. “API가 보는 데이터베이스, 직접 확인해볼게.”
솔라의 손가락이 빠르게 키보드를 두드렸다.
SELECT * FROM dashboard_summaries ORDER BY updated_at DESC LIMIT 1;
결과가 즉시 터미널 화면에 나타났다. 데이터는 있었다. 비어 있지 않았다. 솔라는 잠시 안도했다. ‘데이터베이스에 데이터는 있네. 그럼 대체 뭐가 문제…’ 거기까지 생각했을 때, 솔라는 결과에 찍힌 updated_at 컬럼의 값을 보고 말문이 막혔다.
2024-05-20 14:02:11
API가 뱉어내던 타임스탬프와 정확히 같은 시각이었다. 거의 30분 전이었다. 솔라는 허탈한 웃음을 터뜨렸다. “아… 창고에 새 물건이 아예 없었구나.”
데이터베이스에 데이터가 존재한다는 사실만으로는 충분하지 않았다. 그 데이터가 ‘최신’이라는 증거가 필요했다. 솔라는 데이터베이스에 데이터가 있다는 사실에 안심하며, 그 내용의 신선도를 확인하는 것을 놓칠 뻔했다.
진단 경로 메모장에 세 번째 항목이 적혔다.
3. 데이터베이스 (PostgreSQL)
진단: dashboard_summaries 테이블 직접 조회관찰: 가장 최신 데이터의 updated_at이 30분 전에 멈춰 있음결론: DB에 최신 데이터가 저장되지 않고 있음
이제 진짜 질문이 시작되었다. API는 데이터베이스에서 읽어올 뿐이고, 데이터베이스는 누군가 넣어준 데이터를 보관할 뿐이다. 그렇다면 이 창고에 물건을 채워 넣는 역할은 누가 하는가? 그리고 왜 그 일을 멈췄는가?
솔라는 다시 루나가 펼쳐놓은 아키텍처 다이어그램을 보았다. PostgreSQL DB를 향해 화살표를 보내는 또 다른 컴포넌트가 있었다. Vision Worker라는 이름의 박스였다. 시스템의 눈(Vision)처럼, 여러 소스로부터 정보를 수집하고 가공해서 데이터베이스에 의미 있는 요약 데이터를 만들어 넣는, 일종의 데이터 생산자(Producer)였다.
“이 Vision Worker라는 녀석이 범인이네. 이 녀석이 일을 안 하고 있었던 거야.”
솔라는 즉시 운영 서버의 로그 시스템에 접속해 Vision Worker의 로그를 검색했다. 화면 가득 로그가 쏟아지는가 싶더니, 이내 스크롤이 멈췄다. 가장 마지막에 찍힌 로그는 불안할 정도로 조용했다.
INFO: Successfully processed and saved summary data. Run ID: 8a2d...
그리고 그 로그가 찍힌 시각. [2024-05-20 14:02:11]. 데이터베이스에 마지막으로 기록된 시각과 소름 돋게 일치했다. 그 이후로는 단 한 줄의 로그도 없었다. 오류 메시지도, 경고도 없이 그냥 멈춰버린 것이다.
드디어 타임라인이 하나로 맞춰졌다. Vision Worker가 14시 2분 11초에 마지막 작업을 하고 멈췄다. 그 결과 데이터베이스에는 더 이상 새로운 데이터가 공급되지 않았다. API는 계속해서 오래된 DB 데이터를 읽어다 프런트엔드에 전달했고, 사용자는 멈춰버린 화면을 보게 된 것이다.
솔라는 눈을 감고 전체 흐름을 되짚었다. 화면의 멈춤에서 시작해, 프런트엔드의 요청, API의 낡은 응답, 데이터베이스의 낡은 기록, 그리고 마침내 일을 멈춘 데이터 생산자의 침묵까지. 흩어져 있던 단서들이 하나의 시간선 위에 정렬되는 기분이었다.
‘데이터 타임라인 대조.’ 문제의 증상이 나타난 곳과 근본 원인이 발생한 곳은 시스템의 다른 계층, 다른 시간대에 있을 수 있다. 각 계층의 타임스탬프를 따라 역주적하는 것이 길을 잃지 않는 방법이었다.
“이제 원인은 찾았네. 이 Vision Worker 프로세스를 재시작하면 일단 해결되겠지.” 솔라가 어느 정도 자신감을 되찾은 목소리로 말했다.
하지만 솔라가 터미널에 재시작 명령어를 입력하려는 순간, 루나가 조용히 질문을 던졌다.
“만약, 그 워커가 멈춘 게 아니라… 우리가 방금 배포한 코드에 논리적인 오류가 있어서, 14시 2분 11초 이후의 데이터를 처리하지 못하게 된 거라면 어떡할까? 재시작해도 똑같은 지점에서 멈추지 않을까?”
4장: 배포된 코드와 프로세스의 불일치 추적
솔라의 손가락이 터미널 창 위에서 허공에 멈췄다. restart 명령어가 손끝에서 맴돌았다. 모든 증거가 Vision Worker라는 데이터 생산자의 침묵을 가리키고 있었고, 재시작은 가장 논리적인 다음 단계처럼 보였다. 하지만 언니, 루나의 마지막 질문이 뇌리에 박혀 떨어지지 않았다.
“만약, 그 워커가 멈춘 게 아니라… 우리가 방금 배포한 코드에 논리적인 오류가 있어서, 14시 2분 11초 이후의 데이터를 처리하지 못하게 된 거라면 어떡할까?”
그 질문은 솔라가 애써 외면하고 있던 불안감을 정면으로 겨누었다. 만약 그렇다면, 재시작은 아무 의미가 없다. 프로세스는 같은 지점에서 또다시 멈출 것이다. 더 최악은, 문제의 원인이 새로 배포된 코드에 있다는 사실을 모른 채, 엉뚱한 곳을 헤매게 될 거란 점이다.
솔라는 책상 위 ‘진단 경로’ 메모를 내려다보았다. 프런트엔드에서 시작해 API, 데이터베이스를 거쳐 데이터 생산자인 워커 프로세스까지, 차근차근 밟아온 여정이 기록되어 있었다. 데이터의 흐름은 명확하게 추적했다. 하지만 데이터의 흐름과 코드의 흐름은 다른 이야기였다. 솔라는 ‘최신 코드가 배포되면 항상 최신 로직이 실행된다’고 너무나 당연하게 믿고 있었다. 테스트까지 모두 통과한 코드였으니 의심할 여지가 없다고 생각했다.
루나는 솔라의 시선이 머무는 곳을 말없이 지켜보다가, 왼쪽 모니터의 CI/CD 파이프라인 화면을 가리켰다. 모든 단계가 초록색으로 빛나며 ‘성공적으로 배포됨’이라고 외치고 있었다. 그리고는 다시 오른쪽 모니터, Vision Worker의 상태를 보여주는 터미널 창을 가리켰다.
“‘배포 성공’이라는 사실과, ‘프로세스가 실행 중’이라는 사실. 이것도 아까처럼 두 개의 다른 사실로 분리해서 봐야 하지 않을까?”
그 말에 솔라는 머리를 한 대 맞은 듯한 기분이 들었다. ‘테스트 통과’와 ‘화면 장애’를 분리했던 것처럼, ‘코드 배포’와 ‘프로세스 실행’을 분리해서 생각해야 한다는 뜻이었다.
“정말… 우리가 배포한 바로 그 코드가 저기서 돌고 있다는 증거가 필요하겠구나.”
솔라는 즉시 행동에 나섰다. 먼저, CI/CD 파이프라인 로그를 확인해 오늘 배포된 코드의 정확한 버전을 확인했다.
Git Commit: a4b1c8f
이것이 바로 몇 시간 전, 솔라가 버그를 수정하고 올린 코드의 고유 식별자였다. 이 코드를 기반으로 새로운 컨테이너 이미지가 만들어졌을 것이다. 다음은 그 이미지가 실제 운영 환경에 잘 떠 있는지 확인할 차례였다. 솔라는 운영 클러스터에 명령어를 날려 Vision Worker가 사용 중인 이미지 정보를 조회했다.
Image: our-registry.com/vision-worker:a4b1c8f
이미지 태그까지 정확하게 일치했다. 솔라는 순간 안도했다. “언니, 봐봐. 최신 코드로 만든 이미지가 정확히 실행되고 있어. 내 코드는 문제가 아니야.” 그녀의 목소리에는 다시 자신감이 실렸다.
하지만 루나는 고개를 젓지 않았다. 대신, 아주 단순한 질문을 하나 더 던졌다.
“그럼 그 프로세스는 언제 시작됐지?”
“언제 시작됐냐니? 배포할 때 새로 시작됐겠지…” 솔라는 대답하다 말고 스스로의 말을 의심했다. ‘…그랬을까?’
솔라는 떨리는 손으로 프로세스의 상세 정보를 조회하는 명령어를 입력했다. 온갖 정보들 사이에서, Started At 이라는 필드가 눈에 들어왔다.
Started At: 2024-05-19 09:30:15
어제 아침이었다.
솔라의 머릿속이 하얘졌다. 오늘 배포가 일어난 시각은 오후 1시 50분. 하지만 지금 일을 하고 있어야 할 Vision Worker 프로세스는 어제 아침에 시작된 구버전 그대로였다. 배포 파이프라인은 최신 코드로 새 이미지를 만들어 레지스트리에 밀어 넣었고, 시스템의 배포 설정은 그 최신 이미지를 가리키도록 업데이트됐다. 하지만 정작 그 변경사항을 적용받아 재시작되어야 할 단 하나의 프로세스가, 그 모든 과정을 무시한 채 어제부터 계속 살아 있었던 것이다.
“말도 안 돼…”
허탈한 탄식이 흘러나왔다. 솔라는 진실을 마주했다. ‘배포’는 코드가 담긴 컨테이너 이미지를 교체하는 행위이지만, 그 이미지를 사용하는 ‘프로세스’가 실제로 재시작되지 않으면 아무 의미가 없었다. 마치 자동차 엔진오일을 새것으로 갈아주겠다고 창고에 가져다만 놓고, 정작 차에는 옛날 오일이 그대로 들어있는 격이었다.
솔라는 비로소 진단 경로 메모장의 마지막 퍼즐 조각을 맞출 수 있었다.
4. 배포 라이프사이클
진단: Git Commit, Container Image, Process 시작 시각 대조관찰:Git Commit: a4b1c8f (오늘 배포)Container Image: ...:a4b1c8f (정상)Process 시작 시각: 어제 09:30 (구버전)
결론: 배포 성공했으나, 대상 프로세스가 재기동되지 않아 구버전 코드가 계속 실행 중.
‘배포 라이프사이클 대조’. 솔라는 새로운 원칙을 마음속에 새겼다. 코드의 버전, 아티팩트(이미지)의 버전, 그리고 실행 중인 프로세스의 라이프사이클을 각각 따로 확인하고 시간순으로 대조해야만 진짜 현실을 볼 수 있었다.
이제야 솔라는 확신을 갖고 터미널에 명령어를 입력했다. 낡은 프로세스를 강제로 종료하고, 새로운 버전의 이미지를 사용해 다시 시작하는 명령어였다. 잠시 후, Vision Worker의 로그 창에 새로운 로그가 찍히기 시작했다. 멈춰 있던 오른쪽 모니터의 대시보드 그래프가 드디어 다시 움직이기 시작했다.
문제는 해결됐다. 하지만 솔라의 마음 한구석에는 찜찜함이 남았다.
“고치긴 했는데… 이건 우리가 운이 좋았던 거 아냐? 만약 이런 상황을 또 마주치면 어떡하지? 우리가 고친 것과, 여전히 우리 시스템에 남아있는 이런 배포 프로세스의 한계를 어떻게 정리해야 할까?”
5장: 최종 QA와 한계 장부 (Limit Ledger) 작성
대시보드 화면의 그래프가 다시 부드럽게 흘러가기 시작한 지 10분이 지났다. 솔라의 책상 위 ‘진단 경로’ 메모에는 클라이언트부터 배포 라이프사이클까지, 겹겹이 쌓인 시스템의 단층을 뚫고 내려간 4단계의 추적 과정이 선명하게 기록되어 있었다. 승리의 기록이었다. 하지만 솔라의 표정은 개운치 않았다. 그녀는 이제 팀 전체에 공유할 장애 보고서 초안을 작성하고 있었다. 하지만 몇 줄을 채 쓰지 못하고 멈춰 섰다.
장애 원인: Vision Worker 프로세스가 구버전으로 실행 중이었음.
조치 사항: 해당 프로세스를 수동으로 재기동하여 해결.
틀린 말은 아니었다. 하지만 이 보고서로는 부족하다는 느낌을 지울 수 없었다. 이것은 그저 ‘이번에는 운 좋게 해결했다’는 무용담일 뿐, 다음에 같은 일이 벌어지는 것을 막아주지는 못한다. 우리가 고친 것은 눈앞의 증상이지, 시스템에 내재된 근본적인 취약점이 아니었다. 이 찜찜함의 정체를 어떻게 설명해야 할까?
그때, 솔라의 모니터를 함께 들여다보던 루나가 새 문서를 열어 타이핑하기 시작했다. 화면에는 아주 단순한 표가 나타났다. 세로로 길게 선이 그어져 있고, 왼쪽에는 ‘수정 완료’, 오른쪽에는 ‘남은 한계’라고 적혀 있었다.
“우리가 했던 여정을 이 표에 다시 정리해 보면 어떨까? 장애 보고서는 성공담을 쓰는 게 아니라, 우리가 무엇을 확인했고, 무엇을 확인하지 못했는지를 증명하는 글이어야 하니까.”
솔라는 루나의 말에 잠시 생각에 잠겼다. 그리고는 키보드를 가져와 ‘수정 완료’ 칸에 커서를 옮겼다.
수정 완료
구버전으로 실행되던 Vision Worker 프로세스를 최신 버전으로 재기동함.
명확한 사실이었다. 이것이 우리가 ‘한’ 일의 전부였다. 솔라는 잠시 망설이다가, ‘남은 한계’ 칸으로 커서를 옮겼다. 무엇을 써야 할까? 솔라는 아까의 불안감을 떠올리며 첫 번째 항목을 적었다.
남은 한계
배포 시, 특정 프로세스(Vision Worker)가 자동으로 재시작되지 않는 경우가 있음.
여기까지 쓰고 나자, 다른 생각들이 꼬리를 물고 이어졌다. 이번 장애를 진단하면서 마주쳤던 다른 문제점들, 의문들이 떠올랐다. ‘자동 테스트는 왜 이걸 못 잡았지?’, ‘프로세스가 멈춘 걸 왜 아무도 몰랐지?’ 자동 테스트가 모든 위험을 제거해 줄 거라 믿었던 처음의 생각이 얼마나 순진했는지 깨달았다. 솔라의 손가락이 다시 움직이기 시작했다.
남은 한계
[높음] 배포 파이프라인이 특정 롱러닝 프로세스의 재기동을 보장하지 못함.[중간] 데이터 생산자(Vision Worker)의 작업 중단에 대한 모니터링 및 알림 부재.[낮음] 자동화된 테스트는 코드 로직만 검증할 뿐, 배포된 프로세스의 런타임 버전까지는 확인하지 않음.
하나씩 적고 보니, 이번 장애는 단순히 하나의 원인으로 발생한 것이 아니었다. 여러 계층에 걸쳐 있던 작은 균열과 사각지대들이 합쳐져 만들어낸 결과물이었다.
루나는 솔라가 작성한 표를 보더니, 아키텍처 문서를 가리켰다. “우리가 이번에 QA 하면서 확인했던 다른 제약 조건들도 있었지? 예를 들면, 특정 매장의 POS 데이터는 아직 연동 전이라 테스트 데이터로 대체했던 거나, 오래된 영상 데이터를 재처리(replay)해서 테스트했던 방식 같은 것들. 그런 것들도 이 시스템의 현재 상태를 설명하는 중요한 한계 아닐까?”
루나의 말에 솔라는 머릿속이 환해지는 기분이 들었다. 맞다. 지금 작성하는 이 문서는 이번 장애에 대한 것만이 아니었다. 이 시스템의 품질을 ‘현재 시점’에서 증명하는 문서였다. 그렇다면 우리가 수정한 것뿐만 아니라, 수정하지 않은 것, 할 수 없었던 것, 알려진 한계점들을 모두 투명하게 드러내야 했다. 그것이 이 시스템을 사용하는 모두에 대한 책임이었다.
“이건 그냥 QA 보고서가 아니구나. 이건… 우리가 어디까지 증명했고, 무엇은 아직 증명하지 못했는지를 기록하는 일종의 ‘한계 장부(Limit Ledger)’ 같은 거야.”
솔라는 스스로 이름 붙인 ‘한계 장부’라는 말이 마음에 들었다. 더 이상 장애 보고서를 쓰는 것이 두렵지 않았다. 솔라는 새로운 마음으로 표를 다시 정리하기 시작했다. 발견된 이슈들을 심각도에 따라 분류하고, 이번에 해결한 것과 여전히 남아있는 문제를 명확히 구분했다. 그리고 시스템이 가진 근본적인 한계점들도 숨기지 않고 덧붙였다.
최종 품질 증명 및 한계 장부 (Limit Ledger)
1. 수정 완료 사항
- 증상: 공개 대시보드 화면 데이터 갱신 중단
- 조치: 구버전 코드를 실행 중이던
Vision Worker프로세스를 최신 버전으로 재기동하여 정상화
2. 진단 경로 요약
- 클라이언트(프런트엔드): 주기적인 데이터 요청(
polling)은 정상. API 응답 데이터가 낡음. - API 서버: API 응답은 빠르나, 30분 전의
stale데이터를 반환. - 데이터베이스: 테이블의 가장 최신 데이터가 30분 전에 멈춰 있음. 데이터 생산자의 문제로 추정.
- 배포/프로세스:
Git commit, 컨테이너image는 최신이었으나, 실제 실행 중인process는 구버전. 배포 시 재기동 누락 확인.
3. 남은 한계 및 후속 조치 제안
- [배포 차단] 해당 사항 없음
- [높음] 배포 불완전성: 롱러닝 프로세스(
Vision Worker)가 배포 시 안정적으로 재기동되지 않음. (→ 배포 스크립트 수정 및 검증 필요) - [중간] 관측성 부족: 데이터 생산 중단 시 이를 감지할 모니터링 및 알림 부재. (→ 워커 작업 시간 기반 알림 추가 필요)
- [낮음] 테스트 범위: 현재 자동 테스트는 배포된 프로세스의 런타임 버전 일치 여부까지는 검증하지 않음.
- [알려진 제약]
- 실제 POS 데이터 미연결 매장 존재 (테스트 데이터 사용)
- 일부 기능 검증을 위해
replay데이터 사용 - 신규 입점 매장 유형에 대한 일반화는 아직 검증되지 않음
솔라는 완성된 ‘한계 장부’를 바라보았다. 이것은 더 이상 변명의 글이나 실패의 기록이 아니었다. 시스템의 현재 모습을 가장 정직하게 담아낸, 살아있는 품질 증거였다. 솔라는 이제야 비로소, ‘문제를 해결했다’고 자신 있게 말할 수 있었다. 그리고 팀 동료들에게 주저 없이 ‘전송’ 버튼을 눌렀다.