서비스 학습
SERVICE MAP · 2026-09-12 기준 · 파일 14개 · 이동 25개

요청 하나가
어느 파일을 어떤 순서로 지나는가

노드는 파일이고 선은 요청이 옮겨 가는 순간이다. 선마다 그 이동이 일어나는 조건이 적혀 있다. 범례에서 흐름 하나를 고르면 그 경로만 남는다.

읽는 법

선의 색과 모양이 함께 흐름을 구분한다. 색만으로 구분하지 않는 이유는 색각 이상에서도 읽히게 하기 위해서다. 아래 버튼을 누르면 그 흐름만 남고, 선마다 조건이 글자로 나타난다. 다시 누르면 전체로 돌아온다.

전체 보기에서는 선이 겹치므로 글자를 숨겨 둔다. 선 위에 마우스를 올리면 그 선 하나의 조건만 볼 수 있다. 화면이 좁으면 그림을 가로로 밀어서 본다.

노드를 누르면 그 파일의 해설 문서로 간다. 우리 파일 12개가 모두 연결돼 있고, 브라우저와 추론 서버는 우리 코드가 아니라 눌리지 않는다.

표시
흰 상자be/ 안의 우리 파일 · 눌러서 해설 문서로 이동
회색 상자우리 코드가 아닌 것 — 브라우저와 macOS 추론 서버
점선 테두리 상자아직 이 흐름에 참여하지 않는 파일 · 눌러서 이동 가능

연결도

왼쪽 위가 브라우저, 오른쪽 끝이 macOS에서 도는 TurboFieldfareServer다. 가운데 세 열이 be/app/ 안이고, 아래쪽 낮은 줄은 요청이 아니라 서버가 시작할 때 한 번만 도는 조립 경로다.

흐름별로 읽기

서버 시작 시 1회 · 조립

요청과 무관하게 프로세스가 뜰 때 한 번만 일어난다. main.py가 설정을 읽고, HTTP client 두 개를 만들어 Adapter에 넣고, router.py가 모아 둔 규칙을 application에 복사한다.

이 경로가 끝난 뒤에야 나머지 다섯 흐름이 가능해진다. main.py/healthz라는 글자를 모른다 — 묶음 하나만 받아 붙일 뿐이다.

GET /healthz

가장 짧다. 엔진을 부르지 않는다. 추론 서버가 꺼져 있어도 200이 나오며, 그것이 이 endpoint의 존재 이유다. FastAPI 프로세스가 HTTP에 답할 수 있다는 사실만 증명한다.

GET /readyz

Depends를 거쳐 Adapter를 받아 probe 전용 연결(최대 2개)로 추론 서버를 두 번 두드린다. 생성 요청이 연결을 다 쓰고 있어도 상태 확인은 자기 연결로 답한다.

실패하면 원인이 무엇이든 503 하나다. 예/아니오 질문이기 때문이다.

POST /api/v1/chat · 성공

가장 긴 경로다. 검증 → 번역 → 생성 → 되돌아오기까지 여덟 번 옮겨 간다. 중요한 분기는 두 번째 선이다 — 공백 메시지나 상한 초과는 schemas/chat.py에서 422로 끝나고 엔진까지 가지 않는다. 8GB 환경에서 30초짜리 생성을 낭비하지 않기 위해서다.

실패 · 오류를 status 로

연결 실패·시간 초과·429·비200이면 Adapter가 InferenceError 하나를 던진다. 같은 예외를 두 route가 다르게 해석한다 — 채팅은 매핑표로 여섯 가지 status로 나누고, readyz는 전부 503이다. 이 갈림이 route 계층이 하는 일이다.

smoke.py · 웹 없이 관찰

브라우저를 거치지 않는 유일한 경로다. create_app()으로 lifespan만 빌려 client를 얻은 뒤 generate()를 직접 부른다. HTTP 계층을 뺀 채 추론 연결만 확인할 때 쓴다.

아직 연결되지 않음

