Big Project · AI's EYE 16

대시보드 KPI와 분석 이미지가 서로 다른 프레임을 보여주던 동기화 장애 해결 사례

상태 API도 최신이고 이미지 API도 최신이라면 화면은 자동으로 같은 순간을 보여줄 것 같다. 그런데 왜 인원 수와 박스 이미지가 어긋났는지 이해하기 어렵다.

근거 · 프로젝트 문서 · 코드 · 테스트

1장: 삐걱거리는 대시보드의 순간

솔라는 모니터 한구석에 떠 있는 짧은 문장에서 눈을 떼지 못했다. 흥미로워 보여 읽기 시작한 장애 분석 사례 아티클이었다. 매장 관리 대시보드에서 발생한 동기화 문제라는데, 다른 복잡한 설명보다 유독 한 문장이 솔라의 발목을 잡았다.

"KPI는 다음 프레임인데 이미지는 이전 프레임인 순간이 생겼다."

“이게 말이 되나?”

솔라는 혼잣말을 중얼거리며 미간을 찌푸렸다. 글의 설명에 따르면 대시보드의 구성은 간단했다. 한쪽에는 컴퓨터 비전 기술로 분석한 현재 매장 안의 인원수(KPI)가 숫자로 표시된다. 다른 한쪽에는 그 숫자가 어떤 근거로 나왔는지 보여주는, 사람마다 네모 상자가 쳐진 분석 이미지가 함께 뜬다. 당연히 두 정보는 실시간으로 갱신되어야 한다.

솔라의 생각은 명확했다. ‘인원수’를 알려주는 API도 ‘최신’ 값을 가져올 것이고, ‘분석 이미지’를 보여주는 API도 ‘최신’ 이미지를 가져올 것이다. 시스템의 모든 부분이 각자 가장 최신 정보를 부지런히 가져다 보여준다면, 화면에 표시되는 결과는 당연히 같은 순간의 모습이어야 했다. 그런데 ‘숫자는 다음 장면, 이미지는 이전 장면’이라니. 마치 두 개의 시계가 똑같이 ‘현재 시각’을 가리키고 있는데, 하나는 3시 5분, 다른 하나는 3시 4분을 보여주는 것과 같은 모순처럼 느껴졌다.

이 찜찜함을 떨치지 못한 솔라는 거실에서 조용히 책을 읽고 있던 루나에게 다가갔다.

“언니, 잠깐만. 이상한 게 있어.”

루나는 읽던 페이지에 손가락을 꿴 채 고개를 들었다. 솔라는 자신의 노트북 화면을 루나 쪽으로 돌렸다.

“이것 봐. 매장 인원수를 보여주는 대시보드에 대한 글인데, 인원수는 10명이라고 나오는데 그 근거가 되는 이미지에는 사람이 8명만 있는 식의 문제가 생겼대. 둘 다 ‘최신’ 데이터를 가져와서 보여주는 거잖아? 그런데 왜 숫자는 다음 장면인데, 이미지는 이전 장면인 상황이 생긴다는 거지? 각자 최선을 다해 최신 정보를 보여주면, 당연히 같은 순간을 보여줘야 하는 거 아니야?”

솔라의 질문에는 ‘최신’이라는 단어에 힘이 실려 있었다. 그것은 반박하기 어려운, 당연한 사실처럼 들렸다.

루나는 솔라의 질문에 바로 답하는 대신, 잠시 주변을 둘러보았다. 그리고는 거실 벽에 걸린 디지털 벽시계와 자신의 손목에 찬 스마트 워치를 차례로 가리켰다.

“솔라, 저 벽시계랑 내 시계 좀 봐. 둘 다 뭘 보여주고 있지?”

“시간. 지금 현재 시간.”

“맞아. 둘 다 각자의 기준에서 가장 ‘최신’의 현재 시각을 보여주고 있어. 저 벽시계는 아마 1초마다 숫자가 바뀔 거고, 내 스마트 워치는 훨씬 더 짧은 주기로 화면을 업데이트하겠지. 둘 다 틀리지 않았고, 둘 다 ‘최신’이야.”

루나는 말을 이으며 자신의 스마트 워치를 솔라의 눈앞에 가까이 가져갔다. 화면에는 시, 분, 초, 그리고 밀리초까지 표시되고 있었다. 빠르게 숫자가 흐르는 밀리초 단위는 눈으로 따라잡기 어려울 정도였다.

“자, 그럼 내가 ‘지금!’ 하고 외치는 순간, 저 벽시계의 ‘초’와 내 시계의 ‘밀리초’를 동시에 읽는다고 상상해 봐. 우리가 아무리 정확하게 ‘지금’이라는 한순간을 포착하려 해도, 두 시계가 보여주는 정보의 조합이 항상 같을까? 벽시계는 15초를 막 넘어가고 있는데, 내 시계는 15.892초를 가리키고 있을 수도 있어. 다음 ‘지금!’에는 벽시계는 16초가 되었는데 내 시계는 15.998초일 수도 있고.”

루나는 스마트 워치를 내려놓고 솔라를 바라보았다.

