Big Project · AI's EYE 07

읽기 패턴에 따른 다층적 데이터 저장 구조 설계

StoreState를 한 테이블에 계속 저장하면 최신 조회와 장기 분석을 모두 해결할 수 있을 것 같다. 왜 같은 상태를 여러 계층에 나눠 저장하는지 의문이다.

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

1장: 문제 인식: 하나의 테이블로는 충분하지 않다

솔라의 시선이 모니터의 한 문장에 머물렀다. 기술 아티클에 담긴, 시스템 개선 사례를 설명하는 짧은 문장이었다.

원본 StoreState 119,650건이 쌓인 뒤 최신 상태, 30초 이력과 시간 집계를 따로 두고 원본 보관 기준을 7일로 정했다.

문장을 구성하는 단어는 모두 아는 것이었다. 하지만 조합된 문장은 솔라가 가진 상식적인 모델에 의문을 던졌다. ‘StoreState’라는 이름의 데이터라면, 발생하는 순서대로 하나의 저장소에 차곡차곡 쌓아두는 것이 가장 자연스럽고 단순한 방법이 아닌가?

“언니, 잠깐 이것 좀 봐줄래?”

솔라는 생각의 매듭을 풀기 위해 거실 반대편에 있는 루나를 불렀다. “데이터를 한 테이블에 그냥 쭉 저장하면, 필요할 때 최신 데이터도 볼 수 있고 나중에 통계도 낼 수 있는 거 아니야? 그런데 여기서는 왜 데이터를 ‘최신’, ‘이력’, ‘집계’ 이렇게 셋으로 나누고, 심지어 원본은 7일만 보관한다고 하지? 일을 더 복잡하게 만드는 것 같은데.”

루나는 솔라의 화면을 잠시 들여다보더니, 고개를 끄덕였다. “네 말이 맞아. 모든 걸 한곳에 모아두는 게 가장 직관적이지.”

루나는 잠시 생각하더니, 근처에 있던 커다란 스케치북과 펜을 가져왔다. 그녀는 스케치북 한 페이지 가득 커다란 직사각형을 그리고, 그 안에 가로줄 몇 개를 그어 테이블 모양을 만들었다.

“이게 그 팀이 처음 만들었던 StoreState 테이블이라고 해보자.” 루나는 테이블 상단에 store_id, timestamp, people_count 같은 항목들을 적었다. “그리고 지금 이 안에는 네가 읽은 대로 데이터가 약 12만 건 쌓여 있어.”

“응, 거기까진 알겠어.” 솔라가 답했다.

“좋아. 이제 우리가 이 데이터 시스템의 주인이야. 그리고 지금부터 세 군데서 각기 다른 데이터 요청이 들어올 거야. 솔라 네가 이 요청들을 직접 처리해 봐.”

루나는 첫 번째 가상 요청을 제시했다. “첫 번째는 서비스의 메인 대시보드야. ‘A 매장의 현재 방문객 수’를 실시간으로 보여줘야 해. 사용자가 화면을 새로고침할 때마다 1초의 망설임도 없이 바로 숫자가 떠야 하지. 어떻게 이 테이블에서 데이터를 가져올래?”

솔라는 즉시 답했다. “간단해. A 매장의 데이터 중에서 timestamp가 가장 마지막인 한 건을 찾으면 되지.”

“맞아. 하지만 ‘12만 건 중에서’ 찾아야 해. 요청이 들어올 때마다.” 루나는 ‘매번’이라는 단어에 힘을 주어 말했다. 솔라는 순간 말문이 막혔다. 수십만 개의 데이터 레코드 더미 맨 밑바닥까지 뒤져서 맨 위에 있는 종이 한 장을 찾는 모습이 머릿속에 그려졌다. 빠르긴 하겠지만, 매번 하기엔 어딘가 비효율적인 느낌이었다.

루나는 스케치북 귀퉁이에 ‘요청 1: 즉각적인 최신 상태’라고 적고는, 두 번째 요청을 꺼냈다.

“이번엔 분석팀에서 들어온 요청이야. ‘어제 오후 3시부터 5분 동안 B 매장 방문객 수 추이가 이상한데, 정확한 흐름을 보고 싶다’고 해. 분석을 위해 30초 간격의 데이터가 필요하대.”

솔라는 잠시 고민했다. “음… 일단 B 매장 데이터로 한정하고, 어제 오후 3시에서 3시 5분 사이의 시간으로 범위를 좁혀야겠네. 그다음엔… 30초 간격으로 된 데이터를 뽑아내야 하고.” 생각만 해도 아까보다 훨씬 복잡했다. 특정 시간 범위의 데이터를 모두 꺼낸 다음, 그 안에서 다시 30초 간격에 맞는 데이터를 골라내는 작업은 꽤나 성가실 것 같았다.

루나는 이번에도 솔라의 설명을 듣고 스케치북에 ‘요청 2: 특정 구간의 흐름 분석’이라고 메모했다.

“마지막 요청이야. 이건 경영팀에서 왔어. ‘지난 한 달간, 우리 모든 매장의 시간대별 평균 방문객 수’를 보고서로 만들어 달래.”

솔라는 자기도 모르게 한숨을 내쉬었다. 이건 앞선 두 요청과 차원이 다른 문제였다. 테이블의 거의 모든 데이터를 훑어야 했다. 매장별로, 날짜별로, 시간대별로 데이터를 그룹화한 뒤 평균을 계산해야 한다. 12만 건의 데이터를 전부 스캔하는 작업은 분명 시간이 걸릴 것이다. 지금은 12만 건이지만, 데이터가 수백만, 수천만 건으로 늘어난다면 이 계산은 몇 분, 혹은 그 이상이 걸릴지도 모른다.

루나는 펜을 내려놓고 스케치북을 솔라에게 돌려 보여주었다. 스케치북에는 하나의 거대한 테이블 그림과 함께, 성격이 완전히 다른 세 가지 요청이 나란히 적혀 있었다.

‘요청 1: 즉각적인 최신 상태’ ‘요청 2: 특정 구간의 흐름 분석’ ‘요청 3: 장기 데이터 통계’

“봐봐, 솔라. 우리는 그냥 ‘데이터를 읽는다’고 생각했지만, 사실은 완전히 다른 세 종류의 ‘읽기’를 하고 있었어. 하나는 빛의 속도로 ‘최신’ 값 하나를 원했고, 다른 하나는 좁은 시간 범위의 ‘흐름’을 보려 했고, 마지막은 방대한 데이터의 ‘요약’을 필요로 했지.”

