Backend 07

MVC 역할 분리: 웹 요청 처리를 위한 설계의 지혜

요청이 오면 그냥 한 코드에서 처리하면 되는데 왜 Model, View, Controller로 나누는지 모르겠다.

근거 · 교안 p52-p55

MVC 역할 분리: 웹 요청 처리를 위한 설계의 지혜 대표 이미지

1장: 요청 처리, 왜 한 덩어리면 안 되나요?

솔라의 손가락이 노트북 화면 위를 초조하게 맴돌았다. 화면 한편에는 방금 막 만들기 시작한 작은 게시판 프로젝트의 코드가, 다른 한편에는 웹 프레임워크 입문서의 한 페이지가 떠 있었다. 솔라는 미간을 찌푸린 채 입문서의 한 문장을 노려보았다.

"MVC 패턴은 애플리케이션을 모델(Model), 뷰(View), 컨트롤러(Controller)라는 세 가지 역할로 구분하여, 비즈니스 로직과 UI 로직 간의 종속성을 줄입니다."

“종속성을 줄인다…” 솔라는 문장을 소리 내 곱씹었다. 하지만 머릿속은 의문으로 가득 찼다. “아니, 그냥 게시글 목록 보여달라는 요청이 오면, 데이터베이스에서 글 가져와서, HTML 태그 붙여서 보내주면 끝 아닌가? 이걸 왜 굳이 파일 세 개로 나눠서 복잡하게 만들어야 하지? 일이 두 배가 되는 것 같은데.”

솔라의 시선이 자신이 방금 작성한 코드로 향했다. getPostList() 라는 함수 하나가 전부였다. 그 안에는 데이터베이스에 접속하는 코드, SQL 쿼리를 실행하는 코드, 그리고 결과로 나온 게시글 목록을 <li> 태그로 감싸 HTML 문자열로 만드는 코드가 순서대로 담겨 있었다. 직관적이고 간단했다. 요청이 들어오면 위에서부터 아래로 쭉 실행되면 끝. 솔라는 이 간결함이 마음에 들었다. ‘분리’라는 건 오히려 일을 방해하는 군더더기처럼 느껴졌다.

답답한 마음에 솔라는 노트북을 들고 거실에 있는 언니 루나에게 다가갔다. 루나는 조용히 책을 읽고 있었다.

“언니, 이것 좀 봐. 웹 개발에서는 왜 이렇게 일을 복잡하게 만들려는 거야?”

솔라는 루나 옆에 앉아 노트북 화면을 보여주며 투덜거렸다.

“게시글 목록 보여주는 기능 하나 만드는데, 어떤 책에서는 Model, View, Controller로 역할을 나누래. 요청 받고, 데이터 처리하고, 화면 그리는 걸 다 다른 파일에서 하라는 거잖아. 내 코드는 그냥 함수 하나로 끝인데. 이게 훨씬 간단하고 효율적이지 않아?”

루나는 솔라의 화면을 조용히 들여다보았다. 모든 로직이 한데 섞여 있는 getPostList() 함수를 잠시 살피던 루나는 말없이 자신의 노트북을 열었다. 그리고는 솔라가 만든 것과 비슷하지만 조금 더 기능이 추가된, 가상의 게시판 앱 코드를 화면에 띄웠다.

“이 코드 한번 볼까? 솔라 네가 만든 거랑 비슷해. 게시글 목록을 받아서 화면에 뿌려주는 함수야. 여기엔 추가로, 관리자가 쓴 글은 제목을 굵게 표시하고, 게시글 작성일은 ‘n일 전’ 형식으로 보여주는 기능이 들어있어.”

솔라는 루나의 화면 속 코드를 훑어보았다. 데이터베이스에서 글 목록을 가져오는 부분, 사용자 정보를 확인해서 관리자인지 체크하는 부분, 날짜를 계산하는 부분, 그리고 이 모든 정보를 조합해 HTML을 한 줄 한 줄 만들어내는 부분이 뒤섞여 있었다. 확실히 자신의 코드보다는 복잡했지만, 여전히 하나의 함수 안에 있었다.

루나가 화면의 특정 부분을 가리키며 물었다.

“여기서, 디자이너가 ‘n일 전’이라는 표시가 마음에 안 든대. ‘YYYY-MM-DD’ 형식으로 바꿔달라고 하면 어떻게 해야 할까?”

“음… 그건 간단하지.” 솔라는 자신 있게 말했다. “여기, 날짜 계산하는 부분을 찾아서 포맷만 바꿔주면 되잖아.”

솔라의 손가락이 코드를 따라 움직였다. 날짜를 계산하고 문자열을 만드는 로직은 HTML 태그를 생성하는 루프 한가운데에 있었다.

“그래. 그럼 이번엔 기획자가 새로운 요구사항을 줬다고 해보자. 글 목록을 최신순이 아니라, 조회수가 높은 순으로 정렬하고 싶대.”

“그것도… 할 수 있지.” 솔라의 목소리는 조금 작아졌다. “데이터베이스에 쿼리 날리는 부분을 수정해야겠네. ORDER BY createdAt DESCORDER BY viewCount DESC로 바꾸면…”

