Big Project · AI's EYE 30

합성 주문 Worker와 What-if Simulator: 목적이 다른 '가짜'의 설계

둘 다 코드로 만든 가짜 주문 흐름이라 합성 주문기와 What-if 시뮬레이션이 같은 기능처럼 보인다. 왜 하나는 DB에 저장하고 다른 하나는 저장하지 않는지 궁금하다.

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

1장: 루나: 가짜 데이터에도 종류가 있다

솔라는 노트북 화면의 두 문단 사이에 시선을 고정한 채 미동도 하지 않았다. 시스템 아키텍처 문서의 한 부분이었다. 한쪽에는 ‘합성 주문 Worker(Synthetic Order Worker)’라는 이름이, 다른 한쪽에는 ‘What-if Simulator’라는 이름이 붙어 있었다. 둘 다 실제 고객이 일으킨 주문이 아닌, 코드에 의해 생성된 가상 데이터를 다룬다는 설명이 따라붙었다.

솔라의 미간이 살짝 찌푸려졌다. “둘 다 그냥 ‘가짜’ 주문이잖아.”

혼잣말처럼 중얼거린 목소리에, 옆에서 조용히 자신의 책을 읽고 있던 루나가 고개를 들었다. 솔라는 기다렸다는 듯이 화면을 가리켰다.

“언니, 이것 좀 봐. 합성 주문 Worker랑 What-if Simulator. 둘 다 코드로 가짜 주문 흐름을 만드는 거 아냐? 그런데 마치 전혀 다른 기능인 것처럼 분리해서 설명하고 있어. 이해가 안 가. 그냥 하나는 데이터베이스에 저장하고, 다른 하나는 저장하지 않는다는 차이뿐인 것 같은데.”

솔라의 손가락은 ‘합성 주문은 운영 테이블에 demo event를 적재한다’는 문장과 ‘What-if 결과는 실제 이력에 저장하지 않는다’는 문장을 번갈아 가리키고 있었다. 겉보기에는 명백한 차이였지만, 솔라에게는 그저 부수적인 구현 정책의 차이로만 보였다. 근본은 같은 ‘가짜 데이터 생성기’라는 생각에 변함이 없었다.

루나는 솔라의 말을 잠자코 듣더니, 읽던 책 페이지에 얇은 책갈피를 끼워 넣고는 몸을 돌려 솔라의 노트북 화면을 들여다보았다. 잠시 문서를 읽어 내려가던 루나는 아무 말 없이 키보드에 손을 얹었다.

“어떤 데이터를 만드는지 직접 한번 보자.”

루나는 터미널 창 두 개를 나란히 화면에 띄웠다. 그리곤 한쪽 창에는 특정 로그 파일을 조회하는 명령을, 다른 한쪽 창에는 API를 호출하는 듯한 명령어를 입력했다. 곧 양쪽 창에 비슷한 형식의 데이터 덩어리가 나타났다.

[좌측 터미널]

{
  "event_id": "evt_abc123",
  "order_id": "syn-ord-20240510-001",
  "type": "ORDER_STATE_CHANGED",
  "payload": {
    "status": "ACCEPTED",
    "timestamp": "2024-05-10T10:01:05Z"
  },
  "source": "synthetic_order_simulator"
}

[우측 터미널]

{
  "run_id": "sim-xyz789-run01",
  "results": [
    {
      "order_id": "whatif-ord-1",
      "predicted_completion_time": "2024-05-10T11:30:00Z"
    }
  ],
  "source": "simulation",
  "parameters": { "staff_count": 5 }
}

솔라는 두 결과를 비교하기 시작했다.

“음… 왼쪽은 주문 상태가 바뀐 이벤트 기록 같고, 오른쪽은 시뮬레이션 결과 예측 시간이네. 데이터 구조는 조금 다르지만, 둘 다 ‘가짜’ 주문 ID를 가지고 있고. 결국 시스템 내부에서 돌아다니는 가짜 데이터라는 점은 똑같잖아.”

솔라의 목소리에는 자신의 처음 생각이 틀리지 않았다는 확신이 묻어났다. 그녀의 눈에는 syn-ord-...whatif-ord-... 같은 ID는 모두 ‘실제 주문이 아님’을 나타내는 표식일 뿐이었다.

