Mini Project 5 02

json-server에서 Spring Boot API 계약으로: 백엔드 전환의 진짜 의미

4차에서도 fetch로 책을 조회하고 저장했다면 왜 API 정의서와 backend endpoint를 다시 만들어야 하는지 헷갈린다.

근거 · 교안 frontend 분석/API 계약 파트

json-server에서 Spring Boot API 계약으로: 백엔드 전환의 진짜 의미 대표 이미지

1장: fetch, 같은 fetch 다른 느낌: json-server 통신의 암묵적 허점

솔라의 손가락이 노트북 트랙패드 위에서 잠시 멈췄다. 화면 한쪽에는 이전에 만들었던 ‘책 관리 프로그램’의 소스 코드가, 다른 한쪽에는 새로 시작할 프로젝트의 안내 문서가 열려 있었다. 솔라의 시선은 bookService.js라는 파일 이름에 머물렀다. 익숙하고 자신 있는 코드였다.

http://localhost:3000/books라는 주소를 향해 fetch 함수로 책 목록을 가져오고(GET), 새 책을 추가하고(POST), 내용을 수정하고(PATCH), 지우는(DELETE) 코드. 직관적이고 명쾌했다. 그런데 새로 받은 안내 문서의 한 문장이 자꾸만 마음에 걸렸다.

“5차의 첫 큰 변화는 json-serverdb.json이 맡던 REST 데이터 흐름을 Spring Boot의 /books API 계약으로 옮기는 일이다.”

‘큰 변화’라는 단어가 유독 도드라져 보였다. 솔라는 고개를 갸웃거렸다.

“언니, 이거 좀 이상해. 지난번 프로젝트에서 fetch로 데이터 가져오고 저장하는 거 다 해봤잖아. 그런데 이제 와서 서버를 바꾼다고 이게 왜 ‘큰 변화’라는 거지? 그냥 fetch에 넘기는 주소만 바꾸면 되는 거 아니야?”

부엌에서 커피를 내리던 루나가 솔라 쪽으로 고개를 돌렸다. 루나는 잠시 생각에 잠긴 듯하다가, 조용히 솔라의 옆으로 다가와 화면을 들여다보았다.

“그 bookService.js 코드, 지금 다시 실행하면 어떻게 될까?”

“똑같이 되겠지. localhost:3000/books에서 책 목록을 읽어 올 거고…”

솔라는 자신 있게 대답하다가 말끝을 흐렸다. 뭔가 중요한 걸 놓친 기분이었다. 루나는 말없이 솔라의 화면 아래쪽에 떠 있는 터미널 창 하나를 가리켰다. 솔라는 지난 프로젝트를 마친 후 터미널을 깨끗하게 정리해 두었다. 당연히 json-server를 실행하던 프로세스도 꺼져 있었다.

“아.”

짧은 탄식이 터져 나왔다.

루나는 아무 말 없이 솔라가 직접 깨닫기를 기다려 주었다. 솔라는 멋쩍게 웃으며 중얼거렸다.

“서버를 안 켰네. 이걸 켜야 주소로 요청을 보내도 누가 받아줄 테니까.”

“한번 직접 확인해 볼까? 정말 그런지.”

루나의 말에 솔라는 고개를 끄덕였다. 굳이 앱 전체를 실행할 필요도 없었다. 브라우저의 개발자 콘솔을 열고, 예전에 작성했던 fetch 코드를 한 줄 복사해 붙여넣었다.

fetch('http://localhost:3000/books')

엔터 키를 누르자마자, 콘솔 창에 붉은색 에러 메시지가 순식간에 나타났다. net::ERR_CONNECTION_REFUSED. 솔라가 예상했던 그대로였다.

“연결이 거부됐네. 당연하지. 저 주소에 아무도 없으니까.”

솔라는 혼잣말처럼 말하며 턱을 괬다. 늘 당연하게 npm start 같은 명령어로 서버를 켜고 시작했기에, 서버가 ‘켜져 있다’는 사실의 무게를 제대로 느낀 적이 없었다. fetch 함수가 마치 혼자서 모든 일을 다 처리하는 마법 주문처럼 느껴졌었다. 하지만 방금 마주한 붉은 에러 메시지는 분명하게 말하고 있었다. fetch는 편지를 보내는 우체부일 뿐, 편지를 받을 주소에 누군가 살고 있어야만 의미가 있다고.