솔라의 눈이 코드의 윗부분으로 향했다. SQL 쿼리가 담긴 문자열을 찾는 순간, 솔라는 잠시 멈칫했다. 조회수 순으로 정렬하는 건 데이터에 관한 문제인데, 그 코드를 수정하기 위해 조금 전에 날짜 표시 형식을 바꾸려 들여다봤던 복잡한 HTML 생성 로직을 다시 지나쳐야 했다. 서로 아무 상관없는 두 가지 수정사항이 같은 코드 덩어리 안에서 아슬아슬하게 붙어 있었다.

루나는 아무 말 없이 기다렸다. 그 침묵 속에서 솔라는 깨달았다. 만약 조회수 정렬 로직을 수정하다가 작은 실수를 하면, 최악의 경우 데이터베이스 쿼리 전체가 망가져 페이지 자체가 열리지 않을 수도 있었다. 단순히 화면에 보이는 날짜 형식을 바꾸는 작업과는 비교도 안 되는 위험이었다.

“어라…”

솔라의 입에서 나직한 소리가 새어 나왔다.

“조회수 정렬, 그러니까 ‘무엇을 보여줄지’를 바꾸는 거랑… 날짜 형식, 그러니까 ‘어떻게 보여줄지’를 바꾸는 게… 같은 곳에 있네. 정렬 순서를 바꾸려다가 실수로 날짜 표시까지 망가뜨릴 수도 있겠다. 반대도 마찬가지고.”

솔라는 다시 자신이 처음 불평했던 책의 문장을 떠올렸다. 비즈니스 로직과 UI 로직 간의 종속성을 줄입니다. 이제야 ‘종속성’이라는 단어가 피부에 와 닿았다. 그것은 추상적인 개념이 아니었다. 조회수 정렬 로직(비즈니스 로직)과 날짜 표시 형식(UI 로직)이 한데 얽혀, 하나를 고치려 할 때 다른 하나를 깨뜨릴 위험을 감수해야 하는 이 껄끄러운 상황 그 자체였다.

“한곳에 다 모아두는 게… 마냥 편한 건 아니었구나.”

솔라는 중얼거렸다. 분리가 왜 필요한지에 대한 완전한 답을 얻진 못했지만, 적어도 모든 것을 한 덩어리로 두는 것이 어떤 문제를 낳는지, 그 문제의 이름이 ‘종속성’이라는 것은 똑똑히 알게 되었다.

하지만 새로운 의문이 고개를 들었다.

“알겠어. 이렇게 얽히는 게 문제라는 건 알겠는데… 그래서 이걸 어떻게 나눈다는 거야? 나눈다고 이 문제가 정말 해결이 돼? 각자 다른 파일에 있으면 서로 어떻게 소통하는데? 그건 또 다른 종류의 복잡함 아닐까?“

2장: 얽힌 실타래: 종속성의 비용과 Controller의 등장

솔라의 머릿속은 온통 ‘종속성’이라는 단어로 가득 차 있었다. 어제의 깨달음 이후, 그녀는 루나가 보여줬던 가상의 코드를 자신의 노트북에 그대로 옮겨 적어두었다. 데이터베이스 쿼리, 관리자 권한 확인, 날짜 계산, HTML 생성이 뒤엉킨 getPostList() 함수가 화면을 채우고 있었다. 분명 문제는 인식했지만, 그 문제가 얼마나 심각한지는 아직 막연했다. 그냥 ‘조심해서 코딩하면 되는’ 수준의 문제 아닐까?

“그래서 이 얽힌 게 구체적으로 뭐가 어떻게 문제라는 건데?”

혼잣말처럼 중얼거리는 솔라의 목소리를 들었는지, 거실 소파에 있던 루나가 다가왔다.

“아직도 그 코드를 보고 있네.”

“응. 얽혀있어서 위험하다는 건 알겠는데, 이게 정말로 개발을 못 할 정도의 문제인가 싶어서. 서로 조심하면 되잖아.”

루나는 말없이 솔라의 옆에 앉아 화면을 보았다. 그러더니 키보드를 가리키며 제안했다.

“좋아. 그럼 우리 둘이서 이 코드를 동시에 수정한다고 상상해보자. 역할 놀이 같은 거야. 솔라, 너는 프론트엔드 개발자. UI를 담당해. 나는 데이터를 다루는 백엔드 개발자. 어때?”

“역할 놀이?” 솔라는 흥미가 동했다. “재밌겠네. 뭘 바꾸면 되는데?”

“프론트엔드 개발자인 솔라의 임무는, 아까 말했던 대로 날짜 표시를 ‘n일 전’에서 ‘YYYY-MM-DD’ 형식으로 바꾸는 거야. 아주 중요한 긴급 수정 요청이라고 치자.”

“오케이. 문제없어.”

“백엔드 개발자인 내 임무는, 조회수 순 정렬 기능을 추가하는 거야. 이것도 아주 중요한 업데이트지.”

루나는 장난스럽게 웃으며 덧붙였다. “규칙은 간단해. 우리는 서로 다른 자리에서 일하고 있어서, 지금 당장 대화는 할 수 없어. 각자 자기 일을 시작하는 거야. 시작!”

솔라는 키보드 위로 손을 가져갔다. 프론트엔드 개발자 솔라, 출동. 그녀는 getPostList() 함수 안에서 날짜를 처리하는 부분을 찾기 시작했다. for 루프 안, 복잡한 HTML 문자열을 만드는 코드 사이에 날짜 계산 로직이 숨어 있었다. new Date()와 복잡한 계산식. 솔라는 그 부분을 지우고 새로운 날짜 포맷팅 코드를 타이핑하기 시작했다.

