Week 19 ํ์ต ์ ๋ฆฌ
Product Serving ๊ฐ์
๋ชจ๋ธ์ด๋ ํ๋ก๊ทธ๋จ์ ๊ฐ๋ฐํ ํ์๋ ์ด๋ฅผ ํด๋ผ์ด์ธํธ๊ฐ ์ค์ ๋ก ์ฌ์ฉํ ์ ์๋๋ก ์ ํ์ผ๋ก ๋ฐฐํฌํด์ผ ํฉ๋๋ค. ์ด๋ฅผ serving์ด๋ผ๊ณ ํ๋ฉฐ, ๋ํ์ ์ธ ์๋ก ChatGPT๊ฐ prompt๋ฅผ ํตํด ์ฌ์ฉ์ ์์ฒญ์ ์๋ตํ๋ ๋ฐฉ์์ ๋ค ์ ์์ต๋๋ค.
serving ๋ฐฉ์์ ํฌ๊ฒ Batch Serving๊ณผ Online (Real Time) Serving ๋ ๊ฐ์ง๋ก ๊ตฌ๋ถ๋๋ฉฐ, ๋ฌธ์ ์ํฉ, ์ ์ฝ ์กฐ๊ฑด, ์ธ๋ ฅ, ๋ฐ์ดํฐ ์ ์ฅ ํํ, ๋ ๊ฑฐ์ ์์คํ
์ ๋ฌด ๋ฑ์ ๋ฐ๋ผ ์ ํฉํ ๋ฐฉ์์ ์ ํํ ์ ์์ต๋๋ค.
1. Batch Serving
1-1. ๊ฐ๋ ๋ฐ ํน์ง
- ์ ์:
๋ฐ์ดํฐ๋ฅผ ์ผ์ ํ ๋ฌถ์ ๋จ์๋ก ์ฒ๋ฆฌํ์ฌ ์๋นํ๋ ๋ฐฉ์์ ๋๋ค. - ์ ์ฉ ์ํฉ:
- ์ค์๊ฐ ์๋ต์ด ํ์์ ์ด์ง ์์ ๊ฒฝ์ฐ
- ๋๋์ ๋ฐ์ดํฐ๋ฅผ ํ๊บผ๋ฒ์ ์ฒ๋ฆฌํ ๋
- ์ ๊ธฐ์ (์ผ๋ณ, ์๋ณ, n์๊ฐ ๋จ์)์ผ๋ก ์์ ์ด ์ํ๋์ด๋ ๋๋ ๊ฒฝ์ฐ
- ์ธ๋ ฅ์ด ๋ถ์กฑํ๊ฑฐ๋ RDB, ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ค๋ฅผ ํ์ฉํ ๋
์์:
Netflix์ ์ฝํ
์ธ ์ถ์ฒ ์์คํ
์ n์๊ฐ ๋จ์๋ก ์์ธก ๊ฒฐ๊ณผ๋ฅผ ์์ฑํด DB์ ์ ์ฅํ ํ, DB์ ์์ธก ๊ฒฐ๊ณผ๋ฅผ ์ฝ์ด์ ์๋นํ๋ ๋ฐฉ์์
๋๋ค.
1-2. Batch ํจํด ๊ตฌ์ฑ ์์
Batch ํจํด์ ํฌ๊ฒ 3๊ฐ์ ํํธ๋ก ๊ตฌ์ฑํ ์ ์์ต๋๋ค.
-
Job Management Server
- ์์ ์ ์ด๊ดํ๋ ์๋ฒ
- Apache Airflow ๊ฐ์ ๋๊ตฌ๋ฅผ ํ์ฉํด ํน์ ์๊ฐ์ ์ฃผ๊ธฐ์ ์ผ๋ก batch job์ ์คํ
-
Job
- ํน์ ์์ ์คํ์ ํ์ํ ๋ชจ๋ ํ๋์ ํฌํจ
- Python ์คํฌ๋ฆฝํธ, Docker ์ด๋ฏธ์ง ๋ฑ์ผ๋ก ๊ตฌํ
- ์: ๋ชจ๋ธ ๋ก๋, ๋ฐ์ดํฐ ๋ก๋ ๋ฑ
-
Data
- ์์ธก ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํ DB๋ ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ค
์ฅ์ :
- ๊ธฐ์กด ์ฝ๋๋ฅผ ์ฌ์ฌ์ฉํ ์ ์์ผ๋ฉฐ, ๋ณ๋์ API ์๋ฒ ๊ฐ๋ฐ ์์ด๋ ์๋น์ด ๊ฐ๋ฅ
- ์๋ฒ ๋ฆฌ์์ค๋ฅผ ์ ์ฐํ๊ฒ ๊ด๋ฆฌํ ์ ์์
๋จ์ :
- ๋ณ๋์ ์ค์ผ์ค๋ฌ(์: Apache Airflow)๊ฐ ํ์
2. Online (Real Time) Serving
2-1. ๊ฐ๋ ๋ฐ ํน์ง
- ์ ์:
ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญํ ๋๋ง๋ค ์ฆ๊ฐ์ ์ผ๋ก ๋ชจ๋ธ์ด ์์ธก ๊ฒฐ๊ณผ๋ฅผ ์์ฑํด ์๋นํ๋ ๋ฐฉ์์ ๋๋ค. - ์ ์ฉ ์ํฉ:
- ์ฆ๊ฐ์ ์ธ ์๋ต์ด ํ์์ ์ธ ๊ฒฝ์ฐ
- ๊ฐ๋ณ ์์ฒญ์ ๋ํด ๋ง์ถคํ ์ฒ๋ฆฌ๊ฐ ํ์ํ ๋
- ๋์ ๋ฐ์ดํฐ์ ๋์ํด์ผ ํ ๋
์์:
- ์ ํ๋ธ ์ถ์ฒ ์์คํ (์๋ก๊ณ ์นจ ์๋ง๋ค ์ถ์ฒ ๊ฒฐ๊ณผ๊ฐ ๋ณ๋)
- ๋ฒ์ญ ์๋น์ค
2-2. Online Serving ํจํด: Web Single ํจํด
Web Single ํจํด ๊ตฌ์ฑ ์์:
-
Inference Server
- FastAPI, Flask ๋ฑ์ผ๋ก ๋จ์ผ REST API ์๋ฒ๋ฅผ ๊ฐ๋ฐํ์ฌ ๋ฐฐํฌ
- ์๋ฒ๊ฐ ์คํ๋ ๋ ๋ชจ๋ธ์ ๋ก๋ํ๊ณ , ์ ์ฒ๋ฆฌ ๋ก์ง์ด ํจ๊ป ํฌํจ๋จ
-
Client
- ์๋น์ค๋ฅผ ์์ฒญํ๋ ์ฌ์ฉ์ ์ธํฐํ์ด์ค(์น, ์ฑ ๋ฑ)
-
Data
- ์์ฒญ ์ ํจ๊ป ์ ๊ณต๋๋ ์ ๋ ฅ ๋ฐ์ดํฐ
-
Load Balancer
- Nginx, Amazon ELB ๋ฑ์ ์ฌ์ฉํด ํธ๋ํฝ์ ๋ถ์ฐ, ์๋ฒ ๊ณผ๋ถํ ๋ฐฉ์ง
์ฅ์ :
- ๋น ๋ฅธ ์ถ์์ ์ค์๊ฐ ์์ธก์ด ๊ฐ๋ฅ
- ๋จ์ผ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด๋ก ๊ตฌํํ ์ ์์ด ์ํคํ ์ฒ๊ฐ ๋จ์
๋จ์ :
- ๊ตฌ์ฑ ์์ ์ค ํ๋๋ผ๋ ๋ณ๊ฒฝ๋๋ฉด ์ ์ฒด ์ ๋ฐ์ดํธ ํ์
- ๋ชจ๋ธ์ด ํฌ๊ฑฐ๋ ๋ก๋ฉ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ๊ฒฝ์ฐ ์ด๊ธฐ ์๋ต ์ง์ฐ ๊ฐ๋ฅ
3. Serving ์ฒ๋ฆฌ ๋ฐฉ์: Synchronous vs. Asynchronous
3-1. Synchronous ํจํด
- ํน์ง:
ํ๋์ ์์ ์ด ์๋ฃ๋ ๋๊น์ง ๋ค์ ์์ ์ ์์ํ์ง ์๊ณ ๊ธฐ๋ค๋ฆฌ๋ ๋ฐฉ์ - ์ ์ฉ:
๋๋ถ๋ถ์ REST API๊ฐ ์ด ๋ฐฉ์์ ์ฌ์ฉํ๋ฉฐ, ์์ธก ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ํด๋ผ์ด์ธํธ ๋ก์ง์ด ์ฆ๊ฐ์ ์ผ๋ก ๋ฌ๋ผ์ ธ์ผ ํ ๋ ์ ์ฉ - ์ฅ์ :
์ํคํ ์ฒ ๋ฐ ์ํฌํ๋ก์ฐ๊ฐ ๋จ์ - ๋จ์ :
๋์์ ๋ค์์ ์์ฒญ์ด ๋ค์ด์ค๋ฉด ์ฒ๋ฆฌ์ ์ด๋ ค์์ด ๋ฐ์
3-2. Asynchronous ํจํด
- ํน์ง:
ํ๋์ ์์ ์ ์์ํ ํ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ ๋์ ๋ค๋ฅธ ์์ ์ ์ํํ ์ ์๋ ๋ฐฉ์- ํด๋ผ์ด์ธํธ์ ์์ธก ์๋ฒ ์ฌ์ด์ ๋ฉ์์ง ํ(์: Apache Kafka)๋ฅผ ๋์ ํ์ฌ ์์ฒญ(push)๊ณผ ๊ฒฐ๊ณผ ํ์(pull) ๋ฐฉ์์ผ๋ก ๋ณ๋ ฌ ์ฒ๋ฆฌ
- ์ฅ์ :
ํด๋ผ์ด์ธํธ์ ์์ธก ํ๋ก์ธ์ค๊ฐ ๋ถ๋ฆฌ๋์ด, ํด๋ผ์ด์ธํธ๊ฐ ์์ธก ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ๋ ๋ค๋ฅธ ์์ ์ ์ํ ๊ฐ๋ฅ - ๋จ์ :
๋ณ๋์ ํ ์์คํ ๊ตฌ์ถ์ด ํ์ํ์ฌ ์ ์ฒด ๊ตฌ์กฐ๊ฐ ๋ณต์กํด์ง๋ฉฐ, ์์ ํ ์ค์๊ฐ ์์ธก์๋ ์ ํฉํ์ง ์์ ์ ์์
๊ฒฐ๋ก
์ ํ ์๋น์ ๊ฐ๋ฐํ ํ๋ก๊ทธ๋จ์ด๋ ๋ชจ๋ธ์ ์ค์ ์๋น์ค์ ์ ์ฉํ๊ธฐ ์ํ ์ค์ํ ๋จ๊ณ์ ๋๋ค.
- Batch Serving:
๋๋ ๋ฐ์ดํฐ๋ ์ ๊ธฐ์ ์์ ์ ์ ํฉํ๋ฉฐ, ๊ธฐ์กด ์ฝ๋ ์ฌ์ฌ์ฉ๊ณผ ์ ์ฐํ ์๋ฒ ๋ฆฌ์์ค ๊ด๋ฆฌ์ ์ฅ์ ์ด ์์ต๋๋ค. - Online (Real Time) Serving:
์ฆ๊ฐ์ ์ธ ์๋ต๊ณผ ๋ง์ถคํ ์ฒ๋ฆฌ๊ฐ ํ์ํ ๊ฒฝ์ฐ ์ ํฉํ๋ฉฐ, Web Single ํจํด, Synchronous, Asynchronous ํจํด ๋ฑ ๋ค์ํ ๊ตฌ์กฐ๋ก ๊ตฌํํ ์ ์์ต๋๋ค.
๊ฐ ๋ฐฉ์์ ์ํฉ๊ณผ ์๊ตฌ์ฌํญ์ ๋ฐ๋ผ ์ ํ๋ ์ ์์ผ๋ฉฐ, ๋ฌธ์ ์ํฉ, ์ ์ฝ ์กฐ๊ฑด, ๋ฐ์ดํฐ ์ ์ฅ ๋ฐฉ์, ์์คํ ํ๊ฒฝ ๋ฑ์ ์ข ํฉ์ ์ผ๋ก ๊ณ ๋ คํด ์ต์ ์ ์๋น ๋ฐฉ์์ ๊ฒฐ์ ํ ์ ์์ต๋๋ค.
Apache Airflow๋ก Batch Serving ์ํฌํ๋ก์ฐ ๊ตฌ์ถํ๊ธฐ
Apache Airflow๋ ์ํฌํ๋ก์ฐ ๊ด๋ฆฌ ๋ฐ ์ค์ผ์ค๋ง ๋๊ตฌ๋ก, ๋ชจ๋ธ ํ์ต์ด๋ ์์ธก๊ณผ ๊ฐ์ ์ฃผ๊ธฐ์ ์ธ ์์ (batch serving)์ ์๋ํํ๋ ๋ฐ ์ ์ฉํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ํ์ต์ 1์ฃผ์ผ์ 1๋ฒ, ์์ธก์ 10๋ถ๋ง๋ค ์คํํ๋ ๋ฑ์ ์ค์ผ์ค๋ง์ด ๊ฐ๋ฅํฉ๋๋ค.
์ด๋ฒ ํฌ์คํธ์์๋ Airflow์ ๊ธฐ๋ณธ ๊ฐ๋ , ์ค์น ๋ฐ ํ๊ฒฝ ์ค์ , DAG ์์ฑ ๋ฐฉ๋ฒ, ๊ทธ๋ฆฌ๊ณ Slack ์ฐ๋์ ํตํ ์๋ฆผ ๊ธฐ๋ฅ๊น์ง ์์ธํ ์์๋ณด๊ฒ ์ต๋๋ค.
1. Apache Airflow ๊ธฐ๋ณธ ๊ฐ๋
DAG (Directed Acyclic Graph)
- ์ ์: ์์ (Task)๋ค์ ํ๋ฆ๊ณผ ์์๋ฅผ ์ ์ํ๋ ๊ตฌ์กฐ์ ๋๋ค.
- ์ญํ : Airflow์์ ์คํํ ์์ ์ ์ด๋ป๊ฒ ์ฐ๊ฒฐํ ์ง ๊ฒฐ์ ํฉ๋๋ค.
Operator
- ์ ์: Airflow์ ์์ ์ ํ์ ๋ํ๋ด๋ ํด๋์ค์ ๋๋ค.
- ์์:
- BashOperator: Bash ๋ช ๋ น์ด ์คํ
- PythonOperator: Python ํจ์ ํธ์ถ
- SQLOperator: SQL ์ฟผ๋ฆฌ ์คํ
Scheduler
- ์ญํ : DAG๋ฅผ ๋ชจ๋ํฐ๋งํ๋ฉด์, ์คํ ์์ ์ ๋ง์ถฐ ์์ ์ ์์ฝํฉ๋๋ค.
Executor
- ์ ์: ์์ ์ด ์ค์ ์คํ๋๋ ํ๊ฒฝ์ ๋๋ค.
- ์์:
- LocalExecutor: ๋ก์ปฌ ํ๊ฒฝ์์ ์คํ
- CeleryExecutor: ๋ถ์ฐ ํ๊ฒฝ์์ ์์ ์คํ
2. Apache Airflow ์ค์น ๋ฐ ์ด๊ธฐ ์ค์
์ค์น
์ต์ ๋ฒ์ ์์ ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ถฉ๋ ๋ฐ ๋ฒ๊ทธ๊ฐ ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก, ์์ ์ ์ธ ๋ฒ์ ์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
์์) Python 3.11.7 ๊ธฐ์ค, Airflow 2.6.3
$ pip3 install apache-airflow==2.6.3
ํ๊ฒฝ ๋ณ์ ์ค์
Airflow์ ํ ๋๋ ํ ๋ฆฌ๋ฅผ ์ง์ ํฉ๋๋ค.
$ vi ~/.bashrc
ํ์ผ ๋งจ ์๋์ ๋ค์ ๋ด์ฉ์ ์ถ๊ฐ:
export AIRFLOW_HOME=your_directory
๋ณ๊ฒฝ ์ฌํญ ์ ์ฉ์ ํฐ๋ฏธ๋ ์ฌ์์ ๋๋ ๋ค์ ๋ช
๋ น์ด ์คํ:
$ source ~/.bashrc
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๊ธฐํ
Airflow๋ ๊ธฐ๋ณธ์ ์ผ๋ก SQLite๋ฅผ ์ฌ์ฉํฉ๋๋ค.
$ airflow db init
์ด ๋ช
๋ น์ผ๋ก airflow.cfg
์ airflow.db
ํ์ผ์ด ์์ฑ๋ฉ๋๋ค.
Admin ๊ณ์ ์์ฑ
Airflow Web UI์ ๋ก๊ทธ์ธํ ๊ณ์ ์ ์์ฑํฉ๋๋ค.
$ airflow users create --username admin --password your_password --firstname gildong --lastname hong --role Admin --email id@gmail.com
Webserver ๋ฐ Scheduler ์คํ
-
Airflow Webserver ์คํ:
$ airflow webserver --port 8080
๋ธ๋ผ์ฐ์ ์์ http://localhost:8080์ ์ด๊ณ ์ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธํฉ๋๋ค.
-
Airflow Scheduler ์คํ:
๋ณ๋์ ํฐ๋ฏธ๋์์ ์คํ:
$ airflow scheduler
3. DAG ์์ฑํ๊ธฐ
DAG๋ Airflow์์ ์์ ์ ํ๋ฆ๊ณผ ์์๋ฅผ ์ ์ํฉ๋๋ค.
- DAG ํ์ผ ์ ์ฅ ํด๋:
AIRFLOW_HOME
๋ด์dags
๋๋ ํ ๋ฆฌ๋ฅผ ์์ฑํฉ๋๋ค. - DAG ํ์ผ ์์ฑ:
์๋๋ ๋ ์ง ์ถ๋ ฅ๊ณผ "Hello world!" ๋ฉ์์ง๋ฅผ ์ถ๋ ฅํ๋ ๊ฐ๋จํ DAG ์์ ์ ๋๋ค.
from airflow import DAG
from airflow.operators.bash import BashOperator
from airflow.operators.python import PythonOperator
from datetime import datetime
default_args = {
"owner": "gildong",
"depends_on_past": False, # ์ด์ DAG์ task ์ฑ๊ณต ์ฌ๋ถ์ ๋ฐ๋ผ์ ํ์ฌ task๋ฅผ ์คํํ ์ง ๊ฒฐ์ . False๋ ๊ณผ๊ฑฐ task์ ์ฑ๊ณต ์ฌ๋ถ์ ์๊ด ์์ด ์คํ
"start_date": datetime(2024, 1, 1),
"end_date": datetime(2024, 1, 8)
}
def print_hello():
print("Hello world!")
#####################################################################
# Part 1. DAG ์ ์
with DAG(
dag_id = "basic_dag",
default_args=default_args,
schedule_interval="30 0 * * *", # ๋งค์ผ UTC 00:30 AM์ ์คํ / ํ ๋ฒ๋ง ์คํํ๊ณ ์ถ๋ค๋ฉด "@once"
tags=["my_dags"]
) as dag:
#####################################################################
# Part 2. task ์ ์
task1 = BashOperator(
task_id="print_date",
bash_command="date" # ์คํํ bash command
)
task2 = PythonOperator(
task_id="print_hello",
python_callable=print_hello
)
#####################################################################
# Part 3. task ์์ ์ ์
task1 >> task2
Cron ํํ์ ๊ฐ๋จ ์ ๋ฆฌ
- ๊ตฌ์ฑ:
- 1๋ฒ์งธ ์๋ฆฌ: ๋ถ (0-59)
- 2๋ฒ์งธ ์๋ฆฌ: ์ (0-23)
- 3๋ฒ์งธ ์๋ฆฌ: ์ผ (1-31)
- 4๋ฒ์งธ ์๋ฆฌ: ์ (1-12)
- 5๋ฒ์งธ ์๋ฆฌ: ์์ผ (0-6; 0=์ผ์์ผ)
- ์์:
"30 0 * * *"
: ๋งค์ผ 00:30 AM์ ์คํ"@once"
: ํ ๋ฒ๋ง ์คํ
์์ฑํ DAG ํ์ผ์ basic.py
๋ก ์ ์ฅ ํ, Airflow Web UI์์ ํ์ธํ ์ ์์ต๋๋ค.
4. Slack ์ฐ๋์ผ๋ก ์๋ฆผ ๋ฐ๊ธฐ
Airflow์์ task ์คํจ ์ Slack์ผ๋ก ์๋ฆผ์ ๋ฐ์ ์ฆ๊ฐ์ ์ผ๋ก ๋ฌธ์ ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
4-1. Airflow Slack Provider ์ค์น
Python 3.11.7, Airflow 2.6.3๊ณผ ํธํ๋๋ ๋ฒ์ (8.6.0) ์ค์น:
$ pip3 install 'apache-airflow-providers-slack[http]'==8.6.0
4-2. Slack API Key ๋ฐ๊ธ ๋ฐ Webhook ์ค์
- Slack API Apps ํ์ด์ง์์ "Create New App > From scratch"๋ฅผ ์ ํํฉ๋๋ค.
- App Name๊ณผ Workspace๋ฅผ ์ค์ ํ ํ, Basic Information ํญ์์ Incoming Webhooks๋ฅผ ํ์ฑํํฉ๋๋ค.
- "Add New Webhook to Workspace"๋ฅผ ํตํด ํน์ ์ฑ๋์ ๋ํ Webhook URL(์:
https://hooks.slack.com/services/~~~~~~~~/1234567
)์ ๋ฐ๊ธ๋ฐ์ต๋๋ค.
4-3. Airflow์ Webhook ๋ฑ๋ก
Airflow Web UI์์ Admin > Connections๋ก ์ด๋ ํ, ์ Connection์ ์ถ๊ฐํฉ๋๋ค.
- Connection id: ์)
slack_webhook
- Connection type: HTTP
- Host:
https://hooks.slack.com/services
- Password:
/~~~~~~~~~~/1234567
(๋น๋ฐ ์ ๋ณด)
4-4. Slack ์๋ฆผ ์ฝ๋ ์์ฑ
์๋ ์ฝ๋๋ฅผ utils/slack_alert.py
ํ์ผ๋ก ์ ์ฅํฉ๋๋ค.
from airflow.providers.slack.operators.slack_webhook import SlackWebhookOperator
SLACK_DAG_CONN_ID = "slack_webhook" # Connection id์ ์
๋ ฅํ ๋ณธ์ธ์ด ์๋ณ ๊ฐ๋ฅํ ์ด๋ฆ
def send_message(slack_msg):
return SlackWebhookOperator(
task_id="slack_webhook",
slack_webhook_conn_id=SLACK_DAG_CONN_ID,
message=slack_msg,
username="Airflow-alert"
)
def fail_alert(context):
slack_msg = """
Task Failed!
Task: {task}
Dag: `{dag}`
Execution Time: {exec_date}
""".format(
task=context.get("task_instance").task_id,
dag=context.get("task_instance").dag_id,
exec_date=context.get("execution_date")
)
alert = send_message(slack_msg)
return alert.execute(context=context)
์ด์ DAG ํ์ผ์์ ์๋์ ๊ฐ์ด Slack ์๋ฆผ ํจ์๋ฅผ importํ๊ณ , on_failure_callback
์ธ์๋ก ๋ฑ๋กํฉ๋๋ค.
with DAG(
dag_id = "basic_dag",
default_args=default_args,
schedule_interval="30 0 * * *",
tags=["my_dags"],
on_failure_callback=fail_alert
) as dag:
์ฑ๊ณต ์ ์๋ฆผ์ ๋ฐ๊ณ ์ถ๋ค๋ฉด, ๋ฉ์์ง๋ฅผ ์ฑ๊ณต์ผ๋ก ๋ณ๊ฒฝํ ํ on_success_callback
์ธ์๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค.
๊ฒฐ๋ก
Apache Airflow๋ ๋ณต์กํ ์ํฌํ๋ก์ฐ๋ฅผ ์ฝ๊ฒ ์ ์ํ๊ณ , ๋ฐฐ์น ์ค์ผ์ค๋ง์ ํตํด ๋ฐ๋ณต์ ์ธ ์์ ์ ์๋ํํ ์ ์๋ ๊ฐ๋ ฅํ ๋๊ตฌ์ ๋๋ค.
- ์ค์น ๋ฐ ํ๊ฒฝ ์ค์ : ์์ ์ ์ธ ๋ฒ์ ๊ณผ ํ๊ฒฝ ๋ณ์ ์ค์ , ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๊ธฐํ, Admin ๊ณ์ ์์ฑ
- DAG ์์ฑ: ์์ ์ ํ๋ฆ, Operator๋ฅผ ์ด์ฉํ Task ์ ์, Cron ํํ์์ ํ์ฉํ ์ค์ผ์ค๋ง
- Slack ์ฐ๋: ์์ ์คํจ ์ ์ฆ๊ฐ์ ์ธ ์๋ฆผ์ผ๋ก ๋ฌธ์ ๋์
์๋ฒ ์ํคํ ์ฒ์ Web API ์ดํดํ๊ธฐ
ํ๋ ์น/๋ชจ๋ฐ์ผ ์ ํ๋ฆฌ์ผ์ด์ ์ ํต์ฌ์ ์๋ฒ ์ํคํ ์ฒ์ API ์ค๊ณ์ ๋๋ค. ์ด ํฌ์คํธ์์๋ ์๋ฒ ์ํคํ ์ฒ์ ์ข ๋ฅ์ ๊ฐ๊ฐ์ ํน์ง, ๊ทธ๋ฆฌ๊ณ API์ ๊ธฐ๋ณธ ๊ฐ๋ ๋ฐ REST API์ ๊ตฌ์ฑ ์์์ URL, HTTP ์์ฒญ์ ๋ํ ๊ธฐ๋ณธ ๊ฐ๋ ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
1. ์๋ฒ ์ํคํ ์ฒ
1-1. ๋ชจ๋๋ฆฌ์ ์ํคํ ์ฒ (Monolithic Architecture)
- ์ ์:
๋ฐ์ดํฐ๋ฒ ์ด์ค, ๋ชจ๋ธ ๋ฑ ๋ชจ๋ ๋ก์ง์ด ํ๋์ ํฐ ์๋ฒ์์ ๊ตฌํ๋๋ ํํ์ ๋๋ค. - ํน์ง:
- ํด๋ผ์ด์ธํธ๋ ๋จ์ผ ์๋ฒ์ ์์ฒญ์ ๋ณด๋ด๊ณ , ์๋ฒ ๋ด๋ถ์์ ๋ชจ๋ ์์ฒญ์ ์ฒ๋ฆฌ ํ ๋ฐํํฉ๋๋ค.
- ๊ฐ๋ฐ ์ด๊ธฐ์๋ ๊ตฌ์กฐ๊ฐ ๋จ์ํ๊ณ ์ง๊ด์ ์ด์ง๋ง, ์๋น์ค ํ์ฅ ์ ๋ณต์ก๋๊ฐ ์ฆ๊ฐํ์ฌ ์์ ์ด๋ ์ถ๊ฐ ๊ฐ๋ฐ์ด ์ด๋ ค์์ง ์ ์์ต๋๋ค.
1-2. ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ (Microservice Architecture)
- ์ ์:
๊ธฐ๋ฅ๋ณ๋ก ์ฌ๋ฌ ๊ฐ์ ์์ ์๋ฒ(์: DB ์๋ฒ, ๋ชจ๋ธ ์๋ฒ ๋ฑ)๋ก ๋ก์ง์ ๋ถ๋ฆฌํ์ฌ ๊ฐ๋ฐํ๋ ํํ์ ๋๋ค. - ํน์ง:
- ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญํ๋ฉด ์ค์ ์๋ฒ๊ฐ ์ด๋ฅผ ๊ฐ๊ฐ์ ๋ด๋ถ ์๋ฒ๋ก ๋ถ๋ฐฐํ์ฌ ์ฒ๋ฆฌ ํ ๊ฒฐ๊ณผ๋ฅผ ๋ชจ์ ์๋ตํฉ๋๋ค.
- ์๋ฒ ๋จ์๋ก ๋ ๋ฆฝ์ ์ธ ์์กด์ฑ ๋ฐ ํ๊ฒฝ ์ค์ ์ด ๊ฐ๋ฅํ์ง๋ง, ์ ์ฒด ๊ตฌ์กฐ๋ ์๋์ ์ผ๋ก ๋ณต์กํด์ง ์ ์์ต๋๋ค.
2. API์ ๊ธฐ๋ณธ ๊ฐ๋
API๋?
- API (Application Programming Interface):
์ํํธ์จ์ด ์์ฉ ํ๋ก๊ทธ๋จ๋ค์ด ์๋ก ์ํธ์์ฉํ ์ ์๋๋ก ์ ์ํ ์ธํฐํ์ด์ค๋ฅผ ์ด์นญํฉ๋๋ค.
Web API
- ์ ์:
HTTP(Hyper Text Transfer Protocol)๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์น ๊ธฐ์ ์ ํ์ฉํด ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๋ API์ ๋๋ค. - ์ฃผ์ ์ข
๋ฅ:
- REST: ์์(Resource) ๊ธฐ๋ฐ์ ์ํ ์ ์ก
- GraphQL: ํด๋ผ์ด์ธํธ๊ฐ ํ์ํ ๋ฐ์ดํฐ๋ง ์ฟผ๋ฆฌํ ์ ์๋ ๋ฐฉ์
- RPC: ์๊ฒฉ ํ๋ก์์ ํธ์ถ ๋ฐฉ์
3. REST API
REST API๋ ์์(Resource)์ ํํํ๊ณ ์ํ๋ฅผ ์ ์กํ๋ ๋ฐ ์ค์ ์ ๋ ์ํคํ ์ฒ์ ๋๋ค. HTTP๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ๋ฉฐ, ์์ฒญ์ ํตํด ์ด๋ค ์์ ์ ์ํํ๋์ง ์ฝ๊ฒ ํ์ ํ ์ ์์ต๋๋ค.
3-1. REST API ๊ตฌ์ฑ ์์
-
Resource:
- ์์คํ ์์ ๊ด๋ฆฌํ๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ ๊ฐ์ฒด
- ์์: ์ ์์๊ฑฐ๋ ์ฌ์ดํธ์
users
,products
,orders
- URL endpoint ์์:
www.shopping.com/users
www.shopping.com/products
www.shopping.com/orders
-
Method (HTTP ๋ฉ์๋):
- GET: ๋ฆฌ์์ค ์กฐํ
- POST: ๋ฆฌ์์ค ์์ฑ
- PUT: ๋ฆฌ์์ค ์ ์ฒด ์ ๋ฐ์ดํธ
- PATCH: ๋ฆฌ์์ค ์ผ๋ถ ์ ๋ฐ์ดํธ
- DELETE: ๋ฆฌ์์ค ์ญ์
-
Representation of Resource:
- ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ์ฃผ๊ณ ๋ฐ๋ ๋ฆฌ์์ค์ "ํํ" (๋ณดํต JSON ๋๋ XML)
- ์์:
- ์์ฒญ:
GET /users/1
- ์๋ต:
{ "id": 1, "name": "Alice", "email": "alice@mail.com" }
- ์์ฒญ:
4. URL์ ๊ตฌ์ฑ ์์
URL์ ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ ์์ฒญํ ๋ฆฌ์์ค์ ์์น๋ฅผ ๋ํ๋ด๋ฉฐ, ๋ค์๊ณผ ๊ฐ์ด ๊ตฌ์ฑ๋ฉ๋๋ค.
- Schema:
- ์ฌ์ฉ ํ๋กํ ์ฝ (์:
http
,https
)
- ์ฌ์ฉ ํ๋กํ ์ฝ (์:
- Host:
- ์๋ฒ์ IP ์ฃผ์๋ ๋๋ฉ์ธ ์ด๋ฆ
- Port:
- ํต์ ํฌํธ ๋ฒํธ (์:
8080
)
- ํต์ ํฌํธ ๋ฒํธ (์:
- Path ๋๋ Endpoint:
- ๋ฐํํ ๋ฆฌ์์ค์ ๊ฒฝ๋ก
- ์์:
/users
,/predict
,/train
๋ฑ
- Query Parameter:
- ํน์ ๋ฆฌ์์ค์ ๋ํ ์ถ๊ฐ ์ ๋ณด ์ ๊ณต์ด๋ ํํฐ๋ง
- ์์:
http://localhost:8080/users?name=alice
- **Path Parameter:**
- ๋ฆฌ์์ค์ ์ ํํ ์์น๋ฅผ ์ง์
- ์์:
```bash
https://localhost:8080/users/alice
```
---
## 5. HTTP Header์ Payload
CLI ํ๊ฒฝ์์๋ HTTP ์์ฒญ ์ Header์ Payload๋ฅผ ํจ๊ป ์ฌ์ฉํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, `curl`์ ์ฌ์ฉํ POST ์์ฒญ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
```bash
curl -X POST -H "Content-Type: application/json" -d "{'name':'alice'}" http://localhost:8080/users
- curl:
- CLI ํ๊ฒฝ์์ HTTP ์์ฒญ์ ์ํํ๋ ๋ช ๋ น์ด
- -X POST:
- HTTP ๋ฉ์๋ POST๋ฅผ ์ฌ์ฉ (๋ฆฌ์์ค ์์ฑ)
- -H "Content-Type: application/json":
- ์ ์ก ๋ฐ์ดํฐ๊ฐ JSON ํ์์์ ๋ช ์ํ์ฌ HTTP Header์ key-value ํํ๋ก ์ ์ฅ
- -d "{'name':'alice'}":
- ์ค์ ์ ์กํ๋ ค๋ ๋ฐ์ดํฐ๊ฐ HTTP Payload์ ํฌํจ๋จ
- http://localhost:8080/users:
- ์์ฒญ์ ๋ณด๋ผ ๋ชฉ์ ์ง URL
6. HTTP Status Code
์๋ฒ๋ ํด๋ผ์ด์ธํธ ์์ฒญ์ ๋ฐ๋ผ ์ํ ์ฝ๋๋ฅผ ๋ฐํํ์ฌ ์์ฒญ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ์๋ ค์ค๋๋ค.
- 1XX (์ ๋ณด): ์์ฒญ์ ๋ฐ์์ผ๋ฉฐ, ํ๋ก์ธ์ค๊ฐ ์งํ ์ค์
- 2XX (์ฑ๊ณต): ์์ฒญ์ด ์ฑ๊ณต์ ์ผ๋ก ์ฒ๋ฆฌ๋จ
- 3XX (๋ฆฌ๋ค์ด๋ ์ ): ์์ฒญ ์๋ฃ๋ฅผ ์ํด ์ถ๊ฐ ์์ ์ด ํ์ํจ
- 4XX (ํด๋ผ์ด์ธํธ ์ค๋ฅ): ์์ฒญ ๋ฌธ๋ฒ ์ค๋ฅ ๋๋ ์์ฒญ ์ฒ๋ฆฌ ๋ถ๊ฐ
- 5XX (์๋ฒ ์ค๋ฅ): ์๋ฒ๊ฐ ์์ฒญ ์ฒ๋ฆฌ๋ฅผ ์คํจํจ
๊ฒฐ๋ก
์ด๋ฒ ํฌ์คํธ์์๋ ์๋์ ๋ด์ฉ์ ์ ๋ฆฌํ์์ต๋๋ค.
- ์๋ฒ ์ํคํ ์ฒ: ๋ชจ๋๋ฆฌ์๊ณผ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ์ ๊ฐ๋ ๋ฐ ์ฅ๋จ์
- API ๊ธฐ๋ณธ ๊ฐ๋ : API์ Web API, ๊ทธ๋ฆฌ๊ณ REST API์ ๊ตฌ์ฑ ์์
- URL ๊ตฌ์ฑ ์์: Schema, Host, Port, Path, Query ๋ฐ Path Parameter
- HTTP ์์ฒญ: Header, Payload ์ฌ์ฉ ๋ฐฉ๋ฒ๊ณผ
curl
์์ - Status Code: ๊ฐ ์ฝ๋์ ์๋ฏธ์ ์ญํ
FastAPI๋ฅผ ํ์ฉํ Online Serving API ๊ฐ๋ฐ
FastAPI๋ ํ์ด์ฌ ๊ธฐ๋ฐ์ ์น ํ๋ ์์ํฌ๋ก, ๋น ๋ฅด๊ณ ํจ์จ์ ์ผ๋ก API๋ฅผ ๊ฐ๋ฐํ ์ ์๊ฒ ํด์ค๋๋ค. ๋ณธ ํฌ์คํธ์์๋ ๊ฐ๋จํ ๋จธ์ ๋ฌ๋(ML) ๋ชจ๋ธ์ ๋ก๋ํด ์์ธก ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๋ API ์น ํ๋ก์ ํธ๋ฅผ ์์ ๋ก ์๊ฐํ๋ฉฐ, ํ๋ก์ ํธ ๊ตฌ์กฐ๋ถํฐ ์ค์น, ์ค์ , ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ(์์ธก, ํ์ผ ์ ๋ก๋, ๋ผ์ฐํฐ, ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ๋ฑ)๊น์ง ์ ๋ฐ์ ์ธ ๋ด์ฉ์ ๋ค๋ฃน๋๋ค.
1. ํ๋ก์ ํธ ๊ตฌ์กฐ
์๋์ ๊ฐ์ด ๋๋ ํ ๋ฆฌ๋ฅผ ๊ตฌ์ฑํ์ฌ ๊ฐ ๊ธฐ๋ฅ์ ๋ชจ๋๋ณ๋ก ๋ถ๋ฆฌํฉ๋๋ค.
project_root/
โฃ app/
โ โฃ __init__.py
โ โฃ main.py # FastAPI ์ ํ๋ฆฌ์ผ์ด์
, ๋ผ์ฐํฐ ์ค์ ๋ฐ lifespan ํจ์
โ โฃ config.py # ๋ฐ์ดํฐ๋ฒ ์ด์ค, ๋ชจ๋ธ ๊ฒฝ๋ก, ์คํ ํ๊ฒฝ ๋ฑ์ ์ค์
โ โฃ api.py # ๋ชจ๋ธ ์์ธก ๊ธฐ๋ฅ ๋ฑ API ์๋ํฌ์ธํธ ๊ตฌํ
โ โฃ schemas.py # ์์ฒญ/์๋ต ๋ฐ์ดํฐ ์คํค๋ง ์ ์ (Pydantic ๋ชจ๋ธ)
โ โฃ database.py # ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ๋ฐ ํ
์ด๋ธ ์์ฑ (SQLModel ๋ฑ)
โ โฃ model.py # ML ๋ชจ๋ธ ๋ก๋ ๋ฐ ๊ด๋ จ ํจ์ ์ ์ (์: predict)
โฃ router.py # (ํ์ ์) ์ถ๊ฐ API ๋ผ์ฐํฐ ํ์ผ (์: user, order ๋ฑ ๋ถ๋ฆฌ)
โฃ requirements.txt # ํ์ํ ํจํค์ง ๋ชฉ๋ก
โ README.md
๊ฐ๋จํ ML ๋ชจ๋ธ์ ๋ก๋ํ์ฌ ์
๋ ฅ ๋ฐ์ดํฐ๋ฅผ ์์ธกํ ํ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๊ณ , DB์ ์์ธก ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํ๋ API ์น์ ๊ตฌํํ๋ ์์ ์
๋๋ค.
๋ ์์ธํ ํ๋ก์ ํธ ๊ตฌ์กฐ ์ฐธ๊ณ ์๋ฃ๋ ์๋ ๋งํฌ๋ค์ ํ์ธํด๋ณด์ธ์.
2. FastAPI ์ค์น
$ pip install fastapi uvicorn
uvicorn์ FastAPI ์ ํ๋ฆฌ์ผ์ด์ ์คํ์ ํ์ํ ASGI ์๋ฒ์ ๋๋ค.
3. ๊ฐ ํ์ผ๋ณ ๊ตฌํ ๋ด์ฉ
3-1. config.py
FastAPI ํ๋ก์ ํธ์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค URL, ๋ชจ๋ธ ๊ฒฝ๋ก, ์คํ ํ๊ฒฝ ๋ฑ ์ค์ ์ ๊ด๋ฆฌํฉ๋๋ค.
from pydantic import BaseSettings, Field
class Config(BaseSettings):
db_url: str = Field(default="sqlite:///./db.sqlite3", env="DB_URL")
model_path: str = Field(default="model.joblib", env="MODEL_PATH")
app_env: str = Field(default="local", env="APP_ENV")
config = Config()
์ฐธ๊ณ :
Pydantic์ ํ์ฉํด ํ๊ฒฝ ๋ณ์๋ก๋ถํฐ ์ค์ ๊ฐ์ ๋ถ๋ฌ์ค๋ฏ๋ก, ๋ฐฐํฌ ํ๊ฒฝ์ ๋ง์ถฐ ์ฝ๊ฒ ์กฐ์ ํ ์ ์์ต๋๋ค.
3-2. database.py
SQLModel๊ณผ ๊ฐ์ ORM์ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ๋ฐ ํ ์ด๋ธ ์์ฑ์ ์ํ ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค.
import datetime
from sqlmodel import SQLModel, Field, create_engine
from config import config
class PredictionResult(SQLModel, table=True):
id: int = Field(default=None, primary_key=True)
result: int
created_at: str = Field(default_factory=lambda: datetime.datetime.now().isoformat())
engine = create_engine(config.db_url)
3-3. model.py
๋ชจ๋ธ ๋ก๋์ ๊ด๋ จ๋ ํจ์๋ค์ ์ ์ํฉ๋๋ค.
def load_model(model_path: str):
import joblib
return joblib.load(model_path)
# ํ์ ์ get_model, predict ๋ฑ ๋ค๋ฅธ ํจ์๋ ์ ์ ๊ฐ๋ฅ
3-4. schemas.py
API์์ ์ฃผ๊ณ ๋ฐ์ ๋ฐ์ดํฐ์ ์คํค๋ง(ํํ)๋ฅผ Pydantic์ ์ด์ฉํด ์ ์ํฉ๋๋ค.
from pydantic import BaseModel
class PredictionRequest(BaseModel):
features: list # ์ํฉ์ ๋ง๊ฒ input ๋ฐ์ดํฐ์ ํ์์ ์ ์
class PredictionResponse(BaseModel):
id: int
result: int
3-5. api.py
๋ชจ๋ธ ์์ธก ๋ฐ ๊ฒฐ๊ณผ ์กฐํ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ API ์๋ํฌ์ธํธ๋ฅผ ์์ฑํฉ๋๋ค.
from fastapi import APIRouter, HTTPException, status
from schemas import PredictionRequest, PredictionResponse
from model import load_model # ํน์ get_model ํจ์๋ก ๋ณ๊ฒฝ
from database import PredictionResult, engine
from sqlmodel import Session
router = APIRouter()
def get_model():
# ์ค์ ์ด์์์๋ ๋ชจ๋ธ์ ๋ฉ๋ชจ๋ฆฌ์ ์บ์ฑํ๋ ์ ๋ต์ ์ฌ์ฉ
return load_model("model.joblib")
@router.post('/predict', response_model=PredictionResponse)
def predict(request: PredictionRequest) -> PredictionResponse:
model = get_model()
# ์์ธก: ์์๋ก ์ฒซ ๋ฒ์งธ ์์ธก๊ฐ์ ์ ์ํ์ผ๋ก ๋ณํ
prediction = int(model.predict([request.features])[0])
# ์์ธก ๊ฒฐ๊ณผ๋ฅผ DB์ ์ ์ฅ
prediction_result = PredictionResult(result=prediction)
with Session(engine) as session:
session.add(prediction_result)
session.commit()
session.refresh(prediction_result)
return PredictionResponse(id=prediction_result.id, result=prediction)
@router.get("/predict/{id}", response_model=PredictionResponse)
def get_prediction(id: int) -> PredictionResponse:
with Session(engine) as session:
prediction_result = session.get(PredictionResult, id)
if not prediction_result:
raise HTTPException(
detail="Not Found", status_code=status.HTTP_404_NOT_FOUND
)
return PredictionResponse(id=prediction_result.id, result=prediction_result.result)
3-6. main.py
FastAPI ์ ํ๋ฆฌ์ผ์ด์ ์์ฑ, ๋ผ์ฐํฐ ๋ฑ๋ก, ๊ทธ๋ฆฌ๊ณ ์ ํ๋ฆฌ์ผ์ด์ ์ lifespan(์์/์ข ๋ฃ ์ ์ํํ ์์ )์ ์ ์ํฉ๋๋ค.
from contextlib import asynccontextmanager
from fastapi import FastAPI
from loguru import logger
from sqlmodel import SQLModel
from config import config
from database import engine
from model import load_model
from api import router
@asynccontextmanager
async def lifespan(app: FastAPI):
# ์ฑ ์์ ์ : ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ
์ด๋ธ ์์ฑ
logger.info("Creating database tables")
SQLModel.metadata.create_all(engine)
# ์ฑ ์์ ์ : ๋ชจ๋ธ ๋ก๋ (์ฌ๊ธฐ์ load_model๋ฅผ ํธ์ถ)
logger.info("Loading model")
load_model(config.model_path)
yield
# ์ฑ ์ข
๋ฃ ์ : ํ์ํ ์ ๋ฆฌ ์์
์ถ๊ฐ ๊ฐ๋ฅ
app = FastAPI(lifespan=lifespan)
app.include_router(router)
# ๊ฐ๋จํ ๊ธฐ๋ณธ ๋ฃจํธ ์๋ํฌ์ธํธ
@app.get("/")
def root():
return {"message": "Hello World!"}
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
๋์ ํ์ธ:
์น ๋ธ๋ผ์ฐ์ ์์ http://localhost:8000 ์ ์ ์ "Hello World!" ๋ฉ์์ง ํ์ธ
localhost:8000/docs
์์ ์๋ ์์ฑ๋ Swagger ๋ฌธ์๋ฅผ ํตํด API ํ ์คํธ ๊ฐ๋ฅCLI์์ ์์ธก ์์ฒญ ์์:
curl -X POST "http://0.0.0.0:8000/predict" -H "Content-Type: application/json" -d "{'features': [5.1, 3.5, 1.4, 0.2]}"
์์ธก ๊ฒฐ๊ณผ์ ํจ๊ป DB์ ์ ์ฅ๋ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
4. ์ถ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ
FastAPI๋ ๋ค์ํ HTTP ๋ฉ์๋์ ๋ฐ์ดํฐ ์ ์ก ๋ฐฉ์, ๊ทธ๋ฆฌ๊ณ ๋ฐฐ๊ฒฝ ์์ , ํ์ผ ์ ๋ก๋, ํ๋ก ํธ์๋ ๋ ๋๋ง ๋ฑ ์ฌ๋ฌ ๊ธฐ๋ฅ์ ์ง์ํฉ๋๋ค.
4-1. Path & Query Parameter
-
Path Parameter ์์ :
@app.get("/users/{user_id}") def get_user(user_id: str): return {"user_id": user_id}
-
Query Parameter ์์ :
items_db = [{"item_name": "Apple"}, {"item_name": "Banana"}, {"item_name": "Cake"}] @app.get("/items/") def read_item(skip: int = 0, limit: int = 10): return items_db[skip: skip + limit]
4-2. Form ๋ฐ์ดํฐ ์ฒ๋ฆฌ
Form ๋ฐ์ดํฐ ์ฒ๋ฆฌ๋ฅผ ์ํด python-multipart
ํจํค์ง๋ฅผ ์ค์นํฉ๋๋ค.
pip install python-multipart
๊ทธ๋ฆฌ๊ณ ์๋์ ๊ฐ์ด Form ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
from fastapi import FastAPI, Form
@app.post("/login/")
def login(username: str = Form(...), password: str = Form(...)):
return {"username": username}
4-3. ํ๋ก ํธ์๋ ๋ ๋๋ง (Jinja2)
ํ๋ก ํธ์๋ ํ์ด์ง๋ฅผ ๋ ๋๋งํ๋ ค๋ฉด Jinja2๋ฅผ ์ฌ์ฉํฉ๋๋ค.
pip install Jinja2
from fastapi.templating import Jinja2Templates
from fastapi import Request
templates = Jinja2Templates(directory='./')
@app.get("/login/")
def get_login_form(request: Request):
return templates.TemplateResponse('login_form.html', context={'request': request})
login_form.html
ํ์ผ์ ํ๋ก์ ํธ ๋ฃจํธ๋ ์ง์ ํ ๋๋ ํ ๋ฆฌ์ ์์นํด์ผ ํฉ๋๋ค.
4-4. ํ์ผ ์ ๋ก๋
ํ์ผ ์
๋ก๋ ๊ธฐ๋ฅ๋ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ต๋๋ค. Form์ ๊ตฌํํ ๋์ฒ๋ผย python-multipart
ย ์ค์น๊ฐ ํ์ํ๋ค.
from typing import List
from fastapi import File, UploadFile
from fastapi.responses import HTMLResponse
@app.post("/files/")
def create_files(files: List[bytes] = File(...)):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
def create_upload_files(files: List[UploadFile] = File(...)):
return {"filenames": [file.filename for file in files]}
@app.get("/upload/")
def main():
content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
"""
return HTMLResponse(content=content)
4-5. API Router ๋ถ๋ฆฌ
ํ๋ก์ ํธ๊ฐ ์ปค์ง๋ฉด ์ฌ๋ฌ ์๋ํฌ์ธํธ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๊ธฐ ์ํด ๋ณ๋์ ๋ผ์ฐํฐ ๋ชจ๋๋ก ๋ถ๋ฆฌํ ์ ์์ต๋๋ค.
# router.py
from fastapi import APIRouter
user_router = APIRouter(prefix="/users")
order_router = APIRouter(prefix="/orders")
@user_router.get("/{username}", tags=["users"])
def read_user(username: str):
return {"username": username}
@order_router.get("/{order_id}", tags=["orders"])
def read_order(order_id: str):
return {"order_id": order_id}
๊ทธ๋ฆฌ๊ณ main.py์ ์๋์ ๊ฐ์ด ๋ฑ๋กํฉ๋๋ค.
from router import user_router, order_router
app.include_router(user_router)
app.include_router(order_router)
4-6. ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ (Background Task)
๊ธด ์์ ์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์คํํ์ฌ ์ฆ๊ฐ ์๋ต์ ์ฃผ๊ธฐ ์ํด BackgroundTasks๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
from uuid import UUID, uuid4
from time import sleep
from fastapi import BackgroundTasks
from pydantic import BaseModel, Field
class TaskInput(BaseModel):
id_: UUID = Field(default_factory=uuid4)
wait_time: int
task_repo = {}
def cpu_bound_task(id_: UUID, wait_time: int):
sleep(wait_time)
result = f"task done after {wait_time}"
task_repo[id_] = result
@app.post("/task", status_code=202)
async def create_task_in_background(task_input: TaskInput, background_tasks: BackgroundTasks):
background_tasks.add_task(cpu_bound_task, id_=task_input.id_, wait_time=task_input.wait_time)
return {"task_id": task_input.id_}
@app.get("/task/{task_id}")
def get_task_result(task_id: UUID):
return task_repo.get(task_id, None)
HTTP 202 (Accepted) ์ฝ๋๋ฅผ ๋ฆฌํดํด ๋น๋๊ธฐ ์์ ์ด ๋ฑ๋ก๋์์์ ์๋ฆด ์ ์์ต๋๋ค.
๊ฒฐ๋ก
FastAPI๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ๋จํ ์น API๋ถํฐ ๋ณต์กํ ์จ๋ผ์ธ ์๋น ์์คํ ๊น์ง ๋น ๋ฅด๊ฒ ๊ตฌ์ถํ ์ ์์ต๋๋ค.
- ํ๋ก์ ํธ ๊ตฌ์กฐ ์ค๊ณ: ๊ฐ ๋ชจ๋์ ๋ถ๋ฆฌํ์ฌ ์ ์ง๋ณด์์ฑ๊ณผ ํ์ฅ์ฑ์ ๋์ ๋๋ค.
- ํ๊ฒฝ ์ค์ ๋ฐ ๊ตฌ์ฑ ํ์ผ: Pydantic์ ํ์ฉํด ํ๊ฒฝ๋ณ์ ๋ฐ ์ค์ ์ ๊ด๋ฆฌํฉ๋๋ค.
- ์ฃผ์ ๊ธฐ๋ฅ ๊ตฌํ: ์์ธก ๊ธฐ๋ฅ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฅ, ํ์ผ ์ ๋ก๋, ๋ผ์ฐํฐ ๋ถ๋ฆฌ, ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ๋ฑ ๋ค์ํ ๊ธฐ๋ฅ์ ์ฝ๊ฒ ์ถ๊ฐํ ์ ์์ต๋๋ค.
- ํ๋ก ํธ์๋ ๋ฐ ๋ฌธ์ํ: Swagger UI(
/docs
)๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋ฉฐ, Jinja2๋ฅผ ํ์ฉํด ํ๋ก ํธ์๋๋ ๊ตฌ์ถ ๊ฐ๋ฅํฉ๋๋ค.
Poetry๋ฅผ ์ด์ฉํ ํ์ด์ฌ ํจํค์ง ๋ฐ ์์กด์ฑ ๊ด๋ฆฌ
Poetry๋ ํ์ด์ฌ ํ๋ก์ ํธ์ ํจํค์ง ์ค์น, ๊ฐ์ํ๊ฒฝ ์์ฑ, ์์กด์ฑ ๊ด๋ฆฌ, ๊ทธ๋ฆฌ๊ณ ๋ฐฐํฌ๋ฅผ ์ํ ํจํค์ง ์์ (build, publish)์ ํ ๊ณณ์์ ์ฒ๋ฆฌํ ์ ์๋ ๋๊ตฌ์ ๋๋ค. ์ ํต์ ์ธ pip๋ anaconda์ ๋ฌ๋ฆฌ, Poetry๋ ํ๋ก์ ํธ์ ์ ์ฒด ์๋ช ์ฃผ๊ธฐ๋ฅผ ๊ด๋ฆฌํ ์ ์์ด ์ ์ ๋ ๋ง์ ๊ฐ๋ฐ์๋ค์ด ์ฑํํ๊ณ ์์ต๋๋ค.
1. Poetry๋?
Poetry๋ ๋ค์๊ณผ ๊ฐ์ ์์ ์ ์ํํฉ๋๋ค.
- ํจํค์ง ์ค์น: ํ๋ก์ ํธ์์ ํ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์์กด์ฑ์ ์ค์น
- ๊ฐ์ํ๊ฒฝ ์์ฑ: ๋ ๋ฆฝ์ ์ธ ๊ฐ๋ฐ ํ๊ฒฝ์ ์๋์ผ๋ก ์์ฑํ์ฌ ๊ด๋ฆฌ
- ์์กด์ฑ ๊ด๋ฆฌ: ํ๋ก์ ํธ ์์กด์ฑ์
pyproject.toml
ํ์ผ์ ์ ์ํ๊ณ , ์ ํํ ๋ฒ์ ์poetry.lock
ํ์ผ์ ๊ธฐ๋ก - ํจํค์ง ๋ฐ ๋ฐฐํฌ: ํ๋ก์ ํธ๋ฅผ ๋น๋ํ๊ณ , PyPI์ ๊ฐ์ ์ ์ฅ์์ ๋ฐฐํฌํ ์ ์๋ ๊ธฐ๋ฅ ์ ๊ณต
2. Poetry ์ค์น
Poetry๋ Python 2.7 ๋๋ 3.5 ์ด์์์ ์ค์นํ ์ ์์ต๋๋ค.
Mac / Linux
ํฐ๋ฏธ๋์์ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์คํํฉ๋๋ค.
$ curl -sSL https://install.python-poetry.org | python3 -
Windows
CMD ์ฐฝ ํน์ PowerShell์์ ์๋ ๋ช ๋ น์ด๋ฅผ ์คํํฉ๋๋ค.
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
3. ํ๋ก์ ํธ ์ด๊ธฐํ ๋ฐ ์ค์
ํ๋ก์ ํธ ์ด๊ธฐํ
ํ์ฌ ๋๋ ํ ๋ฆฌ์์ Poetry ํ๋ก์ ํธ๋ฅผ ์ด๊ธฐํํ๋ ค๋ฉด ๋ค์ ๋ช ๋ น์ด๋ฅผ ์ฌ์ฉํฉ๋๋ค.
$ poetry init
์ด ๋ช
๋ น์ด๋ ๋ํํ ์ธํฐํ์ด์ค๋ฅผ ํตํด ํ๋ก์ ํธ์ ์์กด์ฑ, ๋ฒ์ , ์ค๋ช
๋ฑ์ ์
๋ ฅ๋ฐ์ pyproject.toml
ํ์ผ์ ์์ฑํฉ๋๋ค. ์ด๋ฅผ ํตํด ๊ธฐ์กด ๋๋ ํ ๋ฆฌ์์ ์๋ก์ด Poetry ํ๋ก์ ํธ๋ฅผ ์์ํ ์ ์์ต๋๋ค.
๊ฐ์ํ๊ฒฝ ํ์ฑํ
Poetry๋ ํ๋ก์ ํธ๋ณ ๊ฐ์ํ๊ฒฝ์ ์๋์ผ๋ก ๊ด๋ฆฌํฉ๋๋ค. ๋ค์ ๋ช ๋ น์ด๋ก ๊ฐ์ํ๊ฒฝ์ ํ์ฑํํ ์ ์์ต๋๋ค.
$ poetry shell
4. ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น ๋ฐ ๊ด๋ฆฌ
๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น
pyproject.toml
ํ์ผ์ ์ ์๋ ์์กด์ฑ์ ๊ธฐ๋ฐ์ผ๋ก ํ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํ๋ ค๋ฉด ์๋ ๋ช
๋ น์ด๋ฅผ ์คํํฉ๋๋ค.
$ poetry install
ํน์ ํจํค์ง๋ฅผ ์ถ๊ฐํ๋ ค๋ฉด poetry add
๋ช
๋ น์ด๋ฅผ ์ฌ์ฉํฉ๋๋ค.
$ poetry add pandas
์ด๋ ๊ฒ ํ๋ฉด pyproject.toml
ํ์ผ์ ํด๋น ํจํค์ง๊ฐ ์ถ๊ฐ๋๊ณ , ๋์์ poetry.lock
ํ์ผ์๋ ๊ธฐ๋ก๋ฉ๋๋ค.
์ ๊ธํ์ผ (Lock File)
Poetry๋ poetry.lock
ํ์ผ์ ์์ฑํ์ฌ ํ์ฌ ํ๋ก์ ํธ์ ํ์ํ ๋ชจ๋ ์์กด์ฑ๊ณผ ๊ทธ ์ ํํ ๋ฒ์ ์ ๊ธฐ๋กํฉ๋๋ค.
- ์ด ํ์ผ์ GitHub์ ๊ฐ์ ๋ฒ์ ๊ด๋ฆฌ ์์คํ ์ ์ปค๋ฐํด๋๋ฉด, ๋ค๋ฅธ ๊ฐ๋ฐ์๊ฐ ๋์ผํ ํ๊ฒฝ์์ ์์ ํ ์ ์์ต๋๋ค.
poetry.lock
ํ์ผ์ ํตํด ๋ชจ๋ ํ์์ด ๋์ผํ ์์กด์ฑ ๋ฒ์ ์ ๋ณด์ฅ๋ฐ๊ฒ ๋ฉ๋๋ค.
๊ฒฐ๋ก
Poetry๋ ํจํค์ง ์ค์น๋ถํฐ ๊ฐ์ํ๊ฒฝ ๊ด๋ฆฌ, ์์กด์ฑ ๋ฐ ๋ฐฐํฌ๊น์ง ํ์ด์ฌ ํ๋ก์ ํธ์ ์ ๋ฐ์ ์ธ ๊ด๋ฆฌ๋ฅผ ํ๋์ ๋๊ตฌ๋ก ํด๊ฒฐํ ์ ์๋ ๊ฐ๋ ฅํ ๋๊ตฌ์ ๋๋ค.
- ์ค์น ๋ฐ ์ด๊ธฐํ: ๊ฐ๋จํ ๋ช
๋ น์ด๋ก ์์ํ์ฌ,
pyproject.toml
ํ์ผ๋ก ํ๋ก์ ํธ ์ค์ - ๊ฐ์ํ๊ฒฝ ๊ด๋ฆฌ:
poetry shell
์ ํตํด ์์ฝ๊ฒ ๊ฐ์ํ๊ฒฝ ํ์ฑํ - ์์กด์ฑ ๊ด๋ฆฌ:
poetry.lock
ํ์ผ๋ก ๋ชจ๋ ์์กด์ฑ์ ๋ฒ์ ์ ํ์ ํ์ฌ ํ์ ํ๊ฒฝ์์ ์์ ์ฑ์ ๋ณด์ฅ