“지난번 프로젝트가 잘 작동했던 건, 내가 fetch 코드를 잘 짜서만이 아니었구나.”

솔라가 입을 열었다.

json-serverhttp://localhost:3000이라는 주소에서 내가 보낸 요청을 계속 기다리고, /books라는 경로로 오는 요청에는 db.json 파일을 읽어서 답해주기로 약속되어 있었기 때문이었어.”

솔라는 다시 bookService.js 파일을 바라보았다. 이제 코드가 다르게 보였다. GET, POST, PATCH, DELETE 같은 명령어들은 클라이언트 측의 일방적인 선언이 아니었다. 화면 너머에서 그 요청들을 받아 처리해 줄 서버와의 보이지 않는 약속, 일종의 ‘협약’이었던 것이다. 클라이언트가 정해진 주소로 요청을 보내면, 서버는 그에 맞는 응답을 주기로 한 상호 약속.

“맞아.”

루나가 처음으로 입을 열었다.

fetch는 통신의 절반일 뿐이야. 나머지 절반은 언제나 서버의 몫이지. 우리가 json-server를 쓸 때는 그 서버의 역할이 아주 단순하고 암묵적으로 정해져 있었던 거고.”

솔라는 고개를 끄덕였다. 이제야 ‘REST 데이터 흐름을 옮긴다’는 말이 단순한 주소 변경 이상의 의미를 갖는다는 것을 어렴풋이 짐작할 수 있었다. 이것은 단순히 이사 가는 수준이 아니라, 집주인이 바뀌고, 집의 구조와 규칙 자체가 완전히 새로워지는 것에 가까웠다.

그렇다면 새로운 질문이 떠올랐다. json-server라는 친절하고 말 잘 듣는 집주인이 있었는데, 왜 굳이 Spring Boot라는 까다로워 보이는 새 집주인을 만나야 하는 걸까? json-server가 해주던 편리한 약속들로는 부족했던 걸까? 무엇이 달랐기에 ‘가짜 서버’를 ‘진짜 시스템’으로 바꿔야만 했을까.

2장: json-server의 진짜 얼굴: 개발 편의 vs. 실제 백엔드의 책임

솔라의 손가락이 이전 프로젝트 폴더를 탐색했다. 어째서 ‘큰 변화’인지, 왜 친절한 집주인(json-server)을 떠나야 하는지에 대한 의문은 그녀를 다시 과거의 코드로 이끌었다. 그녀는 bookService.js 파일이 아니라, 그 파일과 늘 함께했던 db.json 파일을 열었다. 화면에 익숙한 JSON 구조가 나타났다. 중괄호와 대괄호, 그리고 그 안에 정갈하게 정리된 책들의 데이터.

{
  "books": [
    {
      "id": 1,
      "title": "모던 자바스크립트 Deep Dive",
      "author": "이웅모",
      "cover": "..."
    },
    {
      "id": 2,
      "title": "Clean Code(클린 코드)",
      "author": "로버트 C. 마틴",
      "cover": "..."
    }
  ]
}

이걸 보니 마음이 더 편안해졌다. 모든 데이터가 한눈에 보였다. 솔라는 생각했다. ‘결국 서버가 하는 일은 이 파일에 데이터를 읽고 쓰는 거잖아. 간단하고 좋네. 이게 왜 부족하다는 거지?’ 솔라의 관점에서 json-server는 마치 투명한 유리 상자 같았다. 요청을 보내면 상자 안의 내용이 바뀌는 게 훤히 보였다. 이 단순함이 바로 장점이라고 생각했다.

“그 파일, 직접 한번 고쳐볼래?”

어느새 다가온 루나가 솔라의 화면을 보며 말했다.

“직접?”

“응. 코드 에디터로. 책 제목 하나를 다른 걸로 바꿔봐.”