“두 시계 모두 ‘최신’ 정보를 제공한다는 사실은 변함이 없어. 하지만 각자 정보를 갱신하는 주기와 시점이 다르기 때문에, 우리가 임의의 한순간을 잘라 보면 서로 다른 ‘최신’을 가리키게 되는 거야. 두 ‘최신’이 항상 같은 순간을 보장하지는 않는 거지.”

솔라는 잠시 말이 없었다. 시계 비유를 듣고 나니, 아까부터 머릿속을 맴돌던 문장이 다르게 보이기 시작했다.

“아…”

탄식과 함께 깨달음이 찾아왔다.

“그럼 대시보드도 마찬가지구나. 인원수를 가져오는 쪽이 알려주는 ‘최신’과, 분석 이미지를 가져오는 쪽이 알려주는 ‘최신’이, 사실은 언니 시계와 벽시계처럼 서로 다른 순간의 ‘최신’일 수 있다는 거네.”

솔라는 다시 노트북 화면으로 고개를 돌렸다. 아까는 모순으로 가득 차 보였던 그 문장이 이제는 당연한 현상에 대한 설명으로 읽혔다.

"KPI는 다음 프레임인데 이미지는 이전 프레임인 순간이 생겼다."

“이제 알겠다. 이게 시스템이 고장 나거나 어느 한쪽이 게으름을 피웠다는 뜻이 아니었어. 인원수 정보는 이미 다음 분석 결과를 받아서 화면을 업데이트했는데, 용량이 더 큰 분석 이미지는 아직 이전 것을 보여주고 있는 그 찰나의 순간이 생긴다는 거구나. 각자 독립적으로, 가장 빠르게 ‘최신’이 되려고 했을 뿐인데 말이야.”

문제를 이해하고 나니 후련했지만, 동시에 새로운 의문이 고개를 들었다. 사용자가 대시보드를 볼 때, 숫자와 이미지가 따로 노는 것을 정상이라고 받아들일 리는 없었다. 신뢰가 중요한 데이터라면 더더욱 그랬다.

“그럼 이걸 어떻게 맞춰야 해? 인원수는 10명인데 근거 이미지는 8명만 보여주는 걸 그냥 둘 순 없잖아. 둘 다 ‘최신’이라고 주장만 해서는 안 될 것 같은데… 그렇다면 ‘같은 순간’의 데이터라는 걸 보장해 줄 다른 기준이 필요하다는 뜻인가?“

2장: ‘최신’이라는 환상

솔라는 자신의 책상 앞에 앉아, 방금 전의 깨달음이 남긴 질문을 곱씹고 있었다. ‘같은 순간’을 보장할 다른 기준. 그게 대체 뭘까? 막연한 생각만으로는 부족했다. 솔라는 펜을 들고 노트 위에 네모 두 개를 그렸다. 하나는 ‘서버’, 다른 하나는 ‘대시보드’.

‘각자 최신 데이터를 가져온다…’

솔라는 서버에서 대시보드로 향하는 화살표를 두 개 그었다. 하나는 ‘인원수(KPI)’, 다른 하나는 ‘분석 이미지’. 그녀의 머릿속에 떠오른 첫 번째 해법은 단순했다.

“두 요청을 정확히 똑같은 순간에 보내면 되지 않을까?”

마치 출발선에 선 두 주자에게 동시에 출발 신호를 보내는 것처럼, 대시보드가 서버에 인원수와 이미지를 한 치의 오차도 없이 동시에 요청하는 것이다. 그러면 서버도 같은 순간의 데이터를 주지 않을까? 솔라는 자신의 가설이 꽤 그럴듯하다고 생각하며 화살표 두 개가 같은 지점에서 출발하도록 그림을 수정했다.

이 모습을 본 루나가 조용히 솔라의 옆으로 다가와 노트를 들여다보았다. 솔라는 기다렸다는 듯이 자신의 그림을 설명했다.

“언니, 내 생각엔 말이야. 어차피 갱신 주기가 달라서 시차가 생기는 거라면, 요청 자체를 완전히 동기화하면 해결될 것 같아. 대시보드가 ‘지금 데이터 줘!’ 하고 외칠 때, 인원수랑 이미지를 한 묶음으로 동시에 요청하는 거지. 그럼 서버도 그 순간의 데이터를 세트로 주지 않겠어?”

루나는 고개를 끄덕이며 솔라의 그림 옆에 자신만의 그림을 그리기 시작했다. 훨씬 더 구체적인 그림이었다. ‘서버’라는 상자 안에 ‘인원수 처리기’와 ‘이미지 처리기’라는 작은 상자 두 개를 따로 그렸다. 그리고 대시보드로 향하는 길을 두 갈래의 다른 경로처럼 표현했다.

“좋은 생각이야. 솔라 네가 그린 것처럼, 두 요청이 정확히 같은 시간에 출발했다고 해보자. 마치 두 명의 메신저가 서버에서 대시보드까지 달리기 경주를 하는 것처럼.”

루나는 인원수 화살표 옆에 ‘작은 쪽지’라고 썼고, 이미지 화살표 옆에는 ‘커다란 액자’라고 썼다.

