Week 19 ํ•™์Šต ์ •๋ฆฌ

Product Serving ๊ฐœ์š”

๋ชจ๋ธ์ด๋‚˜ ํ”„๋กœ๊ทธ๋žจ์„ ๊ฐœ๋ฐœํ•œ ํ›„์—๋Š” ์ด๋ฅผ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ œํ’ˆ์œผ๋กœ ๋ฐฐํฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ serving์ด๋ผ๊ณ  ํ•˜๋ฉฐ, ๋Œ€ํ‘œ์ ์ธ ์˜ˆ๋กœ ChatGPT๊ฐ€ prompt๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž ์š”์ฒญ์— ์‘๋‹ตํ•˜๋Š” ๋ฐฉ์‹์„ ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
serving ๋ฐฉ์‹์€ ํฌ๊ฒŒ Batch Serving๊ณผ Online (Real Time) Serving ๋‘ ๊ฐ€์ง€๋กœ ๊ตฌ๋ถ„๋˜๋ฉฐ, ๋ฌธ์ œ ์ƒํ™ฉ, ์ œ์•ฝ ์กฐ๊ฑด, ์ธ๋ ฅ, ๋ฐ์ดํ„ฐ ์ €์žฅ ํ˜•ํƒœ, ๋ ˆ๊ฑฐ์‹œ ์‹œ์Šคํ…œ ์œ ๋ฌด ๋“ฑ์— ๋”ฐ๋ผ ์ ํ•ฉํ•œ ๋ฐฉ์‹์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


1. Batch Serving

1-1. ๊ฐœ๋… ๋ฐ ํŠน์ง•

์˜ˆ์‹œ:
Netflix์˜ ์ฝ˜ํ…์ธ  ์ถ”์ฒœ ์‹œ์Šคํ…œ์€ n์‹œ๊ฐ„ ๋‹จ์œ„๋กœ ์˜ˆ์ธก ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ด DB์— ์ €์žฅํ•œ ํ›„, DB์˜ ์˜ˆ์ธก ๊ฒฐ๊ณผ๋ฅผ ์ฝ์–ด์™€ ์„œ๋น™ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

1-2. Batch ํŒจํ„ด ๊ตฌ์„ฑ ์š”์†Œ

Pasted image 20250311142136.png
Batch ํŒจํ„ด์€ ํฌ๊ฒŒ 3๊ฐœ์˜ ํŒŒํŠธ๋กœ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์žฅ์ :

๋‹จ์ :


2. Online (Real Time) Serving

2-1. ๊ฐœ๋… ๋ฐ ํŠน์ง•

์˜ˆ์‹œ:

2-2. Online Serving ํŒจํ„ด: Web Single ํŒจํ„ด

Pasted image 20250311142148.png
Web Single ํŒจํ„ด ๊ตฌ์„ฑ ์š”์†Œ:

์žฅ์ :

๋‹จ์ :


3. Serving ์ฒ˜๋ฆฌ ๋ฐฉ์‹: Synchronous vs. Asynchronous

3-1. Synchronous ํŒจํ„ด

3-2. Asynchronous ํŒจํ„ด


๊ฒฐ๋ก 

์ œํ’ˆ ์„œ๋น™์€ ๊ฐœ๋ฐœํ•œ ํ”„๋กœ๊ทธ๋žจ์ด๋‚˜ ๋ชจ๋ธ์„ ์‹ค์ œ ์„œ๋น„์Šค์— ์ ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์ค‘์š”ํ•œ ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค.

๊ฐ ๋ฐฉ์‹์€ ์ƒํ™ฉ๊ณผ ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์„ ํƒ๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋ฌธ์ œ ์ƒํ™ฉ, ์ œ์•ฝ ์กฐ๊ฑด, ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐฉ์‹, ์‹œ์Šคํ…œ ํ™˜๊ฒฝ ๋“ฑ์„ ์ข…ํ•ฉ์ ์œผ๋กœ ๊ณ ๋ คํ•ด ์ตœ์ ์˜ ์„œ๋น™ ๋ฐฉ์‹์„ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


Apache Airflow๋กœ Batch Serving ์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌ์ถ•ํ•˜๊ธฐ

Apache Airflow๋Š” ์›Œํฌํ”Œ๋กœ์šฐ ๊ด€๋ฆฌ ๋ฐ ์Šค์ผ€์ค„๋ง ๋„๊ตฌ๋กœ, ๋ชจ๋ธ ํ•™์Šต์ด๋‚˜ ์˜ˆ์ธก๊ณผ ๊ฐ™์€ ์ฃผ๊ธฐ์ ์ธ ์ž‘์—…(batch serving)์„ ์ž๋™ํ™”ํ•˜๋Š” ๋ฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํ•™์Šต์€ 1์ฃผ์ผ์— 1๋ฒˆ, ์˜ˆ์ธก์€ 10๋ถ„๋งˆ๋‹ค ์‹คํ–‰ํ•˜๋Š” ๋“ฑ์˜ ์Šค์ผ€์ค„๋ง์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฒˆ ํฌ์ŠคํŠธ์—์„œ๋Š” Airflow์˜ ๊ธฐ๋ณธ ๊ฐœ๋…, ์„ค์น˜ ๋ฐ ํ™˜๊ฒฝ ์„ค์ •, DAG ์ž‘์„ฑ ๋ฐฉ๋ฒ•, ๊ทธ๋ฆฌ๊ณ  Slack ์—ฐ๋™์„ ํ†ตํ•œ ์•Œ๋ฆผ ๊ธฐ๋Šฅ๊นŒ์ง€ ์ž์„ธํžˆ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