솔라는 흥미로운 제안이라는 듯 고개를 끄덕였다. 그녀는 첫 번째 책의 제목, “모던 자바스크립트 Deep Dive”를 “모던 JS 핵심”이라고 짧게 수정했다. 파일을 저장하고, 지난번처럼 터미널에서 json-server --watch db.json --port 3000 명령어로 서버를 실행했다. 그리고 브라우저를 열어 http://localhost:3000/books/1 주소로 접속했다. 화면에는 그녀가 방금 수정한 내용이 그대로 나타났다.

“잘 되네. 내가 파일을 바꾸니까 서버가 바로 그걸 읽어서 보여주잖아. 편리하네.”

솔라가 만족스러운 표정으로 말했다. json-server가 데이터를 저장하는 방식은 지극히 단순했다. 그냥 텍스트 파일을 읽고, fetch 요청이 오면 그 파일의 내용을 수정할 뿐이었다. 클라이언트가 보내주는 정보를 아무런 의심 없이 그대로 파일에 기록하는, 충실한 비서 같았다.

“그럼 이제 반대로 해보자.”

루나가 말했다.

“앱 화면에서 책을 하나 추가하면 db.json 파일은 어떻게 될까?”

솔라는 지난 프로젝트의 웹 화면을 띄워 새 책 정보를 입력하고 ‘저장’ 버튼을 눌렀다. 화면에 새 책이 추가되었다. 곧바로 db.json 파일을 다시 확인하자, 방금 입력한 책 데이터가 파일 끝에 새로운 객체로 추가되어 있었다.

“오, 이것도 잘 되네. POST 요청을 보내니까 json-server가 알아서 파일에 내용을 추가해 줬어. 정말 말 잘 듣는다.”

솔라의 목소리에는 json-server의 편리함에 대한 감탄이 묻어났다. 클라이언트가 시키는 대로 모든 걸 처리해 주니 프론트엔드 개발에만 집중하기에 완벽한 환경이었다.

루나는 잠시 침묵하다가, 결정적인 질문을 던졌다.

“만약에, db.json 파일에 책 데이터가 아니라 ‘사용자’ 데이터를 추가하고 싶으면 어떻게 할래? 그리고 어떤 책은 특정 사용자만 수정할 수 있게 하고 싶다면? json-server가 그걸 막아줄 수 있을까?”

“어…”

솔라는 말문이 막혔다. json-serverdb.json 파일의 구조에 대해 아무것도 모른다. 그저 books라는 키가 있고, 그 안에 배열이 있다는 것만 알 뿐이다. ‘사용자’라는 개념도, ‘소유권’이라는 규칙도 이해할 리 없었다.

루나는 질문을 이어갔다.

“새로운 책을 등록할 때, 실수로 가격을 숫자가 아닌 문자열("삼만원")로 입력하면 어떻게 될까? json-server는 아마 그것도 그대로 db.json에 저장해 버릴 거야. 그렇지?”

솔라는 고개를 끄덕일 수밖에 없었다. json-server의 ‘편리함’은 사실 ‘무책임함’의 다른 이름이었다. 클라이언트가 주는 데이터가 올바른지, 규칙에 맞는지 전혀 검사하지 않았다. 그저 전달받은 내용을 묵묵히 파일에 받아 적을 뿐이었다. 데이터의 무결성을 지키고, 복잡한 비즈니스 규칙을 처리하는 책임은 전적으로 클라이언트, 즉 프론트엔드 개발자에게 있었다.

이제야 솔라는 깨달았다. json-server는 진짜 ‘서버 시스템’이 아니었다. 프론트엔드 개발을 위해 임시로 만든 ‘가짜 서버’, 즉 모형(mock) 서버였던 것이다. 이 모형은 실제 시스템이 가져야 할 중요한 책임들, 예컨대 데이터 검증, 권한 확인, 비즈니스 로직 처리 같은 것들을 모두 생략하고 있었다.

솔라는 다시 안내 문서의 문장을 떠올렸다. ‘json-serverdb.json이 맡던 편리한 개발용 REST 데이터 흐름.’ 이제 ‘편리한 개발용’이라는 수식어가 왜 붙었는지 명확하게 이해됐다. 진짜 시스템의 복잡한 책임을 잠시 잊고 화면 개발에만 집중할 수 있도록 도와주는, 딱 그만큼의 역할이었던 것이다.

