본문 바로가기
C.W.K.
Stream
Lesson 01 of 04 · published

cwkPippa에서 REST 읽기 — 실제 코드베이스 탐색

~10 min · epilogue, cwkpippa, synthesis, self-reference

Level 0HTTP 입문자
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"이 퀘스트는 cwkPippa를 실제로 동작하는 사례로 활용했다. 이제 운영 코드를 직접 따라가며 앞서 배운 개념이 어떻게 구현되었는지 확인할 차례다. 어떤 부분은 교과서적인 원칙을 따르고, 어떤 부분은 현실적인 요구에 맞게 조정되어 있다."

저장소에서 살펴볼 위치

  • cwkPippa/backend/routes/ — FastAPI router가 모여 있다. 각 파일은 대체로 chat, conversations, folders, Council, artifacts, 관리자 기능처럼 하나의 리소스 계열을 담당한다. 총 412개의 @router decorator에서 여러 HTTP 메서드를 확인할 수 있다.
  • cwkPippa/backend/adapters/ — SSE 스트림을 생성하는 adapter가 있다. 대표적인 파일은 claude.py이며, 그 안의 stream 메서드가 Starlette의 StreamingResponse를 통해 이벤트를 하나씩 전달한다.
  • cwkPippa/backend/main.py — FastAPI 앱 구성, 엄격한 CORS 허용 목록, router 마운트 순서를 정의한다.
  • cwkPippa/backend/store/ — SQLite와 JSONL 영속성 계층이 있으며 복구 로직도 이곳에 있다.
  • cwkPippa/frontend/src/lib/api.ts — 클라이언트 API 계층이다. cwkPippa의 엔드포인트를 React에서 타입이 지정된 wrapper로 호출한다.

각 트랙을 실제 엔드포인트에 연결하기

퀘스트의 각 트랙은 다음과 같은 cwkPippa 엔드포인트에서 구체적으로 확인할 수 있다.

  • 기초 (트랙 1)GET /api/health: 가장 단순한 형태의 엔드포인트다. 상태 코드 200과 JSON 본문을 반환하며 인증이 필요 없다. HTTP 요청의 기본 구조를 살펴보기에 적합하다.
  • 의미론 (트랙 2)PUT /api/conversations/{id}/title: 멱등성을 갖는 이름 변경 요청이다. 같은 title을 두 번 보내도 최종 상태는 같다.
  • REST 설계 (트랙 3)POST /api/conversations: 서버가 UUID를 할당하고 201과 Location을 반환한다.
  • 인증과 보안 (트랙 4) — 모든 엔드포인트에 CORS 허용 목록(main.py의 _allowed_origins)을 적용하고, PIN으로 발급한 세션의 Bearer 토큰을 사용하며, 클라이언트에는 API 키를 두지 않는다.
  • 캐시와 성능 (트랙 5) — Vite로 빌드한 정적 자산에는 public, max-age=31536000, immutable이 적용되고, API 엔드포인트는 기본적으로 no-store를 사용한다. 사용자별 데이터는 공유 캐시에 저장하기 어렵기 때문이다.
  • 스트리밍과 비동기 처리 (트랙 6)POST /api/chat은 SSE를 생성한다. POST /api/council/{id}/finalize는 202와 Location을 반환하는 비동기 polling 패턴을 사용한다.
  • 운영 (트랙 7)backend/main.py에는 request ID 미들웨어가 있고, FastAPI는 /docs에서 OpenAPI 문서를 자동으로 제공한다. 백엔드의 복구 계층은 제한적인 형태의 멱등성도 제공한다.

cwkPippa가 현실에 맞게 조정한 부분

운영 코드베이스가 언제나 교과서와 완전히 일치하는 것은 아니다. cwkPippa에는 다음과 같은 의도적인 절충이 있다.

  • 대부분의 엔드포인트에 ETag가 없음 — 복구 계층이 요청마다 JSON을 다시 구성하므로 바이트 수준에서 안정적인 해시를 만들려면 추가 작업이 필요하다. 실제 캐시 miss 비용이 커질 때 도입할 수 있다.
  • 공식 버전 관리가 없음 — 단일 클라이언트와 백엔드를 함께 배포하므로 /v1/ 접두사가 아직 필요하지 않다.
  • FastAPI의 기본 구조화된 오류 응답{detail: ...}을 그대로 사용한다. {error: {code, message, request_id}} 같은 별도 형식을 만들지 않은 이유는 프런트엔드가 detail만으로 충분하기 때문이다.
  • 동작 엔드포인트(POST /api/council/{id}/finalize) — 순수한 리소스 중심 설계에서 벗어난 혼합형 패턴이다. 작업을 자연스럽게 나타낼 명사형 리소스가 없기 때문에 의도적으로 선택했다.

이러한 절충에는 CLAUDE.md나 기능별 주석으로 근거를 남겨 두었다. 이유를 문서화한 실용주의가 운영 REST API의 현실적인 모습이다.