“인원수 메신저는 가벼운 쪽지만 들고 뛰면 돼. 하지만 이미지 메신저는 크고 무거운 액자를 들고 뛰어야 하지. 둘이 동시에 출발해도, 누가 먼저 대시보드에 도착할까?”

“그야 당연히 쪽지를 든 메신저지.”

“맞아. 그런데 문제는 그것뿐만이 아니야.”

루나는 ‘서버’ 상자 안의 두 처리기에 초점을 맞췄다.

“서버에서는 계속해서 새로운 분석이 이루어져. 1초 전에는 8명을 분석한 결과(프레임 100)가 나왔고, 지금 막 10명을 분석한 결과(프레임 101)가 나왔다고 상상해 봐. ‘인원수 처리기’는 숫자를 계산하는 거라 금방 끝나. 그래서 ‘최신 인원수’ 서랍에 ‘10명’ 쪽지를 바로 넣어뒀어. 하지만 ‘이미지 처리기’는 사람마다 네모를 그리는 등 작업이 더 복잡해서, 아직 ‘프레임 101’의 액자를 만드는 중이야. 그래서 ‘최신 이미지’ 진열대에는 여전히 ‘프레임 100’의 ‘8명짜리’ 액자가 걸려 있지.”

루나는 펜으로 두 메신저가 각자의 창구로 달려가는 모습을 그렸다.

“자, 이제 두 메신저가 동시에 출발했어. 각자 자기 담당 창구로 가서 ‘가장 최신 물건 주세요!’라고 외치는 거야. 인원수 메신저는 ‘10명’이라고 적힌 새 쪽지를 받았어. 하지만 이미지 메신저가 도착한 창구의 진열대에는 아직 ‘8명’짜리 옛날 액자가 최신 물건이야. 새 액자는 아직 제작 중이니까. 그럼 이미지 메신저는 뭘 들고 오게 될까?”

“…8명짜리 액자를 들고 오겠네.”

순간 솔라의 머릿속이 환해졌다. 문제는 요청을 동시에 보내느냐가 아니었다. 요청을 받은 서버의 각 부분이 응답을 준비하는 상태가 다르다는 것이 핵심이었다.

솔라는 자신의 노트에 그렸던 단순한 화살표를 지웠다. 그리고 루나의 그림처럼, 서버 내부에서부터 데이터의 흐름이 나뉘는 것을 그렸다. 인원수(StoreState) API와 분석 이미지 API가 별도의 요청으로 처리된다는 것의 진짜 의미가 보이기 시작했다. 그것은 단순히 두 개의 다른 주소로 데이터를 요청한다는 뜻이 아니었다. 서버 내부에서 데이터를 생성하고, 준비하고, ‘최신’이라고 딱지 붙이는 과정 전체가 독립적으로 움직인다는 의미였다.

StoreState와 분석 이미지가 별도 요청으로 갱신되어 KPI는 다음 프레임인데 이미지는 이전 프레임인 순간이 생겼다.

이제 이 문장은 장애 보고서의 한 줄이 아니라, 눈앞에 그려진 데이터 흐름도 그 자체였다. 인원수 요청은 이미 다음 프레임의 결과물을 집어 들고 결승선을 통과했는데, 이미지 요청은 아직 이전 프레임의 결과물을 들고 뒤따라오고 있는 상황. 솔라는 펜으로 두 데이터 흐름 사이를 여러 번 오가며 그 시간차를 시각적으로 확인했다. 각자 독립적으로 ‘최신’을 향해 달렸을 뿐인데, 그 결과가 사용자의 화면에서는 뒤섞인 시간의 증거가 되어 나타나는 것이었다.

“아… 요청을 동시에 보내는 건 아무 의미가 없었구나. 각자 달려가는 길이 다르고, 창구에 나와 있는 ‘최신’ 물건이 다른데.”

솔라는 중얼거렸다. ‘최신’이라는 단어가 얼마나 허술한 약속인지 깨달았다. 분산된 시스템에서 ‘최신’은 통일된 구호가 아니라, 각자의 사정을 담은 개별적인 외침에 불과했다.

문제를 파악하자 새로운 질문이 떠올랐다. 그렇다면 이 시간차를 어떻게 극복하고 ‘같은 순간’을 보장할 수 있을까? 두 메신저를 강제로 함께 달리게 할 수도 없는 노릇이었다.

“그럼… 이 쪽지랑 액자가 원래 한 세트였다는 걸 어떻게 알 수 있게 하지? 출발할 때부터 뭔가 같은 표식을 달아서 보내야 하나?”

3장: ‘같은 사건’의 신원 만들기

솔라는 거실 테이블에 넓게 펼쳐진 직소 퍼즐을 멍하니 내려다보고 있었다. 1000조각짜리 풍경화. 완성된 테두리 안쪽으로 하늘색과 숲의 녹색이 드문드문 자리를 잡았지만, 대부분은 아직 비어 있었다. 그녀의 손에는 비슷한 톤의 하늘색 조각이 들려 있었다. 방금 전까지 확신을 갖고 끼워 맞추려던 자리에서 세 번이나 튕겨 나온 조각이었다. 모양도, 색깔도 분명 그 자리 같은데, 막상 넣어보면 미세하게 맞지 않고 겉돌았다.

“아, 이것도 아니네.”