루나는 고개만 살짝 끄덕이며 화면의 한 부분을 턱으로 가리켰다.

“거의 다 봤는데, 딱 한 군데만 더.”

루나의 시선이 머문 곳은 각 데이터의 마지막 줄에 있는 source 필드였다. 솔라는 그제야 두 필드의 값이 미묘하게 다르다는 것을 알아차렸다. 왼쪽은 'synthetic_order_simulator', 오른쪽은 'simulation'.

“하나는… ‘합성 주문 시뮬레이터’에서 왔다고 하고. 다른 하나는 그냥 ‘시뮬레이션’이라고만 되어 있네.”

그 차이를 입 밖에 내는 순간, 솔라의 머릿속에서 무언가 번쩍했다. 그냥 넘어갈 뻔했던 작은 문자열의 차이가, 두 데이터의 본질적인 정체성을 말해주고 있었다.

솔라는 다시 좌측 데이터를 뜯어보았다. source 필드는 이 데이터의 출처를 밝히고 있었지만, 그 외의 event_id, order_id, type, payload 같은 필드들은 실제 주문 이벤트와 거의 동일한 구조를 가지고 있었다. 마치 시스템 안에서 ‘나는 진짜 주문 이벤트인 척할 거야. 출처만 살짝 다를 뿐이야’라고 말하는 듯했다. 이 데이터는 시스템의 다른 부분들이 실제 주문 데이터처럼 취급해주길 기대하는 것처럼 보였다. 대시보드에 표시되거나, 통계에 잡히거나, 주문 목록에 나타나는 식으로 말이다.

반면 우측 데이터는 완전히 달랐다. run_id라는, 이 시뮬레이션 실행 자체를 식별하는 ID가 가장 먼저 눈에 띄었다. 데이터의 목적이 개별 주문의 상태를 따라가는 것이 아니라, ‘만약 직원이 5명이라면?’이라는 가정 하에 실행된 한 판의 분석 결과를 전달하는 데 있음을 명확히 보여줬다. source: 'simulation'이라는 표식은 ‘이건 시스템의 현재 상태와 무관한 가상 실험의 결과일 뿐이니, 절대로 실제 데이터와 섞지 마시오’라는 강력한 경고처럼 느껴졌다.

“아…”

낮은 탄식이 솔라의 입에서 흘러나왔다.

“알겠다. 둘 다 가짜는 맞는데… 종류가 다른 가짜였어. 하나는 시스템 안에서 ‘살아있는’ 데모 상태를 유지하기 위한 가짜이고, 다른 하나는 ‘만약에’라는 질문에 답하기 위해 딱 한 번 쓰고 버려지는 가짜구나.”

솔라는 자신이 처음에 ‘저장 여부’만 다르다고 생각했던 것이 얼마나 얕은 관찰이었는지 깨달았다. 저장 정책은 결과일 뿐, 원인은 아니었다. 진짜 차이는 데이터가 생성되는 순간부터 부여된 ‘정체성’과 ‘목적’에 있었다. 시스템은 source라는 이름표를 통해 두 종류의 가짜를 명확히 구분하고, 그에 따라 완전히 다른 취급을 하고 있었던 것이다.

고개를 든 솔라의 눈에는 더 이상 혼란이 없었다. 대신 더 깊은 질문이 자리 잡고 있었다.

“그럼 언니, 이제 이게 더 궁금해졌어. 왜 우리 시스템에는 이 두 종류의 ‘가짜’가 둘 다 필요한 걸까? 왜 하나는 현재 상태를 꾸준히 보여주는 생산자처럼 행동해야 하고, 다른 하나는 미래를 예측하는 분석가 역할만 하고 사라져야 하는 거지?”

2장: 루나: 목적이 생명주기를 결정한다

솔라의 질문이 끝나기 무섭게, 루나는 노트북의 트랙패드를 움직여 빈 텍스트 편집기 창을 화면 중앙에 띄웠다. 이전의 터미널 창과 아키텍처 문서는 한쪽으로 밀려났다. 그저 하얗기만 한 창에 커서가 깜빡였다.