“그렇구나… json-server는 우리를 그냥 믿어줬던 거네. 우리가 알아서 잘하겠지, 하고. 하지만 실제 시스템은 그러면 안 되는 거구나. 서버는 클라이언트를 믿으면 안 되고, 스스로 데이터를 지킬 책임이 있는 거였어.”

솔라의 혼잣말에 루나가 조용히 미소 지었다.

문득 새로운 궁금증이 생겼다. json-server 같은 암묵적이고 무조건적인 신뢰 관계가 아니라면, 진짜 서버와 클라이언트는 어떻게 소통해야 할까? 서버가 어떤 책임을 지고, 클라이언트는 무엇을 기대할 수 있는지, 서로 다른 일을 하는 개발자들이 어떻게 그 규칙들을 미리 맞출 수 있을까? 보이지 않는 벽 너머의 백엔드와 명시적인 약속이라도 해야 하는 걸까?

3장: 명시적인 약속, API 계약: 프론트와 백엔드의 공동 청사진

솔라의 방에는 정적이 흘렀다. 이전 프로젝트 폴더는 닫았지만, 머릿속은 오히려 더 복잡해졌다. json-server가 데이터의 유효성이나 권한을 전혀 책임지지 않는 ‘가짜 서버’라는 사실을 깨닫고 나니, 진짜 시스템은 어떻게 만들어야 하는지에 대한 의문이 꼬리를 물었다. 보이지 않는 벽 너머에서 각자의 일을 하는 프론트엔드 개발자와 백엔드 개발자는 대체 어떻게 서로를 믿고 일하는 걸까?

솔라는 빈 노트를 펼치고 펜을 들었다. ‘서버에게 바라는 점’이라고 제목을 붙이고는 피식 웃었다. 그녀는 프론트엔드 개발자 입장에서 서버가 해주었으면 하는 일들을 적기 시작했다.

  1. GET /books 요청하면 -> 책 목록 전부 주기
  2. POST /books 요청하면 -> 새 책 정보 저장해주기
  3. 책 제목이 비어있으면 저장 안 하기 (이건 서버가 해줘야지!)

막상 세 번째 규칙을 적고 나니, 지난번 루나와의 대화가 떠올랐다. json-server는 이런 규칙을 지켜줄 수 없었다. 그렇다면 진짜 서버는 이 규칙을 어떻게 알게 될까? 내가 마음속으로만 생각하면 모를 텐데.

“그거, 백엔드 개발자한테 보낼 요구사항 목록이야?”

어느새 다가온 루나가 솔라의 노트를 흥미롭게 들여다보며 물었다.

“아니, 그냥… json-server랑은 어떻게 달라야 할지 생각해 보다가. 진짜 서버랑 일하려면 서로 미리 말을 맞춰야 할 것 같아서.”

“좋은 생각이네. 그럼 우리, 지금 그걸 한번 해볼까? 내가 백엔드 개발자 할게. 솔라 너는 프론트엔드 개발자고. 방금 네가 적은 그 목록을 가지고, 우리 프로젝트의 규칙을 함께 정해보는 거야.”

루나는 의자를 당겨 솔라의 맞은편에 앉았다. 갑작스러운 역할극 제안이었지만, 솔라는 이게 자신의 궁금증을 해결할 좋은 기회라는 걸 직감했다.

“좋아. 그럼 저, 프론트엔드 개발자 솔라인데요.” 솔라가 어색하게 연기를 시작했다. “책 목록을 화면에 보여줘야 해서요, 책 데이터 전체가 필요해요. GET /books로 요청 보낼게요.”

“네, 백엔드 개발자 루나입니다.” 루나가 진지하게 받아쳤다. “요청 접수했습니다. GET /books로 요청하시면, 제가 책 목록을 배열 형태로 드릴게요. 그런데, 각 책마다 어떤 정보가 필요하신가요? idtitle만 드리면 될까요?”

“아니요! 목록에 저자랑 표지 이미지도 작게 보여줘야 해요. id, title, author, cover 전부 필요해요.”

“알겠습니다. 그럼 id는 숫자, 나머지는 전부 문자열 형태로 드리는 걸로 약속하죠. 응답 형식은 이렇게요.”

루나는 솔라의 노트 한편에 작은 JSON 객체 모양을 끄적였다. { "id": 1, "title": "...", "author": "...", "cover": "..." }.

