[SK네트웍스 Family 엔코아AI캠퍼스] AI 오케스트레이션 캠프 3기_9월 21일 회고

0. 오늘 한눈에 보기

주제 및 기술: FastAPI, HTTP, REST, Uvicorn, uv, Path·Query·Body 매개변수, Annotated, StrEnum, HTTPX

핵심 성과/성장 (한 줄 요약): Python 함수를 HTTP 요청과 연결해 도서 목록 API를 구현하고, 입력 검증과 상태 코드의 차이를 이해했다.

1. 핵심 개념 정리

키워드: 클라이언트·서버, HTTP 요청·응답, REST, JSON, Path, Query, 상태 코드

개념 및 도입 이유

FastAPI는 Python 함수를 HTTP 요청의 메서드와 경로에 연결해 웹 API를 만들 수 있도록 돕는 프레임워크다. Python 파일을 직접 실행할 때는 내가 실행을 시작하지만, 웹 API에서는 서버가 요청을 기다리다가 연결된 함수를 실행한다.

Uvicorn은 FastAPI 애플리케이션이 HTTP 요청을 받을 수 있도록 구동하는 ASGI 웹 서버다. 개발 중에는 다음과 같이 실행할 수 있다.

uv run fastapi dev 파일경로 --port 포트번호

HTTP는 클라이언트와 서버가 요청과 응답을 주고받는 규칙이며, REST는 URL로 자원을 나타내고 HTTP 메서드로 동작을 구분하는 설계 원칙이다.

  • GET /books: 책 목록 조회
  • POST /books: 책 생성
  • GET /books/2: 2번 책 조회
  • GET /books?keyword=파이썬&limit=1: 검색 조건에 맞는 책 조회

Annotated는 타입 힌트에 부가 정보를 붙이는 문법이다. FastAPI는 이 정보를 읽어 요청 본문의 구조나 값의 범위를 검증한다.

title: Annotated[str, Body(embed=True)]
limit: Annotated[int, Query(ge=1, le=20)] = 10

StrEnum은 허용할 문자열 값을 미리 정의할 때 사용한다. FastAPI에서는 쿼리 값이 정의된 문자열 중 하나인지 검증하고 API 문서에 선택지를 표시할 수 있다.

내가 이해한 동작 흐름

브라우저·HTTPX → HTTP 요청 → Uvicorn → FastAPI 라우팅·입력 검증
→ Python 함수 실행 → JSON 응답과 상태 코드 반환

FastAPI는 요청의 메서드와 경로를 확인한 뒤 해당 함수를 실행한다. 입력값이 조건에 맞지 않으면 함수 실행 전에 422 응답이 발생할 수 있다.

헷갈렸던 점 & 주의할 점

  • 127.0.0.1은 내 컴퓨터를, 8000과 같은 숫자는 요청을 받을 서버의 포트를 뜻한다.
  • 같은 /books 경로라도 GET과 POST는 서로 다른 함수에 연결된다.
  • Path는 특정 자원을 식별하며 생략할 수 없고, Query는 필터링·검색·페이지 범위 지정에 사용하며 기본값을 둘 수 있다.
  • ge는 숫자의 최솟값을, min_length는 문자열의 최소 길이를 검사한다.
  • 422는 입력의 형식이나 범위가 잘못된 경우이고, 404는 유효한 값으로 조회했지만 데이터가 없는 경우다.
  • 서버 연결 실패는 서버에 요청이 도달하지 않은 상태다. 서버가 요청을 받고 반환하는 404와 다르다.
  • total은 검색 조건에 맞는 전체 개수이고, itemsoffsetlimit을 적용해 실제 반환하는 목록이다.
  • 메모리의 목록은 서버를 재시작하면 초기화된다.
  • uv.lock은 의존성 버전을 재현하기 위한 기록이므로 Git에 올리지만, .venv는 각 컴퓨터에서 다시 만드는 환경이므로 올리지 않는다.

2. 실습 및 구현

구현 목표: 도서 생성·목록 조회·단건 조회 기능을 제공하고, 제목 검색과 페이지 범위 지정 및 잘못된 입력 검증을 구현한다.

핵심 코드 및 동작 결과

from typing import Annotated