루나는 말없이 키보드를 두드려 두 개의 제목을 만들었다.

# 합성 주문 Worker (Synthetic Order Worker)

# What-if Simulator

솔라는 언니가 무엇을 하려는지 짐작하고 조용히 지켜봤다. 지난번처럼 단순히 데이터를 나란히 놓고 비교하는 것과는 다른 무언가가 시작되고 있었다. 루나는 키보드에서 손을 떼고 솔라를 향해 턱짓했다.

“솔라, 네가 아까 한 질문이 핵심이야. 왜 우리에겐 생산자와 분석가가 둘 다 필요할까? 그 질문에 답하려면, 각 시스템의 데이터가 누구를 위한 것인지, 어떤 목적을 위해 존재하는지부터 채워봐야 해.”

“목적…” 솔라는 중얼거리며 화면을 응시했다. ‘가짜 데이터’라는 공통점에서 벗어나, 이제는 둘의 역할을 규정해야 할 시간이었다.

솔라는 먼저 ‘합성 주문 Worker’ 아래에 커서를 옮겼다. “이건… 실제 POS 기기가 없는 환경에서 데모 운영을 위한 거라고 했지. 그럼 이 데이터를 보는 사람은 데모를 보는 사람들이겠네. 가령 영업팀이 고객사에게 우리 시스템 대시보드를 보여줄 때?”

“그럴 수 있겠네.” 루나가 짧게 답했다.

“그럼 목적은 ‘데모 시연용 데이터 제공’이겠다.” 솔라는 자신 있게 타이핑했다. 하지만 무언가 부족하다고 느꼈다. 개발 편의성을 위해 만든 것 아닐까 하는 처음 생각에서 크게 벗어나지 않는 것 같았다. “아니면 그냥 우리 개발자들이 화면이 잘 작동하는지 보려고 만든 걸 수도 있고…”

“그 데이터가 한 번만 생성되고 멈춘다면, 데모 시연에 충분할까?”

루나의 질문은 솔라의 생각을 다른 방향으로 틀었다. 맞다. 데모는 ‘살아있는’ 시스템을 보여주는 것이다. 텅 빈 화면이나 어제 날짜에서 멈춰버린 주문 목록을 보여줄 수는 없다. 데모가 진행되는 바로 그 순간에도 새로운 주문이 들어오고, 상태가 바뀌는 것처럼 보여야 한다.

“아니, 충분하지 않아. 계속 움직여야 해. 지금 막 들어온 주문처럼 보여야 하고, 방금 막 조리가 완료된 것처럼 보여야 하고… 그러니까 이 시스템의 진짜 목적은 단순히 가짜 데이터를 만드는 게 아니야. ‘현재 시점의 활발한 운영 상태를 지속적으로 연출’하는 거구나.”

솔라는 기존에 입력했던 내용을 지우고 새로 써 내려갔다.

목적: 실제 매장처럼 현재 시점에도 주문이 계속 발생하고 처리되는 ‘살아있는’ 데모 상태를 유지하고 공급하기 위함. 이 시스템은 현재를 살아가는 ‘생산자’ 역할을 한다.

화면에 찍힌 ‘생산자’라는 단어가 솔라의 머릿속에 명확히 각인되었다. 이 데이터는 누군가의 질문에 답하기 위해 존재하는 것이 아니었다. 그저 시스템의 현재 상태 그 자체가 되기 위해 존재했다.

이제 솔라의 시선은 ‘What-if Simulator’로 향했다. 이건 훨씬 쉬웠다.

“이건 ‘만약에’라는 질문에 답하는 거니까. 사용자는 미래의 운영을 계획하는 사람이겠지. 점장님이나 본사 운영 담당자 같은. ‘내일 아르바이트생을 한 명 더 쓰면 주문 처리가 얼마나 빨라질까?’ 같은 질문.”

솔라는 망설임 없이 타이핑했다.

목적: 행사나 인력 변동 같은 특정 조건 하에서 미래에 어떤 결과가 나올지 예측하고 비교하기 위함. 이 시스템은 가상 질문에 답하는 ‘분석가’ 역할을 한다.