“좋아요. 다음은 책 등록 기능이에요. 사용자가 입력한 책 제목과 저자 정보를 POST /books로 보낼게요. 잘 저장해 주세요.”

“잠깐만요.” 루나가 솔라의 말을 막았다. “사용자가 실수로 제목을 비워둔 채로 저장 버튼을 누르면 어떡하죠? 그래도 그냥 저장하나요?”

“아뇨! 그건 안 되죠. 아까 제가 적었잖아요. 제목이 비어있으면 저장하면 안 된다고.” 솔라가 자신의 노트를 가리켰다.

“그럼 제가 요청을 거절하고 에러를 보내드려야겠네요. ‘제목은 필수입니다’ 같은 메시지와 함께요. 저자 이름이 너무 길어서 데이터베이스에 저장할 수 없을 때도 마찬가지고요. 이런 세세한 규칙들을 미리 정해야, 나중에 서로 ‘왜 이렇게 만들었냐’고 싸울 일이 없겠죠.”

대화가 오갈수록 솔라의 노트는 빼곡해졌다. 단순히 ‘무엇을 한다’는 목록이 아니었다. 어떤 주소(URL)와 어떤 방식(method)으로 요청하고, 어떤 데이터(payload)를 실어 보낼 것이며, 성공했을 때와 실패했을 때 각각 어떤 모양(response)의 응답을 받을지에 대한 구체적인 약속들이 쌓여갔다.

솔라는 잠시 펜을 내려놓고 노트를 찬찬히 살펴보았다. 이것은 한쪽이 다른 쪽에게 일방적으로 요구하는 문서가 아니었다. 프론트엔드와 백엔드라는, 서로 다른 세상에 사는 두 개발자가 만나 함께 그릴 건물의 ‘설계도’였다. 각자 자기 층을 만들기 전에, 기둥은 어디에 세울지, 배관은 어디로 연결할지, 창문 크기는 어떻게 맞출지 함께 정하는 과정이었다.

그제야 솔라는 새로 받은 프로젝트 안내 문서의 한 문장을 제대로 이해할 수 있었다.

‘API 계약은 URL, method, payload, response를 포함하며 frontend와 backend 간의 명시적인 약속이다.’

이것은 귀찮은 서류 작업이 아니었다. 혼란을 막고 협업을 가능하게 하는 최소한의 안전장치이자, 공동의 목표를 향한 청사진이었던 것이다.

“이제 알겠다… json-server를 쓸 때는 이런 ‘계약’이 필요 없었어. 그냥 내가 집주인이자 세입자였으니까. 하지만 진짜 협업을 하려면, 서로 뭘 해줄 수 있고 뭘 해줄 수 없는지, 어떻게 소통할지 명확하게 약속해야 하는구나.”

솔라는 자신이 ‘API 계약’이라 부르기 시작한 그 설계도를 보며 고개를 끄덕였다. 하지만 문득 새로운 의문이 들었다. 이 계약서에 적힌 GET /books, POST /books 같은 조항들은 json-server를 쓸 때와 주소나 메서드 이름이 똑같았다. 그렇다면 이 계약을 지키는 ‘진짜 시스템’인 Spring Boot는 json-server와 구체적으로 무엇을 다르게 구현하게 되는 걸까? 진짜 시스템의 ‘책임’이란 이 계약서 어디에 어떻게 녹아드는 걸까?

4장: Spring Boot로 옮겨간 API 계약: 백엔드의 새로운 책임

솔라의 책상 위에는 그녀가 직접 손으로 적은 ‘API 계약’ 초안이 놓여 있었다. 삐뚤빼뚤한 글씨로 적힌 GET /books, POST /books 같은 항목들이 어제의 고민을 고스란히 담고 있었다. 하지만 솔라의 시선은 그 노트가 아닌, 루나가 막 노트북 화면에 띄워준 프로젝트의 공식 README.md 파일에 고정되었다. ‘API 명세’라는 제목 아래, 훨씬 더 상세하고 깔끔하게 정리된 목록이 펼쳐져 있었다.