결국 솔라는 항복을 선언하며 손에 든 조각을 테이블 위, 다른 수십 개의 하늘색 조각들 무더기 위에 던져 버렸다. 각자 ‘하늘’의 일부라고 주장하는 파편의 산이었다. 인원수 데이터와 분석 이미지가 각자 ‘최신’이라고 외치던 상황과 어이가 없을 정도로 닮아 있었다. 그저 파란색이라는 이유만으로, ‘최신’이라는 이유만으로 한데 모일 수 없는 조각들. 솔라는 지난번 루나와 나눴던 대화의 마지막 질문을 떠올렸다. ‘같은 사건’이라는 걸 보장해 줄 기준은 뭘까.

“퍼즐 맞추는 것 같네.”

어느새 다가온 루나가 흩어진 퍼즐 조각들을 보며 나지막이 말했다.

“응. 분명 같은 하늘인데, 제자리가 아니래. 이 쪽지랑 액자가 원래 한 세트였다는 걸 알려줄 표식이 필요하다고 생각했는데, 이 퍼즐도 똑같아. 이 조각이 어느 하늘에 속한 조각인지 알려줄 무언가가 필요해.”

솔라는 뒤죽박죽인 조각들을 가리키며 한숨 섞인 목소리로 말했다. 그녀의 생각은 ‘같은 데이터베이스 레코드를 참조하면 되지 않을까?’ 하는 단순한 아이디어에 잠시 머물렀다. 마치 퍼즐 조각들을 모두 한 상자에 담아두면 된다는 식의 생각이었다. 하지만 인원수와 이미지가 각자 다른 길을 달려온다는 것을 떠올리면, 그 방법만으로는 부족하다는 것을 직감할 수 있었다.

루나는 말없이 고개를 끄덕이더니, 테이블 위에 흩어진 조각 두 개를 집어 들었다. 둘 다 비슷한 녹색 톤의 숲 조각이었다.

“만약 이 그림 앞면이 아예 없다고 상상해 보자. 색깔도, 모양도 힌트가 될 수 없다면, 이 두 조각이 서로 이웃인지, 아니면 완전히 다른 구역의 조각인지 어떻게 알 수 있을까?”

“그건 불가능하지. 단서가 아무것도 없는데 어떻게 알아?”

솔라가 반문했다. 그러자 루나는 집어 든 조각을 뒤집어 그 뒷면을 보여주었다. 솔라는 무심코 그 뒷면을 들여다보다가 깜짝 놀랐다. 잿빛의 두꺼운 종이일 거라고 생각했던 조각의 뒷면에는, 연필로 쓴 것 같은 희미한 글씨가 적혀 있었다.

[ F-7 ]

다른 조각의 뒷면에도 마찬가지였다.

[ H-12 ]

“어…?”

“어릴 때 아빠랑 같이 맞추던 퍼즐이야. 너무 복잡해서 결국 맞추지 못할 것 같으니까, 아빠가 몰래 모든 조각 뒷면에 이렇게 좌표를 적어두셨어. 가로로는 알파벳, 세로로는 숫자를 매겨서.”

루나는 솔라의 손에 F-7 조각을 쥐여주며 말했다.

“이제 우리는 이 조각의 ‘정체성’을 알아. 이 조각은 더 이상 ‘어딘가의 녹색 숲 조각’이 아니야. 정확히 ‘F열 7행’에 위치해야 하는, 세상에 단 하나뿐인 조각이지. H-12 조각과는 전혀 다른 사건의 일부인 거야.”

순간 솔라의 머릿속에 흩어져 있던 생각의 조각들이 ‘찰칵’하고 맞춰지는 소리가 들렸다. 표식. 바로 이거였다. 데이터의 내용물(그림)이 아니라, 데이터 자체에 ‘신원’을 부여하는 것.

솔라는 황급히 노트북을 열어 다시 그 장애 분석 아티클을 띄웠다. 그리고 스크롤을 내려 해결책 부분을 찾았다. 전에는 복잡한 코드로만 보였던 문장이 이제는 루나가 보여준 퍼즐 조각의 뒷면처럼 선명하게 읽혔다.

같은 프레임의 상태·occupancy·snapshot metadata에 동일한 frame_id, captured_at, processed_at, roi_version을 넣었다.

“아! 알겠다! 이게 그 좌표였구나!”

솔라는 흥분하며 화면을 가리켰다.

frame_id가 바로 퍼즐의 고유 번호 같은 거네. ‘101번 프레임 분석 결과’라는 식으로. 그리고 captured_at은 이 퍼즐의 원본 사진을 찍은 정확한 시각! ‘2024년 5월 10일 15시 30분 05.123초’에 찍힌 사진이라는 증표인 거지.”

그녀는 이제 더 이상 루나의 설명이 필요 없다는 듯 혼자서 그림을 완성해 나갔다.

“그러니까 서버에서 카메라 영상을 분석해서 ‘10명’이라는 인원수(KPI) 데이터를 만들 때, 이 데이터에 이름표를 붙여주는 거야. frame_id: 101, captured_at: ... 이렇게. 그리고 동시에 ‘10명’이 보이는 분석 이미지 데이터를 만들 때도, 똑같은 이름표를 붙여주는 거지. frame_id: 101, captured_at: ....”