두 시스템의 역할과 목적이 나란히 정리되자, 솔라는 숨을 한번 골랐다. 화면에는 단순히 두 개의 시스템에 대한 설명이 아니라, 서로 다른 역할을 맡은 두 명의 가상 직원에 대한 프로필이 그려져 있었다. 한 명은 쉬지 않고 현재의 무대를 채우는 배우(생산자)였고, 다른 한 명은 무대 뒤에서 다음 전략을 짜는 참모(분석가)였다.

“이제 알겠다.” 솔라가 말했다.

“뭘?”

“왜 하나는 저장하고, 다른 하나는 저장하지 않는지. 이건 그냥 개발 편의성 문제가 아니었어. 목적이 완전히 다르니까 당연한 귀결이었네.”

솔라는 흥분한 목소리로 말을 이었다. “합성 주문 Worker가 만든 데이터는 ‘현재 상태’를 연출해야 하니까, 당연히 시스템의 상태를 기록하는 운영 데이터베이스에 저장되어야 해. 그게 저장되지 않으면 대시보드에는 아무것도 보이지 않을 테니까. 이 데이터에게 ‘저장’은 자신의 존재 이유를 증명하는 행위야.”

“반면에 What-if Simulator의 데이터는… 이건 그냥 ‘만약에’라는 질문에 대한 한 번의 대답일 뿐이잖아. 이 대답을 진짜 주문 이력에 저장해버리면 어떻게 되겠어? 있지도 않았던 미래의 예측이 과거의 사실처럼 기록되는 거잖아. 그건 데이터를 오염시키는 일이지. 이 데이터에게 ‘저장되지 않는 것’이 오히려 올바른 설계인 거네.”

자신이 가졌던 “저장 여부만 다른 것 아닐까?”라는 생각이 얼마나 단편적인 관점이었는지 깨닫는 순간이었다. 저장 정책은 시스템을 설계할 때 가장 마지막에 붙이는 장식이 아니었다. 시스템의 가장 근본적인 목적과 역할로부터 파생되는, 필연적인 결과물이었다.

솔라는 자신이 정리한 텍스트 파일을 보며 고개를 끄덕였다. 이제 두 ‘가짜’는 더 이상 헷갈리지 않았다. ‘현재를 살아가는 생산자’와 ‘미래를 점치는 분석가’. 명확한 역할이 생기자 모든 것이 제자리를 찾은 듯했다.

하지만 그 순간, 또 다른 질문이 꼬리를 물고 이어졌다.

“좋아, 목적이 역할을 결정하고, 역할 때문에 저장 정책이 달라진다는 건 이해했어. 그런데 언니, 그 ‘목적’이라는 게 어떻게 코드 레벨에서 ‘이 데이터는 DB에 영원히 살아남아라’, ‘너는 API 응답이 끝나면 즉시 소멸해라’ 같은 명령으로 구체화되는 거지? 목적과 실제 구현 사이에는 아직 한 단계가 더 있는 것 같아.”

3장: 루나: 생명주기가 곧 저장 정책이다

솔라가 이전 장에서 정리했던 텍스트 파일이 화면에 그대로 떠 있었다. ‘생산자’와 ‘분석가’라는 선명한 역할 구분 아래, 두 시스템의 목적이 명확하게 정리되어 있었다. 하지만 솔라의 마지막 질문이 남긴 공백—그 ‘목적’이라는 추상적 개념이 어떻게 구체적인 저장/소멸 명령으로 이어지는가—은 아직 채워지지 않은 채였다.

그때, 루나가 말없이 트랙패드에 손을 올렸다. 커서가 움직이더니, 솔라가 정리한 ‘합성 주문 Worker’와 ‘What-if Simulator’ 프로필 각각의 아래에 새로운 제목을 추가했다.

## 데이터 생명주기 (Data Lifecycle)

루나는 아무런 설명도 덧붙이지 않고, 그저 깜빡이는 커서를 남겨둔 채 키보드에서 손을 뗐다. 그 하나의 단어가 솔라가 붙들고 있던 질문의 다음 단계를 명확하게 지목하고 있었다. ‘목적’과 ‘구현’ 사이의 빠진 고리는 바로 ‘생명주기’였다. 데이터가 언제 태어나서, 누구를 위해 살다가, 어떻게 죽음을 맞이하는지에 대한 이야기.