솔라는 두 문서를 번갈아 보았다. 기본적인 구조는 비슷했다. 도서 전체 조회는 GET /books, 특정 책 조회는 GET /books/{id}. 책을 등록, 수정, 삭제하는 POST, PATCH, DELETE 요청도 익숙했다. 하지만 README 문서에는 솔라의 노트에는 없던 항목들이 눈에 띄었다.

  • GET /books/count: 전체 책 개수 조회
  • GET /books?title=...&author=...&detail=...: 제목, 저자, 내용으로 검색
  • GET /books?page=...: 페이지별로 나누어 조회
  • PATCH /books/{id}/cover: AI로 생성된 표지 이미지만 따로 저장

솔라는 이 목록을 보고도 여전히 개운치 않은 표정이었다. ‘책임의 분담’과 ‘명시적 계약’이라는 개념은 이해했지만, 그 결과물이 json-server와 비슷해 보이는 URL 목록이라는 점이 혼란스러웠다.

“언니, 결국 GET /books는 똑같잖아. 그럼 Spring Boot 백엔드도 우리가 POST로 책 데이터를 보내면, 그걸 그냥 어딘가에 잘 저장해 주는 거 아니야? json-serverdb.json에 했던 것처럼 말이야. 경로 이름만 조금 더 다양해진 것 말고, 백엔드가 구현하는 방식에 무슨 근본적인 차이가 있는 거지?”

솔라의 질문에는 ‘진짜 시스템의 책임’이라는 말이 아직은 추상적으로 들린다는 뉘앙스가 섞여 있었다.

루나는 대답 대신, 화면의 README 문서에서 한 줄을 손가락으로 가리켰다.

PATCH /books/{id}/cover

“이 요청을 json-server에게 보낸다고 상상해 보자. 이게 가능할까?”

솔라는 잠시 생각에 잠겼다. json-server에게 PATCH 요청을 보내면, 요청 본문에 담긴 내용으로 기존 데이터를 덮어썼다. 만약 책의 다른 정보는 그대로 두고 cover 필드만 바꾸고 싶다면, cover 데이터만 담아서 보내면 됐다.

“음… 가능할 것 같은데? cover 속성만 바꿔서 보내면 json-server도 알아서 업데이트해주니까.”

“맞아. json-server는 그게 가능해.” 루나는 고개를 끄덕였다. “그럼 이건 어때?”

루나는 이번엔 검색 관련 명세를 가리켰다. GET /books?detail=...

“책의 ‘상세 설명(detail)’ 안에 특정 키워드가 포함된 책만 찾아달라는 요청이야. json-server가 이걸 처리할 수 있을까?”

솔라는 즉시 대답하지 못했다. json-servertitle=... 처럼 최상위 속성을 기준으로 필터링하는 기능은 지원했다. 하지만 객체 안의 긴 문자열 내용까지 검색하는 기능이 있었던가? 아마 없었을 것이다. json-server는 데이터의 ‘구조’만 알 뿐, 그 안의 ‘의미’나 ‘내용’까지 해석하지는 못했다.

그 순간, 솔라는 중요한 차이를 깨달았다.

“아… json-serverdb.json 파일에 대한 범용적인 읽기/쓰기 기능만 제공하는구나. 우리가 detail 필드 안에 있는 내용을 검색해달라고 해도, 그걸 알아듣고 처리할 별도의 로직이 없어. 그냥 있는 그대로의 데이터 덩어리를 줄 뿐이지.”

솔라는 다시 README의 API 명세 목록을 바라보았다. 이제는 그저 주소 목록으로 보이지 않았다. 백엔드 개발자가 구현해야 할 ‘기능 명세서’로 보이기 시작했다.

  • GET /books/count: 데이터베이스에 저장된 모든 책의 수를 세어서, 숫자 하나만 응답으로 보내주는 기능을 만들어야 한다.
  • GET /books?detail=...: 요청으로 들어온 검색어로 책의 상세 설명 필드를 검색하는 로직을 구현해야 한다.
  • PATCH /books/{id}/cover: 책의 다른 정보는 절대 건드리지 않고, 오직 cover 이미지 경로만 안전하게 업데이트하는 독립된 기능을 만들어야 한다.

