Mini Project 5 10
즐겨찾기 기능의 해부: 개인화된 서비스의 탄생
즐겨찾기는 책 카드에 하트 하나 붙이는 기능처럼 보인다. 하지만 사용자별 상태로 저장하려면 book 자체가 아니라 user-book 관계를 다뤄야 한다.
근거 · GitHub favorites relation 구현
1장: 즐겨찾기: ‘책의 속성’이 아닌 ‘사용자와 책의 관계’
솔라는 거실 소파에 앉아 노트북으로 새로 단장한 온라인 서점을 둘러보고 있었다. 수많은 책 표지들이 화면을 가득 채웠다. 스크롤을 내리던 솔라의 손가락이 멈췄다. 얼마 전부터 읽고 싶었던 책이었다. 솔라는 망설임 없이 책 표지 아래에 있는 작은 하트 아이콘을 클릭했다. 회색이었던 하트가 선명한 붉은색으로 채워졌다.
“좋아, 찜해 뒀어.”
솔라는 만족스러운 미소를 지으며 중얼거렸다. 참 편리한 기능이라고 생각했다. 그저 책에 달린 속성 하나를 바꾸는 것처럼 간단해 보였다. 책 데이터에 ‘즐겨찾기 여부’라는 스위치가 있어서, 클릭하면 켜지고 다시 클릭하면 꺼지는 식일 것이다. 화면의 하트 아이콘은 그 스위치 상태를 보여주는 예쁜 표시에 불과하다고 생각했다.
“재미있어?”
어느새 다가온 루나가 노트북 화면을 들여다보며 물었다.
“응. 이 서점 UI가 깔끔해서 좋아. 특히 이 즐겨찾기 기능. 그냥 책에다 하트 표시 하나 붙이는 거잖아. 간단하고 직관적이야.”
솔라는 자신이 생각한 즐겨찾기 기능의 작동 방식을 설명했다. ‘이 책은 즐겨찾기 된 책’이라는 꼬리표가 책 자체에 붙어있을 거라고.
루나는 잠시 생각에 잠기더니, 고개를 끄덕이는 대신 질문을 던졌다.
“만약 내가 내 계정으로 이 서점에 들어와서 솔라 네가 방금 찜한 그 책을 보면, 나한테도 붉은 하트가 보일까?”
“아니? 언니는 그 책을 찜한 적 없으니까 당연히 회색 하트로 보이겠지.”
솔라는 당연하다는 듯 대답했다. 하지만 대답을 내뱉는 순간, 무언가 이상하다는 느낌이 스쳤다.
루나는 아무 말 없이 솔라를 바라봤다. 그 침묵이 솔라의 머릿속을 더 복잡하게 만들었다. 만약 즐겨찾기가 정말 책에 원래부터 포함된 정보라면, 마치 책의 가격이나 페이지 수처럼, 누가 보든 동일해야 한다. 내가 책에 붉은 하트를 ‘붙였다면’, 그건 책에 일어난 변화이므로 다른 사람에게도 똑같이 보여야 마땅했다.
하지만 그렇지 않다. 내가 찜한 책은 나에게만 찜한 것으로 보인다. 언니의 화면에서는, 그리고 다른 모든 사람의 화면에서는 여전히 ‘찜하지 않은’ 상태일 것이다.
솔라의 머릿속에서 두 개의 상반된 사실이 충돌했다. ‘즐겨찾기 상태는 책에 기록되는 정보다.’ ‘즐겨찾기 상태는 사용자마다 다르다.’
이 두 문장은 동시에 사실일 수 없었다. 솔라는 잠시 미간을 찌푸렸다. 자신이 처음 가졌던 생각이 틀렸다는 것을 깨달았다. 하트 아이콘은 책에 붙어있는 꼬리표가 아니었다. 만약 그랬다면 모든 사람이 그 꼬리표를 봐야만 한다.
“아…”
솔라는 낮은 탄성을 내뱉었다. 새로운 그림이 머릿속에 그려지기 시작했다. 책과 사용자가 있고, 즐겨찾기는 그 둘 중 어느 한쪽에 속한 것이 아니었다.
“하트 표시는… 책의 정보가 아니구나. 이건 ‘나’와 ‘이 책’ 사이의 문제였어.”
솔라는 자신의 노트북 화면과 가상의 ‘언니의 노트북 화면’을 머릿속에서 나란히 떠올렸다. 두 화면에는 똑같은 책이 있다. 하지만 내 화면의 하트는 붉은색이고, 언니 화면의 하트는 회색이다. 책 자체는 변하지 않았다. 변한 것은 ‘솔라’라는 사용자와 특정 ‘책’을 연결하는 보이지 않는 끈이었다. 하트 아이콘은 그 끈이 존재한다는 사실을 나에게만 보여주는 신호등 같은 것이었다.
“그래. 즐겨찾기는 책에 대한 정보가 아니라, ‘나’와 책 사이의 ‘관계’를 보여주는 거였어.”
솔라가 중얼거렸다. 시선이 명확해지자 복잡했던 문제가 단순하게 보였다. 온라인 서점은 수많은 책 정보를 가지고 있고, 동시에 수많은 사용자 정보를 가지고 있다. 그리고 ‘누가 어떤 책을 즐겨찾기 했는지’라는 ‘관계’ 정보를 그 둘과 별개로 관리하고 있는 것이다.
그렇게 생각하니 모든 것이 제자리를 찾았다. 개인화된 서비스라는 말의 의미가 피부로 와 닿았다. 모두에게 똑같은 책 목록을 보여주는 것이 아니라, 나와 책의 ‘관계’를 바탕으로 나만을 위한 화면을 구성해주는 것. 그것이 즐겨찾기 기능의 본질이었다.
한 가지 의문이 풀리자 곧바로 새로운 질문이 꼬리를 물었다.
“좋아, ‘관계’라는 건 알겠어. 그런데 이 보이지 않는 관계는 대체 어디에, 어떻게 저장되는 거지? 내가 이 하트를 누르면 컴퓨터 세상에서는 정확히 무슨 일이 일어나는 걸까?”
2장: 관계의 탄생: 백엔드에서 userId와 bookId를 다루는 법
솔라는 거실 테이블에 노트를 펼쳐놓고 펜을 들었다. 머릿속에 떠오른 ‘관계’라는 개념을 눈에 보이는 그림으로 옮겨보고 싶었다. 먼저 ‘사용자’ 목록을 나타내는 네모 상자를 그리고, 그 옆에 ‘책’ 목록을 위한 또 다른 상자를 그렸다. 솔라라는 항목과, 방금 찜했던 『코스모스』라는 항목에 동그라미를 쳤다.
이제 이 둘을 어떻게 연결할까? 솔라는 두 동그라미 사이에 선을 그었다. 이 선이 바로 ‘즐겨찾기 관계’였다. 그런데 이 ‘선’은 어디에 저장되는 걸까? 솔라의 펜이 잠시 허공에서 멈칫했다. 가장 간단한 방법은 책 정보에 ‘누가 찜했는지’를 기록하는 것이었다. 솔라는 ‘책’ 상자 안에 favorited_by_user라는 항목을 추가하려다 멈칫했다. 한 권의 책을 수천 명이 찜할 수도 있는데, 그때마다 책 정보를 수정하는 건 뭔가 이상했다. 그렇다면 반대로 ‘사용자’ 정보에 favorite_books 목록을 넣는 건 어떨까? 그것도 마찬가지였다. 한 사람이 수백 권의 책을 찜하면 사용자 정보가 너무 길고 복잡해질 것이다.
“결국 처음 생각으로 돌아오잖아.”
솔라는 결국 ‘책’ 상자의 『코스모스』 항목 옆에 isFavorite = true 라고 작게 적었다가, 이내 검게 덧칠해서 지워버렸다. 즐겨찾기는 책에 저장되는 정보가 아니라는 걸 겨우 깨달았는데, 데이터 저장 방식을 상상하니 다시 그 함정으로 빠져드는 기분이었다.
“그림이 복잡해 보이네.”
부엌에서 물을 마시고 온 루나가 솔라의 노트에 그려진 어지러운 선들과 지워진 자국들을 보며 말했다.
“‘관계’라는 건 알겠는데, 그걸 컴퓨터가 어떻게 저장하는지 모르겠어. 내가 하트를 누르면, 서버 컴퓨터에서 『코스모스』라는 책 데이터를 찾아서 뭔가 표시를 남겨야 할 것 아냐. 그런데 그 표시는 나한테만 보여야 하고. 도저히 모르겠어.”
솔라는 자신이 방금 지워버린 isFavorite = true를 가리켰다. 이것이 틀렸다는 건 알지만, 다른 방법이 떠오르지 않았다.
루나는 솔라의 노트 한편에 새로운 그림을 그리기 시작하는 대신, 질문의 방향을 틀었다.
“좋아. 잠시 데이터베이스는 잊어보자. 솔라 네가 하트 버튼을 ‘클릭’하는 순간, 네 노트북이 서점으로 어떤 ‘요청’ 메시지를 보내게 될까? 그 메시지 안에는 어떤 정보가 꼭 들어있어야 할까?”
“메시지?”
솔라는 잠시 상상에 잠겼다. 내 노트북이 저 멀리 있는 서점 컴퓨터에게 말을 거는 장면.
“음… 일단 ‘어떤 책’을 찜했는지 알려줘야 하니까, 책의 고유한 번호, bookId 같은 게 필요할 거야. 그리고…”
솔라는 잠시 말을 멈췄다. 지난 장에서 깨달았던 핵심이 떠올랐다. 즐겨찾기는 사용자마다 다르다.
“…그리고 ‘누가’ 찜했는지도 알려줘야 해! 그렇지 않으면 서점은 이 요청이 누구로부터 온 건지 알 수 없잖아. 그러니까 내 아이디, userId도 보내야겠네.”
“바로 그거야.”
루나가 말했다. “즐겨찾기 기능의 핵심은 그 두 가지 정보, userId와 bookId에서 시작돼. 이제 그 메시지를 받은 서버가 어떻게 움직이는지 따라가 보자.”
루나는 솔라의 노트 빈 공간에 세 개의 상자를 차례로 그렸다. 그리고 각각 ‘Controller’, ‘Service’, ‘Repository’라고 이름을 붙였다.
“서버는 그냥 하나의 덩어리가 아니야. 각자 역할이 다른 여러 부서로 나뉘어 있어. 네가 보낸 ‘userId와 bookId가 담긴 요청’은 먼저 ‘컨트롤러(Controller)’라는 안내 데스크에 도착해.”
루나는 첫 번째 상자를 펜으로 톡톡 쳤다.
“컨트롤러는 ‘아, 즐겨찾기 요청이구나. userId는 솔라이고, bookId는 코스모스네’ 하고 확인만 하고, 복잡한 일은 하지 않아. 그저 이 요청을 처리할 담당 부서인 ‘서비스(Service)’에게 그대로 전달할 뿐이야.”
펜이 두 번째 상자인 ‘서비스’로 옮겨갔다.
“서비스 계층이 진짜 실무자야. userId와 bookId를 받아서 ‘이 사용자가 진짜 있는 사용자인가? 이 책이 판매 중인 책이 맞나? 이미 즐겨찾기 한 상태인데 또 요청한 건 아닌가?’ 같은 비즈니스 규칙을 판단하고 결정해. 모든 확인이 끝나면, ‘좋아, 이 둘을 연결하는 즐겨찾기 관계를 새로 만들어 줘’ 또는 ‘기존 관계를 삭제해 줘’ 라는 최종 지시를 내려.”
솔라의 눈이 반짝였다. 이제 마지막 상자가 남았다.
“그럼 리포지토리(Repository)는…?”
“데이터베이스 창고지기야.”
루나가 마지막 상자를 가리키며 말했다.
“서비스의 지시를 받아서, 실제 데이터베이스에 기록하거나 삭제하는 일을 하지. 우리 시스템의 이 부분을 ‘FavoriteRepository’라고 부르는데, 이름 그대로 즐겨찾기(favorites) 데이터만 전문적으로 다루는 곳이야. 중요한 건, 이 FavoriteRepository가 ‘책’ 데이터를 직접 수정하는 게 아니라는 점이야. favorites라는 별도의 테이블에 ‘userId는 OOO, bookId는 XXX’ 같은 관계 기록을 한 줄 추가할 뿐이지.”
솔라는 세 개의 상자가 화살표로 연결된 그림을 멍하니 바라보았다. 컨트롤러에서 서비스로, 서비스에서 리포지토리로. userId와 bookId라는 두 개의 작은 열쇠가 복잡한 기계의 톱니바퀴들을 차례로 돌리는 모습이 눈앞에 그려졌다.
“아…! 그래서 ‘관계’가 따로 저장된다는 거구나.”
솔라는 무릎을 쳤다. 책 데이터를 직접 수정하는 것이 아니라, ‘솔라’와 ‘코스모스’를 연결하는 한 줄의 기록이, 완전히 새로운 공간에 새로 생겨나는 것이었다. 개인화된 서비스의 핵심 데이터는 바로 이렇게, 컨트롤러-서비스-리포지토리라는 체계적인 분업을 통해 탄생하고 관리되고 있었다.
복잡했던 머릿속이 깨끗하게 정리되는 기분이었다. 하지만 동시에 새로운 의문이 고개를 들었다.
“그런데 언니, 좀 이상해. 방금 설명만 들어보면 안내 데스크 갔다가, 실무자 결재받고, 창고에 기록까지 남기는 거잖아. 꽤 여러 단계를 거치는 것 같은데… 내가 즐겨찾기 하트를 누르면, 화면에선 그냥 즉시 색깔이 바뀌어. 전혀 기다리는 느낌이 없단 말이야. 어떻게 그게 가능하지?”
3장: 즉각적인 반응의 비밀: 낙관적 업데이트(Optimistic Update)
솔라는 앞 장에서 루나와 함께 그렸던 노트를 다시 펼쳤다. ‘Controller → Service → Repository’. userId와 bookId가 담긴 요청이 서버의 각 부서를 거쳐 데이터베이스라는 창고에 기록되기까지의 여정. 깔끔한 분업 구조가 마음에 들었지만, 볼수록 한 가지 의문이 짙어졌다.
솔라는 펜을 들어 요청의 흐름을 나타내는 화살표 옆에 작은 시계 아이콘을 그려 넣었다. 안내 데스크를 거치고, 실무자의 검토를 받고, 창고에 기록을 남기는 데는 아무리 빨라도 시간이 걸릴 터였다. 물리적인 거리가 있고, 처리 과정이 존재하니까. 하지만 솔라가 경험한 현실은 달랐다. 노트북 화면에서 하트를 누르는 순간, 색은 0.1초의 망설임도 없이 즉시 바뀌었다. 노트 위의 논리적인 시간 흐름과 손끝의 즉각적인 경험 사이에 놓인 간극이 솔라를 혼란스럽게 했다.
“언니, 역시 이상해.”
솔라는 시계 아이콘을 톡톡 치며 말했다.
“이 그림대로라면 분명히 몇 단계를 거치잖아. 안내 데스크, 실무자, 창고지기까지. 그런데 내 화면은 마치 이 모든 과정을 건너뛴 것처럼 반응해. 내가 하트를 누르자마자 서버 컴퓨터까지의 모든 일이 끝났다는 듯이. 어떻게 이게 가능하지? 내 노트북이 미래라도 예측하는 걸까?”
솔라의 질문에는 순수한 의문이 가득했다. 그녀는 UI가 서버로부터 최종적인 ‘성공’ 응답을 받은 후에야 화면을 바꿔주는 것이 당연하다고 생각하고 있었다.
루나는 솔라의 노트에 그려진 시계 아이콘을 잠시 바라보더니, 고개를 끄덕였다.
“솔라 네 노트북이 미래를 예측하는 건 아니야. 대신 아주 낙관적인 가정을 하고 있지.”
루나는 솔라의 노트북을 가리켰다.
“두 가지 시나리오를 상상해 보자. 첫 번째는 ‘정직하지만 느린’ 서점이야. 네가 하트 버튼을 누르면, 화면에 작은 로딩 아이콘이 빙글빙글 돌아가. 그리고 서버에서 ‘네, 즐겨찾기 관계가 성공적으로 저장되었습니다’라는 확답이 올 때까지 아무것도 변하지 않아. 1초든 2초든 기다린 후에야 비로소 하트가 붉게 바뀌는 거지.”
“생각만 해도 답답하네. 버튼을 누를 때마다 화면이 멈추는 거잖아.”
솔라는 즉시 고개를 저었다.
“맞아. 그럼 두 번째 시나리오. ‘성급하지만 빠른’ 서점이야. 네가 하트를 누르는 순간, 서점은 서버에 요청을 보내는 동시에, ‘음, 이 요청은 99% 성공할 거야!’라고 멋대로 믿어버리는 거지. 그리고 그 믿음을 바탕으로 서버의 응답을 기다리지 않고 일단 하트를 붉은색으로 바꿔버려.”
솔라의 눈이 동그래졌다.
“기다리지 않고 먼저 바꾼다고? 그래도 돼? 만약에… 서버에서 실패하면 어떡해?”
“바로 그게 핵심이야.”
루나는 손가락으로 가상의 되감기 버튼을 누르는 시늉을 했다. “만약 인터넷이 끊기거나 서버에 예상치 못한 문제가 생겨서 ‘실패’ 응답이 돌아오면, 그때 가서 슬쩍 원래대로 되돌리는 거야. 붉게 변했던 하트를 다시 회색으로 바꾸는 거지. 사용자에게는 ‘잠시 오류가 발생했습니다’ 같은 메시지를 보여주면서.”
솔라는 잠시 침묵했다. 두 번째 시나리오가 머릿속에서 생생하게 그려졌다. 하트를 누르자마자 붉게 변하는 만족감, 그리고 아주 가끔, 뭔가 잘못되었을 때 하트가 원래 색으로 돌아가는 희귀한 경험. 이것은 자신이 매일 겪고 있는 바로 그 경험이었다.
“아…! 속은 거였네!”
솔라는 탄성을 내질렀다.
“내 화면의 즉각적인 반응은 진짜가 아니었던 거야. 진짜 서버에 관계가 저장되어서가 아니라, 그냥 내 컴퓨터가 ‘잘 되겠지’ 하고 먼저 보여준 거였어. 일종의 연극이었네!”
그 순간, 모든 조각이 맞춰졌다. 사용자가 느끼는 ‘즉각적인 반응’은 빠른 서버 속도 덕분이 아니었다. 그것은 프론트엔드, 즉 사용자의 눈에 보이는 화면단에서 세심하게 설계된 ‘경험’이었다. 서버의 일이 성공할 것이라고 ‘낙관적’으로 가정하고 UI를 먼저 업데이트하는 기법.
“그래, 바로 그거야. 개발자들은 이 기법을 ‘낙관적 업데이트(Optimistic Update)’라고 불러.”
루나의 말에 솔라는 고개를 끄덕이며 노트에 ‘낙관적 업데이트’라고 적었다. 그리고 컨트롤러 상자 앞에, ‘프론트엔드 UI 즉시 변경’이라는 새로운 상자를 그리고 화살표를 그었다. 그 옆에는 작게 ‘(실패 시, 원상복구)’라고 덧붙였다. 드디어 논리와 경험의 간극이 메워졌다.
즐겨찾기 기능의 해부가 끝났다. 그것은 단순히 책의 속성을 바꾸는 것이 아니었다. 사용자와 책의 ‘관계’를 userId와 bookId로 정의하고, 이 관계를 백엔드의 체계적인 계층을 통해 관리하며, 프론트엔드에서는 낙관적 업데이트라는 기법으로 최상의 사용자 경험을 만들어내는, 잘 짜인 시스템이었다.
새로운 관점을 얻은 솔라는 문득 온라인 서점의 다른 기능들로 시선을 돌렸다. 책 리뷰 밑에 있는 ‘좋아요’ 버튼, 작가를 구독하는 ‘팔로우’ 버튼. 이전에는 그저 별생각 없이 누르던 아이콘들이 이제는 다르게 보였다.
솔라는 ‘좋아요’ 버튼을 가리키며 혼잣말처럼 중얼거렸다.
“이것도 마찬가지겠지. 내가 이 버튼을 누르면, 화면의 숫자는 일단 1이 올라갈 거야. 서버에서 ‘좋아요 관계’가 실제로 저장되기 전에 말이야. 그러다 와이파이가 끊기면, 잠시 후에 숫자가 다시 원래대로 돌아가겠지. 사용자 경험을 위한 똑똑한 속임수네.”
솔라는 더 이상 ‘즉각적인 반응’에 속지 않았다. 대신 그 속임수 뒤에 숨겨진, 사용자를 위한 깊은 고민과 영리한 기술적 설계를 읽어낼 수 있게 되었다. 그것은 세상을 더 깊이 이해하게 된 것과 같은 명확하고 즐거운 감각이었다.