fe/src/App.tsx의 전송 버튼은 지금도 화면에만 문장을 추가한다. 브라우저에서 POST /api/v1/chat으로 가는 선은 9단계에서 생긴다. 서버 쪽은 이미 준비돼 있다.

전체 이동 목록

그림의 모든 선을 표로 옮긴 것이다. 그림을 보기 어려운 환경에서도 같은 내용을 읽을 수 있고, 검색도 된다. 범례에서 흐름을 고르면 이 표도 함께 강조된다.

흐름출발도착언제 이 선을 탄다
서버 시작 시 1회 · 조립app/main.pycore/config.pycreate_app() 이 APP_* 환경변수를 읽어 Settings 를 만든다
서버 시작 시 1회 · 조립app/main.pyapi/router.pyinclude_router(api_router) 로 규칙을 app 에 복사한다
서버 시작 시 1회 · 조립app/main.pyengines/turbofieldfare.pylifespan startup 에서 생성용·probe용 client 2개를 만들어 Adapter 에 주입한다
서버 시작 시 1회 · 조립api/router.pyapi/routes/system.pyimport 시 접두사 없이 붙는다 → /healthz · /readyz
서버 시작 시 1회 · 조립api/router.pyapi/routes/chat.pyimport 시 prefix=/api/v1 로 붙는다 → /chat
GET /healthz브라우저api/routes/system.py브라우저가 GET /healthz 를 보내면
GET /healthzapi/routes/system.pyschemas/system.py엔진을 부르지 않고 HealthResponse 로 직렬화한다
GET /readyz브라우저api/routes/system.py브라우저가 GET /readyz 를 보내면
GET /readyzapi/routes/system.pyapi/dependencies.pyDepends(get_inference_engine) 이 실행된다
GET /readyzapi/dependencies.pyengines/turbofieldfare.pyapp.state 의 Adapter 를 돌려주고 check_ready() 를 부른다
GET /readyzengines/turbofieldfare.pyTurboFieldfareServerprobe client(최대 2연결)로 GET /health 와 GET /v1/models 를 부른다
POST /api/v1/chat · 성공브라우저api/routes/chat.py브라우저가 POST /api/v1/chat 을 보내면
POST /api/v1/chat · 성공api/routes/chat.pyschemas/chat.py본문을 검증한다 · 공백이나 상한 위반이면 여기서 422 로 끝난다
POST /api/v1/chat · 성공api/routes/chat.pyengines/base.py통과하면 ChatMessage 를 Message 로 바꾸고 옵션 128/0.2 를 서버가 붙인다
POST /api/v1/chat · 성공engines/base.pyengines/turbofieldfare.pyGenerationRequest 로 engine.generate() 를 부른다
POST /api/v1/chat · 성공engines/turbofieldfare.pyTurboFieldfareServer생성 client(최대 4연결)로 POST /v1/chat/completions 를 stream=false 로 부른다
POST /api/v1/chat · 성공TurboFieldfareServerengines/turbofieldfare.py응답 JSON 을 wire model 로 재검증해 GenerationResult 로 만든다
POST /api/v1/chat · 성공engines/turbofieldfare.pyapi/routes/chat.pyGenerationResult 를 route 로 돌려준다
POST /api/v1/chat · 성공api/routes/chat.py브라우저ChatResponse 를 200 으로 내보낸다
실패 · 오류를 status 로engines/turbofieldfare.pyapi/routes/chat.py연결 실패·시간 초과·429·비200 이면 InferenceError(code) 를 던진다
실패 · 오류를 status 로api/routes/chat.py브라우저매핑표로 400·413·429·502·503·504 와 {error_code} 를 내보낸다
실패 · 오류를 status 로api/routes/system.py브라우저readyz 는 실패가 무엇이든 503 not_ready 하나로 답한다
smoke.py · 웹 없이 관찰engines/smoke.pyapp/main.pycreate_app() 으로 lifespan 을 빌려 client 를 얻는다
smoke.py · 웹 없이 관찰engines/smoke.pyengines/turbofieldfare.py웹 port 없이 generate() 를 직접 불러 실제 생성을 관찰한다
아직 연결되지 않음fe/src/App.tsx브라우저9단계에서 연결 예정 · 지금 전송 버튼은 화면에만 문장을 추가한다