그때, 루나가 “아.” 하고 낮은 소리를 냈다.

“왜?”

“나, 백엔드 개발자 루나는, 지금 정렬 순서를 바꾸려고 ORDER BY 구문을 수정해야 해. 함수 맨 위에 있는 SQL 쿼리를 바꿔야 하지.”

솔라의 손이 멈칫했다. 자신이 지금 수정하고 있는 곳은 함수의 중간 부분. 루나가 수정해야 할 곳은 함수의 시작 부분. 같은 파일, 같은 함수였다.

“그래서?” 솔라가 물었다.

“솔라 네가 지금 파일을 수정하고 있으니, 나는 이 파일의 최신 버전을 알 수 없어. 내가 지금 내 컴퓨터에 있는 버전의 코드를 수정해서 저장하면, 네가 바꾼 날짜 포맷팅 코드가 날아가 버릴 수도 있어. 반대로 네가 먼저 저장하면, 내가 작업하던 걸 다시 네 파일에 합쳐야 하고. 만약 우리가 동시에 저장하려고 하면? 충돌이 나겠지.”

루나는 말을 이었다.

“그래서 보통 이런 상황에선 이렇게 말하게 돼. ‘솔라 씨, postHandler.js 파일 수정 다 끝날 때까지 저 그거 안 건드릴게요. 다 되면 알려주세요.’ 그럼 나는? 솔라가 날짜 포맷 하나 바꾸는 동안, 내 중요한 기능 개발은 그냥 손 놓고 기다려야 하는 거야.”

”…”

솔라는 아무 말도 할 수 없었다. 방금 전까지 ‘조심하면 되지’라고 생각했던 문제가 전혀 다른 차원의 문제로 다가왔다. 이건 실수의 문제가 아니었다. ‘비용’의 문제였다. 한 사람이 코드를 잡고 있는 동안 다른 사람은 기다려야 하는 시간 비용. 두 사람의 변경 사항을 합치기 위해 코드를 비교하고 검토해야 하는 조정 비용. 최악의 경우, 잘못 합쳐서 코드가 망가질 때 발생하는 위험 비용.

“이건… 완전 비효율적이네.” 솔라가 항복하듯 말했다. “고작 날짜 표시 하나 바꾸는데, 다른 중요한 기능 개발이 멈춰야 한다니. 이게 바로 종속성의 폐해구나.”

단순히 코드가 얽혀있어 위험하다는 차원을 넘어, 개발 과정 전체를 느리고 성가시게 만드는 구체적인 ‘비용’이 눈앞에 그려졌다.

루나는 고개를 끄덕이며, 빈 종이를 가져와 간단한 그림을 그리기 시작했다.

“바로 그거야. 그래서 사람들은 첫 번째 분리를 시작했어. 이 모든 요청을 직접 처리하는 함수를 두는 대신, 문지기를 두는 거지.”

루나는 종이 위에 네모 하나를 그리고 ‘Controller’라고 썼다. 그리고 여러 개의 화살표가 그 네모를 향하도록 그렸다. 각 화살표 위에는 /posts(글 목록 보기), /posts/new(글쓰기), /login(로그인) 같은 주소가 적혀 있었다.

“이 ‘컨트롤러’라는 문지기는 교통경찰 같아. 요청이 어디로 가야 할지만 알려주고 자기는 실제 일을 하지 않아. /posts 요청이 오면 ‘아, 이건 글 목록을 다루는 전문가에게!’ 하고 넘겨주고, /login 요청이 오면 ‘이건 사용자 인증 전문가에게!’ 하고 넘겨주는 거야.”

솔라는 그림을 뚫어지게 쳐다봤다. “그럼… UI를 바꾸는 사람이랑 데이터 정렬을 바꾸는 사람은 애초에 다른 전문가를 찾아가게 되는 거네? 교통경찰은 그냥 길만 알려주니까, 두 사람이 만날 일도 없고.”

“맞아. 프론트엔드 개발자와 백엔드 개발자는 이제 거대한 getPostList() 함수 하나를두고 싸울 필요가 없어진 거지. 컨트롤러가 ‘글 목록 보여줘’라는 요청을 받으면, 데이터를 처리하는 담당자에게 일을 시키고, 그 결과를 받아서 화면을 그리는 담당자에게 전달만 해주면 되니까. 각 담당자는 자기 일에만 집중할 수 있게 돼.”

솔라는 책에서 봤던 문장, 비즈니스 로직과 UI 로직의 종속성을 줄인다는 말이 다시 떠올랐다. 컨트롤러는 그 줄이기를 시작하는 첫 번째 관문이었던 것이다. 모든 것을 한데 섞어 처리하는 대신, 요청의 종류에 따라 책임질 담당자를 지정해주는 것. 그것만으로도 개발자들이 서로의 발을 밟는 일은 극적으로 줄어들 터였다.

“알겠다. 컨트롤러가 일종의 분배 센터 역할을 하는 거구나.”

마음이 한결 가벼워진 솔라가 말했다. 하지만 그림을 계속 들여다보던 그녀의 눈에 새로운 질문이 떠올랐다.

