테스트 스크립트
set -eo pipefail
COLOR_GREEN=`tput setaf 2;`
COLOR_NC=`tput sgr0;`
echo "Starting black"
poetry run black .
echo "OK"
echo "Starting ruff"
poetry run ruff check --select I --fix
poetry run ruff check --fix
echo "OK"
echo "Starting mypy"
poetry run mypy .
echo "OK"
echo "Starting pytest with coverage"
poetry run coverage run -m pytest
poetry run coverage report -m
poetry run coverage html
echo "OK"
echo "${COLOR_GREEN}All tests passed successfully!${COLOR_NC}"
getattr , hasattr
getattr(object, name[, default])
- object → 속성을 가져올 객체
- name → 문자열로 된 속성 이름
- default(선택) → 속성이 없을 때 대신 반환할 값 (없으면 AttributeError 발생)
class User:
def __init__(self, username, age):
self.username = username
self.age = age
user = User("kihoon", 25)
print(getattr(user, "username"))
print(getattr(user, "age"))
print(getattr(user, "email", "no email"))
- hasattr(object, name) : 객체가 특정속성을 가지고 있는지 확인하는 내장 함수
- object: 속성을 확인할 객체
- name: 문자열로 된 속성 이름
class User:
def __init__(self, username):
self.username = username
user = User("kihoon")
print(hasattr(user, "username"))
print(hasattr(user, "age"))
- setattr(object, name, value) : 객체의 속성(변수)을 동적으로 설정할때 사용
- 속성 설정/추가
- object: 속성을 설정할 대상 객체
- name: 속성 이름 (문자열)
- value: 속성에 넣을 값
class User:
pass
user = User()
setattr(user, "username", "kim")
setattr(user, "age", 25)
print(user.username)
print(user.age)
username: str | None = None
- username: str | None
- username 변수는 문자열(str) 이거나 아예 값이 없는 상태(None)일 수 있다는 뜻
- = None
- 기본값을 None으로 지정
- 이 변수를 꼭 넣지 않아도 되고, 값을 안 주면 자동으로 None이 들어감
items
- kwargs.items() : 딕셔너리의 (키, 값) 쌍을 반환
kwargs = {"username": "kim", "age": 25}
for key, value in kwargs.items():
print(key, value)
- 1번째 반복 : username kim 출력
- key = "username"
- value = "kim"
- 2번째 반복 : age 25 출력
@classmethod
- @classmethod에서는 첫 번째 인자로 cls를 받는데, 이건 메서드를 호출한 클래스 자신을 의미
class UserModel:
_data = []
_id_counter = 1
@classmethod
def get(cls, **kwargs):
for user in cls._data:
...
- cls는 UserModel 그 자체(클래스)를 가리킴
- 따라서 cls._data는 사실상 UserModel._data와 똑같음
상속의 경우
class UserModel:
_data = []
@classmethod
def get(cls):
return cls._data
class AdminUserModel(UserModel):
_data = ["admin"]
print(UserModel.get())
print(AdminUserModel.get())
- UserModel을 상속한 AdminUserModel 같은 클래스가 있다면, cls는 AdminUserModel이 됨
유효성 검사 옵션
| 옵션 | 의미 | 예시 코드 | 허용되는 값 |
|---|
gt=n | n보다 커야 함 (greater than) | user_id: int = Path(gt=0) | 1, 2, 3, ... |
ge=n | n 이상 (greater or equal) | age: int = Query(ge=18) | 18, 19, 20, ... |
lt=n | n보다 작아야 함 (less than) | score: int = Query(lt=100) | ..., 97, 98, 99 |
le=n | n 이하 (less or equal) | level: int = Query(le=10) | 0, 1, 2, ..., 10 |
from fastapi import FastAPI, Path, Query
app = FastAPI()
@app.get("/users/{user_id}")
def read_user(
user_id: int = Path(gt=0),
age: int = Query(ge=18),
score: int = Query(lt=100),
level: int = Query(le=10)
):
return {"user_id": user_id, "age": age, "score": score, "level": level}
@app.get("/users/{user_id}")
def read_user(user_id: int = Path(gt=0)):
return user_id
@app.get("/search")
def search(q: str = Query(min_length=3, max_length=10)):
return {"query": q}
from pydantic import BaseModel, Field
class User(BaseModel):
username: str = Field(min_length=3, max_length=20, regex="^[a-zA-Z0-9_]+$")
age: int = Field(ge=18, le=100)
@app.post("/users")
def create_user(user: User):
return user
async
async def는 "이 함수는 비동기 함수(Asynchronous Function)" 라는 선언
| 구분 | def (동기 함수) | async def (비동기 함수) |
|---|
| 실행 방식 | 순차적으로 한 줄씩 실행됨 | 여러 작업을 동시에(병렬처럼) 진행 가능 |
| CPU 점유 | 한 작업이 끝나야 다음 실행 | I/O 대기 시간 동안 다른 작업 수행 가능 |
| 반환값 | 일반 값 (ex: int, str, dict) | Coroutine 객체 반환 |
| 호출 방법 | 그냥 func() | await func() 로 호출해야 함 |
| 사용 예 | 단순 계산, 로컬 처리 | 네트워크 요청, DB 조회, 파일 I/O 등 느린 작업 |
- def는 한개의 작업이 끝나야 다음 작업이 실행 가능한데
- async def는 동시의 여러개의 작업이 가능함 (병렬느낌)
- FastAPI는 내부적으로 비동기 서버(ASGI) 기반
- DB, API, 파일 입출력 같은 I/O 작업이 많은 웹 서버에서 async def를 쓰면
- CPU를 낭비하지 않고 동시에 여러 요청을 처리할 수 있음
from fastapi import FastAPI
app = FastAPI()
@app.get("/data")
async def get_data():
await asyncio.sleep(3)
return {"message": "3초 후 응답 완료"}
- 비동기 함수를 실행하려면 await를 쓰거나, asyncio.run() 같은 함수를 사용해야 한다
import asyncio
async def hello():
print("안녕!")
await asyncio.sleep(1)
print("잘가!")
asyncio.run(hello())
- 예외적으로 FastAPI 같은 프레임워크에서는 asyncio 안 써도 됨
- FastAPI 내부에는 이미 이벤트 루프가 돌아가고 있어서 개발자가 직접 asyncio.run()을 쓸 필요가 없음
- 하지만 “FastAPI는 asyncio 위에서 동작하지만, asyncio를 직접 쓴다면 import asyncio는 해야 한다.(ex: await asyncio.sleep(3))
from fastapi import FastAPI
app = FastAPI()
@app.get("/test")
async def test():
return {"msg": "async 함수지만 asyncio import 필요 없음"}
model_config
- Pydantic v2에서 이전의 방식 class Config: 대신 모델의 설정(config) 을 지정하는 방법
- model_config = {"extra": "forbid"}
- 정의되지 않은 필드(예상치 못한 값)가 들어오면 에러를 내라
| 설정 | 의미 | 사용 상황 |
|---|
ignore | 정의되지 않은 필드는 무시 | 가벼운 데이터 처리 시 |
allow | 정의되지 않은 필드도 허용 | 유연한 입력 허용 시 |
forbid | 정의되지 않은 필드면 에러 발생 ⚠️ | API 검증을 엄격히 할 때 (✅ 추천) |
class User(BaseModel):
model_config = {"extra": "forbid"}
model_config = {"extra": "allow"}
model_config = {"extra": "ignore"}
username: str
model_dump()
model_dump()는 Pydantic v2에서 새로 도입된 인스턴스 메서드
- BaseModel을 상속한 객체(모델 인스턴스)에 대해 사용할 수 있다.
- model_dump()는 단순히 딕셔너리로 바꾸는 것뿐 아니라
- exclude_none=True : None값 제외 (user.model_dump(exclude_none=True))
- include={'username'} : 특정 필드만 포함
- exclude={'password'} : 특정 필드 제외
@app.post("/users")
def create_user(data: UserCreateRequest):
user = UserModel.create(**data.model_dump())
return user.id
- data.model_dump()
- 요청으로 받은 UserCreateRequest 객체를 딕셔너리로 변환해서 create() 함수의 인자로 넘기기 위한 것
| 구분 | Pydantic v1 | Pydantic v2 | 설명 |
|---|
| 딕셔너리 변환 | model.dict() | model.model_dump() | 모델 인스턴스를 dict로 변환 |
| JSON 변환 | model.json() | model.model_dump_json() | JSON 문자열로 변환 |
async 와 await의 기본 관계
- async: 비동기 함수(코루틴) 정의 = “비동기로 실행될 수 있는 함수”를 만든다.
- await: 비동기 작업의 결과를 기다림 = “그 함수가 끝날 때까지 잠시 멈춘다”
동기 비동기 코드 비교
def make_ramen():
boil_water()
put_noodle()
eat()
async def make_ramen():
await boil_water()
await put_noodle()
await eat()
의존성
- “경로 함수(엔드포인트)가 동작하는 데 필요한 외부의 로직이나 자원을 주입받는 방식”
- “경로 함수에서 필요로 하는 공통 자원을 외부 함수로 분리하고,
- FastAPI가 알아서 실행하고 주입해주는 구조”
| 역할 | 설명 | 예시 |
|---|
| 공통 로직 분리 | 인증, DB 연결, 설정 로드 등 공통 부분을 재사용 | get_current_user, get_db |
| 자동 주입 | FastAPI가 알아서 실행 순서를 조정하고 값을 주입 | Depends(get_db) |
| 테스트 용이성 | 테스트 시 mock 객체를 넣기 쉬움 | 유닛 테스트 시 DB 대체 |
| 가독성 향상 | 경로 함수가 “핵심 기능”만 담당 | 인증/검증은 분리 |
예제
from fastapi import Depends, HTTPException
def get_current_user(token: str = "fake-token"):
if token != "fake-token":
raise HTTPException(status_code=403, detail="Invalid token")
return {"username": "kihoon"}
@app.get("/users/me")
async def read_user(current_user: dict = Depends(get_current_user)): ⭐️
return current_user
def get_db():
db = connect_to_database()
try:
yield db
finally:
db.close()
@app.get("/items/")
async def get_items(db=Depends(get_db)): ⭐️
return db.query("SELECT * FROM items")
- yield는 요청 전후 처리를 함께 관리할 때 사용
- “DB 세션 열기 → 요청 처리 → 세션 닫기” 구조
미들웨어
- 요청(Request) → 미들웨어 ⭐️ → 엔드포인트(경로 함수) → 응답(Response)
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http") ⭐️
async def my_middleware(request: Request, call_next):
print("📥 요청이 들어옴:", request.url.path)
response = await call_next(request)
print("📤 응답이 나감:", response.status_code)
return response
- request: 들어온 HTTP 요청 객체
- call_next: 요청을 다음 단계로 넘겨주는 함수 (엔드포인트 실행 담당)
- response: 실제 엔드포인트에서 반환된 응답 객체
예시
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(round(process_time, 3))
return response
- 결과적으로 클라이언트는 응답 헤더에서 처리 시간을 볼 수 있음:
X-Process-Time: 0.005
JWT (JSON Web Token)
- “사용자의 인증 정보를 안전하게 주고받기 위한 토큰 형식”
- 즉, 서버가 인증된 사용자임을 증명하는 증명서 같은 것을 발급해 주는 것
- 이후 사용자가 이걸 요청마다 들고다니면, 서버는 다시 로그인 검사를 안해도 됨
JWT 방식 (토큰 기반)
- 로그인 성공 시 서버가 토큰을 발급
- 클라이언트는 그걸 로컬스토리지 / 헤더에 저장
- 이후 요청 시 헤더에
Authorization: Bearer <JWT> 붙여서 전송
- 서버는 DB 안 보고 토큰 자체를 검증해서 신뢰
- 장점
- 서버가 “상태를 저장하지 않음”(stateless)
- 확장성 높고 빠름
JWT 구조
- 세 부분으로 나눠져 있으며 각 부분은 Base64로 인코딩되어 하나의 문자열로 이어짐
| 구분 | 이름 | 역할 |
|---|
| 1️⃣ | Header | 토큰 타입(JWT)과 해싱 알고리즘 정보 (HS256, RS256 등) |
| 2️⃣ | Payload | 실제 데이터(사용자 ID, 만료 시간 등) |
| 3️⃣ | Signature | 위 두 부분을 비밀키로 서명한 값 — 위조 방지용 |
Header:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:
{
"sub": "user123",
"name": "Kihoon",
"exp": 1739659223
}
# 결과
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyMTIzIiwibmFtZSI6IktoaG9vbiIsImV4cCI6MTczOTY1OTIyM30.
YH4I0B3gQG3xM7bPrlK6E4Zw5n6G7N-s6H0Md7K7owk
JWT 동작 흐름
- 로그인 시
- 클라이언트가 ID/PW로 로그인
- 서버가 검증 후 JWT 생성 (access_token)
- 클라이언트는 그걸 저장 (보통 Authorization 헤더나 로컬스토리지)
- 인증이 필요한 경우
- 클라이언트가
Authorization: Bearer <token> 붙여서 요청
- 서버가 토큰을 복호화 및 검증
- 정상 토큰이면 사용자 정보 추출 → 요청 수행
FastAPI 예시
from datetime import datetime, timedelta
from jose import JWTError, jwt
SECRET_KEY = "mysecretkey"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_token(token: str):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
return None
Annotated[X,Y]
- “이 값의 타입은 X인데, 추가적인 정보 Y도 함께 붙인다” 라는 뜻
from typing import Annotated
Age = Annotated[int, "단위: 세"] ⭐️
def set_age(age: Age):
print(age)
- 이 코드는 사실상
def set_age(age: int) 과 똑같이 작동
- “이 Age는 int이면서 설명 정보가 있다” 라는 메타데이터를 함께 기록
- 메타데이터: API 전체(혹은 각 경로)에 대한 설명 정보를 담는 데이터
- 메타데이터는 실제 API 동작에는 영향을 주지 않음
- 자동 문서화(Swagger UI, ReDoc) 에서 설명을 표시하기 위해 사용
| 입력 위치 | Swagger에서 표시 | 예시 |
|---|
| 헤더(Header) | “Headers” 영역 | X-Token: … |
| 쿼리(Query) | “Query params” 영역 | ?q=… |
| 본문(Body) | “Request body” 영역 | JSON / form 등 |
- Swagger에선 “사용자가 값을 넣는 입력창이 생긴다”는 점에서 비슷하게 보임
- 하지만 FastAPI가 어디에서 값을 꺼내는지가 완전히 다르다.
일반 매개변수 / 헤더 매개변수 비교
@app.get("/items/")
async def read_items(q: str | None = None):
return {"q": q}
@app.get("/items/")
async def read_items(x_token: Annotated[str, Header()]):
return {"token": x_token}
@app.get("/check/")
async def check(
q: str | None = None,
x_token: Annotated[str | None, Header()] = None,
):
return {"query": q, "header": x_token}
- q 값은 URL의 쿼리스트링(?q=hello)에서 가져옴.
- x_token 값은 HTTP 헤더(X-Token) 에서 가져옴.
헤더도 사람이 입력하는 값이고 중요한점은 “입력 위치(어디에 넣느냐)” 와 “의미(무엇을 위한 데이터냐)” 가 다르다는 것
왜 이렇게 나뉘어져 있을까
| 구분 | 위치 | 용도 | 예시 |
|---|
| 쿼리(Query) | URL ?q=value | 검색/필터/옵션 | /search?q=python |
| 본문(Body) | 요청 내용(JSON, form 등) | 실제 데이터 | { "username": "kihoon" } |
| 헤더(Header) | 요청 메타정보 | 인증, 형식 등 | Authorization: Bearer abc |
| 위치 | 예시 | 목적 | FastAPI에서 사용하는 방식 |
|---|
| 쿼리(Query) | /items?q=apple | 요청 데이터나 검색 조건 | q: str or Query() |
| 본문(Body) | { "username": "kihoon" } | 실제 리소스 데이터 (POST/PUT 등) | user: UserSchema or Body() |
| 헤더(Header) | X-Token: abc123 | 인증, 형식, 언어, 클라이언트 정보 등 | x_token: Header() |