교과서는 규칙을 가르치고, 운영은 규칙을 언제 조정하며 그 근거를 어떻게 기록해야 하는지 가르친다. cwkPippa를 실제 사례로 읽으면 리소스 URI, 의미론에 맞는 메서드, 상태 코드 같은 원칙과 ETag 생략, 혼합형 동작 엔드포인트, 연기된 버전 관리 같은 현실적인 선택을 함께 볼 수 있다. 원칙에서 벗어난 지점마다 이유가 있고, 그 이유가 문서화되어 있다.

권장 읽기 순서

REST 학습 관점에서 cwkPippa를 처음 살펴볼 때는 다음 순서가 적합하다.

  1. backend/main.py로 시작 — 앱 구성, CORS, 미들웨어를 약 5분 동안 살펴본다.
  2. 다음은 backend/routes/health.py다. 파일이 없다면 가장 단순한 router를 선택한다. — 기본적인 GET과 FastAPI decorator 패턴을 확인한다.
  3. 다음은 backend/routes/chat.py — SSE 응답을 반환하는 POST와 스트리밍 패턴을 살펴본다.
  4. 다음은 backend/routes/conversations.py — 복구 로직을 포함한 전체 CRUD 흐름을 살펴본다.
  5. 다음은 backend/routes/council/routes.py — finalize 작업에 사용하는 202와 Location 기반 비동기 패턴을 살펴본다.
  6. 마지막은 frontend/src/lib/api.ts — 클라이언트에서 각 엔드포인트를 호출하는 방식을 확인한다.

Pippa의 설계 회고

Pippa가 REST를 떠올릴 때 cwkPippa가 먼저 연상되는 이유는 이 코드베이스를 오랫동안 다루며 설계 선택을 반복해서 확인했기 때문이다. 처음부터 끝까지 잘 설계된 REST API를 하나 완성하면 그 패턴은 이후 설계 판단의 기준이 된다. 교과서는 어휘를 제공하고, 실제 코드베이스는 그 어휘를 실무 감각으로 바꿔 준다.

Code

ripgrep으로 cwkPippa의 REST 표면 목록 만들기·bash
# 자기 cwkPippa REST 표면 직접 걸어
cd ~/projects/cwkPippa

# 파일 당 route 수 세기 (대략 표면 지도)
rg -c '@router\.' backend/routes/ | sort -t: -k2 -n -r | head -20

# 모든 @router decorator 봐 (verb + path 지도)
rg '@router\.(get|post|put|patch|delete)' backend/routes/ | head -40

# SSE endpoint 찾기 (StreamingResponse 사용)
rg 'StreamingResponse|text/event-stream' backend/

# 202 + Location 패턴 (async) 찾기
rg 'HTTP_202_ACCEPTED|status_code=202' backend/

# CORS allowlist 검사
rg '_allowed_origins' backend/
단순화한 예시: 한 파일에서 상태 코드, Location, 멱등성 살펴보기·python
# 많은 패턴 한 번에 데모하는 snippet — backend/routes/conversations.py 에서
# (paraphrased / simplified; full 버전 위해 real 파일 읽어)
from fastapi import APIRouter, HTTPException, Response, status
from backend.store import conversations

router = APIRouter(prefix='/api/conversations', tags=['conversations'])

@router.get('/{cid}')
async def get_conversation(cid: str):
    # 모든 GET 에 healing run — aborted turn 재빌드 + dangling parent cleanup
    convo = await conversations.heal_and_load(cid)
    if not convo:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail='conversation not found')
    return convo

@router.post('', status_code=status.HTTP_201_CREATED)
async def create_conversation(response: Response):
    new_id = await conversations.create()
    response.headers['Location'] = f'/api/conversations/{new_id}'
    return {'id': new_id, 'created_at': now_utc().isoformat()}

@router.put('/{cid}/title')
async def rename(cid: str, payload: dict):
    # Idempotent — 같은 title 두 번 = 같은 state
    await conversations.update_title(cid, payload['title'])
    return {'id': cid, 'title': payload['title']}

@router.delete('/{cid}', status_code=status.HTTP_204_NO_CONTENT)
async def delete(cid: str):
    await conversations.delete(cid)
    # 204 — body 없음

External links

Exercise

cwkPippa 저장소 또는 중간 규모의 FastAPI 코드베이스를 다음 순서로 살펴본다. (1) main.py에서 CORS 허용 목록과 미들웨어 순서를 확인한다. (2) 리소스 router 하나(cwkPippa에서는 conversations.py)를 골라 HTTP 메서드와 path 패턴을 확인한다. (3) chat route에서 SSE가 생성되는 지점을 찾는다. 각 단계에서 교과서만으로는 예상하기 어려운 선택을 하나씩 식별하고, 그 선택이 필요한 이유를 적는다. 보너스 과제로 CLAUDE.md나 저장소 README를 읽고 추론한 이유가 문서의 설명과 일치하는지 확인한다.
Hint
교과서적인 REST에서 벗어난 지점이 가장 좋은 학습 자료가 되기도 한다. cwkPippa의 GET에 포함된 복구 계층이 한 예다. 일반적인 API는 GET 요청에서 상태를 변경하지 않지만, cwkPippa에는 그렇게 설계한 이유가 있다. ETag가 없는 점도 또 다른 예다. 이런 차이를 발견하고 근거를 설명하는 능력은 실제 시스템을 운영 가능한 형태로 설계하는 데 중요하다.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.