그제야 솔라는 자신이 처음 봤던 문장을 다시 떠올렸다. ‘최신 상태’, ‘30초 이력’, ‘시간 집계’. 그 단어들이 더 이상 낯설게 느껴지지 않았다.

“아…” 솔라가 나지막이 탄성을 내뱉었다. “그래서 나눈 거구나. 이 세 가지 다른 읽기 요청에 각각 가장 빠르게 답할 수 있는 전용 테이블을 따로 만들어 준 거네.”

하나의 큰 테이블로 모든 것을 해결하려던 초기 모델이 무너지는 순간이었다. 단순함의 대가로 모든 종류의 요청에 어중간한 속도로 응답하거나, 특정 요청에는 재앙과 가까운 성능을 보여줄 수 있다는 사실을 깨달았다. 데이터를 나누는 복잡함은, 사실 각기 다른 ‘읽기 패턴’의 요구를 만족시키기 위한 최적의 선택이었던 것이다.

솔라의 머릿속에 새로운 질문이 떠올랐다.

“이제 왜 나눠야 하는지는 알겠어. 그런데 하나의 테이블이 이 요청들을 처리할 때 정확히 어떤 일이 벌어지길래 성능이 나빠지는 거지? 데이터가 많아지면 왜 느려지는 건지, 그 병목 현상이 구체적으로 궁금해졌어.”

2장: 성능 병목: 성장하는 데이터와 지연되는 집계

솔라의 질문에 루나는 바로 대답하는 대신, 어제 함께 그렸던 스케치북을 다시 가져왔다. 거대한 테이블 그림과 그 옆에 나란히 적힌 세 가지 읽기 요청이 그대로 남아 있었다. 루나는 말없이 펜을 들어 ‘요청 3: 장기 데이터 통계’ 항목 아래에 작은 글씨를 추가했다.

확인 당시 약 12만 건, 전체 집계에 2초 소요.

이 새로운 정보는 솔라가 가졌던 의문의 방향을 미세하게 틀었다. 솔라는 성능 병목의 원리를 파고들고 싶었지만, 루나가 제시한 ‘2초’라는 구체적인 숫자는 다른 생각을 하게 만들었다.

“2초?” 솔라가 의아하다는 듯 되물었다. “12만 건이나 되는 데이터 전체를 훑어서 통계를 내는데 2초밖에 안 걸렸다고? 그 정도면 엄청 빠른 거 아니야? 이게 왜 문제가 돼?”

솔라에게 2초는 문제라기보다는 오히려 성공적인 결과처럼 보였다. 12만이라는 숫자가 주는 막연한 무게감에 비해 2초라는 응답 시간은 가볍고 경쾌하게 느껴졌다. 이 정도면 단일 테이블로도 충분히 괜찮은 것 아니냐는 처음의 생각에 다시 힘이 실리는 듯했다.

“지금은 괜찮을지도 모르지.” 루나는 솔라의 반응을 예상했다는 듯 차분하게 말했다. “하지만 그 시스템은 1초에 한 번씩 새로운 StoreState 데이터를 받는다고 했어. 멈춰있는 시스템이 아니라는 뜻이야.”

루나는 솔라에게 계산기를 건넸다. “하루면 데이터가 몇 개나 더 쌓일까?”

솔라는 잠시 생각하다가 계산기를 두드리기 시작했다. 1초에 1개. 1분이면 60개, 1시간이면 3,600개.

“하루는 24시간이니까… 3600 곱하기 24는… 86,400개. 매일 약 8만 6천 건이 늘어나는 거네.”

“맞아. 그럼 바로 내일, 이 테이블의 전체 데이터는 몇 건이 될까?” 루나의 질문이 이어졌다.

“기존 12만 건에 8만 6천 건을 더하면… 약 20만 6천 건.” 솔라가 답했다.

“좋아, 그럼 가장 단순하게 생각해서, 데이터 양과 집계 시간이 정비례한다고 가정해보자. 내일 장기 데이터 통계 요청은 몇 초가 걸릴까?”

솔라의 손이 다시 계산기로 향했다. 2초 곱하기, 20만 6천 나누기 12만. 화면에 뜬 숫자는 약 3.43이었다. 하루 만에 2초가 3.4초로 늘어나는 셈이었다. 별것 아닌 것 같기도 했지만, 어딘가 찜찜한 기분이 들었다.

“일주일 뒤는 어떨까? 한 달 뒤는?” 루나가 덧붙였다.

솔라는 멈추지 않고 계산을 이어갔다. 일주일 뒤 데이터: 120,000 + (86,400 * 7) ≈ 725,000건 예상 소요 시간: 2초 * (72.5만 / 12만) ≈ 12초

한 달 뒤 데이터: 120,000 + (86,400 * 30) ≈ 2,700,000건 예상 소요 시간: 2초 * (270만 / 12만) ≈ 45초

계산 결과가 화면에 나타나는 순간, 솔라는 숨을 멈췄다. 처음 ‘2초’라는 숫자를 들었을 때의 가벼운 마음은 온데간데없었다. 한 달 뒤, 경영팀이 월간 보고서를 보기 위해 버튼을 누르면 거의 1분 가까이 화면만 쳐다보고 있어야 한다는 뜻이었다.

“말도 안 돼… 그냥 한 달만 지났을 뿐인데, 2초짜리 조회가 45초가 된다고?”

“지금은 데이터가 약 12만 건뿐이지만, 이건 시스템이 막 시작했을 때의 이야기일 뿐이야. 서비스가 안정적으로 운영되면 데이터는 계속 쌓이고, 이 시간은 점점 더 길어지겠지. 석 달 뒤에는 2분이 넘을 수도 있어.”

루나의 말에 솔라는 ‘2초’라는 숫자를 완전히 다른 시각으로 보게 되었다. 그것은 성능의 지표가 아니라, 앞으로 닥쳐올 재앙의 크기를 예측하게 해주는 ‘경고등’이었다. 데이터가 늘어날수록 특정 읽기 패턴, 특히 테이블 전체를 훑어야 하는 집계 작업의 성능 저하는 단순한 비례 관계를 넘어 기하급수적으로 심각해질 수 있다는 사실을 체감했다.