이것들은 db.json 같은 단순한 파일 하나로는 절대 처리할 수 없는, 명백한 ‘백엔드의 책임’이었다. API 계약서의 각 조항은 프론트엔드에게는 ‘사용할 수 있는 기능의 목록’이었고, 백엔드에게는 ‘구현해야 할 비즈니스 로직의 지도’였던 것이다. json-server가 제공하던 암묵적이고 범용적인 규칙 대신, Spring Boot 백엔드는 이 계약서에 명시된 책임을 하나하나 코드로 구현해야 했다.

“이제야 ‘큰 변화’라는 말의 진짜 의미를 알겠어. 주소가 바뀐 게 아니라, 서버의 역할과 책임이 완전히 새로 정의된 거구나. Spring Boot는 json-server처럼 순진하게 데이터를 받아 적는 비서가 아니라, 계약서에 명시된 규칙대로만 움직이는 시스템인 거네.”

솔라는 API 계약서가 프론트엔드와 백엔드 사이의 다리일 뿐만 아니라, 백엔드 내부 구현의 복잡성을 담고 있는 청사진임을 이해했다.

하지만 그 순간, 새로운 현실적인 문제가 떠올랐다.

“좋아, 백엔드 개발자분들이 이 계약서대로 열심히 기능을 만들고 있겠지. 그런데 나는 프론트엔드 개발자로서, 그 기능들이 완성되었는지, 계약대로 잘 작동하는지 어떻게 확인할 수 있지? 내 화면 개발이 다 끝날 때까지 기다렸다가 연결해봐야만 알 수 있는 건가?”

5장: Postman으로 확인하는 API 계약: 프론트엔드 없는 검증

솔라는 자신의 책상 앞에 앉아, 모니터에 떠 있는 칸반 보드를 멍하니 바라보고 있었다. ‘백로그(Backlog)’ 목록에 있던 ‘책 목록 UI 구현’ 카드를 ‘진행 중(In Progress)’으로 막 옮긴 참이었다. 하지만 키보드 위로 가져간 손은 움직일 줄을 몰랐다. 어제 루나와 함께 API 명세를 뜯어보며 Spring Boot 백엔드의 ‘책임’에 대해 이해했지만, 그 이해가 현실의 작업 순서 앞에서 발목을 잡고 있었다.

GET /books가 계약서대로 잘 작동하는지 보려면, 일단 목록을 보여줄 화면부터 만들어야 하잖아. 목록을 만들고, fetch 코드를 짜서 연결하고… 만약 그때 가서 API가 제대로 동작하지 않으면? 내가 만든 화면 코드를 다시 고쳐야 하는 건가? 뭔가 순서가 뒤바뀐 것 같은데…’

프론트엔드 개발은 마치 레고 블록을 조립하는 것과 같아서, 아래층이 튼튼해야 위층을 쌓을 수 있다. 지금 솔라에게는 API라는 아래층이 보이지 않는 안개 속에 있는 것처럼 느껴졌다. 안개가 걷히길 기다렸다가 집을 짓기 시작해야 하는 걸까?

“API가 완성됐는지 확인하려고, 아직 있지도 않은 기능을 전부 만들어보는 건 너무 비효율적인 것 같은데.”

혼잣말처럼 중얼거리는 솔라의 목소리를 들었는지, 루나가 조용히 다가왔다. 솔라의 고민이 담긴 칸반 보드와 멈춰있는 손을 잠시 바라보던 루나는, 노트북을 가져와 솔라 옆에 앉았다.

“프론트엔드 화면을 만드는 대신, 브라우저가 하는 일의 딱 절반만 흉내 내 볼까?”

“브라우저가 하는 일의 절반?”

“응. fetch 함수가 했던 일 말이야. 정해진 주소로, 정해진 방법의 요청을 ‘보내는’ 역할. 응답을 받아서 예쁘게 화면에 ‘그려주는’ 일은 잠시 잊고.”

루나는 익숙한 듯 새로운 프로그램을 실행했다. 솔라가 처음 보는 화면이었다. 복잡해 보였지만, 자세히 보니 익숙한 단어들이 눈에 들어왔다. GET이라는 글자가 보이는 드롭다운 메뉴, URL을 입력하는 긴 주소창, 그리고 커다란 ‘Send’ 버튼. 마치 브라우저의 숨겨진 부품들을 밖으로 꺼내 늘어놓은 듯한 모습이었다.