“좋아, 교통정리까지는 알겠어. 그런데 컨트롤러가 넘겨준다는 그 ‘글 목록 전문가’랑 ‘사용자 인증 전문가’는 대체 누구야? 걔네들은 또 무슨 일을 어떻게 나누어서 하는데?“

3장: Controller 이후: Model과 View, 각자의 책임

솔라는 어제 루나가 그려줬던 그림 위에 자신의 펜을 가져다 댔다. 중앙에는 ‘Controller’라는 네모 상자가 교통경찰처럼 버티고 있었다. 솔라는 그 상자에서 뻗어 나가는 두 개의 화살표 끝에, 어젯밤 내내 머릿속을 맴돌던 ‘전문가들’을 그려 넣었다. 글 목록 전문가, 사용자 인증 전문가. 하지만 그림을 그려 넣고 보니 어딘가 이상했다. 두 개의 전문가 상자는 아무런 특징 없이 똑같이 생긴 네모일 뿐이었다.

“결국 컨트롤러가 시키는 일을 하는 하청업체 같은 거 아닌가?”

솔라는 중얼거렸다. 교통정리가 끝난 후의 세상. 컨트롤러가 ‘이 일은 네가 해!’라고 지시하면, 그 지시를 받은 전문가들은 그냥 묵묵히 자기 일을 처리할 뿐이다. 그게 글 목록이든 사용자 인증이든, 본질적으로는 컨트롤러의 명령을 수행하는 똑같은 종류의 코드 덩어리처럼 느껴졌다. 그렇다면 굳이 이들을 또 나눌 이유가 있을까? 솔라는 글 목록 전문가 상자 옆에 괄호를 치고 ‘Model?’, ‘View?’ 라고 썼다가 지웠다. 이 둘은 같은 팀일까, 아니면 또 다른 종류의 분리가 필요한 걸까?

“그림이 완성되지 않는 표정이네.”

어느새 다가온 루나가 솔라의 그림을 들여다보며 말했다.

“컨트롤러가 교통정리를 해준다는 건 알겠어. 그런데 그 다음에 일을 넘겨받는 애들은 그냥 컨트롤러의 부하직원 같은 거 아닐까? 결국 시키는 일 하는 건 똑같은데, 얘네들 사이에도 역할 구분이 필요해? ‘글 목록 전문가’랑 ‘사용자 인증 전문가’는 그냥 이름만 다른 같은 부류의 코드 아니야?”

루나는 솔라의 질문에 고개를 끄덕이는 대신, 주방 쪽을 가리켰다.

“솔라, 저녁으로 스테이크를 먹는다고 상상해보자. 네가 손님이야. 레스토랑에 들어가서 ‘미디엄 레어 스테이크 하나 주세요’라고 주문했어. 이 주문은 누가 받을까?”

“음… 지배인이나 웨이터?”

“맞아. 그 사람이 우리 웹 세계의 컨트롤러, 즉 교통경찰이야. 손님의 요청을 받고, 이걸 어디로 전달해야 할지 결정하지.” 루나는 말을 이었다. “자, 그럼 지배인은 주문서를 들고 어디로 갈까?”

“주방으로 가겠지. 셰프한테.”

“정답. 셰프는 주문서대로 스테이크를 구워야 해. 이게 셰프의 책임이지. 최고의 고기를 고르고, 정확한 온도로, 알맞은 시간 동안 굽는 것. 즉, ‘맛있는 미디엄 레어 스테이크’라는 데이터의 본질을 만들어내는 거야.”

루나는 잠시 말을 멈추고, 솔라가 그림을 따라오는지 확인했다.

“자, 이제 셰프가 완벽한 스테이크를 다 구웠어. 접시에 올려놓기만 하면 돼. 그런데 바로 그때, 레스토랑 사장님이 주방으로 뛰어 들어와서 소리치는 거야. ‘오늘부터 우리 레스토랑 컨셉이 바뀌었어! 모든 음식은 하얀 원형 접시가 아니라, 검은색 사각 돌 플레이트에 나가야 해! 가니쉬도 로즈마리에서 타임으로 바꿔!’”

솔라는 자기도 모르게 “헉” 소리를 냈다.

루나가 물었다. “이 변경사항을 처리해야 할 사람은 누구일까? 이 새로운 ‘보여주는 방식’에 대한 책임은 누가 져야 할까? 방금 막 최고의 스테이크를 구워낸 셰프가 해야 할 일일까?”

솔라는 잠시 고민했다. “셰프가 할 수도 있겠지. 자기가 만든 요리니까… 하지만…” 솔라의 말끝이 흐려졌다.

“하지만 셰프는 고기 굽는 전문가이지, 플레이팅 전문가는 아니지. 셰프의 주된 관심사는 ‘스테이크가 맛있게 익었는가?‘야. ‘어떤 접시에 어떻게 담겨야 예쁜가?‘는 전혀 다른 종류의 문제지. 만약 셰프가 플레이팅까지 신경 쓰다가 고기를 태우면 어떡해?”

“아…”

“그래서 대부분의 고급 레스토랑에는 ‘플레이팅’만 전문으로 하는 사람이 있어. 셰프는 요리라는 본질에만 집중해서 최상의 결과물, 즉 ‘데이터’를 만들어내. 그리고 플레이팅 담당자는 그 데이터를 받아서 어떻게 ‘보여줄 것인가’만 고민하는 거야. 그 사람은 스테이크를 어떻게 굽는지는 몰라도 돼. 그냥 잘 구워진 스테이크를 받아서, 정해진 규칙대로 예쁘게 담아내기만 하면 되는 거지.”