“게다가 더 큰 문제는,” 루나가 스케치북의 ‘요청 1: 즉각적인 최신 상태’를 펜으로 톡톡 두드리며 말했다. “저 45초짜리 무거운 집계 쿼리가 데이터베이스를 붙들고 있는 동안, 실시간으로 매장 상황을 봐야 하는 대시보드 요청은 어떻게 될까?”

“아…”

그제야 모든 조각이 맞춰졌다. 느린 쿼리 하나가 다른 모든 것을 마비시킬 수 있었다. ‘빠른’ 요청과 ‘무거운’ 요청을 한 테이블에서 처리하는 것은, 마치 1차선 도로에 스포츠카와 덤프트럭을 함께 달리게 하는 것과 같았다. 결국 모든 차가 덤프트럭의 속도에 맞춰 기어가게 될 터였다.

솔라는 고개를 끄덕였다. 이제 단일 테이블의 한계를 명확히 이해했다. 성장하는 데이터 앞에서 단일 구조는 필연적으로 병목을 만날 수밖에 없었다.

“알겠어. 하나의 테이블에 모든 걸 쌓아두는 건 미래에 재앙을 부르는 거구나. 그럼 이 문제를 어떻게 해결한 거지? 그 아티클의 팀은 최신 상태, 30초 이력, 시간 집계… 이 서로 다른 요청들을 각각 어떻게 효율적으로 처리하는 구조를 만든 거야?“

3장: 최신 상태: 항상 가장 빠른 조회를 위해

솔라의 마지막 질문에, 루나는 스케치북의 다음 페이지를 넘기는 것으로 답을 대신했다. 이전 페이지를 가득 채웠던 거대한 단일 테이블과 복잡한 계산식은 이제 과거의 것이 되었다. 새 페이지에는 깔끔하게 구획된 세 개의 작은 직사각형이 그려져 있었다. 루나는 각 사각형 위에 이름을 붙였다.

current_store_states store_state_history hourly_store_metrics

하나의 도로를 달리던 스포츠카와 덤프트럭을 위해 각기 다른 전용 차선을 만들어 준 듯한 그림이었다. 솔라의 시선은 가장 위에 있는 current_store_states라는 이름에 꽂혔다. ‘현재 매장 상태들’. 이름부터가 첫 번째 읽기 요청, ‘즉각적인 최신 상태’를 위한 것임이 분명했다.

“이게 그 팀이 만든 해결책이구나. 요청 종류별로 테이블을 나눈 거네.” 솔라는 고개를 끄덕였다. 하지만 이내 새로운 의문이 고개를 들었다. “그런데 언니, 이 첫 번째 테이블, 꼭 필요해? 그냥 원래 있던 큰 테이블에서 특정 매장의 마지막 데이터 하나만 가져오면 되잖아. ORDER BY timestamp DESC LIMIT 1 같은 걸로. 인덱스 잘 걸어두면 그것도 엄청 빠르지 않아?”

솔라의 생각은 합리적이었다. 수백만 건의 데이터 속에서도 특정 조건의 마지막 한 건을 찾는 것은 데이터베이스의 기본기 중 하나였다. 굳이 테이블을 하나 더 만들어 관리하는 것은 번거로운 중복 작업처럼 느껴졌다.

루나는 솔라의 말을 가만히 듣고는, 펜을 들어 current_store_states 사각형과, 이전 페이지의 거대한 store_states 테이블을 나란히 비교할 수 있도록 스케치북을 펼쳤다.

“네 말이 맞아. 큰 테이블에서 찾는 것도 충분히 빠를 수 있어. 하지만 여기서 중요한 건 ‘빠르다’의 기준이야.”

루나는 거대한 store_states 테이블 그림 옆에 작은 돋보기 아이콘을 그렸다. “매장이 100개 있고, 데이터가 총 100만 건 쌓여있다고 해보자. 우리가 ‘A 매장의 최신 상태’를 요청하면, 데이터베이스는 이렇게 일해. 먼저 인덱스를 써서 100만 건 중 A 매장에 해당하는 데이터들을 찾아. 그게 만 건이라고 치자. 그 다음, 그 만 건 중에서 시간이 가장 마지막인 데이터를 찾기 위해 정렬하거나, 인덱스 끝으로 이동해야 하지. 아주 짧은 순간이지만, ‘찾고’ ‘비교하는’ 과정이 분명히 존재해.”

솔라는 돋보기가 만 개의 데이터 더미를 훑는 모습을 상상했다. 눈 깜짝할 사이에 끝나겠지만, 어쨌든 ‘일’을 하는 건 맞았다.

이어서 루나는 current_store_states 사각형 안을 채워나갔다. 그녀는 그 안에 딱 100개의 줄만 그렸다. 그리고 각 줄의 시작에 store_A, store_B, store_C… 와 같이 매장 ID를 적었다.

store_idtimestamppeople_count
store_A
store_B
store_Z

“이 테이블은 구조가 달라. 이 테이블에는 매장별로 데이터가 딱 한 줄씩만 존재해. 총 100줄이지. 새로운 데이터가 들어오면 기존 줄을 그냥 덮어쓰는 거야. 여기서 ‘A 매장의 최신 상태’를 요청하면 어떻게 될까?”

루나는 돋보기 대신, 손가락 아이콘을 그려 store_A 줄을 바로 가리켰다.

“데이터베이스는 그냥 ‘A 매장’이라는 열쇠로 맞는 서랍을 바로 여는 것과 같아. 찾거나 비교할 필요가 없어. 다른 데이터는 아예 쳐다보지도 않지. 이건 ‘검색’이 아니라 ‘조회’에 가까워.”

그제야 솔라는 두 방식의 근본적인 차이를 깨달았다. ORDER BY를 쓰는 방식은 아무리 빨라도 ‘작업’이었다. 반면 전용 테이블에서 가져오는 것은 거의 비용이 들지 않는 ‘결과 확인’에 가까웠다.

“아…” 솔라가 탄성을 내뱉었다. “단순히 몇 밀리초 더 빠르냐의 문제가 아니구나. 하나는 가변적인 작업을 하는 거고, 다른 하나는 항상 일정한 속도를 보장하는 거네.”

“맞아. 그리고 더 중요한 게 있어.” 루나는 이전 장에서 45초까지 늘어났던 집계 쿼리를 가리켰다. “덤프트럭이 도로를 막고 있을 때, 스포츠카는 어떻게 해야 할까? 갓길로 달려야지. 이 current_store_states 테이블이 바로 그 전용 갓길이야. 다른 무거운 쿼리가 데이터베이스를 얼마나 힘들게 하든 상관없이, 이 테이블에 대한 요청은 거의 아무런 방해도 받지 않고 즉시 응답할 수 있어.”

