Big Project · AI's EYE 05
JSON을 넘어선 API 계약: 의미, 시간, 품질, 규칙, 그리고 협의의 기술
서비스끼리 JSON을 주고받기만 하면 자연스럽게 연동될 것처럼 보인다. 왜 schema와 시간·품질·프레임 규칙까지 함께 정해야 했는지 모호하다.
근거 · 프로젝트 문서 · 코드 · 테스트
1장: 필드 이름의 착각: ‘인원수’가 ‘고객수’가 아닌 이유
솔라의 모니터에는 여러 서비스가 얽힌 복잡한 아키텍처 대신, 담백한 JSON 구조 하나가 떠 있었다. StoreState라는 이름의 객체였다. 솔라는 최근 여러 팀이 함께 달라붙어 만든 매장 데이터 공통 규약 문서를 살펴보고 있었다. Vision, API, Dashboard, AICC 등 이름만 들어도 머리가 복잡해지는 시스템들이 이 StoreState라는 약속을 통해 매장 상황 데이터를 주고받는다고 했다.
“언니, 이거 봐봐. 결국 서비스끼리 데이터를 주고받는다는 건, 이렇게 JSON 형식만 잘 맞추면 되는 거 아니야?”
솔라는 자신감에 찬 목소리로 루나를 불렀다. 그녀의 마우스 커서는 visible_person_count라는 필드를 가리키고 있었다.
“예를 들면 이 필드. visible_person_count. 이름만 봐도 딱 알겠네. ‘보이는 사람 수’니까 매장에 들어온 고객 수라는 뜻이잖아. 마케팅팀에서는 이 숫자로 구매 전환율을 계산하고, 운영팀에서는 매장 혼잡도를 이걸로 파악하면 되겠지. 간단하네.”
솔라는 이미 머릿속으로 이 데이터가 각 팀의 대시보드에 멋지게 그려지는 모습까지 상상하고 있었다. 서비스 연동이라는 거대한 과제도 결국엔 잘 정의된 필드 목록 하나로 해결되는 듯 보였다.
루나는 솔라의 화면을 잠시 들여다볼 뿐, 바로 대답하지 않았다. 대신 책상 위에 놓인 포스트잇 두 장과 펜을 집어 들었다. 한 장에는 ‘마케팅팀’, 다른 한 장에는 ‘운영팀’이라고 적었다.
“좋아, 솔라. 방금 네가 말한 두 팀을 여기 소환해 보자.”
루나는 ‘마케팅팀’ 포스트잇을 솔라의 왼편에, ‘운영팀’ 포스트잇을 오른편에 놓았다.
“두 팀 모두 StoreState를 받아서 visible_person_count가 ‘10’이라는 값을 확인했어. 자, 마케팅팀의 솔라 기획자는 이 숫자로 무엇을 할 수 있을까?”
“그야 당연히 ‘오늘의 고객 수: 10명’이라고 보고서에 쓰겠지. 그리고 그날 팔린 물건이 1개면, ‘구매 전환율: 10%’라고 계산할 거고.”
솔라는 망설임 없이 대답했다. 너무나 당연한 흐름이었다.
“좋아. 그럼 이번엔 운영팀의 솔라 매니저 차례야. visible_person_count가 ‘10’이라는 값을 봤어. 운영팀은 이 숫자로 뭘 할까?”
“음… 매장이 너무 붐비는지 확인하겠지? 우리 매장은 동시에 20명 이상 들어가면 위험하니까, ‘현재 인원 10명, 아직 여유 있음’ 이렇게 판단할 거야.”
“그런데 바로 그 순간, 매장 직원이 교대하면서 CCTV 카메라 앞을 지나갔다면? 그리고 매장 입구에 들어올까 말까 망설이는 사람이 잠시 화면에 잡혔다가 사라졌다면? Vision 시스템의 CCTV는 그 둘도 사람으로 인식해서 visible_person_count에 포함시켰어.”
루나의 말에 솔라의 표정이 미묘하게 굳었다.
“어… 그럼 마케팅팀 보고서에 잡힌 ‘고객’ 중에는 직원이나 그냥 지나가던 사람이 포함될 수도 있겠네. 구매 전환율 10%가 아니게 되잖아. 실제 고객이 8명이었다면 12.5%인데.”
“바로 그거야.”
루나는 말을 이었다.
“마케팅팀은 이 필드를 ‘매장에 들어온 순수 고객 수’라고 해석했지만, 이 데이터를 만든 Vision팀은 ‘CCTV 화각 내에 잠시라도 잡힌 사람의 수’라고 정의했어. 직원, 통행인, 심지어 마네킹을 사람으로 잘못 인식한 경우까지 포함될 수 있는, 약간의 노이즈가 낀 숫자지. 운영팀은 이걸 경험적으로 알고 있어서 ‘정확한 인원수’가 아니라 ‘혼잡도 경향’을 보는 지표로만 사용했고.”
솔라는 두 포스트잇을 번갈아 보았다. 같은 visible_person_count: 10이라는 값에서 출발했지만, 한쪽은 잘못된 비즈니스 분석으로 이어지고 다른 한쪽은 의도대로 쓰이고 있었다. 필드 이름이 명확해 보였지만, 그 이름 아래에는 서로 다른 해석과 가정이 숨어 있었다.
“그럼… visible_person_count는 CCTV에서 보이는 인원이지, 정확한 고객 수라고 단정하면 안 되는 거였구나.”
솔라는 혼잣말처럼 중얼거리며 자신이 처음 가졌던 생각이 얼마나 순진했는지 깨달았다. JSON 필드 이름은 데이터의 ‘주소’일 뿐, 그 안에 든 ‘내용물’이 무엇인지, 어떻게 써야 하는지까지 알려주진 않았다. 서비스 간의 약속, 즉 계약은 단순히 필드 목록을 공유하는 것을 훨씬 넘어서는 문제였다. ‘보이는 사람 수’라는 이름 뒤에 숨겨진 ‘직원이나 통행인이 포함될 수 있음’이라는 맥락과 한계까지 함께 공유해야 진짜 ‘계약’이 성립되는 것이었다.
“이제 알겠어. 이 JSON은 그냥 데이터 구조가 아니라, 여러 팀이 함께 읽고 동의해야 하는 한 권의 ‘계약서’ 같은 거구나. 그리고 그 계약서에는 각 항목의 뜻을 풀어쓴 ‘용어 정의집’이 반드시 필요하겠네.”
솔라는 visible_person_count 필드 옆에 자신만의 주석을 달기 시작했다. ‘CCTV 화각 내 감지된 객체 수. 고객 수와 다를 수 있음.’
문득 JSON 구조에 있던 다른 필드들이 눈에 들어왔다. captured_at, processed_at 같은 시간 관련 필드와 quality_status 같은 상태 값들이 보였다.
‘그래, 이제 데이터의 ‘의미’를 정의해야 한다는 건 알겠어. 하지만 시간이나 품질 같은 건 그냥 있는 그대로 쓰면 되는 거 아닐까? 시간은 시간이고, 상태는 상태지. 거기에 무슨 다른 해석이 필요하겠어?’
솔라는 방금 얻은 깨달음에 만족하며, 아직 발견하지 못한 다음 문제의 씨앗을 무심코 넘겨짚고 있었다.
2장: 시간의 이중성: captured_at과 processed_at 중 무엇을 믿을까?
visible_person_count의 숨은 의미를 파악한 솔라는 자신감이 붙었다. 이제 서비스 계약의 본질을 이해한 것 같았다. 그녀는 곧바로 다음 단계로 나아갔다. 수집된 StoreState 데이터를 기반으로 시간대별 매장 방문객 추이 보고서를 만들어보기로 한 것이다.
솔라는 비어있는 스프레드시트 파일을 열고, 시스템 로그에서 StoreState JSON 객체 몇 개를 복사해왔다. 그녀의 화면에는 비슷한 구조의 데이터 덩어리들이 나란히 놓였다.
// 로그 #1
{
"store_id": "A101",
"camera_id": "cam_01",
"visible_person_count": 8,
"captured_at": "2023-10-26T15:00:01Z",
"processed_at": "2023-10-26T15:05:15Z",
...
}
// 로그 #2
{
"store_id": "A101",
"camera_id": "cam_01",
"visible_person_count": 2,
"captured_at": "2023-10-26T15:04:59Z",
"processed_at": "2023-10-26T15:05:18Z",
...
}
“시간대별로 사람 수를 집계하려면, 어떤 시간 필드를 써야 하지?”
솔라는 두 개의 시간 필드, captured_at과 processed_at을 보며 잠시 고민했다. ‘captured’는 ‘포착된’ 시간, ‘processed’는 ‘처리된’ 시간. 그녀는 ‘처리된’ 시간인 processed_at이 시스템에 최종적으로 기록된 공식적인 시간일 거라고 넘겨짚었다. ‘데이터가 생성된 시간이 중요하지, 카메라에 찍힌 시간이 뭐 그리 중요하겠어?’ 그녀는 processed_at을 기준으로 데이터를 집계하기 시작했다. 15시 05분에 8명, 또 15시 05분에 2명… 데이터는 차곡차곡 정리되었다.
잠시 후, 솔라가 만든 꺾은선 그래프가 화면에 나타났다. 그런데 그래프의 모양이 어딘가 이상했다. 오후 3시 정각에는 방문객이 거의 없다가, 3시 5분경에 갑자기 뾰족하게 치솟는 모양새였다.
“언니, 이것 좀 봐. 우리 시스템 데이터로는 3시 5분에 사람이 몰렸다고 나오는데, 매장 운영팀 일지에는 3시 정각에 근처에서 행사가 끝나면서 손님이 한꺼번에 들어왔다고 적혀 있단 말이야. 왜 5분이나 차이가 날까?”
솔라의 목소리에는 당혹감이 묻어났다. 데이터가 현실을 제대로 반영하지 못하고 있었다.
루나는 솔라의 그래프와 운영팀 일지를 번갈아 보더니, 묵묵히 솔라의 모니터에 떠 있는 원본 JSON 객체를 가리켰다. 특히 첫 번째 로그를 손가락으로 짚었다.
captured_at: "2023-10-26T15:00:01Z"
processed_at: "2023-10-26T15:05:15Z"
아무 말 없이 두 필드를 가리키는 루나의 손끝에서, 솔라는 순간 무언가 잘못되었음을 직감했다. 두 시간 사이에는 5분이 넘는 간극이 있었다.
“아…!”
짧은 탄성과 함께 솔라의 머릿속에서 조각들이 맞춰졌다.
“captured_at은 CCTV 카메라가 매장의 상황을 ‘포착한’ 실제 세상의 시간. processed_at은 Vision 서버가 그 이미지 파일을 받아서 분석을 마치고 StoreState라는 결과를 ‘만들어낸’ 시스템의 시간.”
그제야 5분의 차이가 의미하는 바가 보였다. 오후 3시 정각에 매장에 들어온 손님들은 분명 그 시간에 카메라에 찍혔다. 하지만 네트워크 지연이나 서버의 부하 때문에, 그 영상 데이터는 5분 뒤에나 처리되어 시스템에 기록된 것이었다. 솔라가 만든 그래프는 매장의 방문객 추이가 아니라, 데이터 처리 서버의 작업 지연 상태를 그리고 있었던 셈이다.
“내가 완전히 잘못 짚었네. 매장에서 일어난 일을 분석하려면 실제 사건이 발생한 captured_at을 써야 했어. 만약 시스템 성능을 모니터링하고 싶었다면, 두 시간의 차이를 계산해서 ‘데이터 처리 지연 시간’으로 활용할 수도 있었을 테고.”
솔라는 자신이 얼마나 단순하게 생각했는지 깨달았다. visible_person_count처럼 시간 필드 역시 이름만으로는 충분하지 않았다. ‘시간’이라는 단어 뒤에 ‘어떤 사건을 기준으로 측정된 시간인가?’라는 맥락에 대한 합의가 필요했다. 서비스 계약은 필드의 이름과 자료형뿐만 아니라, 그 값이 측정되는 기준점까지 명확히 정의해야 하는, 훨씬 더 정교한 약속이었던 것이다.
솔라는 서둘러 스프레드시트의 기준 시간을 captured_at으로 변경했다. 그러자 그래프는 마법처럼 운영팀의 일지와 일치하는 모양으로 바뀌었다. 오후 3시 정각에 방문객 수가 급증하는, 현실을 올바르게 반영하는 그래프가 완성되었다.
뿌듯한 마음으로 완성된 그래프를 보던 솔라의 시선이 다시 원본 JSON 데이터의 한구석에 머물렀다. 이전에는 무심코 지나쳤던 필드들이었다.
"roi_version": "v1.2",
"quality_status": "LOW"
‘좋아, 이제 데이터의 의미도 알았고, 시간 기준도 바로 잡았어. 그런데… 데이터 품질 상태가 ‘낮음’(LOW)이라고? 이건 무슨 뜻이지? 이 데이터, 그냥 믿고 써도 되는 걸까?’
방금 해결한 문제보다 더 근본적인 질문이 솔라의 머릿속에 떠올랐다. 데이터가 존재한다는 사실만으로 그 유효성이 보장되는 것은 아닐지도 모른다는 불안감이었다.
3장: 보이지 않는 품질: quality_status와 roi_version의 중요성
시간대별 방문객 추이 그래프를 성공적으로 바로잡은 솔라는 한층 더 나아가기로 했다. 지난 한 주간의 StoreState 데이터를 모두 모아 주간 보고서를 만드는 것이었다. 데이터가 많아질수록 더 의미 있는 패턴을 발견할 수 있을 거라는 기대에 부풀었다.
수천 건의 JSON 로그를 테이블 형태로 변환하자, 이전에는 보이지 않던 새로운 풍경이 펼쳐졌다. quality_status 필드에는 LOW뿐만 아니라 HIGH, MEDIUM 같은 값들이 섞여 있었고, roi_version 필드에도 v1.2와 v1.3이 함께 나타났다. 솔라는 잠시 고민에 빠졌다. 분석의 정확도를 높이려면, 당연히 품질이 낮은 데이터는 제외해야 하지 않을까?
솔라는 quality_status가 HIGH인 데이터만 남도록 필터를 걸었다. 그러자 수천 건이던 데이터가 수백 건으로 뚝 떨어졌다. 데이터가 너무 줄어든 게 마음에 걸렸지만, ‘쓰레기 데이터로 분석하느니 깨끗한 데이터만 쓰는 게 낫다’고 스스로를 설득했다. 그녀는 확신에 찬 목소리로 옆자리의 루나에게 말을 걸었다.
“언니, 이 StoreState 계약서, 점점 더 똑똑해지는 기분이야. 데이터의 의미(visible_person_count)도 알려주고, 시간의 기준(captured_at)도 알려주더니, 심지어 데이터의 품질(quality_status)까지 알려주잖아. 이것 봐, LOW 퀄리티 데이터를 거르니까 진짜배기만 남았어. 이걸로 분석해야 결과가 정확하겠지?”
루나는 필터링된 테이블과 그 옆에 흐릿하게 비활성화된 나머지 데이터들을 잠시 바라보았다. 그러고는 솔라의 테이블에서 quality_status가 LOW로 표시된 데이터 두 개를 복사해 새 시트에 붙여넣었다. 두 데이터는 visible_person_count 값이 ‘5’로 동일했다.
// 데이터 A: B103 매장, 화요일 저녁
{
"store_id": "B103",
"visible_person_count": 5,
"captured_at": "2023-10-24T19:30:05Z",
"quality_status": "LOW",
"roi_version": "v1.2"
}
// 데이터 B: A101 매장, 수요일 오후
{
"store_id": "A101",
"visible_person_count": 5,
"captured_at": "2023-10-25T14:00:10Z",
"quality_status": "HIGH",
"roi_version": "v1.3"
}
루나는 데이터 B의 quality_status를 LOW로 수정한 뒤 솔라에게 물었다. 단, roi_version은 그대로 두었다. 이제 솔라의 눈앞에는 겉보기엔 거의 똑같은 두 개의 데이터가 놓였다. 둘 다 visible_person_count는 5, quality_status는 LOW다.
“솔라, 여기 두 개의 데이터가 있어. 둘 다 ‘보이는 사람 수’가 5이고, 품질은 ‘낮음’이야. 이 두 개의 ‘5’는 같은 의미일까?”
“음… 일단 숫자는 같고 품질도 같으니까… 같은 거 아닐까? 둘 다 믿을 수 없는 데이터라는 뜻이겠지.”
솔라는 자신 없이 대답했다. 함정이 있는 질문 같았다.
“데이터 A는 어젯밤 폭우로 매장 앞 가로등이 나가서, 어두운 화면을 Vision 시스템이 겨우 분석한 결과야. 사람의 형체인지 그림자인지 긴가민가했지만, 시스템은 ‘대략 5명’이라고 판단했어. 이 경우 quality_status가 ‘LOW’가 된 이유는 ‘조명 부족’이야.”
루나는 말을 이었다.
“한편, 데이터 B는 사실 HIGH 퀄리티 데이터였어. 맑은 날 오후에 찍힌 선명한 영상이었지. 그런데 이 데이터의 roi_version은 v1.3이야. 최근 매장 입구의 혼잡을 막기 위해, 계산대 앞의 ‘실제 구매 의사가 있는 고객’만 집계하도록 roi_version을 v1.2에서 v1.3으로 업데이트했거든. 만약 이 데이터를 v1.2 규칙을 기준으로 분석한다면, 이건 ‘잘못된 기준을 적용한 데이터’가 돼. 그래서 내가 임의로 quality_status를 LOW로 바꾼 거야.”
솔라의 머릿속이 복잡해졌다. 첫 번째 ‘5’는 ‘불확실한 관측값’이었다. 두 번째 ‘5’는 ‘정확하지만 다른 기준으로 센 값’이었다. 같은 ‘5’라는 숫자, 같은 LOW라는 상태 값이지만, 그 의미와 원인은 전혀 달랐다.
“그럼… quality_status는 데이터가 얼마나 믿을만한지 알려주는 신호등 같은 거고, roi_version은 그 데이터를 만들어낸 ‘규칙의 버전’이구나. 버전이 다르면, 같은 visible_person_count라도 측정 범위가 다르니까 직접 비교하면 안 되는 거였어.”
“맞아. 서비스 계약은 데이터 값만 약속하는 게 아니야. 그 값이 어떤 조건(조명, 카메라 상태 등)에서 생성되었는지 알려주는 ‘품질 상태’와, 어떤 규칙(roi_version)에 따라 측정되었는지 알려주는 ‘정의 버전’까지 포함해야 해. 이 정보가 없으면, 우리는 모든 숫자를 맹신하거나, 혹은 모든 숫자를 불신하는 함정에 빠지게 돼.”
솔라는 자신이 만든 보고서 필터를 다시 바라보았다. 단순히 quality_status가 LOW라고 해서 무조건 버리는 것은 매우 위험한 판단이었다. ‘조명 부족’ 때문인 데이터는 버리는 게 맞을 수 있지만, ‘roi_version 불일치’ 때문이라면 다른 버전의 데이터와 따로 분류해서 분석해야 했다. 데이터가 존재한다는 사실만으로는 충분하지 않았다. 그 데이터의 ‘신뢰도’와 ‘맥락’을 함께 읽을 수 있어야 비로소 유효한 정보가 되었다.
“이제야 알겠어. 이 StoreState는 그냥 사실을 나열한 보고서가 아니라, 실험 결과를 담은 논문 같은 거였네. 결과값만 덜렁 주는 게 아니라, ‘이 실험은 이런 환경에서 이런 방법으로 진행했으며, 따라서 결과의 신뢰도는 이 정도입니다’라고 말해주는 거였어.”
솔라는 뿌듯한 마음으로 필터 규칙을 수정하기 시작했다. ‘앞으로 내 대시보드에서는 quality_status가 HIGH이고, roi_version이 최신인 데이터만 골라서 저장하고 보여줘야겠다. 그래야 사용자들에게 혼란을 주지 않을 테니까.’
솔라는 방금 배운 지식을 바탕으로 가장 합리적이라고 생각하는 결정을 내렸다. 하지만 그 결정이 다른 팀에게 어떤 파장을 일으킬지에 대해서는 아직 생각하지 못하고 있었다.
4장: 단 한 번의 진실: frame_id로 지키는 데이터 무결성
솔라는 자신이 새로 만든 대시보드 데이터베이스를 뿌듯하게 바라보고 있었다. 이전 장에서 얻은 깨달음을 바탕으로, 그녀는 StoreState를 수신할 때 quality_status가 HIGH이고 roi_version이 최신인 데이터만 골라서 저장하는 로직을 추가했다. 덕분에 그녀의 대시보드 서비스가 사용하는 데이터베이스는 오직 분석 가치가 높은 ‘깨끗한’ 데이터로만 채워지고 있었다. 다른 서비스가 어떤 데이터를 쓰든, 적어도 대시보드 사용자들은 가장 정제된 정보만을 보게 될 터였다. 각 서비스는 자신에게 맞는 최적의 데이터 저장 정책을 가지면 된다고, 솔라는 굳게 믿었다.
바로 그때, AICC팀의 동료 한 명이 자리로 다가와 조심스럽게 말을 걸었다.
“솔라님, 혹시 어제 오후 3시경 A101 매장 데이터에 이상한 점 없었나요? 저희 이상 상황 감지 모델이 그 시간대 데이터를 전혀 처리하지 못한 것 같아서요.”
솔라는 자신 있게 자신의 데이터베이스를 조회했다. 해당 시간대의 데이터는 존재하지 않았다.
“아, 그 데이터는 quality_status가 LOW여서 저희 대시보드 시스템에서는 자동으로 필터링됐어요. 아마 조명이 안 좋았거나 해서 분석 가치가 없는 데이터였을 거예요.”
그러자 동료의 표정이 난감하게 변했다. “저희 팀은 바로 그 ‘품질 낮은’ 데이터가 필요할 때가 많거든요. 시스템이 왜 그런 판단을 내렸는지 역추적해서 모델을 개선해야 해서요. 그런데 대시보드 쪽 데이터베이스에는 아예 기록조차 남지 않은 거군요.”
솔라의 머릿속이 순간 하얘졌다. 자신의 ‘똑똑한’ 필터가 다른 팀에게는 꼭 필요한 정보를 차단하는 벽이 되어버린 것이었다.
루나는 두 사람의 대화를 잠자코 듣고 있다가, 빈 회의실의 화이트보드 앞으로 솔라를 데려갔다. 그녀는 보드마커로 커다란 상자 두 개를 그렸다. 왼쪽에는 ‘대시보드 DB (솔라 정책)’, 오른쪽에는 ‘원본 이력 DB’라고 적었다.
“자, Vision 시스템에서 데이터가 하나 도착했어.”
루나는 가상의 JSON 데이터를 읊으며 보드에 적었다.
{ store_id: "A101", frame_id: "abc-123", quality_status: "HIGH", count: 10 }
“이 데이터는 두 데이터베이스에 어떻게 저장될까?”
“품질이 HIGH니까… 둘 다 저장되겠지.” 솔라가 대답했다. 루나는 양쪽 상자에 ‘10’이라는 숫자를 적었다.
“다음 데이터. 이번엔 품질이 낮아.”
{ store_id: "A101", frame_id: "abc-124", quality_status: "LOW", count: 2 }
솔라는 잠시 머뭇거리다 대답했다. “내 DB에는 저장되지 않고, 원본 이력 DB에만 저장되겠네.” 루나는 오른쪽 상자에만 ‘2’라는 숫자를 추가했다. 두 데이터베이스의 상태가 달라지기 시작했다.
“이제 진짜 문제가 발생해. 네트워크에 잠깐 문제가 생겨서, Vision 시스템이 방금 보냈던 HIGH 품질 데이터를 한 번 더 보냈어.”
루나는 처음과 똑같은 데이터를 다시 읊었다.
{ store_id: "A101", frame_id: "abc-123", quality_status: "HIGH", count: 10 }
“자, 솔라. 네 대시보드 DB에는 이제 무슨 일이 생기지?”
솔라는 자신의 코드를 떠올렸다. 품질이 HIGH면 저장한다. 그뿐이었다. “…똑같은 데이터가 한 번 더 저장되겠네. 그럼 총 방문객 수가 10명이 아니라 20명으로 잘못 집계될 거야.” 솔라의 목소리가 작아졌다.
“그럼 ‘원본 이력 DB’는 어떻게 이 문제를 피할 수 있을까?”
루나의 질문에 솔라는 답을 찾지 못했다. 루나는 말없이 StoreState 계약서의 한 구절을 가리켰다.
“frame_id가 있는 상태는 매장·카메라별 한 번만 원본 이력에 저장한다.”
그제야 모든 것이 명확해졌다. frame_id는 영상의 각 프레임에 부여된 고유한 신분증 같은 것이었다. 원본 이력 데이터베이스는 데이터를 저장하기 전에, ‘이 매장의 이 카메라에서 이 frame_id를 가진 데이터가 이미 저장된 적이 있는가?’를 확인하는 규칙을 가지고 있었다. 중복된 데이터라면 몇 번을 더 받아도 조용히 무시했다. 데이터의 ‘무결성’, 즉 데이터가 중복이나 누락 없이 완전하고 정확한 상태를 유지하도록 지켜주는 규칙이었다.
솔라는 자신이 얼마나 위험한 생각을 했는지 깨달았다. JSON 객체는 각 서비스가 마음대로 해석하고 저장해도 되는 독립적인 정보 꾸러미가 아니었다. 그것은 모든 서비스가 함께 공유하고 신뢰해야 할 ‘단 하나의 진실’을 구성하는 조각이었다. 그리고 그 진실을 어떻게 기록하고 유지할지에 대한 규칙, 즉 데이터 무결성 규칙은 서비스 계약의 가장 핵심적인 부분 중 하나였다. 한 서비스의 자의적인 저장 정책이 시스템 전체의 데이터 일관성을 깨뜨리고, 다른 서비스의 기능을 마비시킬 수도 있었다.
“내 결정이… 다른 팀의 데이터까지 망가뜨릴 뻔했구나. 대시보드만 생각했는데, 사실은 우리 모두가 같은 역사를 기록하는 중이었어. 어떤 데이터를 저장하고, 어떤 데이터를 무시할지는 개별 서비스가 아니라 우리 모두의 약속이어야 했네.”
화이트보드에 그려진 두 개의 DB는 이제 확연히 다른 값을 담고 있었다. 하나는 멋대로 편집된 부정확한 복사본, 다른 하나는 엄격한 규칙 아래 보호받는 진실의 원본이었다.
솔라는 자신의 자리로 돌아와 방금 추가했던 필터링 코드를 지우기 시작했다. 그리고 StoreState 계약 문서를 다시 열었다. 이 계약이 얼마나 많은 팀들의 암묵적인 신뢰와 약속 위에 세워져 있는지 새삼 느꼈다. 문득 새로운 의문이 떠올랐다.
‘이토록 중요한 규칙이라면, 누군가 더 좋은 규칙을 발견했을 때 어떻게 바꿔야 하지? 이 계약서는 한번 정해지면 절대 바꿀 수 없는 건가? 아니면… 필요할 때마다 내가 고쳐서 모두에게 알려주면 되는 걸까?’
5장: 협의의 기술: API 계약 변경, 제안해야 하는 이유
솔라는 자신이 저지른 실수의 파편들을 지워나갔다. 대시보드 서비스에 추가했던 ‘똑똑한’ 필터링 코드는 사실 다른 팀의 중요한 데이터를 가로막는 ‘벽’이었다. 코드를 지우면서, 솔라는 여러 서비스가 공유하는 StoreState 계약이 얼마나 섬세한 신뢰의 그물망 위에 세워져 있는지 비로소 실감했다. 시스템 전체의 진실을 지키는 규칙의 무게를 깨닫자, 새로운 질문이 자연스레 고개를 들었다. 이토록 중요한 계약이라면, 더 나은 규칙을 발견했을 때 어떻게 바꿔야 할까?
솔라는 자신의 아이디어를 시험해 보고 싶었다. 1장에서 문제가 되었던 visible_person_count 필드. 만약 여기에 is_staff라는 boolean 필드를 추가하면, 고객과 직원을 명확히 구분할 수 있어 분석의 정확도가 훨씬 높아질 터였다. ‘이건 정말 좋은 개선이야!’ 솔라는 생각했다. 그녀는 StoreState의 정의가 담긴 공용 문서를 열고, visible_person_count 바로 아래에 is_staff: boolean 한 줄을 추가했다. 그리고는 팀 메신저에 메시지를 입력하기 시작했다. ‘여러분, 제가 StoreState에 is_staff 필드를 추가했어요. 이제부터 고객 수 분석이 더 정확해질 겁니다! 다들 코드 업데이트하세요!’
전송 버튼을 누르기 직전, 솔라는 망설였다. 지난번 AICC팀 동료의 난감한 표정이 떠올랐기 때문이다. 자신의 선한 의도가 또 다른 혼란을 낳을 수도 있다는 불안감이 손가락을 멈추게 했다.
“언니, 이 계약서 말이야… 내가 더 좋게 만들 수 있을 것 같거든? 필드 하나만 추가하면 돼. 이렇게 고치고 모두에게 알려주면, 다들 좋아하겠지?”
솔라는 자신의 모니터를 보여주며 루나에게 물었다. 모니터에는 수정된 StoreState 문서와 전송 직전의 메시지가 떠 있었다.
루나는 솔라의 화면을 잠시 보더니, 대답 대신 회의실의 커다란 디지털 화이트보드를 켰다. 그녀는 복잡한 아키텍처 다이어그램 대신, 네 개의 단순한 상자를 그렸다. 그리고 각각 ‘Vision’, ‘API’, ‘Dashboard’, ‘AICC’라고 적었다. 네 개의 상자는 화살표로 연결되어, StoreState 데이터가 흘러가는 경로를 보여주고 있었다.
“좋아, 솔라. 네가 방금 is_staff 필드를 추가하고 ‘전송’ 버튼을 눌렀다고 상상해 보자. 가장 먼저 무슨 일이 일어날까?”
루나는 ‘Dashboard’ 상자를 톡톡 두드렸다.
“음… 우리 대시보드 팀은 제일 먼저 코드를 수정하겠지. 새로 생긴 is_staff 필드를 읽어서 ‘직원 제외’ 필터를 만들 거야.”
“바로 그 순간, Vision 시스템은 여전히 is_staff 필드가 없는 옛날 버전의 StoreState를 보내고 있어. 대시보드 서비스는 어떻게 될까?”
“아…” 솔라의 얼굴에 당혹감이 스쳤다. “필드가 없으니까… 에러가 나거나, 아니면 모든 사람을 고객으로(is_staff가 false인 것으로) 처리해 버리겠네. 결국 아무것도 바뀌지 않거나, 오히려 데이터가 더 엉망이 되는구나.”
루나는 ‘Vision’ 상자에서 ‘Dashboard’ 상자로 향하는 화살표 위에 작은 X 표시를 그었다. 첫 번째 단절이었다.
“좋아, 그럼 Vision팀이 솔라의 메시지를 보고 부랴부랴 시스템을 업데이트했다고 치자. 이제 is_staff 필드를 담아서 보내기 시작했어. 하지만 중간에 있는 API팀은 아직 아무것도 몰라. 그럼 어떻게 될까?”
“API 서비스는… 계약서에 정의되지 않은 필드는 그냥 무시하거나, 보안 정책 때문에 아예 누락시킬 수도 있어. Vision팀이 애써서 보낸 is_staff 정보가 우리 대시보드까지 오지 못하고 중간에 사라지겠네.”
루나는 ‘API’ 상자 위에 두 번째 X 표시를 그었다.
“최악의 시나리오도 있어. AICC팀은 아무런 공지도 받지 못했어. 갑자기 로그에 정체불명의 is_staff라는 데이터가 찍히기 시작해. AICC의 데이터 처리 파이프라인은 예상치 못한 입력값 때문에 오류를 내뿜으며 멈춰버릴 수도 있지. 장애의 원인을 찾기 위해 AICC팀은 밤을 새워야 할지도 몰라. 솔라 네가 보낸 단 하나의 메시지 때문에.”
화이트보드에는 X 표시가 가득했다. 솔라의 ‘좋은 개선’은 시스템 전체를 마비시키는 연쇄 장애의 시발점이 되어 있었다. 단 한 줄의 코드 수정이 얼마나 큰 파장을 일으킬 수 있는지 눈앞에 생생하게 그려졌다.
“그럼… 어떻게 해야 하는데? 더 좋은 아이디어가 있어도 그냥 묻어둬야 해?” 솔라가 답답한 듯 물었다.
“아니.” 루나가 화이트보드의 모든 X 표시를 지우며 말했다. “그래서 우리에겐 ‘제안’이라는 절차가 있는 거야.”
루나는 StoreState 계약서의 마지막 페이지를 화면에 띄웠다. 거기에는 단 한 문장이 적혀 있었다.
“공통 API 계약은 각 기능에서 임의로 수정하지 않고 팀에 제안하도록 정의했다.”
“‘제안’은 허락을 구하는 과정이 아니야. 우리가 함께 지키기로 한 약속을, 더 나은 방향으로 나아가기 위해 모두의 지혜를 모으는 과정이지.”
루나는 솔라가 처음에 열었던 공용 문서 옆에, 새로운 ‘계약 변경 제안서’ 템플릿을 열었다. 템플릿에는 ‘변경 제안 내용’, ‘제안 이유’, ‘예상 영향 범위’, ‘기술적 고려사항’ 같은 항목들이 적혀 있었다.
솔라는 그제야 깨달았다. API 계약을 수정하는 것은 코드를 고치는 기술적인 행위 이전에, 여러 이해관계자가 얽힌 시스템의 질서를 바꾸는 사회적인 행위였다. ‘제안’이라는 절차는 단순히 번거로운 과정이 아니라, 변경으로 인한 파급효과를 모든 관련자가 미리 예측하고, 충격을 최소화하며, 함께 더 나은 방향으로 나아가기 위한 가장 효율적이고 안전한 장치였던 것이다.
“알겠어, 언니. 이건 그냥 코드가 아니었어. 여러 나라가 맺은 조약 같은 거였네. 한 나라가 마음대로 조항을 고치면 조약 전체가 깨져버리니까. 바꾸려면 모두가 모여서 함께 논의하고, 모두가 동의하는 새 조약에 함께 서명해야 하는 것처럼.”
솔라는 자리로 돌아왔다. 그리고 전송 직전이던 메신저 창을 가차 없이 닫아버렸다. 대신, 방금 루나가 보여준 ‘계약 변경 제안서’ 템플릿을 복사해 새 문서를 열었다. 그녀는 문서의 제목을 입력하기 시작했다.
[제안] StoreState 계약 개선: is_staff 필드 추가를 통한 고객 분석 정확도 향상
솔라는 이제 더 이상 데이터의 순진한 소비자가 아니었다. 데이터의 의미, 시간, 품질, 무결성을 이해하고, 이제는 그 약속의 생명주기까지 관리할 줄 아는, 시스템의 책임 있는 구성원으로 성장해 있었다. 그녀의 손끝에서, JSON을 넘어선 진정한 의미의 ‘계약’이 새롭게 쓰여지고 있었다.