from fastapi import Body, FastAPI, HTTPException, Query, status

app = FastAPI(title="Book API", version="0.1.0")

books = [
    {"id": 1, "title": "파이썬으로 시작하는 FastAPI", "available": True},
    {"id": 2, "title": "클린 코드", "available": True},
    {"id": 3, "title": "데이터베이스 첫걸음", "available": False},
    {"id": 4, "title": "파이썬으로 시작하는 FastAPI2", "available": False},
]


@app.get("/health")
def check_status() -> dict:
    return {"status": "ok"}


@app.post("/books", status_code=status.HTTP_201_CREATED)
def create_book(title: Annotated[str, Body(embed=True)]) -> dict:
    book = {
        "id": len(books) + 1,
        "title": title,
        "available": True,
    }
    books.append(book)
    return book


@app.get("/books/{book_id}")
def get_book(book_id: str) -> dict:
    try:
        parsed_book_id = int(book_id)
    except ValueError:
        raise HTTPException(
            status_code=422,
            detail="book_id는 1 이상의 정수를 입력해주세요.",
        )

    if parsed_book_id < 1:
        raise HTTPException(
            status_code=422,
            detail="book_id는 1 이상의 정수를 입력해주세요.",
        )

    book = next(
        (book for book in books if book["id"] == parsed_book_id),
        None,
    )

    if book is None:
        raise HTTPException(status_code=404, detail="없는 book_id 입니다.")

    return book


@app.get("/books")
def list_books(
    keyword: Annotated[
        str | None,
        Query(description="조회할 book의 title"),
    ] = None,
    offset: Annotated[int, Query(ge=0, description="offset 지정")] = 0,
    limit: Annotated[
        int,
        Query(ge=1, le=20, description="조회할 book 개수"),
    ] = 10,
) -> dict:
    result = (
        books
        if keyword is None
        else [
            book
            for book in books
            if keyword.casefold() in book["title"].casefold()
        ]
    )

    total = len(result)
    return {"items": result[offset : offset + limit], "total": total}

주요 동작은 다음과 같다.

  • POST /books는 제목을 받아 새 책을 생성하고 201 Created를 반환한다.
  • GET /books/{book_id}는 1 이상의 정수 ID를 조회한다.
  • 정수로 변환할 수 없거나 1보다 작은 ID에는 422를 반환한다.
  • 형식은 맞지만 존재하지 않는 ID에는 404를 반환한다.
  • GET /books는 제목을 대소문자 구분 없이 검색한다.
  • 검색 결과가 없어도 정상적인 요청이므로 200과 빈 목록을 반환한다.
  • total은 검색된 전체 개수이며, items에는 offset 이후 최대 limit개의 책이 담긴다.

구현하며 알게 된 인사이트

next()의 두 번째 인자로 None을 지정하면 조건에 맞는 항목이 없을 때 예외 대신 None을 받을 수 있다.

book = next((book for book in books if book["id"] == parsed_book_id), None)

또한 문자열을 정수로 바꿀 수 있는지는 try-except로 처리할 수 있다. int(book_id)에서 ValueError가 발생하면 입력 오류로 판단해 422를 반환한다.

검색과 페이지 범위 지정은 순서도 중요하다. 먼저 키워드 조건으로 전체 결과를 만든 뒤 total을 계산하고, 마지막에 offsetlimit으로 items를 잘라야 한다.

Python의 with 블록은 파일이나 네트워크 연결처럼 사용 후 정리가 필요한 자원을 안전하게 관리한다. 블록 안에서 오류가 발생해도 자원 정리 절차가 실행된다는 점이 핵심이다.

3. 트러블슈팅 & 면접 대비 (STAR)

상황 (Situation)

도서 단건 조회 API에서 조건에 맞는 책 한 권을 찾고, 없는 경우 404를 반환하는 기능을 구현했다.

문제 (Task/Problem)

next() 문법이 익숙하지 않아 두 번째 인자의 역할을 정확히 사용하지 못했다. 또한 숫자로 바꿀 수 없는 경로 문자열을 어떻게 처리해야 할지 몰랐다. 실습 중에는 반환 타입을 빠뜨리거나 raise 뒤에 불필요한 return을 작성하는 실수도 있었다.