최신 상태를 위한 별도 테이블은 단순히 속도를 위한 최적화를 넘어, 시스템 전체의 안정성을 위한 ‘보호 장치’였다. 가장 빈번하고 가장 빠른 응답이 필요한 핵심적인 읽기 요청을 다른 무거운 요청들로부터 완전히 분리하여 예측 가능한 성능을 보장하는 것. 그것이 current_store_states의 진짜 존재 이유였다.

단순히 인덱스로 해결할 수 있다고 생각했던 자신의 초기 모델이 얼마나 안일했는지 깨달았다. 최신성 최적화는 단순히 쿼리를 튜닝하는 문제가 아니라, 읽기 패턴의 특성에 맞게 데이터 구조 자체를 설계하는 문제였던 것이다.

솔라는 명쾌해진 머리로 스케치북의 나머지 두 사각형을 바라보았다.

“알겠어. current_store_states는 실시간 대시보드를 위한 완벽한 해법이네. 이건 항상 ‘현재’만을 보여주잖아. 그런데… 어제 우리가 얘기했던 두 번째 요청, ‘특정 구간의 흐름 분석’은 어떡하지? 예를 들어 ‘과거 5분 동안의 30초 간격 데이터’ 같은 건 이 테이블엔 없잖아. 그리고 원본 데이터는 7일 뒤에 사라질 거고. 그럼 그 중간 어딘가에 있어야 할 이력 데이터는 어디서 오는 거야?“

4장: 표본 이력: 30초 간격으로 의미 있는 흐름을

솔라의 시선은 current_store_states 테이블 너머, 스케치북에 그려진 두 번째 직사각형으로 향했다. ‘과거 특정 구간의 흐름 분석’이라는, 아직 해결되지 않은 요청을 위한 자리였다. 루나는 말없이 펜을 들어 그 사각형, store_state_history 안쪽에 한 줄의 설명을 추가했다.

30초 구간별 마지막 상태 저장

이 짧은 문장은 솔라에게 새로운 혼란을 안겨주었다. 그녀는 이전 장에서 원본 데이터가 1초에 한 번씩 쌓인다는 사실을 기억하고 있었다. 그런데 이 테이블은 30초에 한 번, 그중에서도 마지막 데이터만 저장한다고 했다. 30초 동안 쌓인 나머지 29개의 데이터는 그냥 버려진다는 뜻이었다.

“잠깐만, 언니. 이건 좀 이상한데.” 솔라가 미간을 찌푸리며 말했다. “분석을 하려면 원본 데이터 그대로 써야 하는 거 아니야? 중간 데이터를 마음대로 빼버리면, 마치 영화 필름을 30프레임 중 1프레임만 남기고 다 잘라내는 거랑 같잖아. 그렇게 만든 데이터로 어떻게 정확한 흐름을 분석할 수 있어? 중요한 순간이 그 30초 사이에 있었다면 완전히 놓치게 되는 거잖아. 분석 가치가 떨어질 것 같은데.”

솔라에게 이것은 타협이 아니라 정보의 왜곡처럼 느껴졌다. 장애의 원인을 찾거나 이상 행동의 패턴을 분석해야 하는 중요한 상황에서, ‘표본’ 데이터는 신뢰할 수 없는 증거가 될 수 있다는 우려였다.

“네 말이 맞아. 모든 순간이 중요하다면, 원본 데이터 외에는 답이 없지.” 루나는 솔라의 우려를 인정했다. “하지만 우리가 해결하려던 두 번째 요청을 다시 한번 볼까?”

루나는 첫 장에서 적었던 ‘요청 2: 특정 구간의 흐름 분석’ 메모를 가리켰다. 거기에는 ‘어제 오후 3시부터 5분 동안 B 매장 방문객 수 추이가 이상한데, 정확한 흐름을 보고 싶다’는 분석팀의 요청이 적혀 있었다.

“이 분석가는 5분 동안의 ‘흐름’을 보고 싶어 해. 방문객 수가 늘어나는 추세인지, 줄어드는 추세인지, 아니면 갑자기 급증했다가 떨어지는지를 파악하고 싶은 거지.”

루나는 스케치북의 새 페이지에 시간 축을 길게 그리고, 1분 구간을 표시했다. “원본 데이터는 1초마다 상태를 기록해. 1분이면 60개의 점이 찍히겠지.” 그녀는 펜으로 촘촘하게 오르내리는 불규칙한 선을 그렸다. 마치 지진계에 기록된 파형 같았다.

“이게 원본 데이터의 모습이야. 아주 상세하지. 이 그래프를 5분 길이로 늘여놓고 ‘흐름’을 보려면, 우리는 300개(60개 * 5분)의 점을 모두 읽고 머릿속으로 추세선을 그려야 해. 데이터베이스 입장에서도 꽤 많은 데이터를 읽어야 하는 일이고.”

다음으로 루나는 같은 시간 축 바로 아래에 새로운 그래프를 그리기 시작했다. 이번에는 30초 지점과 60초 지점에만 점을 찍고, 그 두 점을 직선으로 연결했다.

“이게 바로 store_state_history에 저장된 데이터로 그린 그래프야. 30초 간격으로 점을 찍었으니 1분에 2개, 5분이면 총 10개의 점만 필요하지.”

두 개의 그래프가 위아래로 나란히 놓였다. 위쪽의 촘촘한 원본 그래프는 미세한 떨림까지 모두 보여주었지만, 전체적인 윤곽은 아래쪽의 성긴 표본 그래프와 놀랍도록 비슷했다. 둘 다 전반적으로 우상향하는 추세를 명확히 보여주고 있었다.

“아…”

솔라는 두 그래프를 번갈아 보며 깨달음을 얻었다. ‘흐름’을 파악하는 데에는 1초 단위의 정밀함이 반드시 필요한 것은 아니었다. 마치 멀리서 산의 능선을 볼 때, 모든 나무 하나하나를 보지 않아도 전체적인 산의 형태를 알 수 있는 것과 같았다. 30초 간격의 데이터는 그 ‘능선’을 그리기에 충분한 정보를 담고 있었다.

“정확성을 조금 포기하는 대신, 효율성을 엄청나게 얻는 거구나.” 솔라가 말했다.

