서버

Tags 서버

키워드 → 태그 규칙을 관리하고(HTTP, admin 앱), 메모 내용에 맞는 태그를 준다(gRPC TagService/Suggest, notes 가 메모를 저장하기 전에 부른다). FastAPI · Python 3.12 · uv. HTTP :8002 · gRPC :9002 (같은 프로세스, gRPC 는 lifespan 에서 띄운다), DB 는 Postgres(psycopg 3 · 연결 풀). 레포 전체 규칙은 루트 AGENTS.md, 이 파일이 이 서버에서는 우선한다.

구조

폴더 = 레이어 (docs/architecture.md 서버 레이어). 의존은 api → domain → storage · clients 방향만.

server/폴더 구조
  • gen/ 을 import 경로에 넣는다 (proto 패키지 app.tag.v1 그대로)
  • 앱 조립: 라우터 · gRPC(lifespan) · security · errors
  • HTTP 라우트 /api/tags/rules + pydantic 모델 (검증 = 문서)
  • gRPC TagService (grpc.server, 스레드풀)
  • 업무 로직: 규칙 관리 · suggest (HTTP · gRPC 공용)
  • Postgres 연결 풀 (PG* 환경 변수, 기본 docker 의 tags DB, 처음 부를 때 연다) · migrate()
  • 저장소 (SQL)
  • 웹 · 모바일 · 데스크톱 앱과 공통 API 클라이언트를 둡니다.
  • 오류 형식 · 422 → 400 · 문서 고치기
  • JSON 본문만
  • pnpm gen: proto → gen/, 앱 → contract/openapi/tags.json
  • 공통 설정: 랜덤 포트 uvicorn · gRPC, 테스트 DB(tags_test), fixture client · channel · clean_db
  • tags/rules/GET_specs · tags/rules/id/DELETE_specs (경로 변수 = 하위 폴더)
  • grpc/TagService/Suggest_specs

폴더 이름은 api/routes · api/rpc 다. http · grpc 로 지으면 표준 라이브러리 http · 라이브러리 grpc 와 헷갈린다.

규칙

  • 쓰기(POST · PUT)는 JSON 본문만 (support/security.py). 모르는 필드는 pydantic 이 버린다.
  • 오류는 { "error" } (support/errors.py): 검증 실패 400 (FastAPI 기본 422 아님), 없는 자원 404 (domain 이 NotFound), 그 외 502, 없는 경로 404, 다른 메서드 405.
  • 바인딩은 HOST (uvicorn --host · gRPC 같이, 기본 127.0.0.1). 밖에 열 때 0.0.0.0, 그 전에 인증 · TLS.
  • DB 는 docker/docker-compose.middleware.yaml 의 Postgres (루트에서 pnpm infra). 연결은 PGHOST · PGPORT · PGDATABASE · PGUSER · PGPASSWORD (기본 127.0.0.1:5432 · tags · app/app).
  • 스키마는 Alembic 리비전(migrations/versions/)이 정한다. 서버가 뜰 때 lifespan 이 storage/db.py 의 migrate() (upgrade head)로 요청을 받기 전에 맞춘다. 테스트도 서버를 띄우며 같이 돈다. pnpm gen 은 lifespan 을 돌리지 않아 DB 가 없어도 된다.
  • 리비전은 op.execute(...) 원시 SQL. SQLAlchemy 는 migrations/env.py 의 연결에만 쓰고 앱은 psycopg. 적용된 리비전은 고치지 않고 새 리비전을 더한다. downgrade 는 두지 않는다(Flyway 처럼 앞으로만).
  • 핸들러 · gRPC 메서드는 def 다. DB 드라이버가 동기라 스레드풀(FastAPI · grpc.server(ThreadPoolExecutor()))에서 돌린다. async def 안에서 DB 를 부르면 이벤트 루프가 막힌다.

명령

pnpm dev          # gen proto → uvicorn --reload (${HOST:-127.0.0.1}:${PORT:-8002}). DB 는 먼저 루트에서 pnpm infra
pnpm test         # gen proto → pytest (DB 가 없으면 5초 안에 멈추고 pnpm infra 를 알린다)
pnpm gen          # gen proto + contract/openapi/tags.json
pnpm typecheck    # gen proto → pyrefly check
pnpm lint         # ruff check + ruff format --check
pnpm format       # ruff check --fix + ruff format
uv run alembic revision -m rule_order --rev-id 0002 # 새 리비전 migrations/versions/0002_rule_order.py (DB 없이)
uv run alembic history                              # 리비전 목록. 적용 상태는 PGDATABASE=tags_test uv run alembic current
uv sync                                             # 의존성 설치 (pyproject.toml · uv.lock)
docker build --build-context workspace=../.. .      # 이미지 (이 폴더에서)

계약

  • FastAPI 가 pydantic 모델 · 라우트에서 문서를 만든다. 모델 이름이 스키마 이름이라 서버 안에서 유일하게(같은 이름 둘은 모듈 경로가 붙은 긴 이름이 된다).
  • operationId 는 함수 이름의 camelCase(list_rules → listRules, main.py 의 generate_unique_id_function). 서버 안에서 유일하게.
  • 검증 실패는 FastAPI 기본 422 · detail 이 아니라 400 { error } 다. 문서도 같게 고친다(support/errors.py 의 openapi()).
  • pydantic 은 문자열 자리에 숫자를 받지 않는다. 모르는 필드는 버린다.

gRPC

  • app.tag.v1.TagService/Suggest: keyword 가 text 에 들어 있는 규칙의 tag (대소문자 무시, 같은 태그는 한 번, 규칙 id 순, 빈 text 는 []). 업무 로직은 domain/rules.py 의 suggest(), rpc 는 넘기기만 한다.
  • python -m server.gen proto 가 grpcio-tools 로 gen/ 에 만든다. 타입 스텁은 mypy-protobuf(--mypy_out · --mypy_grpc_out). grpc 기본 생성 코드에는 타입이 없어 pyrefly 가 서비스 메서드를 잘못 본다.
  • dev · test · typecheck 가 먼저 proto 를 만든다(gen/ 은 커밋하지 않는다). 이미지는 gen 단계에서 만든다.

테스트

  • conftest.py 가 uvicorn 을 port=0 으로 스레드에서 띄우고 httpx 로 부른다. TestClient 는 소켓을 거치지 않아 쓰지 않는다.
  • 파일 tests/tags/rules/id/DELETE_specs.py, 모듈 docstring """DELETE /api/tags/rules/{id}""", 함수 def test_없는_id면_404_Not_Found를_반환한다(client) (pytest 가 test_ 로 모은다). 같은 파일 이름이 여러 폴더에 있어서 --import-mode=importlib.
  • 외부 클라이언트는 app.dependency_overrides 나 교체 지점으로 Fake 를 넣는다. patch('server.clients…') 는 쓰지 않는다(hook 이 막는다).
  • DB 는 docker 의 Postgres 에 있는 tags_test(conftest 가 PGDATABASE 를 바꾼다). 개발 DB tags 는 건드리지 않는다.
  • 서버를 세션 동안 한 번 띄워 같이 쓰므로 DB · Fake 는 테스트마다 비운다(clean_db 는 autouse, alembic_version 을 뺀 테이블을 TRUNCATE). 마이그레이션이 넣은 예시 규칙도 지워지니 테스트는 필요한 규칙을 storage/rules.py 로 직접 만든다. TRUNCATE 는 id 를 되돌리지 않으니 id 는 저장소 · 응답이 준 값을 쓴다(1 이라고 가정하지 않는다).
GitHub 에서 고치기