솔라의 눈이 반짝였다. ‘최신’이라는 모호한 구호 대신, 누구도 부정할 수 없는 명백한 신원이 생긴 것이다.

“이제 인원수 데이터와 이미지 데이터는 더 이상 ‘각자 최신’을 외치는 독립된 정보가 아니야. ‘나는 101번 사건의 증거물입니다’라고 말하는 한 팀인 거지. 둘이 서로 다른 경로로, 다른 속도로 대시보드에 도착하더라도 상관없어. 대시보드는 그저 각 조각의 뒷면에 적힌 frame_id를 확인하고, 같은 번호의 조각들이 모일 때까지 기다렸다가 함께 보여주면 되니까!”

이제야 인원수 10명과 8명짜리 이미지가 뒤섞일 수 없는 이유가 명확해졌다. 10명짜리 인원수 데이터에는 frame_id: 101이라는 신원이, 8명짜리 이미지에는 frame_id: 100이라는 이전 신원이 부여되어 있을 것이기 때문이다. 둘은 애초에 다른 퍼즐의 조각이었던 것이다.

퍼즐 조각의 뒷면을 확인하는 간단한 행위. 그것은 분산된 시스템에서 흩어져 버린 ‘같은 순간’을 다시 하나로 묶어주는 강력한 설계 원칙이었다. 여러 데이터 조각들이 특정 시점의 하나의 논리적 ‘사건’을 가리키도록 고유 식별자를 부여하는 것. 솔라는 이제 ‘사건 신원 부여기’라는 도구를 손에 쥔 기분이었다.

“그래, 이제 완벽히 알겠어. 대시보드는 그냥 같은 frame_id를 가진 짝을 찾아서 보여주기만 하면 되는 거네!”

솔라가 자신감에 찬 목소리로 선언했다. 후련함에 미소가 절로 나왔다. 하지만 루나는 조용히 고개를 저으며 다른 퍼즐 조각 하나를 집어 들었다. 그 조각은 소파 쿠션 틈에 끼어 있다가 방금 발견된, 가장자리 부분이 살짝 닳아버린 조각이었다.

“거의 다 왔어. 그런데 만약 F-8 조각이 배달 사고로 늦게 도착하거나, 아니면 우리가 이미 버렸다고 생각한 F-7 조각이 소파 밑에서 불쑥 튀어나온다면 어떨까? 대시보드가 이미 완성된 그림을 보여준 뒤에 말이야. 그저 같은 번호표를 가졌다고 해서, 언제든, 어떤 순서로든 화면에 나타나도 괜찮은 걸까?”

4장: 일관성을 위한 전송과 제어

솔라의 손바닥 위에는 가장자리가 살짝 닳아버린 퍼즐 조각이 놓여 있었다. F-7. 얼마 전까지만 해도 이 조각의 뒷면을 발견한 것은 모든 문제의 해답처럼 느껴졌다. 데이터에 고유한 ‘사건 신원’을 부여하는 것. 그것만으로 삐걱거리던 대시보드의 시간이 완벽히 맞춰질 거라 믿었다.

하지만 루나의 마지막 질문이 그 확신에 균열을 냈다. 이미 F-8번 프레임으로 완성된 그림 위에, 잊혔던 F-7번 조각이 불쑥 나타난다면? 배달 사고로 F-8의 이미지 조각이 KPI 조각보다 한참 늦게 도착한다면? 대시보드는 그저 같은 번호표를 가졌다는 이유만으로, 제멋대로 도착한 조각들을 화면에 뒤죽박죽 표시해도 괜찮은 걸까?

솔라는 고개를 저었다. 아니, 그건 안 된다. frame_id라는 신원만으로는 부족했다. ‘같은 사건’임을 증명하는 것과, 그 사건을 ‘올바른 순서로, 온전하게’ 전달하는 것은 별개의 문제였다. 그녀는 자신의 생각이 너무 단순했음을 인정해야 했다. ‘메타데이터를 넣었으니 클라이언트는 알아서 잘 맞춰서 표시할 것’이라는 기대는 섣부른 낙관이었다.

“결국 배달의 문제네.”

솔라가 닳아버린 퍼즐 조각을 만지작거리며 중얼거렸다. 루나는 그 말을 듣고는, 옆에 있던 메모지와 펜, 그리고 휴대폰을 집어 들었다.

“내가 서버고, 네가 대시보드라고 해보자. 내가 너에게 중요한 메시지를 보낼 거야.”

루나는 메모지에 ‘101번 사건: 10명 입장’이라고 적은 후, 자신의 휴대폰 화면에 있던 고양이 사진을 보여주었다.

“이 메모지가 인원수(KPI) 데이터, 이 고양이 사진이 분석 이미지라고 생각해. 둘 다 ‘101번 사건’에 대한 증거물이야. 뒷면에는 frame_id: 101이라고 쓰여 있지.”

루나는 먼저 고양이 사진이 띄워진 휴대폰을 솔라에게 건넸다. “자, 이건 ‘100번 사건’의 증거 사진이야. 네가 방금 전에 받아서 갖고 있다고 상상해.”