“바로 그거야.” 루나가 맞장구를 쳤다. “원본 데이터 테이블은 하루에 약 8만 6천 건이 쌓이지만, store_state_history 테이블은 하루에 2,880건(2개/분 * 60분 * 24시간)만 쌓여. 데이터 양이 30분의 1로 줄어드는 거지. 5분치 데이터를 요청해도 10건의 레코드만 읽으면 되니, 조회 속도는 비교할 수 없이 빨라져. 게다가 원본 테이블은 7일 뒤에 정리되지만, 이 표본 이력은 더 오래 보관할 수 있으니 몇 주 전의 흐름 분석도 가능해지지.”

솔라는 고개를 끄덕였다. 그녀가 가졌던 ‘분석 가치가 떨어진다’는 생각은 ‘모든 분석은 원본 데이터로 해야 한다’는 고정관념에서 비롯된 것이었다. 하지만 이제는 알았다. 데이터 저장 전략의 핵심은 무조건적인 정확성 유지가 아니라, ‘읽기 패턴의 목적’에 맞는 최적의 데이터 형태를 제공하는 것이었다. store_state_history는 바로 그 ‘표본 이력 구성 전략’의 완벽한 예시였다. 모든 것을 담으려다 느려지는 대신, 목적에 맞는 핵심만 추려내어 빠르고 효율적인 분석을 가능하게 만든 것이다.

이제 current_store_statesstore_state_history의 역할이 명확해졌다. 하나는 ‘지금 이 순간’을, 다른 하나는 ‘과거의 흐름’을 위한 것이었다. 솔라의 시선은 자연스럽게 마지막 남은 사각형, hourly_store_metrics로 옮겨갔다.

“알겠어. 실시간 조회와 단기 흐름 분석은 이걸로 해결됐네. 그런데… 경영팀에서 요청했던 ‘지난 한 달간 모든 매장의 시간대별 평균 방문객 수’ 같은 건 어떡하지? 한 달 치 store_state_history 데이터를 전부 긁어서 평균을 내는 것도 결국엔 꽤 무거운 작업이 될 것 같은데. 이 세 번째 테이블이 그걸 위한 건가?“

5장: 장기 집계: 시간 단위로 핵심 요약을

이전의 논의로 스케치북 위 두 개의 사각형, current_store_statesstore_state_history는 제 역할을 찾았다. 솔라의 손가락이 마지막으로 남은 빈칸, hourly_store_metrics를 가리켰다. ‘시간 단위 매장 지표’. 이름만으로는 용도가 짐작됐지만, 바로 옆의 store_state_history와 어떻게 다른지 명확하지 않았다.

솔라는 자신감 있는 목소리로 먼저 자신의 생각을 꺼냈다. “언니, 경영팀의 요청, 그러니까 ‘지난 한 달간 모든 매장의 시간대별 평균 방문객 수’ 같은 건 이제 store_state_history 테이블로 해결할 수 있지 않을까? 원본 데이터보다 크기도 30분의 1로 줄었으니까, 이 테이블에서 한 달 치 데이터를 모아서 시간대별로 그룹화하고 평균을 내면 되잖아. 이 정도면 충분히 빠를 것 같은데.”

필요할 때마다 GROUP BY를 사용해 집계하는 것이 합리적이라는, 솔라의 초기 모델이 다시 고개를 드는 순간이었다. 모든 데이터를 담은 원본 테이블이 아니라, 한 차례 정제된 표본 이력 테이블이라면 충분히 감당할 수 있을 거라는 생각이었다.

“좋은 접근이야. 확실히 원본 데이터를 전부 스캔하는 것보다는 훨씬 효율적이겠지.” 루나는 긍정의 의미로 고개를 끄덕였다. 하지만 이내 펜을 들어 솔라의 제안을 숫자로 바꿔보자고 했다. “네가 말한 방식대로, 데이터베이스가 얼마나 많은 일을 해야 할지 직접 계산해 볼까? ‘한 달 치, 모든 매장’을 기준으로.”

솔라는 잠시 생각에 잠겼다가 계산을 시작했다. “store_state_history는 30초에 한 번, 즉 1분에 2건의 데이터를 저장해. 하루면 2 곱하기 60분 곱하기 24시간이니까 2,880건. 한 달이면… 여기에 30일을 곱해서 약 86,400건.” 거기까지는 간단했다. 하지만 루나가 다음 단계를 짚었다. “그건 매장 하나일 때의 이야기지. 매장이 100개라면?”

솔라의 손이 잠시 멈칫했다. “86,400 곱하기 100… 864만 건이네.”

순간 정적이 흘렀다. 조회 요청 한 번에 864만 건의 데이터를 읽고, 정렬하고, 그룹화해서 평균을 계산해야 한다는 의미였다. 이전 장에서 300개의 점을 10개로 줄여 효율성을 얻었던 것과는 차원이 다른 숫자였다. 표본 데이터라고는 해도, 기간과 대상이 넓어지자 다시 ‘무거운’ 작업이 되어버렸다. 2장에서 계산했던 ‘45초짜리 쿼리’의 악몽이 떠올랐다.

“이것도 결국엔 느려지겠구나…” 솔라가 나지막이 중얼거렸다.

바로 그때, 루나가 마지막 빈칸이었던 hourly_store_metrics 사각형 안을 채워 넣었다. 복잡한 설명 대신, 테이블의 구조를 보여주는 간단한 메모였다.

매장 ID, 시간(2023-10-26-15:00), 평균 방문객 수, 누적 방문객 수

“이 테이블은 데이터를 쌓는 방식이 달라.” 루나가 설명했다. “매 시간 정각이 되면, 지난 한 시간 동안의 store_state_history 데이터를 가져와서 평균과 합계를 미리 계산해. 그리고 딱 한 줄의 결과만 이 테이블에 저장하는 거야.”

“미리 계산한다고?”

“응. 질문을 받기 전에, 예상되는 질문의 답을 미리 만들어두는 거지. 이제 이 테이블을 써서 아까 그 요청을 처리해 볼까? ‘한 달 치, 모든 매장’의 시간대별 평균.”

솔라는 다시 계산에 들어갔다. 이번에는 훨씬 간단했다. “hourly_store_metrics에는 시간당 데이터가 딱 한 줄씩 들어가. 하루면 24줄. 한 달이면 24 곱하기 30일이니까 720줄. 매장이 100개라고 해도… 720 곱하기 100은 72,000건.”

솔라는 자신이 계산한 두 숫자를 스케치북 위에 나란히 적었다.