순간 솔라의 머릿속에 번개가 쳤다. 셰프와 플레이팅 담당자. 둘 다 ‘스테이크’라는 데이터를 다루지만, 그들의 책임은 근본적으로 달랐다. 하나는 데이터의 내용과 상태를 책임지고(What), 다른 하나는 데이터의 표현과 형식을 책임진다(How). 이 둘은 결코 ‘같은 부류’의 전문가가 아니었다.

“알겠다!” 솔라는 외쳤다. “셰프가 바로 **모델(Model)**이네! 데이터 자체와, 데이터를 처리하는 규칙, 즉 비즈니스 로직을 책임지는 역할! ‘어떻게 스테이크를 미디엄 레어로 구울 것인가’하는 규칙 말이야!”

솔라의 펜이 빠르게 움직였다. 그녀는 글 목록 전문가라고 뭉뚱그려 썼던 상자를 지우고 두 개의 새로운 상자를 그렸다.

“그리고 플레이팅 담당자가 **뷰(View)**야! 모델이 만든 데이터를 받아서 사용자에게 보여주는 역할만 하는 거지. 데이터의 내용은 절대 건드리지 않고, 오직 ‘어떻게 보여줄 것인가’에만 집중하는 거야. 날짜 형식을 바꾸거나, 관리자 글씨를 굵게 만드는 것처럼!”

솔라는 자신이 처음 가졌던 의문, ‘Model과 View는 결국 Controller가 시키는 대로 하는 같은 부류의 코드 아닌가?‘라는 생각이 얼마나 순진했는지 깨달았다. 컨트롤러는 그냥 주문을 주방에 전달하는 지배인일 뿐, 주방 안에서는 ‘무엇을 만들 것인가’를 책임지는 셰프(Model)와 ‘어떻게 보여줄 것인가’를 책임지는 플레이팅 담당자(View)라는, 명확히 다른 두 전문가가 각자의 역할에 따라 협업하고 있었던 것이다. 이들의 정체성은 달랐다. 셰프의 정체성을 바꾸는 것은 레시피 자체를 바꾸는 것이지만, 플레이팅 담당자의 행동을 바꾸는 것은 그저 장식을 바꾸는 것과 같았다.

책에서 읽었던 문장이 다시 떠올랐다. MVC는 애플리케이션을 Model, View, Controller 세 가지 역할로 구분하여 개발하는 방법론이다. 이제 이 문장은 더 이상 복잡하기만 한 규칙이 아니었다. 각자의 전문 분야와 책임이 다른 전문가들이 서로의 영역을 침범하지 않고 효율적으로 협업하기 위한 지혜로운 설계로 보였다.

솔라는 자신이 새로 그린 다이어그램을 만족스럽게 바라보았다. 교통경찰(Controller), 셰프(Model), 플레이팅 담당자(View). 이제 각자의 역할이 명확히 보였다.

하지만 그림을 완성하고 나니 새로운 질문이 떠올랐다.

“좋아, 이제 각자 무슨 일을 하는지는 알겠어. 컨트롤러는 주문 받고, 모델은 요리하고, 뷰는 접시에 담고… 그런데 손님이 가게에 들어와서 음식을 받아들고 나가는 전체 과정은 어떻게 흘러가는 거지? 컨트롤러가 모델한테 바로 말을 거나? 아니면 뷰한테 먼저 가나? 이 세 명이 함께 일하는 전체적인 순서가 궁금해졌어.”

4장: 전체 그림: MVC가 웹 요청을 처리하는 방식

솔라는 어제 완성한 자신의 다이어그램을 책상 위에 펼쳐두고 있었다. 중앙에는 ‘컨트롤러(교통경찰)‘가, 양옆에는 ‘모델(셰프)‘과 ‘뷰(플레이팅 담당자)‘가 각자의 자리를 지키고 있었다. 각자의 책임은 이제 명확했다. 하지만 이들이 함께 일하는 방식, 즉 레스토랑 전체의 워크플로우는 여전히 흐릿했다.

솔라는 펜을 들어 이들 사이에 화살표를 그려 넣기 시작했다. 손님(사용자)의 요청이 들어오면, 먼저 교통경찰(컨트롤러)에게 간다. 거기까지는 좋았다. 그다음은? 지배인은 주방으로 간다고 했다. 솔라는 컨트롤러에서 모델로 향하는 화살표를 그렸다. 셰프는 요리를 완성한다. 그다음은 플레이팅. 솔라는 모델에서 뷰로 향하는 화살표를 자연스럽게 이었다. 플레이팅이 끝나면 음식이 손님에게 나간다. 뷰에서 사용자로 나가는 화살표가 마지막을 장식했다. 다 그리고 보니, 하나의 깔끔한 흐름이었다. 사용자 → 컨트롤러 → 모델 → 뷰 → 사용자. 마치 컨베이어 벨트처럼. 하지만 솔라는 어딘가 찜찜한 기분을 지울 수 없었다.