솔라는 고개를 끄덕이며 휴대폰을 받았다. 루나는 이어서 ‘101번 사건’이라고 적힌 메모지를 솔라에게 슥 내밀었다.

“이제 내가 ‘101번 사건’의 인원수 정보를 보냈어. 이걸 받은 너는 ‘101번 사건’의 짝이 되는 이미지도 필요하겠지? 나한테 요청해 봐.”

“101번 사건 이미지 줘.”

솔라가 말하자, 루나는 솔라의 옆에 앉아 있던 가상의 조수를 가리켰다.

“그런데 네 옆에 아주 부지런한 조수가 한 명 있다고 해보자. 이 조수는 네가 받은 물건들을 잠시 보관해두는 일을 해. ‘캐시(Cache)’라고 부를게. 이 조수는 네가 나한테 직접 이미지를 요청하러 오기 전에, ‘잠깐만요! 제가 예전에 루나 님한테서 받은 사진이 있는데, 이거 맞죠?’ 하면서 네가 들고 있던 100번 고양이 사진을 다시 건네주는 거야. 나한테 직접 와서 새 사진을 받아 가는 것보다 그게 훨씬 빠르니까.”

“아…”

“그럼 네 손에는 ‘101번 사건’의 메모지와 ‘100번 사건’의 사진이 들려 있게 돼. 분명히 넌 최신 사건 번호를 외쳤는데도 말이지. 이게 바로 소파 밑에서 튀어나온 낡은 F-7 조각이야. 버려진 줄 알았는데, 캐시라는 조수가 ‘이것도 쓸만해요!’라며 불쑥 내민 거지.”

솔라의 미간이 좁혀졌다. 신원만으로는 해결되지 않는 문제가 선명하게 보였다. 과거의 데이터가 ‘빠르다’는 이유로 현재의 데이터를 오염시킬 수 있었다.

“그럼 조수한테 보관하지 말라고 해야겠네.”

“정확해. 그래서 내가 메시지를 보낼 때, 봉투 겉면에 아주 큰 글씨로 도장을 찍어주는 거야.”

루나는 허공에 도장을 찍는 시늉을 했다. “‘이 내용물은 절대 보관하지 마시오(No-Store)! 필요할 때마다 반드시 나에게 직접 와서 새로 받아 가시오.’ 이렇게 말이야. 이게 바로 Cache-Control: no-store라는 규칙이야. 부지런한 조수가 멋대로 옛날 사진을 꺼내 들지 못하게 막는 명시적인 지시지.”

솔라는 노트북을 열어 다시 아티클의 해결책 부분을 확인했다. Cache-Control: no-store라는 코드가 이제는 단순한 명령어가 아니라, 배달 사고를 막기 위한 강력한 통제 장치로 보였다.

상태와 snapshot 전송 순서를 통합했으며 이미지 응답에는 Cache-Control: no-store와 프레임 메타데이터 헤더를 제공했다.

“알겠다. 캐시 문제는 이걸로 해결하고… 그럼 ‘전송 순서를 통합했다’는 건 뭐야?”

“그건 두 번째 문제 때문이야.”

루나는 메모지를 여러 장 준비했다. ‘101번: 10명’, ‘102번: 11명’. 그리고 휴대폰에는 101번 사건에 해당하는 다른 사진을 띄웠다.

“자, 캐시 문제는 해결됐어. 이제 네 조수는 아무것도 보관하지 않아. 내가 ‘101번 사건’의 인원수 메모지와 이미지 사진을 순서대로 보낼게. 그런데 내가 그걸 보내는 동시에, 바로 다음 분석 결과인 ‘102번 사건’의 인원수 메모지도 나왔어. 이건 용량이 작으니까 더 빨리 너한테 도착할 수 있겠지?”

루나는 ‘102번 메모지’를 잽싸게 솔라에게 건네고, 뒤이어 ‘101번 사진’이 담긴 휴대폰을 천천히 내밀었다. 순식간에 솔라의 손에는 ‘102번 사건의 인원수’와 ‘101번 사건의 이미지’가 들렸다. 둘 다 최신 데이터이고 캐시된 것도 아니지만, 또다시 짝이 맞지 않았다.

“이런….”

“이게 바로 ‘전송 순서’의 문제야. 각자 다른 길로 따로따로 보내면, 아무리 번호표를 붙여줘도 중간에 순서가 뒤섞일 수 있어. 그래서 해결책은 간단해.”

루나는 ‘101번 메모지’와 ‘101번 사진이 든 휴대폰’을 하나의 투명한 상자에 함께 넣는 시늉을 했다.

“‘101번 사건’의 증거물들을 하나의 꾸러미로 묶어서 보내는 거야. 인원수 데이터(상태)가 전송될 때, 관련된 이미지(snapshot) 정보도 함께 실어서 보내는 거지. 대시보드는 이 꾸러미를 통째로 받기만 하면 돼. 그러면 102번 메모지가 중간에 끼어들 틈이 없어져. 이것이 ‘전송 순서를 통합했다’는 것의 의미야.”