요청 시 집계 (from store_state_history) : 8,640,000 건 스캔 사전 집계 (from hourly_store_metrics) : 72,000 건 스캔

결과는 충격적이었다. 처리해야 할 데이터의 양이 120배나 차이 났다. 조회 버튼을 누르고 2분을 기다려야 할 보고서가 1초 만에 뜨는 것과 같은 차이였다. GROUP BY를 써도 충분할 거라던 생각은, 장기적이고 반복적인 집계 조회라는 패턴 앞에서는 완전히 틀린 가정이었다.

“말도 안 돼… 그냥 계산 순서를 바꿨을 뿐인데.”

“정확히는, 계산의 ‘시점’을 바꾼 거지.” 루나가 말을 이었다. “사용자가 요청할 때마다 매번 힘겹게 계산하는 대신, 시스템이 한가할 때 미리 꾸준히 계산해두는 거야. 대가가 없는 건 아니야. 시간 단위로 요약했기 때문에 분 단위의 세밀한 분석은 불가능하고, 데이터를 저장할 공간도 추가로 필요하지. 하지만 ‘장기간의 통계’라는 명확한 읽기 패턴 앞에서는 비교할 수 없이 효율적인 방식이야.”

이제야 솔라는 세 번째 테이블의 존재 이유를 온전히 납득했다. 이것은 단순히 데이터를 요약하는 것을 넘어, ‘조회 비용’이라는 관점에서 시스템 부하를 관리하는 전략적 선택이었다. 사용자의 기다림과 데이터베이스의 부담이라는 비용을, 약간의 저장 공간과 미리 계산하는 약간의 수고로 맞바꾼 것이다.

최신성, 표본 이력, 그리고 이제 사전 집계까지. 세 개의 테이블이 각자의 명확한 역할을 가지고 제자리에 섰다. 솔라는 완성된 그림을 만족스럽게 바라보다가, 문득 모든 이야기의 시작점이었던 원본 테이블, store_states를 떠올렸다.

“알겠어. 이제 이 세 테이블이 왜 필요한지 완벽하게 이해했어. 실시간, 단기 분석, 장기 통계. 전부 해결됐네. 그런데 한 가지 이상한 점이 남아있어. 이 모든 데이터는 결국 원본 store_states에서 오는 거잖아. 그런데 왜 맨 처음 글귀에선 그 중요한 원본 데이터를 7일만 보관하고 지워버린다고 했을까? 이 파생 테이블들이 다 있는데도, 원본을 굳이 7일이나 가지고 있는 이유는 뭐고, 또 영원히 보관하지 않고 지우는 이유는 뭐야?“

6장: 원본 보존: 7일간의 역할과 책임

솔라의 질문이 끝나자, 루나는 대답 대신 스케치북에 그려진 세 개의 완성된 테이블, current_store_states, store_state_history, hourly_store_metrics를 물끄러미 바라보았다. 모든 데이터의 원천인 store_states 테이블의 운명에 대한 질문이었다. 루나는 말없이 펜을 들어, 그 모든 테이블의 근원이 되는, 그러나 이제는 흐릿해진 이전 페이지의 거대한 원본 테이블 그림 옆에 새로운 그래프를 그리기 시작했다.

가로축에는 ‘시간(일)’, 세로축에는 ‘누적 데이터 크기’라고 적었다. 그리고 원점에서부터 가파르게 우상향하는 직선을 그렸다. 7일, 30일, 365일 지점을 표시하자, 직선은 멈추지 않고 페이지의 경계를 뚫을 듯이 뻗어 나갔다. 마치 통제 불능 상태로 자라나는 괴물처럼 보였다.

“언니, 이건…” 솔라가 그림의 의도를 파악하려 애썼다.

“이게 바로 우리가 원본 store_states 데이터를 영원히 보관했을 때의 모습이야.”

그림은 단순했지만 압도적이었다. 솔라는 그 끝없는 성장에 맞서는 자신의 생각을 입 밖으로 꺼냈다. “그래도 원본은 중요하잖아. 모든 정보의 원천인데. 뭔가 문제가 생겼을 때 돌아갈 수 있는 유일한 기록이기도 하고. 요즘 스토리지 비용도 저렴한데, 그냥 영원히 보관하면 안 돼? 7일은 너무 짧게 느껴져.”

그것은 솔라가 가진 데이터에 대한 근본적인 믿음이었다. 데이터는 자산이며, 원본은 그 무엇과도 바꿀 수 없는 진실이라는 생각. 삭제는 곧 손실이라고 여기는 관점이었다.

“네 말이 맞아. 스토리지 자체는 저렴해졌지.” 루나는 솔라의 의견에 동의하면서도, 그래프의 세로축 이름, ‘누적 데이터 크기’를 펜으로 툭툭 쳤다. “하지만 데이터가 차지하는 건 단순히 디스크 공간만이 아니거든. 이 그래프가 실제로 어떤 ‘비용’을 만들어내는지 계산해볼까?”

루나는 계산기를 솔라에게 건넸다. “우리가 2장에서 계산했지. 하루에 약 8만 6천 건의 데이터가 쌓여. 한 건당 크기가 아주 작게, 1KB라고만 해보자. 1년이면 데이터가 몇 건이고, 크기는 얼마나 될까?”

솔라는 익숙하게 계산기를 두드렸다. 86,400 건/일 × 365일 = 31,536,000 건/년 31,536,000 KB ≈ 31.5 GB

“1년에 약 3,150만 건, 32기가바이트 정도네. 요즘 세상에 32기가는 그렇게 큰 용량이 아니잖아.” 솔라가 반문했다.

“그럼 5년이면 1억 5천만 건, 160기가바이트. 10년이면 3억 건, 320기가바이트. 이제 아주 작은 크기는 아니지?” 루나는 말을 이었다. “그리고 이건 빙산의 일각이야. 시스템 안정성을 위해 데이터베이스는 보통 여러 개의 복제본을 가져. 재해 복구를 위해 매일 백업도 받아야 하고. 그럼 실제 필요한 디스크 공간은 두세 배가 넘게 돼. 이게 우리가 지불해야 할 첫 번째 비용, ‘스토리지 비용’이야.”

루나는 스케치북에 ‘비용 1: 스토리지와 백업’이라고 적었다.

“더 중요한 비용이 있어.” 그녀는 말을 계속했다. “만약 이 데이터베이스에 심각한 장애가 생겨서 어제 시점의 백업으로 복구해야 한다고 상상해봐. 7일 치 데이터, 약 60만 건을 복구하는 것과 1년 치 데이터 3,150만 건을 복구하는 것 중 어느 쪽이 더 빠르고 안전할까?”

