Big Project · AI's EYE 28
공용 Kakao 채널, 다매장 컨텍스트를 어떻게 라우팅하는가?
다매장 챗봇이라면 매장마다 Kakao 채널과 봇을 따로 만드는 것이 가장 명확해 보인다. 한 채널에서 매장 맥락을 어떻게 잃지 않는지 궁금하다.
근거 · 프로젝트 문서 · 코드 · 테스트
1장: 왜 다매장 채널을 하나로 합쳐야 할까?
솔라는 노트북 화면 한구석에 떠 있는 문장을 물끄러미 바라보고 있었다. 프랜차이즈 카페의 주문 시스템 아키텍처를 설명하는 기술 블로그의 한 구절이었다.
"매장마다 채널을 만들지 않고 공용 채널 하나를 사용하며..."
문장 자체는 명료했지만, 솔라의 머릿속은 오히려 복잡해졌다.
“언니, 이거 좀 이상하지 않아?”
거실에서 화초에 물을 주던 루나가 솔라를 돌아보았다. 솔라가 노트북을 루나 쪽으로 돌리며 말을 이었다.
“전국에 매장이 수백 개인 프랜차이즈인데, 카카오톡 챗봇 채널을 딱 하나만 쓴대. 매장마다 하나씩 만드는 게 훨씬 깔끔하잖아. 그래야 사용자도 안 헷갈리고, 매장별로 이벤트 공지하기도 편할 텐데.”
솔라의 지적은 타당해 보였다. 강남점에서 쓰는 챗봇과 홍대점에서 쓰는 챗봇이 같다면, 사용자는 어느 매장을 상대로 대화하는지 어떻게 알 수 있을까? 시스템은 또 어떻게 구분할까? 매장별로 채널을 분리하는 것이 가장 직관적이고 명확한 해결책처럼 보였다. 그것이 솔라의 첫 번째 판단이었다.
루나는 물뿌리개를 내려놓고 솔라의 옆에 앉았다. 화면의 문장을 잠시 들여다보더니, 복잡한 아키텍처 다이어그램 대신 솔라에게 간단한 질문을 던졌다.
“만약 우리가 100개의 매장을 가진 프랜차이즈의 사장님이라면 어떨까? 네 말대로 매장마다 채널을 하나씩 만들어 보자. 100개의 채널과 100개의 챗봇을 만들었어. 그 다음은?”
“그 다음이라니? 각 매장에 QR 코드 나눠주고, 고객들이 채널 추가해서 주문하면 되지.” 솔라는 자신 있게 대답했다.
“좋아. 그런데 다음 달에 전 매장에서 쓸 수 있는 할인 쿠폰을 발행하기로 했어. 그럼 어떻게 해야 할까?”
“어떻게 하긴, 100개 채널에 전부 쿠폰 발송 공지를 올려야지.”
솔라는 대답을 하면서 무언가 이상한 낌새를 느꼈다. 100번의 반복 작업. 루나는 솔라가 스스로 생각의 실마리를 잡을 때까지 기다려주었다.
“만약… 챗봇에 ‘신메뉴 보기’ 기능을 추가하고 싶으면?” 솔라가 스스로에게 묻듯 중얼거렸다. “100개의 챗봇을 전부 수정해야 하네. 배포도 100번. 혹시 버그라도 있으면… 100개의 챗봇을 다 고쳐야 하고.”
생각이 꼬리를 물수록 ‘매장별 채널’이라는 선택지가 가졌던 명쾌함이 흐려지기 시작했다.
“사용자 입장에서도 생각해볼까? 내가 강남점 근처에 살아서 ‘OO카페 강남점’ 채널을 추가했는데, 주말에 홍대에 놀러 가서 그 카페에 또 가고 싶으면 ‘OO카페 홍대점’ 채널을 새로 검색해서 추가해야 하는 거야? 내 휴대폰엔 OO카페 채널만 몇 개가 되는 거지?”
솔라의 얼굴에 당혹감이 스쳤다. 처음에는 가장 직관적이라고 생각했던 모델이, 관리와 확장, 그리고 사용자 경험이라는 잣대를 들이대자 끔찍한 악몽처럼 변해버렸다. 채널을 하나하나 만들고, 챗봇을 설정하고, 공지사항을 올리고, 기능을 업데이트하는 모든 과정에 ‘매장 수’만큼의 곱셈이 붙는다. 매장이 늘어날수록 관리 비용은 선형적으로 증가하는 게 아니라, 기하급수적으로 폭발할 것 같았다.
솔라는 다시 노트북 화면의 그 문장을 바라보았다.
"매장마다 채널을 만들지 않고 공용 채널 하나를 사용하며..."
이제 이 문장은 ‘이상한 선택’이 아니라 ‘현명한 결정’으로 읽혔다. 개발과 운영의 관점에서 보면, 채널이라는 배포 단위를 하나로 통일하는 것은 관리의 복잡성을 극적으로 낮추는 결정적인 한 수였다.
“알겠다. 왜 채널을 하나로 합쳤는지 이제 알겠어. 100개의 채널을 관리하는 건 상상만 해도 끔찍하네. 하나로 합치는 게 훨씬 효율적이야.”
솔라는 자신의 첫 번째 판단이 틀렸음을 깨끗하게 인정했다. 하나의 문제를 해결하자, 그 해결책이 만든 새로운 문제가 모습을 드러냈다.
“그런데 언니, 그럼 처음의 질문으로 돌아가서… 채널이 하나뿐이라면, 시스템은 대체 어떻게 내가 어느 매장에서 말을 걸고 있는지 아는 거지? 강남점에서 ‘주문할게요’라고 말하는 거랑, 홍대점에서 ‘주문할게요’라고 말하는 걸 어떻게 구분하는데?“
2장: QR/딥링크가 매장을 지정하는 방법: clientExtra.store_id
솔라의 질문이 거실의 공기를 가르며 맴돌았다. 하나의 채널에서 수많은 매장을 어떻게 구분하는가. 타당한 관리 효율성 뒤에 숨겨진, 사용자 경험의 핵심적인 문제였다.
루나는 말없이 부엌으로 가, 어제 포장해 온 커피와 함께 딸려왔던 컵 홀더를 가져왔다. 컵 홀더 한쪽 구석에는 프랜차이즈의 로고와 함께 작은 QR 코드가 인쇄되어 있었다. 루나는 그것을 솔라 앞의 테이블에 가만히 놓았다. 갑자기 등장한 실물 단서에 솔라의 시선이 노트북에서 컵 홀더로 옮겨갔다.
“이게 단서야?” 솔라가 물었다. “이게 첫 번째 단서지.” 루나가 조용히 대답했다.
솔라는 컵 홀더를 집어 들고 QR 코드를 유심히 살폈다. “글쎄… 그냥 카카오톡 채널로 연결되는 링크 아냐? 이걸로 어떻게 내가 ‘어느 매장’에서 주문하는지 알 수 있다는 거야? 모든 매장이 똑같은 QR 코드를 쓰는 게 아니라면 모를까.”
“만약 모든 매장이 다른 QR 코드를 쓴다면?” 루나가 되물었다.
“그럼 또 일이잖아. 100개 매장이면 100개의 QR 코드를 만들고… 잠깐.” 솔라는 말을 멈췄다. 100개의 채널을 만드는 것과 100개의 QR 코드를 만드는 것은 전혀 다른 차원의 문제였다. QR 코드를 생성하고 인쇄해서 나눠주는 것은 채널을 개설하고 챗봇을 배포하는 것에 비하면 훨씬 간단한 일이었다.
“그래, QR 코드가 다 다르다고 치자. 그래도 그냥 채널 입구만 달라지는 거잖아. 일단 채널에 들어가면 다 똑같아지는 거 아니야?” 솔라의 목소리에는 여전히 의심이 남아 있었다. QR 코드는 그저 채널로 가는 문일 뿐, 그 문이 어떤 특별한 정보를 가지고 있을 거라는 생각은 하지 못했다.
루나는 펜을 들어 솔라의 노트북 옆 빈 종이에 웹 주소처럼 보이는 긴 문자열을 그리기 시작했다.
kakaotalk://skill/...'
“QR 코드를 스캔하면, 보통 이런 종류의 ‘딥링크’가 실행돼. 우리 눈에는 보이지 않지만, 이 주소 뒤에 작은 꼬리표를 붙일 수 있어.”
루나는 문자열 끝에 꼬리처럼 새로운 부분을 덧붙였다.
...&action_param={"clientExtra":{"store_id":"STORE_001"}}
솔라는 눈을 가늘게 뜨고 루나가 쓴 문자열을 해독하기 시작했다. store_id 라는 단어가 눈에 확 들어왔다.
“잠깐만. 그럼 강남점 테이블에 있는 QR 코드는 store_id가 ‘GANGNAM’이고, 홍대점 QR 코드에는 ‘HONGDAE’라고 적힌 채로, 사용자가 채널에 들어올 때 그 정보를 챗봇에게 넘겨준다는 뜻이야?”
솔라의 머릿속에서 흩어져 있던 조각들이 순식간에 맞춰졌다. QR 코드는 단순한 문이 아니었다. 문을 통과하는 사람의 옷에 ‘강남점에서 왔음’이라는 이름표를 붙여주는 똑똑한 문이었다. 사용자는 아무것도 인지하지 못하지만, 시스템은 사용자가 어느 매장의 문을 통해 들어왔는지 명확하게 알게 된다.
“맞아. 카카오톡 스킬에는 action.clientExtra라는, 바로 그런 용도를 위한 매개변수 공간이 있어. 채널에 진입하는 QR 코드나 딥링크에 개발자가 원하는 추가 정보를 담아 보낼 수 있지. 우리 시스템은 사용자의 첫마디가 도착했을 때, 가장 먼저 이 clientExtra에 store_id가 있는지 확인해.”
루나는 기술 블로그의 다음 문장을 가리켰다.
"QR·딥링크의 action.clientExtra.store_id가 있으면 해당 매장으로 고정한다."
솔라는 아까와는 완전히 다른 눈으로 그 문장을 읽었다. 이제는 단순한 선언이 아니라, 명시적이고 확실한 첫 번째 라우팅 규칙으로 보였다. 이것은 시스템이 사용자의 의도를 추측하는 것이 아니라, 사용자가 스스로 제공한 명백한 단서(어느 매장의 QR 코드를 스캔했는가)에 따라 대화의 맥락을 결정하는 가장 확실한 방법이었다.
“대박. 그럼 이건 거의 100% 확실한 방법이네. 고객이 매장 안에서 QR 찍었으면, 그 매장이 맞으니까. 다른 가능성을 생각할 필요도 없잖아.” 솔라는 감탄했다. 하나의 공용 채널이라는 제약 속에서 매장 컨텍스트를 유지하는 첫 번째 열쇠, ‘명시적 라우팅’의 원리를 깨달은 순간이었다.
문제를 해결했다는 후련함도 잠시, 솔라의 빛나는 호기심은 곧장 다음 단계의 허점을 파고들었다.
“알겠어. 매장에 와서 QR 코드를 찍는 사람들은 이걸로 완벽하게 해결되겠네. 그런데 언니, 만약 내가 매장에 가지 않고 그냥 집에서 카카오톡 검색창에 ‘OO카페’라고 쳐서 채널에 들어갔다면? 그때는 store_id가 담긴 딥링크가 없잖아. 시스템은 내가 어느 매장 손님인지 어떻게 알아?“
3장: 앞서 고른 매장을 기억하는 SessionStore의 작동 방식
루나가 테이블 위에 놓아둔 컵 홀더의 QR 코드는, 공용 채널이 어떻게 특정 매장을 알아채는지에 대한 명쾌한 첫 번째 해답이었다. 하지만 솔라는 이제 그 QR 코드를 보고 있지 않았다. 대신, 자신의 스마트폰 화면에 집중했다. 명시적인 단서가 없는, 더 애매한 상황을 직접 만들어보기로 한 것이다.
솔라는 카카오톡 검색창에 ‘OO카페’라고 직접 입력해 챗봇 채널에 들어갔다. QR 코드를 통하지 않았으니, clientExtra에 store_id가 담겨 전달될 리 없었다. 이 상태에서 ‘주문할게요’라고 입력하면 시스템은 과연 어떻게 반응할까? 솔라는 궁금증을 안고 메시지를 보냈다. 잠시 후, 챗봇이 답했다.
"안녕하세요, 솔라님. OO카페 강남점입니다. 무엇을 도와드릴까요?"
솔라는 자기도 모르게 “어?” 하는 소리를 냈다. 챗봇은 마치 당연하다는 듯 ‘강남점’을 언급했다. 솔라는 지난 주말에 친구와 강남점에서 커피를 마셨던 것을 떠올렸다.
“언니, 이거 봐. 내가 지난주에 강남점 갔던 걸 기억하고 있어. 내 카카오톡 계정 정보랑 마지막 이용 기록 같은 걸 데이터베이스에 저장해 두는 건가 봐. 진짜 똑똑한데?”
솔라의 추측은 그럴듯했다. 사용자의 마지막 선택을 영구적으로 기억하는 것. 웹 서비스에서 ‘로그인 유지’ 기능처럼 익숙한 방식이었다. 그것이 솔라의 두 번째 판단이었다.
루나는 솔라의 화면을 들여다보며 고개를 저었다. “영구적으로? 정말 그럴까? 만약 네가 어제는 홍대점에, 오늘은 강남점에 갔다면 챗봇은 어떤 매장을 ‘너의 매장’으로 기억해야 할까? 마지막 방문 매장? 아니면 가장 자주 가는 매장?”
루나의 질문에 솔라는 말문이 막혔다. 사용자의 컨텍스트는 고정되어 있지 않다. 시스템이 섣불리 하나의 매장을 ‘영구적인’ 기본값으로 지정하는 것은 오히려 불편을 초래할 수 있었다.
“그럼… 뭔데? 어떻게 기억하는 거야?”
루나는 다시 기술 블로그의 다음 문장을 가리켰다. 솔라가 아까 clientExtra에 감탄하느라 미처 자세히 보지 못했던 부분이었다.
"그 값이 없으면 SessionStore가 사용자가 앞서 고른 매장을 기본 30분 기억하고, 아직 미정이면 매장 선택 버튼을 보여준다."
“데이터베이스가 아니라, SessionStore라는 임시 저장소에 잠시 기억해두는 거야. 웹사이트에서 장바구니 정보가 잠시 유지되는 것처럼.”
“잠깐, 그럼 30분이 지나면 어떻게 되는데?”
“사라지지.” 루나의 대답은 단호했다.
솔라는 혼란스러웠다. “사라진다고? 왜? 기껏 기억해놓고 왜 지워버리는 거야? 비효율적이잖아.”
루나는 솔라의 손에서 스마트폰을 받아 테이블 위에 내려놓고, 간단한 상황극을 제안했다. “우리가 지금부터 시간을 시뮬레이션해보자. 아주 간단한 두 가지 시나리오야.”
루나는 말을 이었다. “시나리오 1: 네가 12시에 강남점에서 커피를 주문했어. 그리고 12시 15분, 지하철을 타고 집으로 가다가 주문을 변경하고 싶어졌지. 챗봇을 다시 열면 어떻게 될까?”
“음… SessionStore에 ‘강남점’ 정보가 30분간 남아있으니까, 챗봇은 내가 강남점 고객인 걸 알고 있겠지. ‘강남점입니다’라고 하면서 주문 변경을 도와줄 거고. 이건 편하네.”
“맞아. 이제 시나리오 2: 똑같이 12시에 강남점에서 주문했어. 그리고 오후 5시, 퇴근하고 집에 와서 저녁 먹고 커피가 생각나서 챗봇을 다시 열었어. 그럼 어떻게 될까?”
솔라는 잠시 생각에 잠겼다. “5시간이 지났으니 30분짜리 세션은 이미 사라졌겠네. 그럼 챗봇은… 내가 어느 매장 손님인지 모르겠구나.”
바로 그 지점이었다. 만약 시스템이 ‘강남점’을 영구적으로 기억했다면, 집 앞에 있는 다른 지점에서 주문하고 싶었던 솔라에게 ‘강남점입니다’라고 말을 걸었을 것이다. 사용자의 상황은 바뀌었는데, 시스템은 과거의 컨텍스트에 갇혀버리는 셈이다.
“알겠다. 30분이라는 시간이 절묘한 기준이구나. 사용자가 한 매장에서 주문을 시작해서 끝낼 때까지의 연속적인 행동은 대부분 30분 안에 일어나니까, 그동안은 편의를 위해 기억해 주는 거야. 하지만 그 시간이 지나면 사용자의 맥락이 바뀌었을 가능성이 높으니까, 기억을 깨끗이 비워서 혼란을 막는 거고.”
솔라는 SessionStore가 단순히 ‘기억하는’ 기능이 아니라, ‘적절한 때에 잊어주는’ 똑똑한 기능임을 깨달았다.
“게다가 기술적인 이유도 있어. 여기서 말하는 SessionStore는 보통 서버의 ‘프로세스 메모리’를 기반으로 만들어. 별도 데이터베이스에 저장하는 게 아니라, 그냥 서버가 잠시 머릿속에 담아두는 것과 같아. 그래서 서버를 재시작하거나 하면 이 기억은 전부 사라져. 영구적으로 보관하기엔 너무 불안정한 기억이지. 하지만 잠깐 동안의 대화 맥락을 유지하기엔 가볍고 빨라서 딱 좋아.”
솔라는 고개를 끄덕였다. 영구적인 기억이라는 첫 추측이 얼마나 순진했는지 알 수 있었다. SessionStore는 명시적인 단서(clientExtra)가 없을 때를 대비한 두 번째 안전장치이자, 사용자의 변화하는 맥락을 존중하는 유연한 장치였다.
솔라는 다시 그 문장을 바라봤다.
"그 값이 없으면 SessionStore가 사용자가 앞서 고른 매장을 기본 30분 기억하고..."
이제 이 문장은 ‘불완전한 기억’이 아니라 ‘의도된 망각’으로 읽혔다. 하지만 문장은 거기서 끝나지 않았다. 솔라의 눈길은 자연스럽게 문장의 나머지 부분으로 향했다.
”…아직 미정이면 매장 선택 버튼을 보여준다.”
새로운 질문이 떠올랐다.
“언니, 그럼 모든 조건이 처음으로 돌아가면 어떻게 되는 거야? QR 코드로 들어온 적도 없고, 최근 30분 내에 이용한 기록도 없는 완전 신규 사용자. clientExtra도 없고, SessionStore에도 아무것도 없는 상태 말이야. 그때 시스템은 뭘 할 수 있지? 그리고, 세션이 ‘강남점’을 기억하고 있더라도 내가 오늘은 ‘홍대점’에서 주문하고 싶으면 어떡해? 매장을 바꿀 방법은 있는 거야?“
4장: 사용자 주도 매장 선택과 세션 초기화
솔라는 지난번 대화의 여운을 느끼며 스마트폰을 잠시 내려놓았다. SessionStore라는 개념은 사용자의 최근 맥락을 30분 동안 똑똑하게 기억했다가, 적절한 때에 잊어주는 영리한 장치였다. 솔라는 잠시 부엌으로 가 시원한 물 한 잔을 마시고 돌아왔다. 창밖은 어느새 해가 기울고 있었다. 30분은 족히 지났을 시간이었다.
다시 스마트폰을 집어 든 솔라는 새로운 실험을 해보기로 했다. 30분이 지났으니, SessionStore에 저장되었던 ‘강남점’이라는 기억은 사라졌을 것이다. QR 코드로 들어간 것도 아니니, clientExtra에 담긴 정보도 없다. 명시적 단서도, 임시 기억도 없는 완전한 백지상태. 솔라는 이 상태에서 챗봇이 어떻게 반응할지 궁금했다. 그녀는 다시 한번 ‘OO카페’ 채널에 ‘주문할게요’라고 입력했다.
이번에는 지난번과 다른 반응이 돌아왔다. 챗봇은 특정 매장 이름을 대는 대신, 여러 개의 버튼을 화면에 띄웠다.
"어느 매장을 이용하시겠어요?"
[강남점] [홍대점] [판교점] [매장 검색]
솔라는 무릎을 쳤다. 이것이었다. 시스템이 사용자의 매장 컨텍스트에 대해 아무것도 모를 때, 추측하거나 오류를 내뱉는 대신 사용자에게 직접 물어보는 것. 가장 안전하고 확실한 방법이었다.
“언니, 이거 봐. 챗봇이 나한테 직접 물어봐. clientExtra도 없고 세션도 만료되니까, 드디어 선택지가 떴어.”
솔라의 목소리에는 자신의 가설이 입증되는 것을 지켜보는 과학자의 흥분이 묻어 있었다. 매장 선택 UI는 시스템이 길을 잃었을 때 나타나는 최후의 안내판인 셈이었다. 솔라는 ‘매장 선택 UI가 항상 최우선으로 등장할 것’이라고 막연히 생각했던 자신의 처음 가정을 떠올렸다. 그게 아니었다. 시스템은 최대한 사용자를 귀찮게 하지 않으려 노력하다가, 도저히 알 수 없을 때만 선택권을 넘겨준다.
루나는 솔라가 스스로 답을 찾아낸 과정을 지켜보다가, 이야기의 다음 조각을 테이블 위에 올려놓았다.
“좋아. 그럼 거기서 홍대점을 선택해봐.”
솔라는 루나의 말대로 [홍대점] 버튼을 눌렀다. 챗봇은 곧바로 “OO카페 홍대점입니다.”라고 응답하며 홍대점 컨텍스트로 대화를 시작할 준비를 마쳤다. 이제 솔라의 SessionStore에는 ‘홍대점’ 정보가 새로 기록되었을 것이다.
“자, 이제 너는 홍대점 고객이야. 그런데 갑자기 마음이 바뀌어서 강남점에서 주문하고 싶어졌어. 그럼 어떻게 해야 할까?”
“음… ‘매장 변경’ 같은 버튼이 있겠지?” 솔라가 챗봇 화면을 살펴보며 말했다. 과연, 메뉴 어딘가에 ‘매장 변경’이라는 버튼이 있었다. “이걸 누르면, 다시 아까처럼 매장 선택 버튼들이 나타나서 고를 수 있게 해주지 않을까?”
솔라의 예측은 지극히 합리적이었다. A를 B로 바꾸는 간단한 교체 작업. 그것이 솔라가 상상한 ‘매장 변경’의 동작 방식이었다.
하지만 루나는 고개를 저었다. “단순히 ‘교체’하는 것과는 조금 달라. 기술 블로그의 그 문장, 마지막 부분을 다시 한번 생각해봐.”
루나가 가리킨 곳에는 아까 스쳐 지나갔던 문장의 뒷부분이 있었다.
"...아직 미정이면 매장 선택 버튼을 보여준다. 매장 변경 요청은 session을 초기화한다."
솔라는 ‘session을 초기화한다’는 구절에 시선을 고정했다. “세션을 초기화한다고? 그냥 매장 정보만 바꾸는 게 아니라?”
“응. ‘홍대점’이라는 정보를 ‘강남점’으로 바꾸는 게 아니야. ‘매장 변경’을 누르는 순간, SessionStore에 들어있던 ‘홍대점’ 정보를 포함한 모든 세션 데이터가 그냥 지워져 버려. 텅 빈 상태가 되는 거지.”
“지워진다고? 왜?”
“그게 더 간단하고 확실하니까.” 루나는 펜을 들어 간단한 흐름을 그렸다.
- 현재 상태:
SessionStore={ store_id: "HONGDAE", ... } - 사용자: ‘매장 변경’ 버튼 클릭
- 시스템 동작:
SessionStore를 통째로 비움.SessionStore={} - 그 다음은?
솔라는 루나가 그린 4번의 빈칸을 바라보았다. 세션이 텅 비워졌다. 매장 정보가 미정이 되었다. 이 상황은 방금 전 솔라가 직접 겪었던 것과 똑같았다.
“아…! 세션이 초기화되면, 시스템 입장에서는 매장 정보가 ‘미정’인 상태가 되는 거구나. 그럼 시스템은 정해진 규칙대로… 다시 매장 선택 버튼을 보여주겠네.”
솔라는 깨달았다. ‘매장 변경’은 복잡한 ‘교체’ 로직이 아니었다. 모든 것을 처음으로 되돌리는 ‘리셋 버튼’이었다. 시스템은 ‘A에서 B로 가는 길’을 만드는 대신, 모든 길을 ‘처음으로 돌아가는 길’ 하나로 통일해버린 것이다. 이렇게 하면 어떤 상태에서든 ‘매장 변경’을 눌렀을 때의 결과가 항상 동일하게 보장된다. 관리해야 할 경우의 수가 극적으로 줄어든다.
“맞아. 사용자에게는 매장을 ‘바꾸는’ 것처럼 보이지만, 시스템 내부에서는 ‘리셋하고 다시 선택하게 하는’ 흐름이야. 이 방식은 사용자가 직접 자신의 컨텍스트를 확실하게 재설정할 기회를 주는 거지.”
솔라는 이제 ‘사용자 주도 매장 선택’과 ‘세션 초기화’라는 두 기능이 어떻게 맞물려 돌아가는지 완벽히 이해했다. 하나는 시스템이 길을 잃었을 때, 다른 하나는 사용자가 길을 바꾸고 싶을 때 사용되지만, 결국 둘 다 ‘사용자에게 선택권을 넘긴다’는 동일한 해결책으로 귀결되었다.
이제 clientExtra라는 명시적 이름표, SessionStore라는 30분짜리 임시 기억, 그리고 마지막 안전장치인 사용자 선택 UI까지. 세 가지 라우팅 장치가 모두 모습을 드러냈다. 각각의 역할은 알겠다. 하지만 이들이 한 시스템 안에서 공존하려면, 분명한 질서가 필요해 보였다.
“언니, 그럼 이제 알겠어. QR 코드, 세션, 매장 선택 버튼. 이렇게 세 가지 방법이 있는 거네. 그런데 만약에… 내가 강남점 QR 코드를 찍어서 채널에 들어왔는데, 내 세션에는 아직 5분 전에 갔던 홍대점 기록이 남아있으면 어떻게 돼? 시스템은 내 말을 들어야 해, 아니면 QR 코드 말을 들어야 해? 이 세 가지 방법 사이에 혹시 우선순위 같은 게 있는 거야?“
5장: 다매장 라우팅의 우선순위와 설계 의도
솔라의 질문은 공중에 뚜렷한 흔적을 남겼다. 서로 다른 맥락을 가진 정보들이 충돌할 때, 시스템은 누구의 손을 들어줄 것인가? QR 코드가 품고 온 명시적인 store_id와, 사용자의 과거 행동이 남긴 SessionStore의 희미한 기억. 이 둘이 맞붙으면 과연 어떤 일이 벌어질까.
루나는 바로 대답하는 대신, 솔라가 펼쳐둔 노트북 옆에 놓인 빈 종이를 자기 앞으로 끌어당겼다. 그리고 펜을 들어 간단한 표를 그리기 시작했다. 표의 머리에는 각각 ‘상황’, ‘시스템 상태’, 그리고 ‘결과’라고 적었다. 첫 번째 ‘상황’ 칸에 루나는 솔라가 던진 질문을 그대로 옮겨 적었다.
| 상황 | 시스템 상태 | 결과 |
|---|---|---|
강남점 QR 스캔 (clientExtra에 ‘GANGNAM’ 있음) | 5분 전 홍대점 이용 (SessionStore에 ‘HONGDAE’ 있음) | ? |
물음표가 찍힌 마지막 칸이 솔라의 시선을 사로잡았다. 각 라우팅 방식이 독립적으로 작동한다고 생각했던 솔라에게 이 표는 처음으로 두 방식의 충돌을 시각적으로 보여주는 증거였다. 그녀의 머릿속은 복잡해졌다.
“음… 그래도 방금 전까지 홍대점에 있었으니까, 시스템은 내가 아직 홍대점 관련 일을 처리할 거라고 생각하는 게 더 자연스럽지 않을까? 사용자의 최근 행동이 더 중요할 것 같아. 그래서… 결과는 홍대점?”
솔라의 추론은 그럴듯했다. ‘가장 최근의 것’이 이긴다는 규칙은 직관적이었다. 하지만 루나는 고개를 젓는 대신, 솔라가 스스로 논리의 빈틈을 발견하게 할 질문을 던졌다.
“그렇다면, 매장 주인이 굳이 돈과 시간을 들여 모든 테이블마다 다른 QR 코드를 인쇄해서 붙여놓은 이유는 뭘까? 사용자가 일부러 스마트폰을 꺼내 특정 매장의 QR 코드를 스캔하는 그 행동은, 시스템이 다른 어떤 것보다 더 명확하게 받아들여야 할 ‘의도’의 표현 아닐까?”
루나의 말에 솔라는 순간 할 말을 잃었다. 그랬다. SessionStore의 기억은 시스템이 사용자의 편의를 위해 ‘추측한’ 맥락이다. 하지만 QR 코드를 찍는 행위는 사용자가 ‘직접’ 자신의 현재 맥락을 시스템에 알리는 명시적인 선언이다. 추측과 선언이 부딪힌다면, 당연히 선언이 이겨야 했다.
“아…!”
솔라는 펜을 집어 들어 물음표를 지우고, 그 자리에 망설임 없이 ‘강남점’이라고 적었다. 그리고는 스스로 다음 시나리오를 ‘상황’ 칸에 추가하기 시작했다.
| 상황 | 시스템 상태 | 결과 |
|---|---|---|
강남점 QR 스캔 (clientExtra O) | 홍대점 세션 (SessionStore O) | 강남점 |
채널 직접 검색 (clientExtra X) | 홍대점 세션 (SessionStore O) |
“만약 QR 없이 그냥 채널에 들어왔고, 세션에 홍대점 기록이 남아있다면… 그땐 clientExtra가 없으니 다음 순서인 세션을 따르겠네.” 솔라는 ‘결과’ 칸에 ‘홍대점’을 적었다. 이어서 마지막 시나리오를 추가했다.
| 상황 | 시스템 상태 | 결과 |
|---|---|---|
강남점 QR 스캔 (clientExtra O) | 홍대점 세션 (SessionStore O) | 강남점 |
채널 직접 검색 (clientExtra X) | 홍대점 세션 (SessionStore O) | 홍대점 |
채널 직접 검색 (clientExtra X) | 세션 없음 (SessionStore X) | 매장 선택 UI |
표를 완성한 솔라는 자신이 방금 하나의 의사결정 모델을 완성했음을 깨달았다. 그것은 혼란스러운 예외들의 집합이 아니라, 명확한 폭포수 모델이었다.
- 가장 먼저,
clientExtra에store_id가 있는가? (명시적 의도)- Yes: 해당 매장으로 컨텍스트 고정.
- No일 경우,
SessionStore에store_id가 있는가? (암묵적 맥락)- Yes: 해당 매장으로 컨텍스트 설정.
- No일 경우, 사용자에게 직접 물어본다. (최후의 수단)
- Yes: 매장 선택 UI 표시.
“이제 알겠어. 이 세 가지 방법은 그냥 나열된 게 아니라, 완벽한 우선순위를 가진 거였어. 가장 확실한 단서부터 차례대로 확인하는 거구나.”
이때, 솔라의 머릿속에 SessionStore의 기술적 한계가 다시 떠올랐다. 서버가 재시작되면 모든 기억이 사라진다는 점.
“그런데 언니, SessionStore가 서버 재시작 한 번에 전부 날아간다는 건 여전히 좀 불안한데. 만약 사용자가 매장을 선택하고 뭔가 하려는 찰나에 서버가 재시작되면, 사용자는 처음부터 다시 해야 하잖아. 이건 치명적인 결함 아니야?”
솔라의 시각에서 SessionStore의 휘발성은 시스템의 불안정성을 의미했다. 그러나 루나는 그 한계가 오히려 시스템을 보호하는 장치라고 설명했다.
“그 기억이 영구적이라고 상상해봐. 서버 데이터베이스에 ‘솔라의 마지막 매장 = 강남점’이라고 저장되어 있어. 그런데 한 달 뒤에 강남점이 폐점했거나 내부 코드가 바뀌었어. 시스템은 이미 존재하지 않는 과거의 정보에 의존해서 계속 오류를 일으키게 될 거야.”
루나는 말을 이었다. “이 기억이 ‘프로세스 메모리’에 기반해 쉽게 사라진다는 것은, 시스템 스스로 이 정보가 ‘절대적인 진실이 아닌, 일시적인 편의를 위한 힌트’에 불과하다는 것을 인정하는 셈이야. 그리고 힌트가 사라졌을 때 시스템은 어떻게 행동하지?”
솔라는 자신이 만든 표를 보았다. SessionStore가 없는 경우, 시스템의 다음 행동은 정해져 있었다.
”…가장 안전한 방법, 즉 사용자에게 다시 물어보는 단계로 넘어가겠지. 아, 고장 나는 대신 가장 안전한 상태로 후퇴하는 거구나.”
SessionStore의 소멸은 결함이 아니었다. 시스템이 예측 불가능한 상황에 빠졌을 때, 모든 것을 원점으로 되돌려 사용자에게 다시 명확한 선택권을 넘겨주는 ‘안전장치’였다.
솔라는 처음 이 주제를 접했을 때 보았던 기술 블로그의 문장을 떠올렸다.
"공용 채널 하나에서 clientExtra.store_id, SessionStore와 매장 선택 순서로 대화 대상 매장을 정한다."
이제 이 문장은 단순한 기능의 나열이 아니었다. 명시성 높은 순서대로 사용자의 의도를 존중하고, 시스템의 안정성까지 고려한 깊은 설계 철학이 담긴 하나의 완성된 라우팅 전략 모델로 읽혔다.
문득, 솔라는 자신이 구상하던 작은 토이 프로젝트에 이 원리를 적용해 볼 수 있겠다는 생각이 들었다. 그녀는 새 메모장을 열고 ‘도서관 좌석 예약 봇’이라는 제목을 적었다.
“언니, 만약에 내가 이런 봇을 만든다면… 이 라우팅 전략을 그대로 쓸 수 있겠어.”
솔라는 키보드를 두드리며 자신만의 라우팅 모델을 설계하기 시작했다.
- 명시적 라우팅: 각 열람실 좌석에 QR 코드를 붙여
room_id와seat_id를clientExtra로 전달. 사용자가 QR을 스캔하면, 바로 해당 좌석 예약 화면으로 안내한다. 이것이 최우선 순위. - 세션 기반 라우팅: QR 없이 채널에 접속하면, 30분 내에 이용했던
room_id를SessionStore에서 확인. ‘최근 이용한 제1열람실에서 좌석을 찾으시겠어요?‘라고 물어본다. - 사용자 선택 라우팅: 위 두 가지 정보가 모두 없으면, ‘어느 열람실을 이용하시겠어요?‘라는 버튼 목록을 보여준다.
불과 한 시간 전까지만 해도 ‘매장마다 채널을 만드는 게 당연하지’라고 생각했던 솔라였다. 이제 그녀는 하나의 공용 채널 안에서 복잡한 컨텍스트를 명쾌하게 다루는 자신만의 시스템을 설계하고 있었다. 그것은 단순히 지식을 얻은 것을 넘어, 문제를 바라보고 해결하는 하나의 강력한 관점을 얻었다는 증거였다.