#AI
#Claude
#Rest API
#Backend
Korean |
English
Written by Paul
February 21, 2026
시리즈: AI 페어 프로그래머와 함께하는 RESTful 리팩토링 여정 Part 1: 문제 발견 — 코드베이스 분석 작성일: 2026-02-21
개발을 하다 보면 이런 순간이 온다.
POST /user/check-handle 엔드포인트를 작성할 때, "이게 맞나?" 하는 찜찜함이 스쳤다. 그냥 넘겼다. 다음엔 POST /event/track-view를 추가하면서 또 그 느낌. 다시 넘겼다. 기능은 동작하고, 마감은 다가오니까.그렇게 2년이 흘렀다.
billets-server에는 25개의 라우트 그룹, 80개가 넘는 엔드포인트가 쌓였다. 그리고 그 찜찜함들도 함께 쌓였다. 어느 날 새 기능을 추가하려다 멈췄다. 이걸 어디에 붙여야 하지?
/subscribe? /event? 아니면 새 prefix를 만들어야 하나?리팩토링을 결심했다. 그리고 이번엔 혼자 하지 않기로 했다.
billets-server란?
billets는 공연 정보를 제공하는 앱이다. 아티스트, 공연장, 이벤트 정보를 모아 사용자에게 보여주고, 구독·알림·RSVP 같은 기능을 제공한다.
서버는 Fastify 5 + Prisma + Zod로 구성된다. JWT 인증, OAuth(Google/Apple), 익명 사용자 지원, AWS S3 이미지 업로드, SES 이메일 발송까지 갖춘 꽤 전형적인 모바일 앱 백엔드다.
문제는 API 설계다. 초기엔 "프론트엔드가 필요한 데이터를 그대로 내려주자"는 실용적인 접근이었다. 그게 2년 동안 굳어졌다.
AI를 페어 프로그래머로
AI에게 코드베이스 전체를 주고 이렇게 물었다.
"현재 API 설계에서 RESTful 원칙을 위반하거나 개선할 수 있는 부분을 전부 찾아줘. 엔드포인트 목록과 실제 라우트 파일을 같이 볼게."
AI는 라우트 파일을 하나씩 읽어나가기 시작했다.
auth.route.ts, user.route.ts, event.route.ts... 25개 파일을 모두 훑고 나서 문제를 카테고리로 분류해 돌아왔다.결과는 예상보다 체계적이었다. 막연히 "좀 정리해야 한다"고 느꼈던 것들이 8개의 구체적인 문제 패턴으로 정리됐다.
발견된 문제들
1. 동사형 URL — RPC처럼 생긴 REST API
REST의 핵심 원칙 중 하나는 URL은 자원(명사)을 가리키고, 행위는 HTTP 메서드로 표현한다는 것이다. 그런데 현재 API엔 이런 엔드포인트들이 있었다.
AI가 짚은 부분이 흥미로웠다.
PATCH /user/activate와 DELETE /user/deactivate가 둘 다 소프트 삭제/복구 로직인데 HTTP 메서드가 다르게 매핑돼 있다는 점이다. 내부적으로는 같은 status 필드를 바꾸는 작업인데, API 표면상엔 전혀 다른 행위처럼 보인다.개선 방향:
2. 프론트엔드 화면 전용 엔드포인트
이 부분은 AI가 가장 날카롭게 지적한 지점이었다.
이건 사실 내가 가장 당연하게 여겼던 부분이다. 메인 화면에 "오늘 밤 공연", "이번 주말 공연", "추천 공연" 섹션이 따로 있으니까 API도 그에 맞게 만든 것이다. 빠르고 편하다.
AI의 반론은 이랬다:
"이렇게 되면 같은 이벤트 데이터를 조회하는 API가 화면마다 하나씩 생겨납니다. API는 클라이언트의 화면 구조에 종속되어선 안 됩니다.GET /events에 쿼리 파라미터를 표준화하면 클라이언트가 자유롭게 조합할 수 있고, API는 재사용 가능해집니다."
논리적으로 맞다. 그리고 실제로
GET /event/new와 GET /event/recent가 서로 다른 "최신"을 의미한다는 것도 이때 처음 명확히 인식했다. 하나는 등록 순서 기준, 하나는 내가 최근에 본 것 기준이다.개선 방향:
3. 중복 라우트
같은 자원에 두 가지 경로가 동시에 살아있었다.
AI가 이걸 "중복"으로 분류했을 때, 나는 "그런가? 둘 다 동작하니 괜찮은 거 아닌가?" 라고 생각했다.
AI의 설명: "중복 라우트가 있으면 어떤 경로가 '정답'인지 팀 내에서도 혼란이 생깁니다. 클라이언트가 어떤 걸 쓰는지 파악하기 어려워지고, 나중에 한쪽만 고치면 나머지가 버그가 됩니다."
실제로 코드를 찾아보니 레거시 라우트들은 구형 앱 버전에서만 호출되고 있었다. 확인하지 않았으면 영원히 몰랐을 것이다.
4. HTTP 상태 코드 불일치
이 부분은 AI가 구체적인 수치를 뽑아줘서 한눈에 파악됐다.
엔드포인트 | 현재 | 올바른 코드 |
POST /subscribe/event | 200 | 201 (리소스 생성) |
POST /subscribe/artist | 200 | 201 |
POST /subscribe/venue | 200 | 201 |
DELETE /subscribe/event | 200 | 204 (내용 없음) |
DELETE /subscribe/artist | 200 | 204 |
DELETE /subscribe/venue | 200 | 204 |
POST /survey/count | 200 | 201 |
POST /fcm/token | 201 | 200 (upsert이므로) |
마지막 줄이 흥미롭다.
POST /fcm/token은 항상 201을 반환하는데, 내부 구현은 upsert다. 이미 존재하면 업데이트하고 없으면 생성한다. 그러니 첫 등록엔 201이 맞지만 이후엔 200이 맞다. 실제로는 매번 201을 반환하고 있었다.5. 자원 계층 구조의 붕괴
/subscribe, /ticket, /poster, /price 같은 prefix들이 독립적으로 존재하는데, 이것들은 모두 다른 자원의 하위 자원이다.이 구조를 보면서 처음엔 "URL이 길어지는 게 단점 아닌가?"라고 반문했다. AI의 답변:
"URL이 길어지는 건 맞습니다. 하지만/ticket?eventId=xxx는 이 티켓이 어떤 자원에 속하는지를 쿼리 파라미터에 숨깁니다./events/xxx/tickets는 URL 자체가 '이벤트 xxx의 티켓들'이라는 의미를 가지므로, 인증·캐싱·권한 체계를 자원 계층에 맞게 구성하기 쉬워집니다."
6. 비표준 헤더 기반 기능
지리 정보와 타임존을 커스텀 헤더로 전달하고 있었다.
x-lat-lng와 x-timezone은 요청의 필터 조건이다. 필터 조건은 쿼리 파라미터가 맞다. 헤더에 넣으면 Swagger 문서에서 표현하기 어렵고, 브라우저 캐싱도 적용되지 않는다.x-anonymous-user-id는 다르다. 이건 인증 맥락의 정보이므로 헤더가 적합하다. AI도 이건 유지하되 문서화를 권장했다.7. 페이지네이션 혼재
세 가지 방식이 뒤섞여 있다. 클라이언트 입장에선 엔드포인트마다 어떤 방식인지 외워야 한다.
8. URL 단수형
RESTful 컨벤션에서 컬렉션 자원은 복수형을 쓴다. 현재는 전부 단수형이다.
이건 작아 보이지만 일관성의 문제다.
/event/1이 개별 이벤트인지 이벤트 컬렉션인지 URL만으로는 모호하다.AI와 나눈 대화에서 인상적이었던 것
8가지 문제를 정리하면서 한 가지를 깨달았다. AI가 찾아낸 것들은 모두 내가 이미 어렴풋이 알고 있었던 것들이다.
POST /user/check-handle을 만들 때 "이건 GET이어야 하는 거 아닌가?" 생각했다. GET /event/tonight을 만들 때 "이게 진짜 도메인 개념인가?" 했다. 근데 그 찜찜함을 처리할 시간이 없었다.AI가 한 건 그 찜찜함들을 체계적으로 분류하고, 이름을 붙이고, 개선 방향을 함께 제시한 것이다. 혼자였다면 "어디서부터 시작해야 하지?"로 끝났을 분석이 80개 엔드포인트에 걸친 구체적인 문제 목록이 됐다.
물론 AI의 모든 제안을 그대로 수용하진 않았다. 몇 가지 판단은 다르게 내렸다.
x-anonymous-user-id헤더: AI는 쿼리 파라미터로 옮기자고 했지만, 인증 맥락 정보는 헤더가 적합하다고 판단해 유지했다.
GET /event/collections: AI는 단순 쿼리 파라미터로 해소하길 권장했지만, "큐레이션 컬렉션"이 도메인 개념으로 성장할 가능성이 있어 독립 자원으로 남겨둘 수 있다고 봤다.
AI와의 협업에서 가장 중요한 건 제안을 검토하는 인간의 판단이다. AI는 패턴을 잘 찾지만, 비즈니스 맥락과 팀의 우선순위는 사람이 더 잘 안다.
다음 편 예고
분석은 끝났다. 이제 고칠 차례다.
문제 목록 중 클라이언트 계약을 건드리지 않고 즉시 고칠 수 있는 것들부터 시작한다.
- dev-only 라우트를 프로덕션 라우트에서 분리
- 레거시 중복 슬러그 라우트 제거
- HTTP 상태 코드 정정
2편: Phase 1 — 비파괴적 내부 정리 에서 계속됩니다.
이 시리즈는 실제 프로덕션 코드베이스를 AI와 함께 리팩토링하는 과정을 기록합니다. 모든 결정에는 근거가 있고, 모든 근거에는 트레이드오프가 있습니다.