솔라는 그 질문에 즉시 대답할 수 없었다. 수십만 권의 책이 꽂힌 작은 동네 도서관과, 수천만 권이 소장된 국립 도서관의 재고 조사를 비교하는 것과 같았다. 후자는 분명 며칠 밤낮이 걸릴, 끔찍하게 고된 작업일 터였다.

“복구 시간이 길어지면 그만큼 서비스는 멈춰 서게 돼. 이건 ‘운영 비용’과 ‘장애 대응 시간’이라는 두 번째 비용이지.”

루나는 ‘비용 2: 관리 및 복구’라고 덧붙였다. 솔라가 ‘저렴하다’고 생각했던 것은 단순한 하드디스크의 가격이었을 뿐, 데이터를 살아있는 시스템으로 유지하기 위한 보이지 않는 비용들은 전혀 고려하지 않았던 것이다.

루나는 그래프를 그린 페이지를 넘겨, 다시 새로운 두 개의 축을 그렸다. 하나는 이전처럼 ‘시간’이었고, 다른 하나는 ‘데이터의 가치’였다.

“이번엔 비용 말고 가치를 생각해보자. 1초 단위의 정밀한 원본 데이터는 언제 가장 가치가 높을까?”

솔라는 잠시 고민했다. “음… 데이터가 막 생성됐을 때? 시스템에 버그가 있어서 집계 테이블이 잘못 만들어졌을 때, 원본을 보고 다시 계산해야 하니까. 아니면 장애 원인을 분석할 때처럼, 아주 상세한 기록이 필요할 때.”

“맞아. 대부분 데이터 생성 후 몇 시간, 혹은 며칠 내에 그 가치가 집중돼 있지.” 루나는 솔라의 말을 따라 가파르게 시작했다가 시간이 지남에 따라 급격히 완만해지는 곡선을 그렸다. 7일이 넘어가는 시점부터는 거의 바닥에 붙어 수평선처럼 이어졌다.

“7일이 지난 원본 데이터는 왜 가치가 급격히 떨어질까?”

“왜냐하면…” 솔라는 잠시 생각하다 스스로 답을 찾았다. ”…그 데이터의 핵심적인 가치는 이미 다른 테이블로 옮겨갔으니까! ‘최신성’은 current_store_states로, ‘흐름 분석’의 가치는 store_state_history로, ‘장기 통계’의 가치는 hourly_store_metrics로. 각자의 역할에 맞게 가공돼서 새로운 생명을 얻은 거구나.”

루나는 두 개의 그래프, 즉 끝없이 치솟는 ‘비용’ 그래프와 시간이 지나면 바닥으로 떨어지는 ‘가치’ 그래프를 나란히 보여주었다.

그제야 솔라는 모든 것을 깨달았다. 원본 데이터를 영원히 보관하는 것은, 가치는 거의 남지 않은 채 보관 비용만 계속해서 잡아먹는 자산을 끌어안고 있는 것과 같았다. 그리고 ‘7일’이라는 숫자는 그 비용과 가치의 곡선이 교차하는 지점을 고려한 전략적인 선택이었다.

“아… 7일은 그냥 삭제하는 기간이 아니구나.” 솔라가 나지막이 말했다. “데이터 처리 시스템에 문제가 생겨서 며칠간 집계가 잘못되더라도, 원본으로 돌아가 복구할 수 있는 최소한의 ‘안전 버퍼’이자, 원본 데이터가 자기 역할을 다하고 다음 주자들에게 임무를 안전하게 넘겨줄 때까지의 ‘유예 기간’이었던 거야.”

원본 데이터를 버리는 것이 두려웠던 초기 생각은, 데이터의 생명주기를 고려하지 못한 단편적인 시각이었음을 깨달았다. ‘데이터 보존 평가’란, 무조건 모든 것을 껴안는 것이 아니라, 시간의 흐름에 따른 데이터의 역할과 가치 변화를 읽고, 비용과의 균형점을 찾아내는 현명한 저울질이었던 것이다.

솔라는 처음 봤던 문장, “원본 StoreState… 원본 보관 기준을 7일로 정했다”를 다시 떠올렸다. 이제 그 결정이 얼마나 많은 고민과 트레이드오프 끝에 내려진 최적의 선택이었는지 선명하게 이해되었다. 최신 상태, 30초 이력, 시간 집계, 그리고 7일간의 원본. 모든 조각이 제자리를 찾았다.

“이제 모든 테이블의 역할과 존재 이유를 알겠어. 그럼 이 모든 설계를 하나로 꿰뚫는 핵심 원칙은 뭐라고 정리할 수 있을까? 왜 이 구조가 단일 테이블보다 압도적으로 좋은 선택인 건지, 처음의 나에게 설명해 줄 마지막 한 문장이 필요해.”

7장: 통합 이해: 읽기 패턴이 정의하는 저장 구조

솔라의 마지막 질문이 거실의 고요함 속에 울렸다. “이 모든 설계를 꿰뚫는 핵심 원칙은 뭘까? 처음의 나에게 설명해 줄 마지막 한 문장이 필요해.” 지난 며칠간의 대화와 스케치북 위에서 펼쳐졌던 수많은 그림과 계산이 그 한 문장을 찾기 위한 여정이었다.

루나는 말없이 스케치북을 집어 들었다. current_store_states, store_state_history, hourly_store_metrics, 그리고 비용과 가치 곡선이 그려진 페이지들을 차례로 지나쳤다. 그러고는 모두의 예상을 깨고, 스케치북의 가장 첫 장을 펼쳐 솔라 앞에 놓았다. 그곳에는 솔라가 처음 품었던 혼란의 근원이 고스란히 남아 있었다. 큼직하게 그려진 단 하나의 테이블, StoreState, 그리고 그 옆에 적힌 문장이었다.

원본 StoreState 119,650건이 쌓인 뒤 최신 상태, 30초 이력과 시간 집계를 따로 두고 원본 보관 기준을 7일로 정했다.

며칠 전, 이 문장은 이해할 수 없는 복잡함이자 불필요한 작업처럼 보였다. 하지만 이제 그 문장은 솔라가 걸어온 길의 이정표처럼 보였다. 루나는 펜을 들어 그 문장 전체에 동그라미를 치고는, 펜을 솔라에게 건넸다. 무언의 신호였다. 대답을 주는 대신, 솔라 스스로 답을 완성하도록 자리를 내준 것이다.