1. Apache Airflow ๊ธฐ๋ณธ ๊ฐœ๋…

DAG (Directed Acyclic Graph)

Operator

Scheduler

Executor


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 ์‹คํ–‰


3. DAG ์ž‘์„ฑํ•˜๊ธฐ

DAG๋Š” Airflow์—์„œ ์ž‘์—…์˜ ํ๋ฆ„๊ณผ ์ˆœ์„œ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

  1. DAG ํŒŒ์ผ ์ €์žฅ ํด๋”:
    AIRFLOW_HOME ๋‚ด์— dags ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  2. 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 ํ‘œํ˜„์‹ ๊ฐ„๋‹จ ์ •๋ฆฌ

Pasted image 20250311142550.png

์ž‘์„ฑํ•œ 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 ์„ค์ •

  1. Slack API Apps ํŽ˜์ด์ง€์—์„œ "Create New App > From scratch"๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.
  2. App Name๊ณผ Workspace๋ฅผ ์„ค์ •ํ•œ ํ›„, Basic Information ํƒญ์—์„œ Incoming Webhooks๋ฅผ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค.
  3. "Add New Webhook to Workspace"๋ฅผ ํ†ตํ•ด ํŠน์ • ์ฑ„๋„์— ๋Œ€ํ•œ Webhook URL(์˜ˆ: https://hooks.slack.com/services/~~~~~~~~/1234567)์„ ๋ฐœ๊ธ‰๋ฐ›์Šต๋‹ˆ๋‹ค.

4-3. Airflow์— Webhook ๋“ฑ๋ก

Airflow Web UI์—์„œ Admin > Connections๋กœ ์ด๋™ ํ›„, ์ƒˆ Connection์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

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๋Š” ๋ณต์žกํ•œ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์‰ฝ๊ฒŒ ์ •์˜ํ•˜๊ณ , ๋ฐฐ์น˜ ์Šค์ผ€์ค„๋ง์„ ํ†ตํ•ด ๋ฐ˜๋ณต์ ์ธ ์ž‘์—…์„ ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.


์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜์™€ Web API ์ดํ•ดํ•˜๊ธฐ

ํ˜„๋Œ€ ์›น/๋ชจ๋ฐ”์ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ•ต์‹ฌ์€ ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜์™€ API ์„ค๊ณ„์ž…๋‹ˆ๋‹ค. ์ด ํฌ์ŠคํŠธ์—์„œ๋Š” ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜์˜ ์ข…๋ฅ˜์™€ ๊ฐ๊ฐ์˜ ํŠน์ง•, ๊ทธ๋ฆฌ๊ณ  API์˜ ๊ธฐ๋ณธ ๊ฐœ๋… ๋ฐ REST API์˜ ๊ตฌ์„ฑ ์š”์†Œ์™€ URL, HTTP ์š”์ฒญ์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ๊ฐœ๋…์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


1. ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜

1-1. ๋ชจ๋†€๋ฆฌ์‹ ์•„ํ‚คํ…์ฒ˜ (Monolithic Architecture)

1-2. ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ (Microservice Architecture)


2. API์˜ ๊ธฐ๋ณธ ๊ฐœ๋…

API๋ž€?

Web API


3. REST API

REST API๋Š” ์ž์›(Resource)์„ ํ‘œํ˜„ํ•˜๊ณ  ์ƒํƒœ๋ฅผ ์ „์†กํ•˜๋Š” ๋ฐ ์ค‘์ ์„ ๋‘” ์•„ํ‚คํ…์ฒ˜์ž…๋‹ˆ๋‹ค. HTTP๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋ฉฐ, ์š”์ฒญ์„ ํ†ตํ•ด ์–ด๋–ค ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š”์ง€ ์‰ฝ๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

3-1. REST API ๊ตฌ์„ฑ ์š”์†Œ


4. URL์˜ ๊ตฌ์„ฑ ์š”์†Œ

URL์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์— ์š”์ฒญํ•  ๋ฆฌ์†Œ์Šค์˜ ์œ„์น˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋ฉฐ, ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.

        
- **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

6. HTTP 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. ๊ฐ ํŒŒ์ผ๋ณ„ ๊ตฌํ˜„ ๋‚ด์šฉ

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์„ ํ™œ์šฉํ•ด ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ๋ถ€ํ„ฐ ์„ค์ •๊ฐ’์„ ๋ถˆ๋Ÿฌ์˜ค๋ฏ€๋กœ, ๋ฐฐํฌ ํ™˜๊ฒฝ์— ๋งž์ถฐ ์‰ฝ๊ฒŒ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


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)