“결국은 이렇게 한 방향으로 쭉 흘러가는 건데… 그럼 모델과 뷰를 굳이 다른 사람이라고 생각할 필요가 있나? 어차피 셰프가 요리해서 바로 옆에 있는 접시에 담는 거랑 똑같잖아.”

그때, 솔라의 다이어그램을 어깨너머로 보고 있던 루나가 조용히 입을 열었다.

“그 흐름, 정말 항상 저렇게만 움직일까? ‘새 글 작성’처럼 무언가 상태를 바꾸는 요청일 때도?”

루나는 ‘새 글 작성’이라는 단어에 힘을 주어 말했다. 지금까지는 단순히 게시글 목록을 ‘조회’하는 경우만 생각했다. 하지만 웹 요청은 조회가 전부가 아니다.

“새 글 작성…?” 솔라는 잠시 생각에 잠겼다. “사용자가 제목이랑 내용을 입력하고 ‘저장’ 버튼을 누르는 순간을 말하는 거지?”

“응. 바로 그 순간부터 우리의 레스토랑이 어떻게 움직이는지, 한 단계씩 따라가 보자.”

루나는 솔라의 다이어그램 옆에 새로운 빈 종이를 놓았다. 역할 놀이의 두 번째 라운드가 시작되는 순간이었다.

1단계: 주문 접수 (사용자 → 컨트롤러)

“솔라, 네가 브라우저에서 ‘새 글 저장’ 버튼을 눌렀어. 그러면 브라우저는 ‘이 주소(/posts)로, 이 데이터(title, content)를 가지고, ‘생성(POST)‘해줘’라는 요청을 서버로 보내. 이 요청 팻말을 가장 먼저 받아 드는 건 누구?”

“교통경찰, 컨트롤러.” 솔라가 망설임 없이 답했다.

2단계: 주방에 일 시키기 (컨트롤러 → 모델)

“맞아. 컨트롤러는 요청을 받았어. ‘새 글을 생성해달라’는 요청이야. 컨트롤러는 이 일을 직접 처리할 능력이 없어. 그럼 누구에게 가야 할까?”

“셰프. 모델에게 가야지.”

“정확해. 컨트롤러는 사용자가 보낸 데이터(제목과 내용)를 들고 주방 문을 두드려. 그리고 셰프(모델)에게 말하지. ‘이 재료들로 요리(데이터 저장) 좀 해주세요.’”

여기까지는 솔라가 그린 다이어그램과 일치했다.

3단계: 요리 (모델의 역할)

“셰프(모델)는 재료를 받아서 요리를 시작해. 재료가 신선한지 확인하고(유효성 검사), 레시피에 따라 데이터베이스라는 거대한 저장고에 새로운 글을 기록하지. 그리고 요리가 성공적으로 끝나면, ‘임무 완수!’라고 외쳐. 중요한 건, 셰프는 자기가 만든 요리를 어떤 접시에 담을지 전혀 신경 쓰지 않는다는 거야. 오직 ‘글을 안전하게 저장한다’는 자신의 책임만 완수할 뿐이야.”

4단계: 보고, 그리고 새로운 결정 (모델 → 컨트롤러 → ?)

루나는 여기서 잠시 말을 멈추고, 솔라의 눈을 보며 물었다.

“자, 셰프가 ‘요리 끝났습니다!’라고 보고했어. 이 보고는 누구에게 갈까?”

“음… 지배인? 컨트롤러?”

“빙고. 셰프는 자신에게 일을 시킨 컨트롤러에게 결과를 보고해. 절대 플레이팅 담당자에게 직접 가지 않아. ‘요리 끝났으니 이제 당신이 담아가시오’라고 말하지 않는다는 거지.”

루나는 펜으로 솔라의 다이어그램에서 모델과 뷰를 잇는 화살표를 가리켰다. 솔라의 얼굴이 살짝 붉어졌다.

“컨트롤러는 ‘성공’이라는 보고를 받았어. 그럼 이제 컨트롤러는 새로운 결정을 해야 해. 손님에게 뭘 보여줄까? 방금 막 작성된 따끈따끈한 글 상세 페이지를 보여줄까? 아니면 ‘글이 성공적으로 등록되었습니다’라는 메시지와 함께 전체 글 목록 페이지로 돌려보낼까?”

순간 솔라의 머릿속이 환해졌다. 컨베이어 벨트가 끊어지는 지점이었다. 컨트롤러는 단순히 요청을 전달만 하는 우편배달부가 아니었다. 모델의 작업 결과를 바탕으로, 다음에 무엇을 할지 결정하는 지휘자였다.

5단계: 플레이팅 지시와 서빙 (컨트롤러 → 뷰 → 사용자)

“컨트롤러가 ‘방금 작성된 글 상세 페이지를 보여주자’고 결정했다고 해보자. 그럼 컨트롤러는 이제 누구를 찾아갈까?”

“플레이팅 담당자, 뷰!”

“맞아. 컨트롤러는 모델로부터 받은 결과물(방금 저장된 글 데이터)을 들고 뷰에게 가서 말해. ‘이 데이터를 가지고, ‘글 상세 페이지’라는 디자인에 맞게 예쁘게 꾸며서 손님께 내어주세요.’ 그럼 뷰는 데이터의 내용은 신경 쓰지 않고, 오직 주어진 디자인 틀에 데이터를 채워 넣어 최종 결과물(HTML)을 만들지. 그리고 이 완성된 요리가 마침내 손님(사용자 브라우저)에게 전달되는 거야.”

