FastAPI에서 정적 라우트가 404를 반환한 이유

유진·2025년 11월 20일

web

목록 보기
2/2

/{code} 동적 라우트가 /urls를 가로채던 문제 해결 기록

URL Shortener 프로젝트를 만들다가 예상치 못한 상황을 마주했다.
프론트엔드 대시보드에서 전체 URL 목록을 불러오기 위해 /urls 엔드포인트를 호출했는데, 계속 404가 떨어지는 문제가 발생했다.

{
  "detail": "Short URL not found"
}

문제는 프론트엔드가 아니라 서버 라우터 쪽에 있었다.


1. 증상

Swagger와 curl에서도 동일하게 /urls가 404를 반환했다.

curl https://url-shorter.onrender.com/urls
# → {"detail": "Short URL not found"}

FastAPI log에서도 /urls 요청이 들어갔는데, 내부에서는 완전히 다른 라우트가 실행되고 있었다.


2. 원인 파악

결론부터 말하면 아래 라우트가 문제였다.

@app.get("/{code}")
def redirect(code: str):
    ...

FastAPI는 라우트를 정의된 순서대로 검사한다.
/{code}는 어떤 문자열이든 모두 매칭되기 때문에 아래처럼 동작한다.

GET /urls → code = "urls" 로 매칭됨

그 결과 FastAPI는 /urls를 정적 라우트로 처리하지 않고, 동적 라우트로 인식한다.

즉,
"/urls"가 아닌 "/{code}"가 실행되고 있었던 것.

그래서 DB에서 "urls"라는 short code를 찾다가 없어서
404("Short URL not found")가 내려온 것이다.

이 상황은 URL Shortener처럼 단일 동적 엔드포인트가 있는 서비스에서 쉽게 발생한다.


3. 해결 방법 – 라우트 순서 재정렬

정적인 라우트를 동적 라우트보다 앞에 놓아야 한다.

잘못된 순서:

@app.get("/{code}")    # 동적
@app.get("/urls")      # 정적 (가려짐)

올바른 순서:

@app.get("/urls")               # 가장 먼저
@app.get("/stats/{code}")       # 부분 동적
@app.get("/{code}")             # 마지막

FastAPI는 선언 순서가 곧 우선순위이다.
정적 → 부분 동적 → 완전 동적 순으로 정리하면 충돌이 없다.


4. 왜 이런 방식으로 동작하는가

FastAPI의 라우팅은 Starlette 라우터를 기반으로 한다.
Starlette는 URL 매칭을 다음 기준으로 처리한다.

  1. 정적 경로(예: /urls, /health)
  2. 부분 동적 경로(예: /stats/{code})
  3. 완전 동적 경로(예: /{code})
  4. 와일드카드 (/{path:path})

문제는 “우선순위”가 아니라 코드 선언 순서가 우선 적용된다는 점이다.
동적 경로를 맨 위에 선언하면 정적 경로보다 먼저 검사되어 그대로 가로채 버린다.


5. 실제 수정 후 결과

라우트 순서를 다시 정렬한 뒤에는 다음과 같이 정상적으로 응답이 내려왔다.

GET /urls → 정상 200  
GET /stats/abc123 → 정상 200  
GET /xyz789 → redirect 정상 작동

정리된 구조는 아래와 같다.

@app.get("/urls")
def list_urls(): ...

@app.get("/stats/{code}")
def stats(code: str): ...

@app.get("/{code}")
def redirect(code: str): ...

6. 마무리

이번 문제는 URL Shortener에서 특히 자주 발생하는 라우트 충돌 이슈였다.
프레임워크의 라우팅 매칭 원리를 정확히 알고 있어야 해결할 수 있으며,
동적 라우트가 하나라도 포함되는 프로젝트라면 라우팅 순서를 항상 신경 써야 한다.

개발 과정에서 흔히 겪을 수 있는 오류이지만,
이 원리를 잘 이해해두면 FastAPI 구조 설계가 훨씬 깔끔해진다.

profile
안드로이드... 좋아하세요?

0개의 댓글