솔라는 마침내 고개를 끄덕였다. 모든 조각이 제자리를 찾았다. frame_id라는 신원 부여는 시작일 뿐이었다. 그 신원이 여행 내내 보존되고, 최종 목적지에서 올바르게 해석되도록 길을 통제해야만 했다. 신호를 무시하고 불쑥 끼어드는 낡은 데이터는 ‘캐시 금지’ 표지판으로 막고, 데이터들이 서로 다른 속도로 달려가다 순서가 엉키는 것은 ‘같은 꾸러미로 묶어 전송’하는 규칙으로 막는다. 이것이 바로 분산 시스템에서 일관성을 지키는 전송 프로토콜이었다.

“이제 진짜 알겠다. 신원을 부여하고, 그 신원이 흔들리지 않도록 전송 경로를 통제하고, 낡은 정보가 끼어들지 못하게 캐시를 제어하는 것. 이 세 가지가 모두 합쳐져야만 대시보드가 ‘같은 순간’을 제대로 보여줄 수 있는 거구나.”

솔라는 자신이 ‘전송 순서/캐시 제어’라는 새로운 도구를 얻었다고 느꼈다. 이제 서버에서부터 대시보드에 이르기까지, 데이터가 거치는 모든 여정을 고려해야 한다는 것을 깨달았다.

“그럼 대시보드는 이 완벽하게 포장된 꾸러미를 받아서, 그저 frame_id를 확인하고 화면에 나란히 보여주기만 하면 끝이네. 이제 정말 빈틈없겠지?”

솔라의 목소리에는 다시 자신감이 실려 있었다. 루나는 이번에는 아무 말 없이, 그저 희미하게 미소만 지었다. 마치 마지막 퍼즐 조각 하나를 아직 손에 쥔 채, 솔라가 스스로 그 빈자리를 찾아내길 기다리는 사람처럼.

5장: 최신성보다 신원: 대시보드의 진실

솔라는 노트북을 닫고 의자 등받이에 몸을 깊게 묻었다. 머릿속이 맑았다. 신원을 부여하고(frame_id), 낡은 정보의 난입을 막고(Cache-Control), 꾸러미로 묶어(전송 순서 통합) 전달한다. 서버에서 출발한 데이터가 대시보드라는 목적지까지 ‘같은 순간’을 잃지 않고 도달하는 완벽한 여정. 이제 빈틈은 없어 보였다.

하지만 뇌리 한구석에 루나의 마지막 표정이 작은 가시처럼 박혀 있었다. 자신감 넘치는 자신의 선언에, 루나는 그저 희미하게 미소만 지었다. 모든 답을 찾았다고 생각했는데, 마치 아직 맞춰지지 않은 마지막 퍼즐 조각이 있다는 듯한 미소였다. ‘대시보드는 그저 frame_id를 확인하고 보여주기만 하면 끝’이라는 자신의 결론에 무슨 문제가 있었던 걸까?

결국 솔라는 궁금증을 참지 못하고 거실로 나갔다. 루나는 스마트폰으로 뉴스를 훑어보고 있었다.

“언니, 아까 왜 그렇게 웃었어? 내가 뭔가 놓친 게 있어? 신원 부여하고, 전송 제어까지 했는데, 여기서 더 뭐가 필요해? 대시보드는 이제 받은 꾸러미를 열어서 같은 frame_id를 가진 것끼리 보여주면 완벽한 거 아니야?”

솔라의 질문에 루나는 스마트폰 화면을 잠시 들여다보다가 고개를 들었다.

“질문을 바꿔볼게. 만약 우리가 뉴스 기자이고, 지금 막 마라톤 대회가 끝났다고 해보자.”

루나는 솔라에게 자신의 스마트폰을, 그리고 탁자 위에 있던 다른 태블릿을 차례로 건넸다.

“나는 A 통신사 기자, 너는 B 통신사 기자야. 둘 다 ‘최신’ 속보를 내보내는 게 목표지. 나는 지금 막 들어온 정보로 속보를 썼어. ‘시민 마라톤, 김 선수가 신기록으로 우승!’이라고. 이보다 더 ‘최신’일 순 없겠지.”

루나는 마치 기사를 발행한 것처럼 태블릿 화면을 탭하는 시늉을 했다.

“그런데 너는 나보다 정보원이 한 명 더 있었어. 그래서 5분 뒤에 새로운 사실을 알게 된 거야. 김 선수가 금지 약물 테스트에 걸려서 실격 처리됐다는 걸. 너는 바로 속보를 내보내겠지. ‘속보: 마라톤 우승자 김 선수, 실격 처리’.”

솔라는 고개를 끄덕였다. 당연한 수순이었다.

“자, 이제 독자들 입장에서는 두 개의 ‘최신’ 뉴스가 존재해. 하나는 ‘김 선수 우승’, 다른 하나는 ‘김 선수 실격’. 솔라 네 말대로라면, 둘 다 ‘최신’이니까 괜찮은 걸까? 독자들은 그냥 ‘어? B 통신사가 더 빠르네’ 하고 납득할까?”

“아니, 그건… 혼란스럽겠지. A 통신사의 뉴스는 이제 틀린 뉴스가 되어버렸잖아. 최신이 아니게 된 거지.”