๋ชจ๋ธ ๋กœ๋“œ์™€ ๊ด€๋ จ๋œ ํ•จ์ˆ˜๋“ค์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

def load_model(model_path: str):
    import joblib
    return joblib.load(model_path)

# ํ•„์š” ์‹œ get_model, predict ๋“ฑ ๋‹ค๋ฅธ ํ•จ์ˆ˜๋„ ์ •์˜ ๊ฐ€๋Šฅ

API์—์„œ ์ฃผ๊ณ  ๋ฐ›์„ ๋ฐ์ดํ„ฐ์˜ ์Šคํ‚ค๋งˆ(ํ˜•ํƒœ)๋ฅผ Pydantic์„ ์ด์šฉํ•ด ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

from pydantic import BaseModel

class PredictionRequest(BaseModel):
    features: list  # ์ƒํ™ฉ์— ๋งž๊ฒŒ input ๋ฐ์ดํ„ฐ์˜ ํ˜•์‹์„ ์ •์˜

class PredictionResponse(BaseModel):
    id: int
    result: int

๋ชจ๋ธ ์˜ˆ์ธก ๋ฐ ๊ฒฐ๊ณผ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” 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)

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)

๋™์ž‘ ํ™•์ธ:


4. ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„

FastAPI๋Š” ๋‹ค์–‘ํ•œ HTTP ๋ฉ”์„œ๋“œ์™€ ๋ฐ์ดํ„ฐ ์ „์†ก ๋ฐฉ์‹, ๊ทธ๋ฆฌ๊ณ  ๋ฐฐ๊ฒฝ ์ž‘์—…, ํŒŒ์ผ ์—…๋กœ๋“œ, ํ”„๋ก ํŠธ์—”๋“œ ๋ Œ๋”๋ง ๋“ฑ ์—ฌ๋Ÿฌ ๊ธฐ๋Šฅ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

4-1. Path & Query Parameter

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 ํŒŒ์ผ์€ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ๋‚˜ ์ง€์ •ํ•œ ๋””๋ ‰ํ† ๋ฆฌ์— ์œ„์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
Pasted image 20250311144549.png

4-4. ํŒŒ์ผ ์—…๋กœ๋“œ

Pasted image 20250311144606.png
ํŒŒ์ผ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ๋„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 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๋ถ€ํ„ฐ ๋ณต์žกํ•œ ์˜จ๋ผ์ธ ์„œ๋น™ ์‹œ์Šคํ…œ๊นŒ์ง€ ๋น ๋ฅด๊ฒŒ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


Poetry๋ฅผ ์ด์šฉํ•œ ํŒŒ์ด์ฌ ํŒจํ‚ค์ง€ ๋ฐ ์˜์กด์„ฑ ๊ด€๋ฆฌ

Poetry๋Š” ํŒŒ์ด์ฌ ํ”„๋กœ์ ํŠธ์˜ ํŒจํ‚ค์ง€ ์„ค์น˜, ๊ฐ€์ƒํ™˜๊ฒฝ ์ƒ์„ฑ, ์˜์กด์„ฑ ๊ด€๋ฆฌ, ๊ทธ๋ฆฌ๊ณ  ๋ฐฐํฌ๋ฅผ ์œ„ํ•œ ํŒจํ‚ค์ง• ์ž‘์—…(build, publish)์„ ํ•œ ๊ณณ์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ์ „ํ†ต์ ์ธ pip๋‚˜ anaconda์™€ ๋‹ฌ๋ฆฌ, Poetry๋Š” ํ”„๋กœ์ ํŠธ์˜ ์ „์ฒด ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด ์ ์  ๋” ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ฑ„ํƒํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


1. Poetry๋ž€?

Poetry๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.


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 ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์—ฌ ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์— ํ•„์š”ํ•œ ๋ชจ๋“  ์˜์กด์„ฑ๊ณผ ๊ทธ ์ •ํ™•ํ•œ ๋ฒ„์ „์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.


๊ฒฐ๋ก 

Poetry๋Š” ํŒจํ‚ค์ง€ ์„ค์น˜๋ถ€ํ„ฐ ๊ฐ€์ƒํ™˜๊ฒฝ ๊ด€๋ฆฌ, ์˜์กด์„ฑ ๋ฐ ๋ฐฐํฌ๊นŒ์ง€ ํŒŒ์ด์ฌ ํ”„๋กœ์ ํŠธ์˜ ์ „๋ฐ˜์ ์ธ ๊ด€๋ฆฌ๋ฅผ ํ•˜๋‚˜์˜ ๋„๊ตฌ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.