솔라는 펜을 들어 자신의 다이어그램에서 모델과 뷰를 잇던 화살표 위에 커다랗게 X자를 그었다. 그리고 새로운 화살표를 그렸다. 모델에서 다시 컨트롤러로 돌아오는 화살표, 그리고 컨트롤러에서 뷰로 나아가는 화살표.

사용자 → 컨트롤러 → 모델 → 컨트롤러 → 뷰 → 사용자

“아…”

탄식이 흘러나왔다.

“모델과 뷰는 서로 얼굴도 모르는 사이였어. 서로 대화하지 않아. 모든 의사소통은… 컨트롤러를 통해서만 이루어지는 거구나. 컨트롤러가 중간에서 모든 걸 지휘하고 있었어.”

복잡하게 나뉘어 있던 세 역할이 하나의 팀으로 움직이는 전체 그림이 비로소 눈앞에 그려졌다. 각자 독립적으로 일하지만, 컨트롤러라는 지휘자를 중심으로 유기적으로 협업하는 오케스트라처럼 보였다. 흐름은 하나가 아니었다. 요청을 처리하는 흐름과, 그 결과를 보여주는 흐름은 컨트롤러에 의해 명확히 분리되고 연결되고 있었다.

솔라는 깔끔하게 정리된 새 다이어그램을 만족스럽게 바라보았다. 하지만 곧 새로운 의문이 고개를 들었다.

“흐름은 이제 알겠어. 그런데… 컨트롤러가 정말 바쁘네. 여기서 일 받고 저기다 전달하고, 결과 받아서 또 다른 데 전달하고… 뭔가 일이 더 많아진 느낌이야. 이렇게까지 복잡한 과정을 거쳐서 우리가 최종적으로 얻는 이득이 대체 뭐야?“

5장: 나누는 것이 남는 것: MVC 역할 분리의 진정한 가치

솔라는 자신의 책상 앞에 앉아 완성된 다이어그램을 뚫어지게 바라보았다. 화살표들은 이제 제자리를 찾았다. 사용자 → 컨트롤러 → 모델 → 컨트롤러 → 뷰 → 사용자. 복잡하게 얽혀 있던 실타래가 마침내 풀리고, 각 역할이 어떻게 협업하는지에 대한 명확한 청사진이 눈앞에 펼쳐졌다. 하지만 솔라의 표정은 어쩐지 개운치 않았다.

그녀의 손가락이 다이어그램 위를 천천히 오갔다. 컨트롤러에서 모델로, 다시 모델에서 컨트롤러로 돌아오는 길. 그리고 다시 컨트롤러에서 뷰로 향하는 길. “여기서 일 받고, 저기다 전달하고, 결과 받아서 또 다른 데 전달하고…” 이 왕복 운동이 꼭 필요한 과정이라는 건 이제 알겠다. 하지만 동시에, 이 과정이 처음의 단순한 일직선 흐름보다 훨씬 더 많은 단계를 거치는 번거로운 길이라는 느낌도 지울 수 없었다. 마치 지름길을 알면서도 일부러 먼 길을 돌아가는 기분이었다.

“결국은 더 많은 일을 하는 거 아니야?”

솔라의 혼잣말에, 곁에서 조용히 책을 읽던 루나가 시선을 돌렸다.

“흐름은 알겠어. 그런데 솔직히 말하면… 이게 꼭 좋은 건지 아직도 잘 모르겠어. 컨트롤러는 거의 우편배달부처럼 여기서 저기로 뛰어다니잖아. 우리가 이렇게 복잡한 과정을 감수하면서까지 얻는 게 대체 뭐야? 그냥 처음부터 끝까지 한 명이 책임지고 처리하는 것보다 이게 정말 더 낫다고 할 수 있어?”

솔라의 목소리에는 초조함이 묻어났다. 역할 분리의 필요성과 작동 방식까지는 이해했지만, 그 ‘가치’에 대한 마지막 의문이 그녀를 붙잡고 있었다.

루나는 솔라의 다이어그램이나 코드를 가리키는 대신, 창밖을 보며 다른 종류의 상상을 제안했다.

“좋아. 우리 레스토랑이 아주 성공적으로 개업해서, 1년이 지났다고 상상해보자.”

“1년 뒤?”

“응. 1년 동안 메뉴도 그대로, 인테리어도 그대로였어. 그런데 어느 날, 아주 실력 있는 새 디자이너가 우리 팀에 합류한 거야. 그 디자이너는 레스토랑의 낡은 메뉴판 디자인이 마음에 안 든대. 모든 메뉴의 글씨체를 바꾸고, 음식 사진 배경을 전부 통일하고, 종이 재질도 바꾸고 싶어 해. 즉, ‘보여주는 방식’을 완전히 뜯어고치고 싶어 하는 거지.”

루나는 솔라를 보며 물었다. “자, 이 상황에서 디자이너는 누구와 함께 일해야 할까? 이 작업을 위해 주방의 셰프를 찾아가 ‘고기 굽는 법을 잠깐 멈추고 메뉴판 디자인에 대한 제 의견을 들어주세요’라고 말해야 할까?”