솔라는 ‘어차피 최신이면 사용자도 납득할 것’이라는 자신의 생각이 흔들리는 것을 느꼈다.

“바로 그거야. 우리가 지금까지 ‘최신’이라는 단어에 집착했던 게 문제의 본질일지도 몰라.”

루나는 태블릿에 메모 앱을 열고 글자를 쓰기 시작했다.

사건 보도 #72: 마라톤 종료, 김 선수 우승 (15:05 발행) 사건 보도 #73: 우승자 실격 처리 (15:10 발행)

“만약 통신사가 단순히 ‘최신 속보’라고 외치는 대신, 이렇게 명확한 ‘사건 번호’를 붙여서 발표했다면 어떨까? 이제 A 통신사는 ‘사건 #72’에 대한 뉴스를 보도한 거고, B 통신사는 ‘사건 #73’에 대한 뉴스를 보도한 게 돼. A의 뉴스가 틀린 게 아니라, ‘사건 #72 시점에서는 사실이었던 정보’가 되는 거지.”

솔라의 눈이 커졌다. frame_id가 퍼즐 조각의 좌표나 꾸러미의 송장이 아니었다. 그것은 ‘사건 번호’였다.

“아…!”

“이제 대시보드가 할 일이 명확해져. 대시보드의 역할은 무조건 가장 빠른 뉴스를 중계하는 게 아니야. ‘사건 #73’의 꾸러미를 받았다면, 그 안에 담긴 ‘인원수: 10명’이라는 기사와 ‘분석 이미지: 10명이 찍힌 사진’을 함께 보여주며 ‘사건 #73에 대한 정확한 보고입니다’라고 증명해 주는 거지. 설령 그 순간 ‘사건 #74’가 서버에서 처리 중이더라도, 지금 화면에 보이는 ‘사건 #73’의 정보가 한 치의 거짓 없는 ‘같은 순간의 진실’임을 보장해 주는 것. 그게 핵심이야.”

솔라는 그제야 마지막 퍼즐 조각이 맞춰지는 것을 느꼈다. 그들이 지금까지 설계한 모든 것은 단순히 최신 데이터를 빠르게 보여주기 위함이 아니었다. 사용자가 숫자와 근거 이미지를 보았을 때, ‘이 둘은 같은 사건에 대한 증거가 맞다’는 절대적인 신뢰를 주기 위함이었다. 최신이냐 아니냐는 그 다음 문제였다. 뒤섞인 ‘최신들’보다, 조금 늦더라도 온전한 ‘하나의 진실’이 훨씬 중요했던 것이다.

솔라는 다시 노트북을 열고 그 장애 분석 아티클의 마지막 문단을 찾아 읽었다.

이 장애는 최신성보다 동일 사건 신원이 중요하다는 사례다.

전에는 기술적 해결책에 대한 건조한 요약처럼 보였던 문장이, 이제는 하나의 단단한 설계 원칙으로 다가왔다. 솔라는 이제 ‘신원 기반 일관성 확립기’라는 이름 없는, 하지만 강력한 도구를 손에 쥔 기분이었다. 어떤 정보가 ‘최신’이라는 주장보다, 그 정보가 어떤 ‘사건’의 일부인지 명확한 신원을 부여하는 것이 시스템의 일관성과 사용자의 신뢰를 확보하는 핵심이었다.

며칠 뒤, 솔라는 자신의 방에서 무언가를 끄적이고 있었다. 최근 시작한 취미인 ‘스마트 화분’을 위한 작은 모니터링 시스템을 구상하는 중이었다. 처음 스케치는 간단했다. 토양 습도 센서에서 ‘현재 습도’를 가져오고, 마지막으로 물을 준 기록을 따로 저장해 ‘마지막 급수 시간’을 표시하는 것이었다.

그녀는 스케치를 그리다 말고 펜을 멈췄다. ‘현재 습도’와 ‘마지막 급수 시간’. 이것은 매장 인원수와 분석 이미지의 다른 버전일 뿐이었다. 내가 방금 물을 줬는데, 아직 습도 센서 값이 갱신되기 전이라면? 화면에는 ‘마지막 급수: 10초 전’, ‘현재 습도: 건조함’이라는 모순된 정보가 뜰 수 있었다.

솔라는 처음 그렸던 스케치를 망설임 없이 지웠다. 그리고 새로운 그림을 그리기 시작했다. ‘물주기’, ‘분무하기’ 같은 행동을 하나의 ‘관리 이벤트’로 정의했다. 그리고 각 이벤트마다 고유한 ‘이벤트 ID’와 ‘시각’을 부여하고, 그 순간의 습도와 온도 같은 모든 센서 값을 한 묶음으로 기록하도록 설계했다.

그녀의 새 설계 노트 한쪽에 이런 문장이 적혔다.

원칙: 각 센서의 ‘최신’ 값을 믿지 말 것. 하나의 ‘관리 이벤트’ 신원을 기준으로 모든 데이터를 묶어서 보여줄 것.

그 모습을 본 루나가 조용히 다가와 솔라의 노트를 들여다보았다. 그리고는 지난번과는 다른, 모든 조각이 제자리를 찾은 것을 확인한 사람의 진짜 미소를 지어 보였다.