“이건 Postman이라는 도구야.”

루나는 프로젝트 README 파일에 적혀 있던 Spring Boot 서버의 주소를 복사해 주소창에 붙여 넣었다. 그리고 그 뒤에 /books를 덧붙였다. 메서드는 이미 GET으로 설정되어 있었다.

“자, 이제 우리가 할 일은 ‘Send’ 버튼을 누르는 것뿐이야.”

솔라는 마른침을 삼키며 루나의 손가락을 따라갔다. ‘Send’ 버튼이 클릭되자, 잠시 후 화면 아래쪽 공간에 하얀 텍스트가 쏟아져 내렸다.

[
  {
    "id": 1,
    "title": "모던 자바스크립트 Deep Dive",
    "author": "이웅모",
    "cover": "..."
  },
  {
    "id": 2,
    "title": "Clean Code(클린 코드)",
    "author": "로버트 C. 마틴",
    "cover": "..."
  }
]

순간 솔라의 눈이 커졌다. 화면도, fetch 코드도, CSS 스타일도 없었다. 하지만 그곳에는 API 계약서에서 약속했던 형태 그대로의, 순수한 데이터가 있었다. 백엔드 서버가 프론트엔드에게 건네주기 위해 준비해 둔 날것 그대로의 응답이었다.

“아…!”

솔라는 짧은 탄성을 내뱉었다. 모든 조각이 맞춰지는 기분이었다.

“이건… 내 웹 애플리케이션인 척 연기하는 거잖아! 내가 fetch 코드를 짜지 않아도, 이 도구가 대신 서버에게 말을 걸고 대답을 받아다 주는 거구나.”

그녀는 이제야 깨달았다. 프론트엔드 개발자는 백엔드 API가 완성될 때까지 하염없이 기다리거나, 불확실한 기반 위에 코드를 쌓아 올릴 필요가 없었다. API 계약이라는 공동의 청사진이 있다면, Postman 같은 도구를 통해 언제든 백엔드 측의 공사가 계약서대로 잘 진행되고 있는지 직접 확인해볼 수 있었다. 마치 건물 공사 현장에서, 건축가가 설계 도면을 들고 와 벽의 두께나 창문의 크기가 맞는지 줄자로 직접 재보는 것과 같았다.

‘Postman은 화면 없이 API 계약을 검증하는 도구로 활용될 수 있다.’

어디선가 본 듯한 이 문장이, 이제는 솔라 자신의 언어가 되어 머릿속에 선명하게 새겨졌다. 이것은 단순히 ‘테스트’를 위한 도구가 아니었다. 프론트엔드와 백엔드가 서로의 작업 진행 상황에 발목 잡히지 않고, 각자의 속도로 나아갈 수 있게 해주는 ‘독립적인 검증 도구’이자 ‘병렬 개발을 위한 열쇠’였다.

솔라는 다시 자신의 칸반 보드로 시선을 돌렸다. 망설임은 사라져 있었다. 그녀는 ‘책 목록 UI 구현’ 카드를 다시 ‘백로그’ 목록으로 옮겼다. 그리고 그 위에 새로운 카드들을 만들기 시작했다.

  • [API 검증] GET /books 응답 확인
  • [API 검증] POST /books 요청 및 결과 확인
  • [API 검증] POST /books 실패 시(제목 누락) 에러 응답 확인

이제 그녀의 작업 계획은 훨씬 더 견고하고 명확해졌다. UI를 만들기 전에, API라는 기반이 약속대로 단단하게 놓였는지 직접 망치로 두드려 확인할 참이었다.

솔라는 미소를 지었다. json-server를 떠나 Spring Boot로 온 것이 왜 ‘큰 변화’였는지 이제는 누군가에게 자신 있게 설명할 수 있었다. 그것은 단순히 서버 주소를 바꾸는 이사가 아니었다. 혼자 살던 원룸에서 나와, 각자의 방과 책임이 명확히 나뉜 하우스메이트와 함께 살게 된 것과 같았다. 그리고 이제 그녀는, 굳이 상대방의 방문을 열어보지 않고도 우리가 함께 살 집이 잘 지어지고 있는지 확인할 똑똑한 방법을 알게 된 것이다.