“그럼… 한번 따라가 보자.” 솔라는 혼잣말처럼 중얼거리며 키보드를 잡았다. “이 데이터들이 언제까지, 어디에 있어야 하는지.”

먼저 ‘합성 주문 Worker’였다.

“이 데이터는 Worker 프로세스에 의해 주기적으로 생성돼. 그리고… ‘현재 상태’를 만들어야 하니까, 당연히 다른 시스템 부분들이 볼 수 있는 곳에 있어야겠지. 바로 운영 DB의 OrderEvent 테이블.” 솔라는 데이터의 탄생과 거처를 써 내려갔다.

- 탄생: Worker가 주기적으로 생성. - 거처: 운영 DB의 OrderEvent 테이블에 source=synthetic_…로 기록.

“그럼 언제까지 살아남아야 할까?” 루나가 옆에서 나지막이 물었다.

솔라는 잠시 생각에 잠겼다. “데모 시연이 끝날 때까지? 아니지. 데모 시연은 언제 있을지 몰라. 그냥… 시스템이 살아있는 한 계속 남아있어야 하는 거 아닐까? 대시보드에서 ‘지난 1주일 데이터’ 같은 걸 조회할 수도 있으니까. 실제 주문 기록처럼, 삭제할 특별한 이유가 없는 한 계속 보존되어야 해. 이 데이터의 삶은 길고 영구적이네.”

- 죽음: 정해진 수명 없음. 실제 데이터처럼 DB에 영구적으로 보존 (나중에 정책에 따라 아카이빙될 수는 있겠지만).

솔라는 자신이 쓴 글을 보았다. ‘생산자’라는 역할은 데이터에게 ‘영속성’이라는 생명주기를 부여했다. 그리고 그 영속적인 생명은 ‘데이터베이스에 저장’이라는 행위를 필연적으로 만들었다.

이제 ‘What-if Simulator’의 차례였다.

“이건 훨씬 간단해.” 솔라는 확신에 찬 목소리로 말했다. “사용자가 API를 통해 ‘직원 5명일 때 어때?’ 하고 물어보는 순간, 바로 그때 데이터가 태어나는 거야.”

- 탄생: 특정 조건과 함께 API 요청이 들어오는 순간, 메모리 위에서 생성.

“그 데이터는 어디에 머물러?”

“어디에도 머물지 않아!” 솔라의 손가락이 빨라졌다. “그냥 계산에만 사용되고, 결과는 API 응답에 담겨서 사용자에게 전달되지. 그리고 그게 끝이야. 요청이 처리되는 그 짧은 순간에만 살아있는 거야.”

- 거처: API 요청을 처리하는 서버의 메모리. - 죽음: API 응답이 전송된 후, 해당 요청 스코프가 사라지면서 즉시 소멸.

솔라는 두 시스템의 생명주기를 나란히 비교했다. 한쪽은 영속적인 삶, 다른 한쪽은 찰나의 삶. 너무나도 명백한 차이였다.

“그런데 언니, 이런 생각이 들 수도 있잖아. What-if 시뮬레이션 결과도 일단 저장해두면 나중에 분석할 때 더 유용하지 않을까? 저장하는 게 그냥 좀 더 비싸고 복잡한 작업일 뿐인 거 아닐까?”

솔라는 자신의 처음 생각과 비슷했지만 이제는 더 정교해진 질문, ‘비용과 복잡성’이라는 관점을 꺼내 보았다. 데이터를 저장하지 않는 결정이 단순히 기술적 제약이나 비용 절감 때문은 아닌지 확인하고 싶었다.

루나는 고개를 저었다.

“그건 비용 문제가 아니야, 솔라. 오히려 그 반대지. 만약 우리가 What-if 시뮬레이션 결과를 운영 DB에 저장한다고 생각해봐. 그럼 무슨 일이 생길까?”

“음… 데이터가 많아지겠지?”