에러 메시지

잘못된 요청 본문이나 경로 입력은 422 Unprocessable Entity로 확인했다.

기대한 결과 vs 실제 결과

  • 기대한 결과: 조건에 맞는 책이 있으면 해당 책을 반환하고, 없으면 404를 반환한다.
  • 실제 결과: next()의 기본값 처리가 올바르지 않아 항목이 없을 때 의도한 분기 처리가 되지 않았다.
  • 기대한 결과: 숫자가 아닌 book_id422를 반환한다.
  • 실제 결과: 문자열을 직접 정수로 변환하는 과정의 예외 처리 방법이 필요했다.

원인 및 조치 (Action)

원인:

  • next(이터레이터, 기본값)에서 두 번째 인자가 검색 실패 시 반환할 기본값이라는 점을 숙지하지 못했다.
  • int() 변환 실패 시 ValueError가 발생한다는 점을 코드에 반영하지 못했다.
  • 반환과 예외 발생의 흐름을 꼼꼼히 확인하지 않았다.

해결 코드/방안:

try:
    parsed_book_id = int(book_id)
except ValueError:
    raise HTTPException(
        status_code=422,
        detail="book_id는 1 이상의 정수를 입력해주세요.",
    )

book = next(
    (book for book in books if book["id"] == parsed_book_id),
    None,
)

if book is None:
    raise HTTPException(status_code=404, detail="없는 book_id 입니다.")

raise가 실행되면 함수 흐름이 종료되므로 이후에 return을 추가하지 않았다. 함수의 반환 타입도 -> dict로 명시했다.

결과 및 배운 점 (Result & Learning)

입력값 자체가 유효하지 않은 경우와 입력은 유효하지만 데이터가 없는 경우를 각각 422404로 구분할 수 있게 됐다. 또한 next()의 기본값과 try-except를 활용해 조회 실패와 형 변환 실패를 명확하게 처리했다.

해결 검증

  • 숫자가 아닌 ID: 422
  • 1보다 작은 ID: 422
  • 존재하지 않는 양의 정수 ID: 404
  • 존재하는 ID: 200과 해당 책 정보
  • 검색 결과 없음: 200, items: [], total: 0

다음에 유사 문제가 생기면 확인할 기준

  1. 오류가 Path, Query, Body 중 어느 입력에서 발생했는지 확인한다.
  2. 입력의 변환 실패인지, 범위 검증 실패인지, 데이터 부재인지 구분한다.
  3. next() 등 내장 함수의 인자와 반환 동작을 확인한다.
  4. raise 이후 도달할 수 없는 코드를 작성하지 않았는지 살핀다.
  5. 필터링 후 total을 계산하고, 그다음 offsetlimit을 적용했는지 확인한다.

4. 회고 및 다음 액션 (KPT & Next)

Keep: 요청 메서드·경로·입력 위치·상태 코드를 함께 비교하고, 결과를 먼저 예상한 뒤 /docs에서 실제 응답을 확인하는 방식을 유지한다.

Problem & Try: next()의 두 번째 인자와 예외 처리처럼 익숙하지 않은 문법에서 오류가 발생했고, 반환 타입이나 raise 이후의 흐름에서도 사소한 실수가 있었다. 다음 실습에서는 함수의 입력, 정상 반환, 예외 분기를 차례로 점검하고 200, 201, 404, 405, 422 사례를 각각 테스트한다.

다음 학습 연결: Path·Query·Body 검증을 더 연습하고, Pydantic을 활용한 요청 본문 모델과 입력 규칙으로 확장한다. HTTPX로 정상 요청과 오류 요청을 자동 실행하며 상태 코드와 응답 본문을 비교해 볼 예정이다.

5. 참고 자료

  • FastAPI 공식 문서: 소개, 경로 매개변수, 쿼리 매개변수, 추가 패키지
  • Python 공식 문서: typing.Annotated, enum.StrEnum, with, next, 예외 처리
  • HTTP 메서드 및 HTTP 상태 코드 문서
  • HTTPX 빠른 시작 문서
  • uv 프로젝트 및 의존성 관리 문서
  • REST 원문 및 JSON 설명 자료