직접 확인하는 방법

그림이 맞는지 실제로 확인할 수 있다. 두 서버를 띄운 뒤 요청을 보내면서 어느 흐름을 탔는지 응답과 로그로 구분한다.

# 터미널 1 — 추론 서버
cd turbo-fieldfare
.build/release/TurboFieldfareServer --model scratch/gemma4.gturbo

# 터미널 2 — FastAPI
cd be && .venv/bin/uvicorn app.main:app --port 8000
#   시작 로그에 service_started 가 뜨면 파란 조립 경로가 끝난 것이다

# 터미널 3 — 흐름별로 하나씩
curl -s http://127.0.0.1:8000/healthz                      # 초록 · 엔진을 안 부른다
curl -s http://127.0.0.1:8000/readyz                       # 노랑 · probe 연결을 쓴다
curl -s -X POST http://127.0.0.1:8000/api/v1/chat \
     -H 'content-type: application/json' \
     -d '{"messages":[{"role":"user","content":"MoE가 뭐야?"}]}'   # 진초록

curl -s -X POST http://127.0.0.1:8000/api/v1/chat \
     -H 'content-type: application/json' \
     -d '{"messages":[{"role":"user","content":"   "}]}'          # 422 · 두 번째 선에서 끝

cd be && .venv/bin/python -m app.engines.smoke                    # 보라 · 웹 없이

추론 서버를 끈 채 같은 요청을 보내면 빨간 경로를 볼 수 있다. 채팅은 503 {"error_code":"unavailable"}, /healthz는 여전히 200이다.

이 그림이 보여주지 않는 것

항목왜 없나
import 관계요청이 옮겨 가는 순간만 그렸다. schemas/chat.pybase.py의 오류 코드를 가져다 쓰는 것 같은 참조는 선이 아니다
streaming아직 계약 자체가 없다. 4단계에서 조각 단위 경로가 생긴다
대화 저장5~7단계. database 노드가 추가될 자리다
인증6단계. 브라우저와 route 사이에 검사 한 겹이 더 생긴다
동시 요청선은 요청 하나의 경로다. 생성 4·probe 2라는 연결 수 제한은 그림에 없다

색을 이렇게 고른 이유

여섯 색을 눈으로 고르지 않고 검증기로 계산해서 정했다. 8개 후보에서 6개를 뽑는 28가지 조합을 전부 돌려, 모든 쌍이 구분되는 조합만 남겼다.

[PASS] Lightness band       all 6 inside L 0.43–0.77
[PASS] Chroma floor         all 6 >= 0.1
[WARN] CVD separation       worst #e34948↔#1baf7a ΔE 6.9 (deutan)
[PASS] Normal-vision floor  worst #008300↔#1baf7a ΔE 15.6
[WARN] Contrast vs surface  #1baf7a 2.82 · #eda100 2.17 — 라벨 또는 표 필요
→ ALL CHECKS PASS

경고 두 개가 붙는 조건이 있고, 이 페이지가 그것을 지킨다.

경고요구 조건이 페이지가 한 것
CVD ΔE 6.9 (6–8 구간)색 외의 보조 부호화가 있어야 함흐름마다 선 모양을 다르게 하고 범례에도 그 모양을 보여준다
대비 3:1 미만 2개보이는 라벨 또는 표가 있어야 함선마다 글자 라벨이 있고 전체 이동 목록 표를 함께 싣는다

그래서 색을 전혀 구분하지 못해도 이 그림을 읽을 수 있다. 선 모양이 다르고, 라벨이 있고, 같은 내용이 표에 있다.

← 이전서비스 학습 홈다음 →Backend 상세