“네가 처음의 너에게 직접 설명해 봐. 이 한 문장에 담긴 모든 결정이 왜 합리적이었는지.”

솔라는 잠시 숨을 고르고 펜을 집어 들었다. 그녀의 시선이 문장의 첫 부분, ‘최신 상태를 따로 두고’에 머물렀다.

“알았어. 먼저, ‘최신 상태’를 분리한 건… 단순히 속도 때문만이 아니었어.” 솔라의 펜이 current_store_states 테이블 그림으로 이어졌다. “가장 빈번하고, 가장 빠른 응답이 필요한 ‘실시간 조회’라는 읽기 패턴을 다른 무거운 요청들로부터 ‘보호’하기 위해서였지. 수백만 건의 데이터 속에서 최신 데이터를 찾는 ‘검색’과, 언제나 딱 한 건만 존재하는 전용 테이블에서 값을 꺼내는 ‘조회’는 근본적으로 다른 차원의 이야기였어. 시스템의 안정성을 위한 방화벽이었던 거야.”

다음으로 그녀의 펜은 ‘30초 이력과’라는 구절로 옮겨갔다.

“두 번째, ‘30초 이력’. 처음엔 원본 데이터를 버리는 게 손실이라고 생각했지만, 이건 ‘흐름 분석’이라는 뚜렷한 목적을 위한 전략이었어.” 솔라는 두 개의 그래프, 촘촘한 원본 데이터의 파형과 성긴 30초 간격의 능선을 떠올렸다. “모든 데이터를 저장하려다가는 분석 자체가 느려지고 비효율적이게 돼. 30분의 1로 줄어든 표본 이력은 세밀함을 조금 포기하는 대신, 훨씬 빠르고 경제적으로 ‘추세’를 파악할 수 있는 힘을 줬어. 모든 분석에 원본이 필요한 건 아니라는 걸 깨달았지.”

펜은 멈추지 않고 ‘시간 집계를 따로 두고’로 나아갔다.

“세 번째, ‘시간 집계’. 이건 ‘반복적인 장기 통계’라는, 가장 무거운 읽기 패턴을 해결하기 위한 해법이었어. 필요할 때마다 수백만 건의 데이터를 붙들고 계산하는 건 재앙이었지.” 솔라는 864만 건과 7만 2천 건이라는, 압도적인 숫자의 차이를 기억했다. “사용자가 요청하기 전에 시스템이 미리 답을 계산해두는 ‘사전 집계’ 방식은, 사용자의 기다림과 시스템의 부하라는 엄청난 비용을 절약하는 현명한 거래였어.”

마지막으로 솔라의 펜은 문장의 끝, ‘원본 보관 기준을 7일로 정했다’에 멈췄다.

“그리고… ‘원본 7일 보관’. 이건 데이터를 버리는 게 아니라, 데이터의 ‘생명주기’를 관리하는 거였어. 원본 데이터의 가치는 시간이 지날수록 급격히 떨어지는 반면, 보관 비용은 계속해서 늘어나니까. 7일은 그 가치와 비용의 균형점이자, 집계 데이터에 문제가 생겼을 때 복구할 수 있는 최소한의 ‘안전 버퍼’였던 거야. 자신의 역할을 다한 데이터가 명예롭게 퇴장할 시간을 주는 거지.”

모든 조각을 자신의 언어로 설명하고 나자, 솔라는 비로소 모든 것을 꿰뚫는 하나의 그림을 볼 수 있었다. 그녀가 찾던 ‘마지막 한 문장’은, 사실 문장이 아니었다. 그것은 관점의 전환이었다.

“아…” 솔라가 나지막이 탄성을 뱉었다. “난 계속 StoreState라는 ‘하나의 데이터’를 어떻게 저장할까 고민했는데, 그게 아니었구나. 이건 처음부터 ‘네 가지 다른 읽기 패턴’을 가진, 완전히 다른 데이터였던 거야. 실시간으로 봐야 하는 데이터, 흐름으로 읽어야 하는 데이터, 요약해서 볼 데이터, 그리고 만일을 위한 원본 데이터. 이들의 정체는 같지만, 쓰임새가 다르니 각자에게 최적화된 집을 따로 마련해준 거였어.”

데이터를 분리하는 것은 시스템을 복잡하게 만드는 일이 아니었다. 오히려 각기 다른 요구사항이라는 복잡함을, 가장 단순하고 명료한 구조로 풀어낸 해답이었던 것이다.

솔라는 새로운 페이지를 넘겼다. 더 이상 질문이 아니었다. 그녀는 자신이 얻은 새로운 관점을 미래의 자신을 위한 도구로 만들기 시작했다. 스케치북 위에 그녀는 ‘읽기 패턴 기반 저장 구조 설계 체크리스트’라는 제목을 적었다.

  1. 조회 목적: 이 데이터를 왜, 누가 읽는가?

    • 실시간 현황 파악 (예: 대시보드)
    • 단기 흐름/추세 분석 (예: 장애 분석)
    • 장기 통계 및 보고 (예: 월간 리포트)
    • 데이터 복구 및 재처리 (예: 장애 대응)
  2. 성능 요구사항: 얼마나 빠르고, 얼마나 자주 응답해야 하는가?

    • 즉각적 응답이 필수인가? → 최신 상태 전용 테이블 고려
    • 대규모 집계가 반복되는가? → 사전 집계 테이블 고려
  3. 데이터의 형태: 원본의 모든 세부사항이 필요한가?

    • 전체적인 흐름만으로 충분한가? → 표본 이력 테이블 고려
  4. 생명주기와 비용: 데이터의 가치는 언제까지 유지되며, 보관 비용은 얼마인가?

    • 가치 대비 비용을 고려한 원본 데이터 보존 정책(TTL) 수립

펜을 내려놓은 솔라는 자신이 만든 체크리스트를 바라보았다. 막연하게 ‘데이터 저장’이라고 생각했던 문제가, 이제는 명확한 질문과 선택지로 이루어진 설계 과정으로 보였다. 그녀는 더 이상 단일 테이블의 단순함에 현혹되지 않을 것이고, 무조건적인 데이터 보존을 고집하지도 않을 터였다. 읽기 패턴이라는 새로운 렌즈를 얻었기 때문이다. 루나는 아무 말 없이, 그저 완성된 체크리스트를 보며 조용히 미소 지을 뿐이었다.