KR_FastAPI - somaz94/python-study GitHub Wiki
FastAPI๋ ํ๋์ ์ด๊ณ ๋น ๋ฅธ ์น ํ๋ ์์ํฌ์ด๋ค.
from fastapi import FastAPI, HTTPException, Query, Path
from pydantic import BaseModel, Field, EmailStr
from typing import List, Optional
app = FastAPI(
title="My API",
description="API ์ค๋ช
",
version="0.1.0"
)
class User(BaseModel):
id: int
name: str
email: EmailStr
is_active: bool = True
class Config:
schema_extra = {
"example": {
"id": 1,
"name": "ํ๊ธธ๋",
"email": "[email protected]",
"is_active": True
}
}
# ์ธ๋ฉ๋ชจ๋ฆฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค
users_db = {}
# ๊ธฐ๋ณธ ๋ผ์ฐํธ
@app.get("/")
async def root():
return {"message": "Hello World"}
# ๊ฒฝ๋ก ๋งค๊ฐ๋ณ์
@app.get("/users/{user_id}", response_model=User)
async def get_user(user_id: int = Path(..., title="์ฌ์ฉ์ ID", ge=1)):
if user_id not in users_db:
raise HTTPException(status_code=404, detail="์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
return users_db[user_id]
# ์์ฒญ ๋ณธ๋ฌธ
@app.post("/users/", response_model=User, status_code=201)
async def create_user(user: User):
if user.id in users_db:
raise HTTPException(status_code=400, detail="์ด๋ฏธ ์กด์ฌํ๋ ์ฌ์ฉ์ ID์
๋๋ค")
users_db[user.id] = user
return user
# ์ฟผ๋ฆฌ ๋งค๊ฐ๋ณ์
@app.get("/users/", response_model=List[User])
async def list_users(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100)
):
return list(users_db.values())[skip : skip + limit]
# PUT ์์ฒญ
@app.put("/users/{user_id}", response_model=User)
async def update_user(user_id: int, user: User):
if user_id not in users_db:
raise HTTPException(status_code=404, detail="์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
users_db[user_id] = user
return user
# DELETE ์์ฒญ
@app.delete("/users/{user_id}", status_code=204)
async def delete_user(user_id: int):
if user_id not in users_db:
raise HTTPException(status_code=404, detail="์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
del users_db[user_id]
return None
โ
ํน์ง:
- ๋น ๋ฅธ ์ฑ๋ฅ
- ์๋ API ๋ฌธ์ํ
- ํ์ ํํธ ์ง์
- OpenAPI ๋ฐ JSON ์คํค๋ง ์๋ ์์ฑ
- Python 3.6+ ๋น๋๊ธฐ ๊ธฐ๋ฅ ํ์ฉ
FastAPI๋ ์๋์ผ๋ก /docs
์ /redoc
์๋ํฌ์ธํธ๋ฅผ ์ ๊ณตํ์ฌ API ๋ฌธ์๋ฅผ ์์ฑํ๋ค.
-
/docs
: Swagger UI๋ฅผ ์ด์ฉํ ๋ํํ API ๋ฌธ์ -
/redoc
: ReDoc์ ์ด์ฉํ ๋ฌธ์
FastAPI์ ๊ฐ๋ ฅํ ์์กด์ฑ ์ฃผ์
์์คํ
์ ํ์ฉํ ์ฝ๋ ๊ตฌ์ฑ ๋ฐฉ๋ฒ์ด๋ค.
from fastapi import Depends, HTTPException, FastAPI
from sqlalchemy.orm import Session
from typing import Annotated, List, Optional
from database import SessionLocal, engine, Base
from models import UserModel
from schemas import UserCreate, UserResponse
app = FastAPI()
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์กด์ฑ
async def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
DB = Annotated[Session, Depends(get_db)]
# ์๋น์ค ํด๋์ค
class UserService:
def __init__(self, db: Session = Depends(get_db)):
self.db = db
def get_user(self, user_id: int):
user = self.db.query(UserModel).filter(
UserModel.id == user_id
).first()
if not user:
raise HTTPException(
status_code=404,
detail="์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค"
)
return user
def create_user(self, user: UserCreate):
db_user = UserModel(**user.dict())
self.db.add(db_user)
self.db.commit()
self.db.refresh(db_user)
return db_user
def get_users(self, skip: int = 0, limit: int = 100):
return self.db.query(UserModel).offset(skip).limit(limit).all()
# ์ปจํธ๋กค๋ฌ์์ ์์กด์ฑ ์ฃผ์
ํ์ฉ
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int,
user_service: UserService = Depends()
):
return user_service.get_user(user_id)
@app.post("/users/", response_model=UserResponse)
async def create_user(
user: UserCreate,
user_service: UserService = Depends()
):
return user_service.create_user(user)
# ํจ์ ํํ์ ์์กด์ฑ
async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
return {"params": commons}
# ์์กด์ฑ ์ฒด์ด๋
async def verify_token(x_token: str = Header(...)):
if x_token != "fake-super-secret-token":
raise HTTPException(status_code=400, detail="X-Token ํค๋๊ฐ ์ ํจํ์ง ์์ต๋๋ค")
return x_token
async def verify_key(x_key: str = Header(...), token: str = Depends(verify_token)):
if x_key != "fake-super-secret-key":
raise HTTPException(status_code=400, detail="X-Key ํค๋๊ฐ ์ ํจํ์ง ์์ต๋๋ค")
return x_key
@app.get("/secure-items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_secure_items():
return [{"item": "Secure Item"}]
โ
ํน์ง:
- ์์กด์ฑ ๊ด๋ฆฌ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ์
- ์๋น์ค ๊ณ์ธต ๋ถ๋ฆฌ
- ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์์กด์ฑ
- ์์กด์ฑ ์ฒด์ด๋
์ ํ๋ฆฌ์ผ์ด์
์ ๋ฐ์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ ๋ฏธ๋ค์จ์ด์ ๋ณด์ ์ค์ ์ด๋ค.
from fastapi import FastAPI, Depends, HTTPException, Security, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.middleware.cors import CORSMiddleware
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
import time
from typing import Dict
app = FastAPI()
# CORS ๋ฏธ๋ค์จ์ด
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # ํ๋ก๋์
์์๋ ํน์ ๋๋ฉ์ธ ์ง์
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ์ฑ๋ฅ ์ธก์ ๋ฏธ๋ค์จ์ด
@app.middleware("http")
async def add_process_time_header(request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
# ๋ณด์ ์ค์
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
SECRET_KEY = "YOUR_SECRET_KEY_HERE" # ํ๋ก๋์
์์๋ ํ๊ฒฝ๋ณ์ ์ฌ์ฉ
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# ์ธ์ฆ
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
"hashed_password": get_password_hash("secret"),
"disabled": False,
}
}
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="์ธ์ฆํ ์ ์์ต๋๋ค",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = fake_users_db.get(username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user: dict = Security(get_current_user)):
if current_user.get("disabled"):
raise HTTPException(status_code=400, detail="๋นํ์ฑํ๋ ์ฌ์ฉ์์
๋๋ค")
return current_user
# ๋ก๊ทธ์ธ ์๋ํฌ์ธํธ
@app.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = fake_users_db.get(form_data.username)
if not user or not verify_password(form_data.password, user["hashed_password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="์ ํจํ์ง ์์ ์ฌ์ฉ์๋ช
๋๋ ๋น๋ฐ๋ฒํธ์
๋๋ค",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user["username"]}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
# ๋ณดํธ๋ ์๋ํฌ์ธํธ
@app.get("/users/me", response_model=Dict)
async def read_users_me(current_user: dict = Depends(get_current_active_user)):
return current_user
โ
ํน์ง:
- JWT ์ธ์ฆ
- ๋ฏธ๋ค์จ์ด ์ฒ๋ฆฌ
- ๋ณด์ ์ค์
- ์ํธํ
- CORS ์ค์
FastAPI์ ๋น๋๊ธฐ ๊ธฐ๋ฅ์ ํ์ฉํ ๊ณ ์ฑ๋ฅ ์ ํ๋ฆฌ์ผ์ด์
๊ตฌํ์ด๋ค.
import asyncio
import httpx
from fastapi import FastAPI, BackgroundTasks, Depends
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.future import select
from typing import List
app = FastAPI()
# ๋น๋๊ธฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/dbname"
async_engine = create_async_engine(DATABASE_URL)
AsyncSessionLocal = sessionmaker(
async_engine, class_=AsyncSession, expire_on_commit=False
)
async def get_async_session():
async with AsyncSessionLocal() as session:
yield session
# ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
def write_log(message: str):
with open("log.txt", mode="a") as log:
log.write(f"{message}\n")
async def send_email_async(email: str, message: str):
# ์ด๋ฉ์ผ ์ ์ก ์๋ฎฌ๋ ์ด์
await asyncio.sleep(1) # API ํธ์ถ ์๋ฎฌ๋ ์ด์
return {"email": email, "message": message, "status": "sent"}
@app.post("/send-notification/{email}")
async def send_notification(
email: str,
background_tasks: BackgroundTasks
):
# ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋ก๊ทธ ๊ธฐ๋ก
background_tasks.add_task(write_log, f"์๋ฆผ ์ ์ก: {email}")
# ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ด๋ฉ์ผ ์ ์ก
background_tasks.add_task(send_email_async, email, "์ค์ ์๋ฆผ์
๋๋ค")
return {"message": "์๋ฆผ์ด ์ ์ก๋์์ต๋๋ค"}
# ๋น๋๊ธฐ HTTP ์์ฒญ
@app.get("/github-stars/{username}")
async def get_github_stars(username: str):
async with httpx.AsyncClient() as client:
response = await client.get(f"https://api.github.com/users/{username}")
if response.status_code != 200:
return {"error": "GitHub API ํธ์ถ ์คํจ"}
user_data = response.json()
# ๋ณ๋ ฌ API ํธ์ถ
repos_response = await client.get(f"https://api.github.com/users/{username}/repos")
repos = repos_response.json()
total_stars = sum(repo["stargazers_count"] for repo in repos)
return {
"username": username,
"repos_count": len(repos),
"total_stars": total_stars
}
# ๋น๋๊ธฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์
async def get_user_async(async_session: AsyncSession, user_id: int):
result = await async_session.execute(
select(User).where(User.id == user_id)
)
return result.scalar_one_or_none()
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
async_session: AsyncSession = Depends(get_async_session)
):
user = await get_user_async(async_session, user_id)
if not user:
raise HTTPException(status_code=404, detail="์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
return user
# ๋ณ๋ ฌ ์ฒ๋ฆฌ
@app.get("/parallel-tasks")
async def parallel_tasks():
start_time = time.time()
# ๋ณ๋ ฌ๋ก ๋น๋๊ธฐ ์์
์คํ
task1 = asyncio.create_task(asyncio.sleep(1))
task2 = asyncio.create_task(asyncio.sleep(1))
task3 = asyncio.create_task(asyncio.sleep(1))
# ๋ชจ๋ ์์
์๋ฃ ๋๊ธฐ
await asyncio.gather(task1, task2, task3)
end_time = time.time()
return {
"message": "๋ชจ๋ ๋ณ๋ ฌ ์์
์๋ฃ",
"time_taken": end_time - start_time # ์ฝ 1์ด
}
โ
ํน์ง:
- ๋น๋๊ธฐ ์ฒ๋ฆฌ
- ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
- ์ฑ๋ฅ ์ต์ ํ
- ๋ณ๋ ฌ API ํธ์ถ
- ๋น๋๊ธฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ์ฐ
WebSocket ๋ฐ ์๋ฒ-์ ์ก ์ด๋ฒคํธ๋ฅผ ํ์ฉํ ์ค์๊ฐ ์ ํ๋ฆฌ์ผ์ด์
๊ตฌํ์ด๋ค.
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends
from fastapi.responses import StreamingResponse
import asyncio
import json
from datetime import datetime
from typing import List, Dict
app = FastAPI()
# WebSocket ๊ด๋ฆฌ์
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def send_personal_message(self, message: str, websocket: WebSocket):
await websocket.send_text(message)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
# WebSocket ์๋ํฌ์ธํธ
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_text()
# ๊ฐ์ธ ๋ฉ์์ง ์ ์ก
await manager.send_personal_message(f"๋น์ : {data}", websocket)
# ๋ธ๋ก๋์บ์คํธ ๋ฉ์์ง ์ ์ก
await manager.broadcast(f"ํด๋ผ์ด์ธํธ #{client_id}์ ๋ฉ์์ง: {data}")
except WebSocketDisconnect:
manager.disconnect(websocket)
await manager.broadcast(f"ํด๋ผ์ด์ธํธ #{client_id}๊ฐ ํด์ฅํ์ต๋๋ค")
# ์๋ฒ-์ ์ก ์ด๋ฒคํธ (SSE)
@app.get("/sse")
async def sse():
async def event_generator():
for i in range(10):
# JSON ๋ฐ์ดํฐ ์์ฑ
data = json.dumps({
"id": i,
"timestamp": datetime.now().isoformat(),
"message": f"์ด๋ฒคํธ #{i}"
})
yield f"data: {data}\n\n"
await asyncio.sleep(1)
return StreamingResponse(
event_generator(),
media_type="text/event-stream"
)
# WebSocket ํด๋ผ์ด์ธํธ ์์ (JavaScript)
"""
const ws = new WebSocket("ws://localhost:8000/ws/1");
ws.onmessage = function(event) {
console.log('๋ฉ์์ง ์์ :', event.data);
};
ws.onclose = function(event) {
console.log('์ฐ๊ฒฐ ์ข
๋ฃ:', event);
};
ws.send("์๋
ํ์ธ์!");
// SSE ํด๋ผ์ด์ธํธ
const eventSource = new EventSource("/sse");
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("์ด๋ฒคํธ ์์ :", data);
};
eventSource.onerror = (error) => {
console.error("SSE ์ค๋ฅ:", error);
eventSource.close();
};
"""
โ
ํน์ง:
- ์๋ฐฉํฅ ์ค์๊ฐ ํต์
- ์๋ฒ-์ ์ก ์ด๋ฒคํธ
- ๋ฉ์์ง ๋ธ๋ก๋์บ์คํ
- ์ฐ๊ฒฐ ๊ด๋ฆฌ
- ๋น๋๊ธฐ ์ด๋ฒคํธ ์คํธ๋ฆฌ๋ฐ
FastAPI ์ ํ๋ฆฌ์ผ์ด์
์ ํ
์คํธ ๋ฐ ๋ฐฐํฌ ๋ฐฉ๋ฒ์ด๋ค.
# test_main.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
def test_create_user():
user_data = {
"id": 1,
"name": "Test User",
"email": "[email protected]",
"is_active": True
}
response = client.post("/users/", json=user_data)
assert response.status_code == 201
assert response.json()["id"] == user_data["id"]
assert response.json()["name"] == user_data["name"]
def test_get_user():
# ๋จผ์ ์ฌ์ฉ์ ์์ฑ
user_data = {
"id": 2,
"name": "Another User",
"email": "[email protected]",
"is_active": True
}
client.post("/users/", json=user_data)
# ์ฌ์ฉ์ ์กฐํ
response = client.get("/users/2")
assert response.status_code == 200
assert response.json()["name"] == user_data["name"]
def test_get_nonexistent_user():
response = client.get("/users/999")
assert response.status_code == 404
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# production ์ค์
ENV PORT=8000
ENV HOST=0.0.0.0
ENV WORKERS=4
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
# gunicorn_conf.py
from multiprocessing import cpu_count
# Gunicorn ์ค์
bind = "0.0.0.0:8000"
workers = cpu_count() * 2 + 1
worker_class = "uvicorn.workers.UvicornWorker"
keepalive = 120
timeout = 120
graceful_timeout = 120
max_requests = 10000
max_requests_jitter = 1000
# ๋ฐฐํฌ ๋ช
๋ น์ด
gunicorn -c gunicorn_conf.py main:app
โ
ํน์ง:
- ์๋ํ๋ ํ ์คํธ
- ์ปจํ ์ด๋ํ
- ์ฑ๋ฅ ์ต์ ํ
- ํ๋ก๋์ ์ค์
- CI/CD ํตํฉ
โ
๋ชจ๋ฒ ์ฌ๋ก:
- ํ์ ํํธ ํ์ฉ: Pydantic ๋ชจ๋ธ ๋ฐ ํ์ ์ด๋ ธํ ์ด์ ์ผ๋ก ์ฝ๋ ์์ ์ฑ ํฅ์
- ์์กด์ฑ ์ฃผ์ ํ์ฉ: ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ ๊ฐ๋ฐ ๋ฐ ํ ์คํธ ์ฉ์ด์ฑ ์ฆ๋
- ๋น๋๊ธฐ ์ฒ๋ฆฌ ์ต์ ํ: ๋์์ฑ ๋ฐ I/O ๋ฐ์ด๋ ์์ ์ต์ ํ๋ก ์ฑ๋ฅ ํฅ์
- ๋ฌธ์ํ ์๋ํ: OpenAPI ์คํค๋ง ๋ฐ Swagger UI๋ก API ๋ฌธ์ ์๋ํ
- ํ ์คํธ ์ฝ๋ ์์ฑ: TestClient๋ฅผ ํ์ฉํ ์๋ํฌ์ธํธ ํ ์คํธ ์๋ํ
- ์๋ฌ ์ฒ๋ฆฌ ํ์คํ: ์ผ๊ด๋ ์๋ฌ ์๋ต์ผ๋ก ํด๋ผ์ด์ธํธ ๊ฐ๋ฐ ํธ์์ฑ ํฅ์
- ์บ์ฑ ์ ๋ต ์๋ฆฝ: Redis๋ FastAPI ์บ์ฑ ๋ฏธ๋ค์จ์ด๋ก ์ฑ๋ฅ ์ต์ ํ
- ๋ณด์ ์ค์ ํ์ธ: ์ ์ ํ ์ธ์ฆ, ๊ถํ ํ์ธ ๋ฐ CORS ์ค์ ๊ตฌํ
- ํ๊ฒฝ ๋ณ์ ๊ด๋ฆฌ: ๊ฐ๋ฐ, ํ ์คํธ, ํ๋ก๋์ ํ๊ฒฝ๋ณ ์ค์ ๋ถ๋ฆฌ
- ๋ก๊น ์์คํ ๊ตฌ์ถ: ๊ตฌ์กฐํ๋ ๋ก๊น ์ผ๋ก ๋๋ฒ๊น ๋ฐ ๋ชจ๋ํฐ๋ง ๊ฐํ
- ๋น์ฆ๋์ค ๋ก์ง ๋ถ๋ฆฌ: ์ปจํธ๋กค๋ฌ์ ์๋น์ค ๋ ์ด์ด ๋ถ๋ฆฌ๋ก ์ฝ๋ ์กฐ์งํ
- ๋ง์ด๊ทธ๋ ์ด์ ๊ด๋ฆฌ: Alembic์ ํ์ฉํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ๊ด๋ฆฌ
- API ๋ฒ์ ๊ด๋ฆฌ: ๊ฒฝ๋ก๋ ํค๋ ๊ธฐ๋ฐ API ๋ฒ์ ๊ด๋ฆฌ ์ ๋ต ์๋ฆฝ
- ๋ ์ดํธ ๋ฆฌ๋ฏธํ : ํธ๋ํฝ ์ ํ์ผ๋ก ์๋น์ค ์์ ์ฑ ๋ณด์ฅ
- ํฌ์ค ์ฒดํฌ ๊ตฌํ: ์ ํ๋ฆฌ์ผ์ด์ ์ํ ๋ชจ๋ํฐ๋ง ์๋ํฌ์ธํธ ์ ๊ณต