“그것보다 더 심각한 문제가 있어. 네가 가게 점장이라고 상상해봐. 오늘 실제 주문이 100건 있었어. 그런데 어제 네가 ‘만약 손님이 2배로 온다면?’이라는 시뮬레이션을 10번 돌려봤고, 그 가상 주문 1000건이 전부 DB에 저장되어 있다면? 오늘 마감 정산을 할 때, 너는 실제 매출과 가상 예측을 구분할 수 있을까?”

“아…”

루나의 비유에 솔라는 할 말을 잃었다. 저장하는 행위 자체가 데이터를 ‘오염’시키는 것이었다. 있지도 않은 미래의 예측이 과거의 ‘사실’처럼 기록되는 순간, 데이터는 신뢰를 잃고 시스템 전체가 거짓말쟁이가 되어버린다.

“What-if 데이터에게 ‘저장되지 않는 것’은 비용이나 편의성 문제가 아니라, 자신의 정체성을 지키는 유일한 방법이었구나. 분석가로서의 역할을 다하기 위해선 절대로 기록에 남으면 안 되는 거였어.”

생명주기가 곧 저장 정책이라는 말이 피부로 와 닿는 순간이었다. 목적이 생명주기를 결정하고, 생명주기는 저장 정책을 결정한다. 이 모든 것이 하나의 논리적 흐름이었다. 데이터를 저장할지 말지는 ‘중요도’나 ‘비용’으로 판단하는 것이 아니라, 그 데이터가 가져야 할 ‘생명주기’에 따라 결정되는 설계의 핵심 원칙이었다.

솔라는 잠시 생각에 잠기더니, 빈 텍스트 파일 하나를 새로 열었다. 그리고 가상의 시나리오를 적어 내려가기 시작했다.

## 신규 기능: ‘주간 근무표 초안’ - **목적:** 점장이 확정되지 않은 주간 근무표를 미리 짜보고, 예상 인건비를 확인하며 여러 버전을 비교할 수 있게 한다.

솔라는 잠시 멈춰 루나를 보았다. 루나는 조용히 고개를 끄덕이며 계속해보라는 눈짓을 보냈다. 솔라는 다시 키보드로 손을 옮겨, 방금 자신이 깨달은 설계 원칙을 적용하기 시작했다.

### 1. 데이터의 목적과 역할 - 현재 시스템의 공식 근무표와는 분리되어야 함. - 확정되기 전까지는 점장 개인의 ‘초안’ 또는 ‘메모’ 역할을 수행. - 즉, 'What-if'처럼 일회성 분석도 아니고, '합성 주문'처럼 영구적인 상태도 아닌, 중간적인 성격의 데이터.

### 2. 데이터 생명주기 설계 - **탄생:** 점장이 ‘초안 만들기’ 버튼을 누를 때 생성. - **삶:** 점장이 초안을 저장하고, 며칠 뒤 다시 불러와 수정할 수 있어야 함. 즉, API 요청보다는 길고, 영구 기록보다는 짧은 생명주기가 필요. - **죽음:** 점장이 ‘초안 삭제’ 버튼을 누르거나, ‘이 초안으로 확정’ 버튼을 눌러 공식 데이터로 전환될 때 소멸.

### 3. 저장 정책 결정 - **결론:** 운영 DB의 공식 schedules 테이블에 저장하면 안 됨. 데이터 오염 발생. - **해결책:** draft_schedules라는 별도의 테이블을 만들어 저장. 이 테이블의 데이터는 인건비 예상 계산에는 사용되지만, 실제 급여 정산이나 운영 대시보드에는 포함되지 않도록 명확히 분리해야 함.

솔라는 자신이 작성한 짧은 설계 노트를 바라보았다. 처음 아키텍처 문서를 보며 혼란스러워하던 모습은 온데간데없었다. ‘저장 여부’라는 단편적인 현상에서 시작된 질문은, ‘목적’, ‘역할’, ‘생명주기’라는 핵심 원리를 거쳐, 이제 새로운 시스템을 설계하는 구체적인 판단 기준으로 자리 잡았다. 가짜 데이터에도 종류가 있다는 것을 넘어, 모든 데이터는 저마다의 목적과 그에 맞는 삶이 있다는 것을 깨닫게 된 것이다.