솔라는 잠시 생각에 잠겼다. 셰프(Model)는 요리에만 집중하고, 플레이팅 담당자(View)는 보여주는 방식에만 집중한다는 사실을 떠올렸다. 메뉴판 디자인은 명백히 ‘보여주는 방식’의 문제였다.

“아니… 셰프는 찾아갈 필요가 없지. 플레이팅 담당자, 그러니까 뷰를 다루는 개발자하고만 이야기하면 되겠네. 셰프는 평소처럼 계속 스테이크를 구우면 되고.”

“바로 그거야.” 루나가 말했다. “셰프는 메뉴판이 바뀌는 걸 알 필요도 없어. 디자이너가 밤새 메뉴판을 금으로 도배하든, LED 전광판으로 바꾸든, 셰프의 일, 즉 ‘데이터를 만들고 처리하는’ 비즈니스 로직에는 아무런 영향이 없어. 이게 우리가 얻는 첫 번째 이점이야. 유지보수성.”

나누어져 있었기 때문에, 한쪽의 대대적인 변화가 다른 쪽에 아무런 영향을 주지 않았다. 솔라가 처음 걱정했던 것처럼, 날짜 형식을 바꾸려다 데이터 정렬 로직을 망가뜨릴 위험 자체가 사라진 것이다.

“그럼 다른 상황을 상상해볼까?” 루나가 말을 이었다. “어느 날, 단골손님에게서 긴급한 연락이 왔어. ‘제 이름에 특수문자(&)가 들어가는데, 그것 때문에 마일리지 포인트가 두 배로 쌓이는 것 같아요!’ 이건 디자인의 문제가 아니지? 명백히 우리 레스토랑의 포인트 적립 규칙, 즉 비즈니스 로직에 버그가 있다는 뜻이야.”

“셰프의 실수네.”

“맞아. 이 버그를 잡아야 해. 그런데 어떻게 잡는 게 가장 효율적일까? ‘특수문자&가 들어간 손님’을 실제로 레스토랑에 모셔 와서, 스테이크를 주문하게 하고, 계산대에서 포스기를 두드려보며 영수증에 포인트가 제대로 찍히는지 확인해야 할까?”

솔라는 고개를 저었다. “그건 너무 번거롭잖아.”

“그렇지. 분리된 세계에서는 그럴 필요가 없어. 우리는 그냥 셰프(모델)만 조용히 주방으로 부르는 거야. 그리고 ‘이름에 &가 들어간 가짜 손님 데이터’를 건네주면서 ‘이 손님의 포인트를 계산해주세요’라고 요청하는 거지. 레스토랑 전체를 가동할 필요 없이, 오직 셰프의 계산법(비즈니스 로직)이 맞는지 틀리는지만 수백 번이라도 빠르고 정확하게 테스트해 볼 수 있어. 이게 두 번째 이점, 테스트 용이성이야.”

솔라의 눈이 빛났다. 복잡해 보였던 컨트롤러의 왕복 운동이 이제 다른 의미로 보이기 시작했다. 그것은 단순한 번거로움이 아니었다. 각 전문가가 자신의 영역을 벗어나지 않고, 다른 전문가의 일에 휘둘리지 않도록 지켜주는 안전한 경계선이었다. 그 경계선 덕분에 디자이너는 마음껏 디자인에만 집중할 수 있고, 셰프는 요리법 테스트에만 집중할 수 있었다.

“나누는 것이… 결국엔 시간을 버는 거였구나.”

처음의 단순함이 주는 편안함은 잠깐이었다. 프로젝트가 커지고, 시간이 흐르고, 요구사항이 바뀔수록, 한데 얽힌 코드는 서로의 발목을 잡는 거대한 족쇄가 될 터였다. 역할 분리는 그 족쇄를 처음부터 채우지 않겠다는 현명한 약속이었다.

솔라는 다시 책에서 읽었던 첫 문장을 떠올렸다. MVC는 애플리케이션을 Model, View, Controller 세 가지 역할로 구분하여 개발하는 방법론이다. 이제 그 문장은 더 이상 딱딱한 정의가 아니었다. 그것은 미래의 혼란을 막고, 협업의 효율을 높이며, 코드의 수명을 늘리는, 시간이 증명한 설계의 지혜로 느껴졌다.

잠시 생각에 잠겨 있던 솔라는 조용히 자신의 노트북을 열었다. 화면에는 그녀가 처음 만들었던, 데이터베이스 쿼리와 HTML 생성이 뒤섞인 getPostList() 함수가 그대로 남아있었다. 처음엔 그 단순함이 자랑스러웠다. 하지만 이제는 곧 터질지 모르는 시한폭탄처럼 보였다.

솔라는 망설이지 않았다. 그녀는 프로젝트 폴더를 열고, 마우스 오른쪽 버튼을 클릭했다.

새 폴더 만들기.

controllers

다시 한번,

models

그리고 마지막으로,

views

세 개의 빈 폴더가 나란히 생성되었다. 아직 안은 텅 비어 있었지만, 솔라는 이미 자신의 코드가 가야 할 길을 본 것 같았다. 그녀는 getPostList() 함수의 데이터베이스 연결 부분을 드래그하기 시작했다. 이 코드가 이사 갈 첫 번째 집은 models 폴더가 될 터였다. 나누는 것은 복잡한 일이 아니라, 각자에게 제자리를 찾아주는 일이었다. 그리고 그것이 결국 모두를 위한 가장 빠른 길이었다.