์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๋™๊ณผ SQLAlchemy๋ฅผ ํ™œ์šฉํ•œ ORM ๊ธฐ๋ฐ˜ ๊ฐœ๋ฐœ

๐Ÿš€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๋™๊ณผ SQLAlchemy๋ฅผ ํ™œ์šฉํ•œ ORM ๊ธฐ๋ฐ˜ ๊ฐœ๋ฐœ

Python์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ฐ€์žฅ ์Šค๋งˆํŠธํ•œ ๋ฐฉ๋ฒ•! ์นœ๊ตฌ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ๋ฐฐ์›Œ๋ณด์ž ๐Ÿ’ก

์•ˆ๋…•! ์˜ค๋Š˜์€ Python ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๊ผญ ์•Œ์•„์•ผ ํ•  SQLAlchemy์™€ ORM์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ ๊ฑฐ์•ผ. ๐Ÿ˜Š ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋‹ค๋ฃจ๋Š” ๊ฒŒ ์ฒ˜์Œ์—” ์ข€ ์–ด๋ ต๊ฒŒ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ๋Š”๋ฐ, ์ฒœ์ฒœํžˆ ๋”ฐ๋ผ์˜ค๋‹ค ๋ณด๋ฉด "์•„, ์ด๊ฒŒ ์ด๋ ‡๊ฒŒ ํŽธํ•œ ๊ฑฐ์˜€๊ตฌ๋‚˜!" ํ•˜๊ณ  ๋ฌด๋ฆŽ์„ ํƒ ์น˜๊ฒŒ ๋  ๊ฑฐ์•ผ. ๋งˆ์น˜ ์ž์ „๊ฑฐ ํƒ€๋Š” ๋ฒ•์„ ๋ฐฐ์šฐ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ง์ด์•ผ!

์š”์ฆ˜ ์›น ๊ฐœ๋ฐœ์ด๋‚˜ ๋ฐ์ดํ„ฐ ๋ถ„์„ ํ”„๋กœ์ ํŠธ๋ฅผ ํ•˜๋‹ค ๋ณด๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ํ•„์ˆ˜์ž–์•„? ๊ทธ๋Ÿฐ๋ฐ SQL ์ฟผ๋ฆฌ๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•˜๋Š” ๊ฑด ์‹ค์ˆ˜ํ•˜๊ธฐ๋„ ์‰ฝ๊ณ , ์ฝ”๋“œ ๊ด€๋ฆฌ๋„ ๋ณต์žกํ•ด์ง€๋”๋ผ๊ณ . ๊ทธ๋ž˜์„œ ๋“ฑ์žฅํ•œ ๊ฒŒ ๋ฐ”๋กœ ORM(Object-Relational Mapping)์ด์•ผ! ๐ŸŽฏ

Python Code ๊ฐ์ฒด ์ง€ํ–ฅ user.name user.save() Database ๊ด€๊ณ„ํ˜• DB SELECT * FROM INSERT INTO ๐Ÿ”„ SQLAlchemy ORM ๋งˆ๋ฒ•์‚ฌ ์‰ฌ์šด ์ฝ”๋“œ ์ž‘์„ฑ ์ž๋™ ๋ณ€ํ™˜ ์•ˆ์ „ํ•œ ์ฟผ๋ฆฌ DB ๋…๋ฆฝ์„ฑ

๐ŸŽช ORM์ด ๋ญ๊ธธ๋ž˜? ๊ธฐ์ดˆ๋ถ€ํ„ฐ ์ฐจ๊ทผ์ฐจ๊ทผ!

์ž, ๋จผ์ € ORM์ด ๋ญ”์ง€๋ถ€ํ„ฐ ์•Œ์•„๋ณด์ž. ORM(Object-Relational Mapping)์€ ๋ง ๊ทธ๋Œ€๋กœ ๐Ÿ”— ๊ฐ์ฒด์™€ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์—ฐ๊ฒฐํ•ด์ฃผ๋Š” ๊ธฐ์ˆ ์ด์•ผ. ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, ์šฐ๋ฆฌ๊ฐ€ Python์—์„œ ์“ฐ๋Š” ํด๋ž˜์Šค์™€ ๊ฐ์ฒด๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ํ…Œ์ด๋ธ”๊ณผ ๋ ˆ์ฝ”๋“œ๋กœ ์ž๋™์œผ๋กœ ๋ณ€ํ™˜ํ•ด์ฃผ๋Š” ๋งˆ๋ฒ• ๊ฐ™์€ ๋…€์„์ด์ง€!

๐Ÿค” ์™œ ORM์„ ์จ์•ผ ํ• ๊นŒ?

์ „ํ†ต์ ์ธ ๋ฐฉ์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋‹ค๋ฃจ๋ ค๋ฉด SQL ์ฟผ๋ฆฌ๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•ด์•ผ ํ•ด. ์˜ˆ๋ฅผ ๋“ค์–ด ์ด๋Ÿฐ ์‹์œผ๋กœ ๋ง์ด์•ผ:

# ์ „ํ†ต์ ์ธ ๋ฐฉ์‹ - SQL ์ง์ ‘ ์ž‘์„ฑ
cursor.execute("SELECT * FROM users WHERE age > 20")
results = cursor.fetchall()

# ๋ฐ์ดํ„ฐ ์‚ฝ์ž…๋„ ๋ณต์žกํ•ด
cursor.execute("INSERT INTO users (name, email, age) VALUES (?, ?, ?)", 
               (name, email, age))

์ด ๋ฐฉ์‹์˜ ๋ฌธ์ œ์ ์€ ๋ญ˜๊นŒ? ๐Ÿคทโ€โ™‚๏ธ

์ „ํ†ต์  SQL ๋ฐฉ์‹์˜ ๋‹จ์ : 1. SQL ๋ฌธ๋ฒ• ์˜ค๋ฅ˜๊ฐ€ ๋Ÿฐํƒ€์ž„์—๋งŒ ๋ฐœ๊ฒฌ๋จ (๊ฐœ๋ฐœ ์ค‘์— ๋ชป ์žก์•„๋ƒ„)
2. ๋ฌธ์ž์—ด๋กœ ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•˜๋‹ˆ๊นŒ IDE์˜ ์ž๋™์™„์„ฑ์ด๋‚˜ ํƒ€์ž… ์ฒดํฌ๊ฐ€ ์•ˆ ๋จ
3. SQL Injection ๊ณต๊ฒฉ์— ์ทจ์•ฝํ•  ์ˆ˜ ์žˆ์Œ
4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋ฐ”๊พธ๋ฉด (MySQL โ†’ PostgreSQL ๋“ฑ) ์ฟผ๋ฆฌ๋ฅผ ๋‹ค์‹œ ์ž‘์„ฑํ•ด์•ผ ํ•จ
5. ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์ด ์–ด๋ ต๊ณ  ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ํž˜๋“ฆ

๋ฐ˜๋ฉด ORM์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋ ‡๊ฒŒ ๋ฐ”๋€Œ์–ด:

# ORM ๋ฐฉ์‹ - Python ๊ฐ์ฒด๋กœ ์ž‘์—…
users = session.query(User).filter(User.age > 20).all()

# ๋ฐ์ดํ„ฐ ์‚ฝ์ž…๋„ ๊ฐ„๋‹จํ•ด
new_user = User(name=name, email=email, age=age)
session.add(new_user)
session.commit()

ํ›จ์”ฌ ์ง๊ด€์ ์ด๊ณ  Python์Šค๋Ÿฝ์ง€? โœจ ์ด๊ฒŒ ๋ฐ”๋กœ ORM์˜ ๋งค๋ ฅ์ด์•ผ!

๐Ÿ’ก ํ•ต์‹ฌ ํฌ์ธํŠธ: ORM์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”์„ Python ํด๋ž˜์Šค๋กœ, ๋ ˆ์ฝ”๋“œ๋ฅผ ๊ฐ์ฒด๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜. ๋งˆ์น˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ Python ๊ฐ์ฒด์˜ ์ €์žฅ์†Œ์ฒ˜๋Ÿผ ๋А๊ปด์ง€๋Š” ๊ฑฐ์ง€!

๐Ÿ SQLAlchemy ์†Œ๊ฐœ - Python์˜ ์ตœ๊ฐ• ORM

Python์—๋Š” ์—ฌ๋Ÿฌ ORM ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์žˆ์ง€๋งŒ, ๊ทธ์ค‘์—์„œ๋„ SQLAlchemy๋Š” ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•˜๊ณ  ์œ ์—ฐํ•œ ์„ ํƒ์ง€์•ผ. Django ORM๋„ ์œ ๋ช…ํ•˜์ง€๋งŒ, SQLAlchemy๋Š” Django ํ”„๋ ˆ์ž„์›Œํฌ์— ์ข…์†๋˜์ง€ ์•Š๊ณ  ๋…๋ฆฝ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒŒ ํฐ ์žฅ์ ์ด์ง€! ๐ŸŽฏ

๐Ÿ“Š SQLAlchemy์˜ ํŠน์ง•

SQLAlchemy๊ฐ€ ํŠน๋ณ„ํ•œ ์ด์œ : 1. ๋‘ ๊ฐ€์ง€ ์‚ฌ์šฉ ๋ฐฉ์‹ ์ œ๊ณต
- Core: SQL ํ‘œํ˜„์‹์„ Python์œผ๋กœ ์ž‘์„ฑ (์ €์ˆ˜์ค€, ์„ธ๋ฐ€ํ•œ ์ œ์–ด)
- ORM: ์™„์ „ํ•œ ๊ฐ์ฒด ์ง€ํ–ฅ ๋ฐฉ์‹ (๊ณ ์ˆ˜์ค€, ํŽธ๋ฆฌํ•จ)

2. ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ง€์›
PostgreSQL, MySQL, SQLite, Oracle, MS SQL Server ๋“ฑ ๊ฑฐ์˜ ๋ชจ๋“  ์ฃผ์š” DB๋ฅผ ์ง€์›ํ•ด!

3. ๊ฐ•๋ ฅํ•œ ์ฟผ๋ฆฌ ๋นŒ๋”
๋ณต์žกํ•œ JOIN, ์„œ๋ธŒ์ฟผ๋ฆฌ, ์ง‘๊ณ„ ํ•จ์ˆ˜ ๋“ฑ์„ Python ์ฝ”๋“œ๋กœ ์ง๊ด€์ ์œผ๋กœ ์ž‘์„ฑ ๊ฐ€๋Šฅ

4. ์„ธ์…˜ ๊ด€๋ฆฌ
ํŠธ๋žœ์žญ์…˜๊ณผ ์—ฐ๊ฒฐ ํ’€๋ง์„ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌํ•ด์ค˜์„œ ์•ˆ์ „ํ•˜๊ณ  ํšจ์œจ์ 

5. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋„๊ตฌ ์—ฐ๋™
Alembic๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๋ฒ„์ „ ๊ด€๋ฆฌ๊ฐ€ ์‰ฌ์›Œ์ง

๐Ÿ”ง SQLAlchemy ์„ค์น˜ํ•˜๊ธฐ

์„ค์น˜๋Š” ์ •๋ง ๊ฐ„๋‹จํ•ด. pip๋กœ ํ•œ ์ค„์ด๋ฉด ๋!

# ๊ธฐ๋ณธ SQLAlchemy ์„ค์น˜
pip install sqlalchemy

# ํŠน์ • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋“œ๋ผ์ด๋ฒ„๋„ ํ•จ๊ป˜ ์„ค์น˜
pip install sqlalchemy pymysql  # MySQL์šฉ
pip install sqlalchemy psycopg2-binary  # PostgreSQL์šฉ
# SQLite๋Š” Python ๊ธฐ๋ณธ ๋‚ด์žฅ์ด๋ผ ๋ณ„๋„ ์„ค์น˜ ๋ถˆํ•„์š”!

๋ฒ„์ „ ํ™•์ธ๋„ ํ•ด๋ณด์ž:

import sqlalchemy
print(sqlalchemy.__version__)  # 2.0 ์ด์ƒ์ด๋ฉด ์ตœ์‹  ๋ฒ„์ „!
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ: SQLAlchemy 2.0๋ถ€ํ„ฐ๋Š” API๊ฐ€ ๋งŽ์ด ๋ฐ”๋€Œ์—ˆ์–ด. ์ธํ„ฐ๋„ท์—์„œ ์˜ˆ์ œ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ๋•Œ ๋ฒ„์ „์„ ๊ผญ ํ™•์ธํ•˜์ž! 1.4 ๋ฒ„์ „๊ณผ 2.0 ๋ฒ„์ „์˜ ์ฝ”๋“œ๊ฐ€ ์ข€ ๋‹ค๋ฅด๊ฑฐ๋“ .

๐Ÿ—๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์„ค์ •ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ SQLAlchemy๋ฅผ ์‚ฌ์šฉํ•ด๋ณด์ž! ์ฒซ ๋‹จ๊ณ„๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์—ฐ๊ฒฐํ•˜๋Š” ๊ฑฐ์•ผ. SQLAlchemy๋Š” Engine์ด๋ผ๋Š” ๊ฐœ๋…์„ ์‚ฌ์šฉํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์—ฐ๊ฒฐ์„ ๊ด€๋ฆฌํ•ด. ๐Ÿ”Œ

๐ŸŽจ ์—ฐ๊ฒฐ ๋ฌธ์ž์—ด(Connection String) ์ดํ•ดํ•˜๊ธฐ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์—ฐ๊ฒฐํ•˜๋ ค๋ฉด ์—ฐ๊ฒฐ ๋ฌธ์ž์—ด์ด ํ•„์š”ํ•ด. ์ด๊ฑด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์ฃผ์†Œ ๊ฐ™์€ ๊ฑฐ์•ผ. ํ˜•์‹์€ ์ด๋ž˜:

# ๊ธฐ๋ณธ ํ˜•์‹
dialect+driver://username:password@host:port/database

# ์‹ค์ œ ์˜ˆ์‹œ๋“ค
# SQLite (ํŒŒ์ผ ๊ธฐ๋ฐ˜)
sqlite:///mydb.db

# MySQL
mysql+pymysql://user:password@localhost:3306/mydb

# PostgreSQL
postgresql+psycopg2://user:password@localhost:5432/mydb

# ๋ฉ”๋ชจ๋ฆฌ ๋‚ด SQLite (ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ์ข‹์•„!)
sqlite:///:memory:

๐Ÿ’ป ์‹ค์ „ ์—ฐ๊ฒฐ ์ฝ”๋“œ

์ด์ œ ์‹ค์ œ๋กœ ์—ฐ๊ฒฐํ•ด๋ณด์ž. ๋จผ์ € SQLite๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ์‹œ์ž‘ํ•ด๋ณผ๊ฒŒ:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# 1. ์—”์ง„ ์ƒ์„ฑ (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ)
engine = create_engine('sqlite:///example.db', echo=True)

# echo=True๋Š” ์‹คํ–‰๋˜๋Š” SQL์„ ์ฝ˜์†”์— ์ถœ๋ ฅํ•ด์ค˜ (๋””๋ฒ„๊น…์šฉ)
# ์‹ค์ œ ์šด์˜์—์„œ๋Š” echo=False๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒŒ ์ข‹์•„

# 2. ์„ธ์…˜ ํŒฉํ† ๋ฆฌ ์ƒ์„ฑ
SessionLocal = sessionmaker(bind=engine)

# 3. ์„ธ์…˜ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
session = SessionLocal()

print("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์„ฑ๊ณต! ๐ŸŽ‰")

MySQL์ด๋‚˜ PostgreSQL์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์ด๋ ‡๊ฒŒ:

# MySQL ์—ฐ๊ฒฐ
engine = create_engine(
    'mysql+pymysql://myuser:mypassword@localhost:3306/mydb',
    pool_size=10,  # ์—ฐ๊ฒฐ ํ’€ ํฌ๊ธฐ
    max_overflow=20,  # ์ตœ๋Œ€ ์ถ”๊ฐ€ ์—ฐ๊ฒฐ ์ˆ˜
    pool_pre_ping=True  # ์—ฐ๊ฒฐ ์œ ํšจ์„ฑ ์ž๋™ ์ฒดํฌ
)

# PostgreSQL ์—ฐ๊ฒฐ
engine = create_engine(
    'postgresql+psycopg2://myuser:mypassword@localhost:5432/mydb',
    pool_size=10,
    echo=False
)
๐Ÿ’ก ํ”„๋กœ ํŒ: ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ฝ”๋“œ์— ์ง์ ‘ ์“ฐ๋ฉด ์•ˆ ๋ผ! ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋‚˜ ์„ค์ • ํŒŒ์ผ์„ ์‚ฌ์šฉํ•˜์ž:
import os
from sqlalchemy import create_engine

DB_USER = os.getenv('DB_USER', 'default_user')
DB_PASSWORD = os.getenv('DB_PASSWORD', 'default_pass')
DB_HOST = os.getenv('DB_HOST', 'localhost')
DB_NAME = os.getenv('DB_NAME', 'mydb')

DATABASE_URL = f'mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}'
engine = create_engine(DATABASE_URL)

๐Ÿ” ์—ฐ๊ฒฐ ํ’€(Connection Pool) ์ดํ•ดํ•˜๊ธฐ

SQLAlchemy๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์—ฐ๊ฒฐ ํ’€์„ ์‚ฌ์šฉํ•ด. ์ด๊ฒŒ ๋ญ๋ƒ๋ฉด, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ๋งค๋ฒˆ ์ƒˆ๋กœ ๋งŒ๋“ค๊ณ  ๋Š๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ๋ฏธ๋ฆฌ ์—ฌ๋Ÿฌ ๊ฐœ ๋งŒ๋“ค์–ด๋‘๊ณ  ์žฌ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์•ผ. ๋งˆ์น˜ ์ˆ˜์˜์žฅ(Pool)์ฒ˜๋Ÿผ! ๐ŸŠโ€โ™‚๏ธ

์—ฐ๊ฒฐ ํ’€ ์ฃผ์š” ํŒŒ๋ผ๋ฏธํ„ฐ: pool_size: ๊ธฐ๋ณธ ์—ฐ๊ฒฐ ๊ฐœ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’: 5)
max_overflow: pool_size ์ดˆ๊ณผ ์‹œ ์ถ”๊ฐ€๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ์—ฐ๊ฒฐ ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’: 10)
pool_timeout: ์—ฐ๊ฒฐ์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ตœ๋Œ€ ์‹œ๊ฐ„(์ดˆ) (๊ธฐ๋ณธ๊ฐ’: 30)
pool_recycle: ์—ฐ๊ฒฐ์„ ์žฌํ™œ์šฉํ•˜๊ธฐ ์ „ ์œ ์ง€ ์‹œ๊ฐ„(์ดˆ) (๊ธฐ๋ณธ๊ฐ’: -1, ๋ฌด์ œํ•œ)
pool_pre_ping: ์—ฐ๊ฒฐ ์‚ฌ์šฉ ์ „ ์œ ํšจ์„ฑ ์ฒดํฌ ์—ฌ๋ถ€ (๊ธฐ๋ณธ๊ฐ’: False)
# ์—ฐ๊ฒฐ ํ’€ ์„ธ๋ฐ€ํ•˜๊ฒŒ ์„ค์ •ํ•˜๊ธฐ
from sqlalchemy.pool import QueuePool

engine = create_engine(
    'mysql+pymysql://user:pass@localhost/mydb',
    poolclass=QueuePool,
    pool_size=20,  # ๊ธฐ๋ณธ 20๊ฐœ ์—ฐ๊ฒฐ ์œ ์ง€
    max_overflow=30,  # ์ตœ๋Œ€ 50๊ฐœ๊นŒ์ง€ ๊ฐ€๋Šฅ
    pool_timeout=30,  # 30์ดˆ ๋Œ€๊ธฐ
    pool_recycle=3600,  # 1์‹œ๊ฐ„๋งˆ๋‹ค ์—ฐ๊ฒฐ ์žฌ์ƒ์„ฑ
    pool_pre_ping=True  # ์‚ฌ์šฉ ์ „ ์—ฐ๊ฒฐ ์ฒดํฌ
)

๐Ÿ“ฆ ๋ชจ๋ธ ์ •์˜ํ•˜๊ธฐ - ํ…Œ์ด๋ธ”์„ ํด๋ž˜์Šค๋กœ!

์ด์ œ ์žฌ๋ฏธ์žˆ๋Š” ๋ถ€๋ถ„์ด์•ผ! ORM์˜ ํ•ต์‹ฌ์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”์„ Python ํด๋ž˜์Šค๋กœ ์ •์˜ํ•˜๋Š” ๊ฑฐ์•ผ. SQLAlchemy์—์„œ๋Š” ์ด๊ฑธ Declarative Base๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ•ด. ๐ŸŽจ

๐ŸŒŸ ๊ธฐ๋ณธ ๋ชจ๋ธ ๋งŒ๋“ค๊ธฐ

๊ฐ„๋‹จํ•œ ์‚ฌ์šฉ์ž(User) ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ค์–ด๋ณด์ž:

from sqlalchemy import Column, Integer, String, DateTime, Boolean
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime

# Base ํด๋ž˜์Šค ์ƒ์„ฑ
Base = declarative_base()

# User ๋ชจ๋ธ ์ •์˜
class User(Base):
    __tablename__ = 'users'  # ์‹ค์ œ ํ…Œ์ด๋ธ” ์ด๋ฆ„
    
    # ์ปฌ๋Ÿผ ์ •์˜
    id = Column(Integer, primary_key=True, autoincrement=True)
    username = Column(String(50), unique=True, nullable=False)
    email = Column(String(100), unique=True, nullable=False)
    password = Column(String(255), nullable=False)
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # ๊ฐ์ฒด๋ฅผ ์ถœ๋ ฅํ•  ๋•Œ ๋ณด๊ธฐ ์ข‹๊ฒŒ (์„ ํƒ์‚ฌํ•ญ)
    def __repr__(self):
        return f"<User(id={self.id}, username='{self.username}')>"

์™€! ์ด๋ ‡๊ฒŒ Python ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ•˜๋ฉด, SQLAlchemy๊ฐ€ ์ž๋™์œผ๋กœ ์ด๊ฑธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”๋กœ ๋งŒ๋“ค์–ด์ค˜. ์‹ ๊ธฐํ•˜์ง€? โœจ

๐Ÿ”จ ํ…Œ์ด๋ธ” ์ƒ์„ฑํ•˜๊ธฐ

๋ชจ๋ธ์„ ์ •์˜ํ–ˆ์œผ๋ฉด ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ค์–ด์•ผ ํ•ด:

# ๋ชจ๋“  ํ…Œ์ด๋ธ” ์ƒ์„ฑ
Base.metadata.create_all(engine)

# ํŠน์ • ํ…Œ์ด๋ธ”๋งŒ ์ƒ์„ฑ
Base.metadata.tables['users'].create(engine)

# ํ…Œ์ด๋ธ” ์‚ญ์ œ (์กฐ์‹ฌํ•ด์„œ ์‚ฌ์šฉ!)
Base.metadata.drop_all(engine)

๐Ÿ“‹ ๋‹ค์–‘ํ•œ ์ปฌ๋Ÿผ ํƒ€์ž…

SQLAlchemy๋Š” ์ •๋ง ๋งŽ์€ ์ปฌ๋Ÿผ ํƒ€์ž…์„ ์ง€์›ํ•ด. ์ž์ฃผ ์“ฐ๋Š” ๊ฒƒ๋“ค์„ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ:

์ฃผ์š” ์ปฌ๋Ÿผ ํƒ€์ž…๋“ค: ์ •์ˆ˜ํ˜•:
- Integer: ์ผ๋ฐ˜ ์ •์ˆ˜
- BigInteger: ํฐ ์ •์ˆ˜ (BIGINT)
- SmallInteger: ์ž‘์€ ์ •์ˆ˜ (SMALLINT)

๋ฌธ์ž์—ด:
- String(length): ๊ณ ์ •/๊ฐ€๋ณ€ ๊ธธ์ด ๋ฌธ์ž์—ด
- Text: ๊ธด ํ…์ŠคํŠธ (๊ธธ์ด ์ œํ•œ ์—†์Œ)
- Unicode: ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž์—ด

์ˆซ์ž:
- Float: ๋ถ€๋™์†Œ์ˆ˜์ 
- Numeric(precision, scale): ์ •๋ฐ€ํ•œ ์†Œ์ˆ˜
- Decimal: ๊ธˆ์œต ๊ณ„์‚ฐ์šฉ

๋‚ ์งœ/์‹œ๊ฐ„:
- Date: ๋‚ ์งœ๋งŒ
- Time: ์‹œ๊ฐ„๋งŒ
- DateTime: ๋‚ ์งœ+์‹œ๊ฐ„
- Interval: ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ

๊ธฐํƒ€:
- Boolean: ์ฐธ/๊ฑฐ์ง“
- JSON: JSON ๋ฐ์ดํ„ฐ (PostgreSQL, MySQL 5.7+)
- LargeBinary: ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ (์ด๋ฏธ์ง€ ๋“ฑ)
- Enum: ์—ด๊ฑฐํ˜•

๐ŸŽฏ ์‹ค์ „ ์˜ˆ์ œ - ๋ธ”๋กœ๊ทธ ์‹œ์Šคํ…œ

์ข€ ๋” ๋ณต์žกํ•œ ์˜ˆ์ œ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊นŒ? ๋ธ”๋กœ๊ทธ ์‹œ์Šคํ…œ์˜ ๋ชจ๋ธ๋“ค์ด์•ผ:

from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Table
from sqlalchemy.orm import relationship
from datetime import datetime

Base = declarative_base()

# ๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„๋ฅผ ์œ„ํ•œ ์—ฐ๊ฒฐ ํ…Œ์ด๋ธ”
post_tags = Table('post_tags', Base.metadata,
    Column('post_id', Integer, ForeignKey('posts.id')),
    Column('tag_id', Integer, ForeignKey('tags.id'))
)

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True, nullable=False)
    email = Column(String(100), unique=True, nullable=False)
    bio = Column(Text)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # ๊ด€๊ณ„ ์„ค์ •: ํ•œ ์‚ฌ์šฉ์ž๋Š” ์—ฌ๋Ÿฌ ํฌ์ŠคํŠธ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Œ
    posts = relationship('Post', back_populates='author')
    comments = relationship('Comment', back_populates='user')

class Post(Base):
    __tablename__ = 'posts'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200), nullable=False)
    content = Column(Text, nullable=False)
    author_id = Column(Integer, ForeignKey('users.id'))
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, onupdate=datetime.utcnow)
    view_count = Column(Integer, default=0)
    
    # ๊ด€๊ณ„ ์„ค์ •
    author = relationship('User', back_populates='posts')
    comments = relationship('Comment', back_populates='post')
    tags = relationship('Tag', secondary=post_tags, back_populates='posts')

class Comment(Base):
    __tablename__ = 'comments'
    
    id = Column(Integer, primary_key=True)
    content = Column(Text, nullable=False)
    user_id = Column(Integer, ForeignKey('users.id'))
    post_id = Column(Integer, ForeignKey('posts.id'))
    created_at = Column(DateTime, default=datetime.utcnow)
    
    user = relationship('User', back_populates='comments')
    post = relationship('Post', back_populates='comments')

class Tag(Base):
    __tablename__ = 'tags'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(50), unique=True, nullable=False)
    
    posts = relationship('Post', secondary=post_tags, back_populates='tags')

์™€! ์ด์ œ ์‚ฌ์šฉ์ž, ํฌ์ŠคํŠธ, ๋Œ“๊ธ€, ํƒœ๊ทธ๊ฐ€ ๋ชจ๋‘ ์—ฐ๊ฒฐ๋œ ์™„์ „ํ•œ ๋ธ”๋กœ๊ทธ ์‹œ์Šคํ…œ์ด ์™„์„ฑ๋์–ด! ๐ŸŽ‰

๐Ÿ’ก ๊ด€๊ณ„(Relationship) ์ดํ•ดํ•˜๊ธฐ:

- One-to-Many: User โ†” Post (ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ๋Ÿฌ ํฌ์ŠคํŠธ)
- Many-to-One: Post โ†” User (์—ฌ๋Ÿฌ ํฌ์ŠคํŠธ๊ฐ€ ํ•œ ์‚ฌ์šฉ์ž์—๊ฒŒ)
- Many-to-Many: Post โ†” Tag (ํฌ์ŠคํŠธ์™€ ํƒœ๊ทธ๋Š” ์„œ๋กœ ์—ฌ๋Ÿฌ ๊ฐœ)

relationship()์˜ back_populates๋Š” ์–‘๋ฐฉํ–ฅ ๊ด€๊ณ„๋ฅผ ์„ค์ •ํ•ด์ค˜. ์ด๋Ÿฌ๋ฉด user.posts๋กœ๋„ ์ ‘๊ทผํ•˜๊ณ , post.author๋กœ๋„ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด!
CRUD ์ž‘์—… ํ๋ฆ„๋„ CREATE ๋ฐ์ดํ„ฐ ์ƒ์„ฑ READ ๋ฐ์ดํ„ฐ ์กฐํšŒ UPDATE ๋ฐ์ดํ„ฐ ์ˆ˜์ • DELETE ๋ฐ์ดํ„ฐ ์‚ญ์ œ โž• ๐Ÿ” โœ๏ธ ๐Ÿ—‘๏ธ ๐Ÿ’พ ๐Ÿ“Š ๐Ÿ”„ โŒ

โœ๏ธ CRUD ์ž‘์—… - ๋ฐ์ดํ„ฐ ๋‹ค๋ฃจ๊ธฐ์˜ ๊ธฐ๋ณธ

์ž, ์ด์ œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์•ผ! CRUD๋Š” Create(์ƒ์„ฑ), Read(์ฝ๊ธฐ), Update(์ˆ˜์ •), Delete(์‚ญ์ œ)์˜ ์•ฝ์ž๋กœ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—…์˜ ๊ธฐ๋ณธ์ด์ง€. SQLAlchemy๋กœ ์ด๊ฑธ ์–ด๋–ป๊ฒŒ ํ•˜๋Š”์ง€ ํ•˜๋‚˜์”ฉ ์•Œ์•„๋ณด์ž! ๐Ÿš€

โž• Create - ๋ฐ์ดํ„ฐ ์ƒ์„ฑํ•˜๊ธฐ

์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฑด ์ •๋ง ๊ฐ„๋‹จํ•ด. ๊ทธ๋ƒฅ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๊ณ  ์„ธ์…˜์— ์ถ”๊ฐ€ํ•˜๋ฉด ๋ผ:

from sqlalchemy.orm import Session

# ์„ธ์…˜ ์ƒ์„ฑ
session = Session(engine)

# 1. ๋‹จ์ผ ์‚ฌ์šฉ์ž ์ถ”๊ฐ€
new_user = User(
    username='john_doe',
    email='john@example.com',
    password='hashed_password_here',
    bio='Python ๊ฐœ๋ฐœ์ž์ž…๋‹ˆ๋‹ค!'
)

session.add(new_user)
session.commit()  # ์‹ค์ œ๋กœ DB์— ์ €์žฅ

print(f"์ƒˆ ์‚ฌ์šฉ์ž ์ƒ์„ฑ! ID: {new_user.id}")  # ID๊ฐ€ ์ž๋™์œผ๋กœ ํ• ๋‹น๋จ

# 2. ์—ฌ๋Ÿฌ ๊ฐœ ํ•œ ๋ฒˆ์— ์ถ”๊ฐ€
users = [
    User(username='alice', email='alice@example.com', password='pass1'),
    User(username='bob', email='bob@example.com', password='pass2'),
    User(username='charlie', email='charlie@example.com', password='pass3')
]

session.add_all(users)
session.commit()

print(f"{len(users)}๋ช…์˜ ์‚ฌ์šฉ์ž ์ถ”๊ฐ€ ์™„๋ฃŒ!")
๐Ÿ’ก commit()์˜ ์ค‘์š”์„ฑ: session.add()๋งŒ ํ•˜๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์—๋งŒ ์žˆ์–ด. session.commit()์„ ํ•ด์•ผ ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋ผ! ๋งˆ์น˜ ๋ฌธ์„œ ์ž‘์„ฑ ํ›„ ์ €์žฅ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์•ผ ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ!

๐Ÿ” Read - ๋ฐ์ดํ„ฐ ์กฐํšŒํ•˜๊ธฐ

๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด์˜ค๋Š” ๋ฐฉ๋ฒ•์€ ์ •๋ง ๋‹ค์–‘ํ•ด. ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด์ž:

# 1. ๋ชจ๋“  ์‚ฌ์šฉ์ž ๊ฐ€์ ธ์˜ค๊ธฐ
all_users = session.query(User).all()
print(f"์ด {len(all_users)}๋ช…์˜ ์‚ฌ์šฉ์ž")

# 2. ์ฒซ ๋ฒˆ์งธ ์‚ฌ์šฉ์ž๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ
first_user = session.query(User).first()
print(f"์ฒซ ๋ฒˆ์งธ ์‚ฌ์šฉ์ž: {first_user.username}")

# 3. ID๋กœ ํŠน์ • ์‚ฌ์šฉ์ž ์ฐพ๊ธฐ
user = session.query(User).get(1)  # ID๊ฐ€ 1์ธ ์‚ฌ์šฉ์ž
# ๋˜๋Š”
user = session.query(User).filter(User.id == 1).first()

# 4. ์กฐ๊ฑด์œผ๋กœ ํ•„ํ„ฐ๋ง
active_users = session.query(User).filter(User.is_active == True).all()

# 5. ์—ฌ๋Ÿฌ ์กฐ๊ฑด (AND)
user = session.query(User).filter(
    User.username == 'john_doe',
    User.is_active == True
).first()

# 6. OR ์กฐ๊ฑด
from sqlalchemy import or_
users = session.query(User).filter(
    or_(User.username == 'alice', User.username == 'bob')
).all()

# 7. LIKE ๊ฒ€์ƒ‰ (๋ถ€๋ถ„ ์ผ์น˜)
users = session.query(User).filter(User.email.like('%@example.com')).all()

# 8. IN ์กฐ๊ฑด
usernames = ['alice', 'bob', 'charlie']
users = session.query(User).filter(User.username.in_(usernames)).all()

# 9. ์ •๋ ฌ
users = session.query(User).order_by(User.created_at.desc()).all()  # ์ตœ์‹ ์ˆœ
users = session.query(User).order_by(User.username.asc()).all()  # ์ด๋ฆ„์ˆœ

# 10. ๊ฐœ์ˆ˜ ์ œํ•œ
recent_users = session.query(User).order_by(User.created_at.desc()).limit(10).all()

# 11. ๊ฐœ์ˆ˜ ์„ธ๊ธฐ
user_count = session.query(User).count()
print(f"์ด ์‚ฌ์šฉ์ž ์ˆ˜: {user_count}")

๋” ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋„ ๊ฐ€๋Šฅํ•ด:

# 12. ํŽ˜์ด์ง€๋„ค์ด์…˜
page = 1
per_page = 20
users = session.query(User)\
    .order_by(User.created_at.desc())\
    .offset((page - 1) * per_page)\
    .limit(per_page)\
    .all()

# 13. ํŠน์ • ์ปฌ๋Ÿผ๋งŒ ์„ ํƒ
usernames = session.query(User.username, User.email).all()
for username, email in usernames:
    print(f"{username}: {email}")

# 14. ์ง‘๊ณ„ ํ•จ์ˆ˜
from sqlalchemy import func
avg_view_count = session.query(func.avg(Post.view_count)).scalar()
max_views = session.query(func.max(Post.view_count)).scalar()

# 15. ๊ทธ๋ฃนํ™”
from sqlalchemy import func
post_counts = session.query(
    User.username,
    func.count(Post.id).label('post_count')
).join(Post).group_by(User.username).all()

for username, count in post_counts:
    print(f"{username}: {count}๊ฐœ์˜ ํฌ์ŠคํŠธ")

โœ๏ธ Update - ๋ฐ์ดํ„ฐ ์ˆ˜์ •ํ•˜๊ธฐ

๋ฐ์ดํ„ฐ ์ˆ˜์ •๋„ ๊ฐ„๋‹จํ•ด. ๊ฐ์ฒด์˜ ์†์„ฑ์„ ๋ฐ”๊พธ๊ณ  commitํ•˜๋ฉด ๋ผ:

# 1. ๋‹จ์ผ ๊ฐ์ฒด ์ˆ˜์ •
user = session.query(User).filter(User.username == 'john_doe').first()
if user:
    user.email = 'newemail@example.com'
    user.bio = '์—…๋ฐ์ดํŠธ๋œ ์†Œ๊ฐœ์ž…๋‹ˆ๋‹ค!'
    session.commit()
    print("์‚ฌ์šฉ์ž ์ •๋ณด ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ!")

# 2. ์—ฌ๋Ÿฌ ๊ฐ์ฒด ํ•œ ๋ฒˆ์— ์ˆ˜์ •
session.query(User).filter(User.is_active == False).update({
    'is_active': True
})
session.commit()
print("๋น„ํ™œ์„ฑ ์‚ฌ์šฉ์ž๋“ค์„ ๋ชจ๋‘ ํ™œ์„ฑํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค!")

# 3. ์กฐ๊ฑด๋ถ€ ์—…๋ฐ์ดํŠธ
session.query(Post).filter(Post.view_count < 10).update({
    'view_count': Post.view_count + 1
})
session.commit()

# 4. ๊ด€๊ณ„๋œ ๊ฐ์ฒด ์ˆ˜์ •
post = session.query(Post).first()
post.title = "์ƒˆ๋กœ์šด ์ œ๋ชฉ"
post.content = "์ƒˆ๋กœ์šด ๋‚ด์šฉ"
# ํƒœ๊ทธ ์ถ”๊ฐ€
new_tag = Tag(name='Python')
post.tags.append(new_tag)
session.commit()

๐Ÿ—‘๏ธ Delete - ๋ฐ์ดํ„ฐ ์‚ญ์ œํ•˜๊ธฐ

์‚ญ์ œ๋Š” ์กฐ์‹ฌํ•ด์„œ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. ํ•œ ๋ฒˆ ์‚ญ์ œํ•˜๋ฉด ๋ณต๊ตฌ๊ฐ€ ์–ด๋ ต๊ฑฐ๋“ ! โš ๏ธ

# 1. ๋‹จ์ผ ๊ฐ์ฒด ์‚ญ์ œ
user = session.query(User).filter(User.username == 'test_user').first()
if user:
    session.delete(user)
    session.commit()
    print("์‚ฌ์šฉ์ž ์‚ญ์ œ ์™„๋ฃŒ!")

# 2. ์กฐ๊ฑด์— ๋งž๋Š” ์—ฌ๋Ÿฌ ๊ฐ์ฒด ์‚ญ์ œ
deleted_count = session.query(Post).filter(Post.view_count == 0).delete()
session.commit()
print(f"{deleted_count}๊ฐœ์˜ ํฌ์ŠคํŠธ ์‚ญ์ œ!")

# 3. ๊ด€๊ณ„๋œ ๊ฐ์ฒด ์ฒ˜๋ฆฌ
# CASCADE ์„ค์ •์ด ๋˜์–ด ์žˆ์œผ๋ฉด ์ž๋™์œผ๋กœ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋„ ์‚ญ์ œ๋จ
user = session.query(User).get(1)
session.delete(user)  # ์ด ์‚ฌ์šฉ์ž์˜ ํฌ์ŠคํŠธ์™€ ๋Œ“๊ธ€๋„ ํ•จ๊ป˜ ์‚ญ์ œ๋  ์ˆ˜ ์žˆ์Œ
session.commit()
โš ๏ธ ์‚ญ์ œ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:

1. ์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฌผ๋ฆฌ์  ์‚ญ์ œ๋ณด๋‹ค ๋…ผ๋ฆฌ์  ์‚ญ์ œ(is_deleted ํ”Œ๋ž˜๊ทธ)๋ฅผ ๊ถŒ์žฅํ•ด
2. CASCADE ์„ค์ •์„ ์ž˜ ํ™•์ธํ•˜์ž. ์˜๋„์น˜ ์•Š๊ฒŒ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ชจ๋‘ ์‚ญ์ œ๋  ์ˆ˜ ์žˆ์–ด
3. ์‚ญ์ œ ์ „์— ํ•ญ์ƒ ๋ฐฑ์—…์„ ํ™•์ธํ•˜์ž!

๐Ÿ”„ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ

์—ฌ๋Ÿฌ ์ž‘์—…์„ ํ•˜๋‚˜์˜ ๋‹จ์œ„๋กœ ๋ฌถ์–ด์„œ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋Š” ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•ด:

try:
    # ์—ฌ๋Ÿฌ ์ž‘์—…์„ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ
    new_user = User(username='newuser', email='new@example.com')
    session.add(new_user)
    
    new_post = Post(title='์ฒซ ํฌ์ŠคํŠธ', content='๋‚ด์šฉ', author=new_user)
    session.add(new_post)
    
    # ๋ชจ๋“  ์ž‘์—…์ด ์„ฑ๊ณตํ•˜๋ฉด ์ปค๋ฐ‹
    session.commit()
    print("๋ชจ๋“  ์ž‘์—… ์„ฑ๊ณต!")
    
except Exception as e:
    # ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋กค๋ฐฑ (๋ชจ๋“  ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ทจ์†Œ)
    session.rollback()
    print(f"์—๋Ÿฌ ๋ฐœ์ƒ! ๋กค๋ฐฑํ•ฉ๋‹ˆ๋‹ค: {e}")
    
finally:
    # ์„ธ์…˜ ์ •๋ฆฌ
    session.close()

์ปจํ…์ŠคํŠธ ๋งค๋‹ˆ์ €๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ๊น”๋”ํ•ด:

from contextlib import contextmanager

@contextmanager
def get_session():
    session = SessionLocal()
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()

# ์‚ฌ์šฉ
with get_session() as session:
    user = User(username='contextuser', email='context@example.com')
    session.add(user)
    # commit๊ณผ close๊ฐ€ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋จ!

๐Ÿ”— ๊ด€๊ณ„(Relationship) ์‹ฌํ™” - ํ…Œ์ด๋ธ” ๊ฐ„ ์—ฐ๊ฒฐ

์‹ค์ œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋Š” ํ…Œ์ด๋ธ”๋“ค์ด ์„œ๋กœ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์–ด. ์ด ๊ด€๊ณ„๋ฅผ ์ž˜ ๋‹ค๋ฃจ๋Š” ๊ฒŒ ORM์˜ ์ง„์งœ ํŒŒ์›Œ์•ผ! ๐Ÿ’ช

๐Ÿ‘ฅ One-to-Many ๊ด€๊ณ„

๊ฐ€์žฅ ํ”ํ•œ ๊ด€๊ณ„์•ผ. ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ๋Ÿฌ ํฌ์ŠคํŠธ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ:

# ์‚ฌ์šฉ์ž์˜ ๋ชจ๋“  ํฌ์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ
user = session.query(User).filter(User.username == 'john_doe').first()
user_posts = user.posts  # relationship ๋•๋ถ„์— ๊ฐ„๋‹จ!

print(f"{user.username}์˜ ํฌ์ŠคํŠธ:")
for post in user_posts:
    print(f"- {post.title}")

# ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ: ํฌ์ŠคํŠธ์˜ ์ž‘์„ฑ์ž
post = session.query(Post).first()
author = post.author
print(f"์ž‘์„ฑ์ž: {author.username}")

# ์ƒˆ ํฌ์ŠคํŠธ ์ถ”๊ฐ€
new_post = Post(
    title="SQLAlchemy ํŠœํ† ๋ฆฌ์–ผ",
    content="ORM์€ ์ •๋ง ํŽธ๋ฆฌํ•ด์š”!",
    author=user  # ๊ด€๊ณ„ ์„ค์ •
)
session.add(new_post)
session.commit()

# ๋˜๋Š” ์ด๋ ‡๊ฒŒ๋„ ๊ฐ€๋Šฅ
user.posts.append(Post(title="๋˜ ๋‹ค๋ฅธ ํฌ์ŠคํŠธ", content="๋‚ด์šฉ"))
session.commit()

๐Ÿ”€ Many-to-Many ๊ด€๊ณ„

ํฌ์ŠคํŠธ์™€ ํƒœ๊ทธ์ฒ˜๋Ÿผ ์„œ๋กœ ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” ๊ด€๊ณ„:

# ํƒœ๊ทธ ์ƒ์„ฑ
python_tag = Tag(name='Python')
orm_tag = Tag(name='ORM')
db_tag = Tag(name='Database')

session.add_all([python_tag, orm_tag, db_tag])

# ํฌ์ŠคํŠธ์— ํƒœ๊ทธ ์ถ”๊ฐ€
post = session.query(Post).first()
post.tags.extend([python_tag, orm_tag, db_tag])
session.commit()

# ํฌ์ŠคํŠธ์˜ ๋ชจ๋“  ํƒœ๊ทธ ์กฐํšŒ
print(f"{post.title}์˜ ํƒœ๊ทธ:")
for tag in post.tags:
    print(f"- {tag.name}")

# ํŠน์ • ํƒœ๊ทธ๋ฅผ ๊ฐ€์ง„ ๋ชจ๋“  ํฌ์ŠคํŠธ ์กฐํšŒ
python_posts = session.query(Post).join(Post.tags).filter(Tag.name == 'Python').all()
print(f"Python ํƒœ๊ทธ๊ฐ€ ์žˆ๋Š” ํฌ์ŠคํŠธ: {len(python_posts)}๊ฐœ")

# ํƒœ๊ทธ ์ œ๊ฑฐ
post.tags.remove(db_tag)
session.commit()

๐ŸŽฏ Eager Loading vs Lazy Loading

๊ด€๊ณ„๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์–ธ์ œ ๊ฐ€์ ธ์˜ฌ์ง€ ๊ฒฐ์ •ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ์ด๊ฒŒ ์„ฑ๋Šฅ์— ํฐ ์˜ํ–ฅ์„ ๋ฏธ์น˜๊ฑฐ๋“ !

Loading ์ „๋žต: Lazy Loading (๊ธฐ๋ณธ๊ฐ’)
- ๊ด€๊ณ„๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์ œ๋กœ ์ ‘๊ทผํ•  ๋•Œ ๊ฐ€์ ธ์˜ด
- ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ์ด์ง€๋งŒ, N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ ๋ฐœ์ƒ ๊ฐ€๋Šฅ

Eager Loading
- ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ด€๊ณ„๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํ•จ๊ป˜ ๊ฐ€์ ธ์˜ด
- ์ฟผ๋ฆฌ ํšŸ์ˆ˜๋Š” ์ค„์ง€๋งŒ, ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€

Select IN Loading
- ๋ณ„๋„ ์ฟผ๋ฆฌ๋กœ ๊ด€๊ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋˜, IN ์ ˆ ์‚ฌ์šฉ
- Lazy์™€ Eager์˜ ์ค‘๊ฐ„ ํ˜•ํƒœ
from sqlalchemy.orm import joinedload, selectinload, subqueryload

# 1. Lazy Loading (๊ธฐ๋ณธ) - N+1 ๋ฌธ์ œ ๋ฐœ์ƒ!
users = session.query(User).all()
for user in users:  # ๊ฐ ์‚ฌ์šฉ์ž๋งˆ๋‹ค ์ถ”๊ฐ€ ์ฟผ๋ฆฌ ๋ฐœ์ƒ
    print(f"{user.username}: {len(user.posts)}๊ฐœ ํฌ์ŠคํŠธ")

# 2. Joined Load - JOIN์œผ๋กœ ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ์˜ค๊ธฐ
users = session.query(User).options(joinedload(User.posts)).all()
for user in users:  # ์ถ”๊ฐ€ ์ฟผ๋ฆฌ ์—†์Œ!
    print(f"{user.username}: {len(user.posts)}๊ฐœ ํฌ์ŠคํŠธ")

# 3. Select IN Load - ๋ณ„๋„ ์ฟผ๋ฆฌ์ง€๋งŒ ํšจ์œจ์ 
users = session.query(User).options(selectinload(User.posts)).all()

# 4. ์ค‘์ฒฉ๋œ ๊ด€๊ณ„๋„ ๊ฐ€๋Šฅ
posts = session.query(Post).options(
    joinedload(Post.author),
    selectinload(Post.comments).joinedload(Comment.user),
    selectinload(Post.tags)
).all()

# ๊ฐ ํฌ์ŠคํŠธ์˜ ์ž‘์„ฑ์ž, ๋Œ“๊ธ€(+๋Œ“๊ธ€ ์ž‘์„ฑ์ž), ํƒœ๊ทธ๋ฅผ ๋ชจ๋‘ ๋ฏธ๋ฆฌ ๋กœ๋“œ!
๐Ÿ’ก N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ๋ž€?

์‚ฌ์šฉ์ž 10๋ช…์„ ์กฐํšŒํ•˜๋Š”๋ฐ 1๋ฒˆ์˜ ์ฟผ๋ฆฌ, ๊ฐ ์‚ฌ์šฉ์ž์˜ ํฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•˜๋Š”๋ฐ 10๋ฒˆ์˜ ์ฟผ๋ฆฌ... ์ด 11๋ฒˆ! ์ด๊ฒŒ N+1 ๋ฌธ์ œ์•ผ. Eager Loading์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์–ด!

๐Ÿ”ง CASCADE ์˜ต์…˜

๋ถ€๋ชจ ๊ฐ์ฒด๊ฐ€ ์‚ญ์ œ๋  ๋•Œ ์ž์‹ ๊ฐ์ฒด๋ฅผ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ์ง€ ์„ค์ •:

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    
    # CASCADE ์˜ต์…˜ ์„ค์ •
    posts = relationship(
        'Post',
        back_populates='author',
        cascade='all, delete-orphan'  # ์‚ฌ์šฉ์ž ์‚ญ์ œ ์‹œ ํฌ์ŠคํŠธ๋„ ์‚ญ์ œ
    )

class Post(Base):
    __tablename__ = 'posts'
    id = Column(Integer, primary_key=True)
    title = Column(String(200))
    author_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
    
    author = relationship('User', back_populates='posts')
CASCADE ์˜ต์…˜๋“ค: all: ๋ชจ๋“  cascade ์ ์šฉ
delete: ๋ถ€๋ชจ ์‚ญ์ œ ์‹œ ์ž์‹๋„ ์‚ญ์ œ
delete-orphan: ๊ด€๊ณ„๊ฐ€ ๋Š์–ด์ง„ ์ž์‹ ์ž๋™ ์‚ญ์ œ
merge: ๋ถ€๋ชจ merge ์‹œ ์ž์‹๋„ merge
save-update: ๋ถ€๋ชจ ์ €์žฅ ์‹œ ์ž์‹๋„ ์ €์žฅ
refresh-expire: ๋ถ€๋ชจ ์ƒˆ๋กœ๊ณ ์นจ ์‹œ ์ž์‹๋„ ์ƒˆ๋กœ๊ณ ์นจ

๐ŸŽจ ๊ณ ๊ธ‰ ์ฟผ๋ฆฌ ๊ธฐ๋ฒ•

์ด์ œ ์ข€ ๋” ๋ณต์žกํ•˜๊ณ  ๊ฐ•๋ ฅํ•œ ์ฟผ๋ฆฌ๋“ค์„ ๋ฐฐ์›Œ๋ณด์ž! ์‹ค์ „์—์„œ ์ •๋ง ์œ ์šฉํ•œ ๊ธฐ๋ฒ•๋“ค์ด์•ผ. ๐Ÿš€

๐Ÿ” ์„œ๋ธŒ์ฟผ๋ฆฌ(Subquery)

์ฟผ๋ฆฌ ์•ˆ์— ๋˜ ๋‹ค๋ฅธ ์ฟผ๋ฆฌ๋ฅผ ๋„ฃ๋Š” ๊ฑฐ์•ผ:

from sqlalchemy import func

# ํ‰๊ท  ์ด์ƒ์˜ ์กฐํšŒ์ˆ˜๋ฅผ ๊ฐ€์ง„ ํฌ์ŠคํŠธ ์ฐพ๊ธฐ
avg_views = session.query(func.avg(Post.view_count)).scalar_subquery()
popular_posts = session.query(Post).filter(Post.view_count > avg_views).all()

# ์„œ๋ธŒ์ฟผ๋ฆฌ๋ฅผ ๋ณ€์ˆ˜๋กœ ๋งŒ๋“ค๊ธฐ
subq = session.query(
    Post.author_id,
    func.count(Post.id).label('post_count')
).group_by(Post.author_id).subquery()

# ํฌ์ŠคํŠธ๋ฅผ 3๊ฐœ ์ด์ƒ ์“ด ์‚ฌ์šฉ์ž ์ฐพ๊ธฐ
active_authors = session.query(User).join(
    subq, User.id == subq.c.author_id
).filter(subq.c.post_count >= 3).all()

print(f"ํ™œ๋ฐœํ•œ ์ž‘์„ฑ์ž: {len(active_authors)}๋ช…")

๐Ÿ”— ๋ณต์žกํ•œ JOIN

์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ”์„ ์—ฐ๊ฒฐํ•˜๋Š” ๋ณต์žกํ•œ ์ฟผ๋ฆฌ:

# INNER JOIN
results = session.query(User, Post).join(Post).all()
for user, post in results:
    print(f"{user.username} - {post.title}")

# LEFT OUTER JOIN (ํฌ์ŠคํŠธ๊ฐ€ ์—†๋Š” ์‚ฌ์šฉ์ž๋„ ํฌํ•จ)
results = session.query(User).outerjoin(Post).all()

# ์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ” JOIN
results = session.query(Post, User, Comment).join(
    User, Post.author_id == User.id
).join(
    Comment, Post.id == Comment.post_id
).all()

# ์กฐ๊ฑด๋ถ€ JOIN
from sqlalchemy import and_
results = session.query(User).join(
    Post,
    and_(
        Post.author_id == User.id,
        Post.view_count > 100
    )
).all()

๐Ÿ“Š ์ง‘๊ณ„์™€ ๊ทธ๋ฃนํ™”

๋ฐ์ดํ„ฐ๋ฅผ ์š”์•ฝํ•˜๊ณ  ๋ถ„์„ํ•˜๋Š” ์ฟผ๋ฆฌ:

from sqlalchemy import func, desc

# ์‚ฌ์šฉ์ž๋ณ„ ํฌ์ŠคํŠธ ๊ฐœ์ˆ˜
post_counts = session.query(
    User.username,
    func.count(Post.id).label('post_count')
).join(Post).group_by(User.username).all()

for username, count in post_counts:
    print(f"{username}: {count}๊ฐœ")

# HAVING ์ ˆ ์‚ฌ์šฉ (๊ทธ๋ฃนํ™” ํ›„ ํ•„ํ„ฐ๋ง)
active_users = session.query(
    User.username,
    func.count(Post.id).label('post_count')
).join(Post).group_by(User.username).having(
    func.count(Post.id) > 5
).all()

# ์—ฌ๋Ÿฌ ์ง‘๊ณ„ ํ•จ์ˆ˜ ๋™์‹œ ์‚ฌ์šฉ
stats = session.query(
    func.count(Post.id).label('total_posts'),
    func.avg(Post.view_count).label('avg_views'),
    func.max(Post.view_count).label('max_views'),
    func.min(Post.view_count).label('min_views')
).first()

print(f"์ด ํฌ์ŠคํŠธ: {stats.total_posts}")
print(f"ํ‰๊ท  ์กฐํšŒ์ˆ˜: {stats.avg_views:.2f}")
print(f"์ตœ๋Œ€ ์กฐํšŒ์ˆ˜: {stats.max_views}")
print(f"์ตœ์†Œ ์กฐํšŒ์ˆ˜: {stats.min_views}")

๐ŸŽฏ ์œˆ๋„์šฐ ํ•จ์ˆ˜(Window Functions)

๊ฐ ํ–‰์— ๋Œ€ํ•ด ๊ด€๋ จ๋œ ํ–‰๋“ค์˜ ์ง‘ํ•ฉ์„ ๊ณ„์‚ฐํ•˜๋Š” ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ:

from sqlalchemy import func, over

# ๊ฐ ์‚ฌ์šฉ์ž์˜ ํฌ์ŠคํŠธ์— ์ˆœ์œ„ ๋งค๊ธฐ๊ธฐ
ranked_posts = session.query(
    Post.title,
    Post.view_count,
    User.username,
    func.row_number().over(
        partition_by=Post.author_id,
        order_by=Post.view_count.desc()
    ).label('rank')
).join(User).all()

for post in ranked_posts:
    print(f"{post.username}์˜ {post.rank}์œ„: {post.title} ({post.view_count} views)")

# ๋ˆ„์  ํ•ฉ๊ณ„
cumulative = session.query(
    Post.created_at,
    Post.view_count,
    func.sum(Post.view_count).over(
        order_by=Post.created_at
    ).label('cumulative_views')
).order_by(Post.created_at).all()

๐Ÿ”Ž ์ „์ฒด ํ…์ŠคํŠธ ๊ฒ€์ƒ‰

PostgreSQL์˜ ์ „์ฒด ํ…์ŠคํŠธ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ํ™œ์šฉ:

from sqlalchemy import func

# PostgreSQL์˜ to_tsvector์™€ to_tsquery ์‚ฌ์šฉ
search_query = 'python & sqlalchemy'
results = session.query(Post).filter(
    func.to_tsvector('english', Post.content).match(search_query)
).all()

# ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋žญํ‚น
ranked_results = session.query(
    Post,
    func.ts_rank(
        func.to_tsvector('english', Post.content),
        func.to_tsquery('english', search_query)
    ).label('rank')
).filter(
    func.to_tsvector('english', Post.content).match(search_query)
).order_by(desc('rank')).all()

๐ŸŽฒ ์œ ๋‹ˆ์˜จ(UNION)๊ณผ ์ธํ„ฐ์„นํŠธ(INTERSECT)

์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ํ•ฉ์น˜๊ฑฐ๋‚˜ ๊ต์ง‘ํ•ฉ์„ ๊ตฌํ•˜๊ธฐ:

# UNION - ๋‘ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ํ•ฉ์น˜๊ธฐ
query1 = session.query(User.username).filter(User.is_active == True)
query2 = session.query(User.username).filter(User.created_at > '2024-01-01')
combined = query1.union(query2).all()

# UNION ALL (์ค‘๋ณต ํฌํ•จ)
combined_all = query1.union_all(query2).all()

# INTERSECT - ๊ต์ง‘ํ•ฉ
common = query1.intersect(query2).all()

# EXCEPT - ์ฐจ์ง‘ํ•ฉ
difference = query1.except_(query2).all()

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

SQLAlchemy๋ฅผ ์ž˜ ์“ฐ๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•˜์ง€๋งŒ, ๋น ๋ฅด๊ฒŒ ์“ฐ๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•ด! ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐฉ๋ฒ•๋“ค์„ ์•Œ์•„๋ณด์ž. ๐Ÿš€

๐Ÿ“ˆ ์ฟผ๋ฆฌ ์ตœ์ ํ™”

์„ฑ๋Šฅ ํ–ฅ์ƒ์„ ์œ„ํ•œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ: 1. N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ ํ•ด๊ฒฐ
- joinedload(), selectinload() ์‚ฌ์šฉ
- ํ•„์š”ํ•œ ๊ด€๊ณ„๋งŒ ๋กœ๋“œ

2. ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ์„ ํƒ
- query(User.id, User.username) ํ˜•ํƒœ๋กœ
- ์ „์ฒด ๊ฐ์ฒด๊ฐ€ ํ•„์š” ์—†์œผ๋ฉด ํŠน์ • ์ปฌ๋Ÿผ๋งŒ

3. ์ธ๋ฑ์Šค ํ™œ์šฉ
- ์ž์ฃผ ๊ฒ€์ƒ‰ํ•˜๋Š” ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค ์ถ”๊ฐ€
- ๋ณตํ•ฉ ์ธ๋ฑ์Šค ๊ณ ๋ ค

4. ๋ฐฐ์น˜ ์ž‘์—…
- bulk_insert_mappings() ์‚ฌ์šฉ
- bulk_update_mappings() ์‚ฌ์šฉ

5. ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์บ์‹ฑ
- ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ์บ์‹œ
- Redis ๋“ฑ ํ™œ์šฉ

๐Ÿ”ง ์ธ๋ฑ์Šค ์ถ”๊ฐ€ํ•˜๊ธฐ

from sqlalchemy import Index

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    email = Column(String(100))
    created_at = Column(DateTime)
    
    # ๋‹จ์ผ ์ปฌ๋Ÿผ ์ธ๋ฑ์Šค
    __table_args__ = (
        Index('idx_username', 'username'),
        Index('idx_email', 'email'),
        Index('idx_created_at', 'created_at'),
    )

# ๋ณตํ•ฉ ์ธ๋ฑ์Šค
class Post(Base):
    __tablename__ = 'posts'
    
    id = Column(Integer, primary_key=True)
    author_id = Column(Integer, ForeignKey('users.id'))
    created_at = Column(DateTime)
    is_published = Column(Boolean)
    
    # ์—ฌ๋Ÿฌ ์ปฌ๋Ÿผ์„ ํ•จ๊ป˜ ์ธ๋ฑ์‹ฑ
    __table_args__ = (
        Index('idx_author_created', 'author_id', 'created_at'),
        Index('idx_published_created', 'is_published', 'created_at'),
    )

โšก ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ

๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ๋Š” ํŠน๋ณ„ํ•œ ๋ฐฉ๋ฒ•์ด ํ•„์š”ํ•ด:

# 1. Bulk Insert - ๋น ๋ฅธ ์‚ฝ์ž…
users_data = [
    {'username': f'user{i}', 'email': f'user{i}@example.com'}
    for i in range(10000)
]

session.bulk_insert_mappings(User, users_data)
session.commit()
print("10,000๋ช… ์‚ฌ์šฉ์ž ์ถ”๊ฐ€ ์™„๋ฃŒ!")

# 2. Bulk Update - ๋น ๋ฅธ ์—…๋ฐ์ดํŠธ
updates = [
    {'id': 1, 'view_count': 100},
    {'id': 2, 'view_count': 200},
    {'id': 3, 'view_count': 300},
]

session.bulk_update_mappings(Post, updates)
session.commit()

# 3. ์ฒญํฌ ๋‹จ์œ„๋กœ ์ฒ˜๋ฆฌ (๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ )
def process_in_chunks(query, chunk_size=1000):
    offset = 0
    while True:
        chunk = query.limit(chunk_size).offset(offset).all()
        if not chunk:
            break
        
        for item in chunk:
            # ์ฒ˜๋ฆฌ ๋กœ์ง
            pass
        
        offset += chunk_size
        session.commit()  # ์ฒญํฌ๋งˆ๋‹ค ์ปค๋ฐ‹

# ์‚ฌ์šฉ
query = session.query(Post)
process_in_chunks(query)

# 4. ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฐฉ์‹ (yield_per)
for user in session.query(User).yield_per(100):
    # 100๊ฐœ์”ฉ ๊ฐ€์ ธ์™€์„œ ์ฒ˜๋ฆฌ
    process_user(user)

๐ŸŽฏ ์ฟผ๋ฆฌ ํ”„๋กœํŒŒ์ผ๋ง

์–ด๋–ค ์ฟผ๋ฆฌ๊ฐ€ ๋А๋ฆฐ์ง€ ์ฐพ์•„๋‚ด๋Š” ๋ฐฉ๋ฒ•:

import time
from sqlalchemy import event
from sqlalchemy.engine import Engine

# ์ฟผ๋ฆฌ ์‹คํ–‰ ์‹œ๊ฐ„ ์ธก์ •
@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    conn.info.setdefault('query_start_time', []).append(time.time())

@event.listens_for(Engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    total = time.time() - conn.info['query_start_time'].pop(-1)
    if total > 0.1:  # 0.1์ดˆ ์ด์ƒ ๊ฑธ๋ฆฐ ์ฟผ๋ฆฌ๋งŒ ๋กœ๊น…
        print(f"๋А๋ฆฐ ์ฟผ๋ฆฌ ๋ฐœ๊ฒฌ! {total:.3f}์ดˆ")
        print(f"SQL: {statement[:100]}...")

# EXPLAIN์œผ๋กœ ์ฟผ๋ฆฌ ์‹คํ–‰ ๊ณ„ํš ๋ณด๊ธฐ
from sqlalchemy import text

query = session.query(User).filter(User.username == 'john')
explain = session.execute(
    text(f"EXPLAIN {query.statement.compile(compile_kwargs={'literal_binds': True})}")
)
for row in explain:
    print(row)

๐Ÿ’พ ์บ์‹ฑ ์ „๋žต

์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ์บ์‹œ๋ฅผ ํ™œ์šฉํ•˜์ž:

from functools import lru_cache
import redis

# Python ๋‚ด์žฅ ์บ์‹œ (๊ฐ„๋‹จํ•œ ๊ฒฝ์šฐ)
@lru_cache(maxsize=128)
def get_user_by_id(user_id):
    return session.query(User).get(user_id)

# Redis ์บ์‹œ (๋ถ„์‚ฐ ํ™˜๊ฒฝ)
redis_client = redis.Redis(host='localhost', port=6379, db=0)

def get_user_cached(user_id):
    # ์บ์‹œ ํ™•์ธ
    cached = redis_client.get(f'user:{user_id}')
    if cached:
        return json.loads(cached)
    
    # DB ์กฐํšŒ
    user = session.query(User).get(user_id)
    if user:
        # ์บ์‹œ ์ €์žฅ (10๋ถ„ TTL)
        redis_client.setex(
            f'user:{user_id}',
            600,
            json.dumps({'id': user.id, 'username': user.username})
        )
    return user

# ์บ์‹œ ๋ฌดํšจํ™”
def update_user(user_id, **kwargs):
    user = session.query(User).get(user_id)
    for key, value in kwargs.items():
        setattr(user, key, value)
    session.commit()
    
    # ์บ์‹œ ์‚ญ์ œ
    redis_client.delete(f'user:{user_id}')

๐Ÿ›ก๏ธ ๋ณด์•ˆ๊ณผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋‹ค๋ฃฐ ๋•Œ๋Š” ๋ณด์•ˆ์ด ์ •๋ง ์ค‘์š”ํ•ด! ์‹ค์ˆ˜ ํ•œ ๋ฒˆ์œผ๋กœ ํฐ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ๊ฑฐ๋“ . ๐Ÿ”’

๐Ÿšซ SQL Injection ๋ฐฉ์ง€

SQLAlchemy๋ฅผ ์ œ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋ฉด SQL Injection์„ ์ž๋™์œผ๋กœ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์–ด:

# โŒ ๋‚˜์œ ์˜ˆ - SQL Injection ์ทจ์•ฝ!
username = request.form['username']
query = f"SELECT * FROM users WHERE username = '{username}'"
# ๊ณต๊ฒฉ์ž๊ฐ€ username์— "admin' OR '1'='1" ์ž…๋ ฅํ•˜๋ฉด ๋ชจ๋“  ์‚ฌ์šฉ์ž ์กฐํšŒ๋จ!

# โœ… ์ข‹์€ ์˜ˆ - SQLAlchemy๊ฐ€ ์ž๋™์œผ๋กœ ์ด์Šค์ผ€์ดํ”„ ์ฒ˜๋ฆฌ
username = request.form['username']
user = session.query(User).filter(User.username == username).first()

# โœ… ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ ์‚ฌ์šฉ
from sqlalchemy import text
result = session.execute(
    text("SELECT * FROM users WHERE username = :username"),
    {"username": username}
)

# โŒ ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง ๊ฒƒ!
session.execute(text(f"SELECT * FROM users WHERE username = '{username}'"))

๐Ÿ” ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ

๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ ˆ๋Œ€ ํ‰๋ฌธ์œผ๋กœ ์ €์žฅํ•˜๋ฉด ์•ˆ ๋ผ:

from werkzeug.security import generate_password_hash, check_password_hash

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    password_hash = Column(String(255))  # ํ•ด์‹œ๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ €์žฅ
    
    def set_password(self, password):
        """๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ•ด์‹œํ™”ํ•ด์„œ ์ €์žฅ"""
        self.password_hash = generate_password_hash(password)
    
    def check_password(self, password):
        """๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ"""
        return check_password_hash(self.password_hash, password)

# ์‚ฌ์šฉ
new_user = User(username='john')
new_user.set_password('my_secret_password')  # ํ•ด์‹œํ™”๋˜์–ด ์ €์žฅ
session.add(new_user)
session.commit()

# ๋กœ๊ทธ์ธ ์‹œ
user = session.query(User).filter(User.username == 'john').first()
if user and user.check_password('my_secret_password'):
    print("๋กœ๊ทธ์ธ ์„ฑ๊ณต!")
else:
    print("๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ‹€๋ ธ์Šต๋‹ˆ๋‹ค.")

๐ŸŽฏ ๊ถŒํ•œ ๊ด€๋ฆฌ

์‚ฌ์šฉ์ž๋ณ„๋กœ ์ ‘๊ทผ ๊ถŒํ•œ์„ ๊ด€๋ฆฌํ•˜๋Š” ํŒจํ„ด:

from enum import Enum

class UserRole(Enum):
    ADMIN = 'admin'
    MODERATOR = 'moderator'
    USER = 'user'

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    role = Column(String(20), default=UserRole.USER.value)
    
    def has_permission(self, required_role):
        """๊ถŒํ•œ ์ฒดํฌ"""
        role_hierarchy = {
            UserRole.ADMIN: 3,
            UserRole.MODERATOR: 2,
            UserRole.USER: 1
        }
        user_level = role_hierarchy.get(UserRole(self.role), 0)
        required_level = role_hierarchy.get(required_role, 0)
        return user_level >= required_level

# ์‚ฌ์šฉ
def delete_post(user, post):
    if not user.has_permission(UserRole.MODERATOR):
        raise PermissionError("๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค!")
    
    session.delete(post)
    session.commit()

๐Ÿ“ ๊ฐ์‚ฌ ๋กœ๊ทธ(Audit Log)

์ค‘์š”ํ•œ ์ž‘์—…์€ ๊ธฐ๋ก์„ ๋‚จ๊ธฐ์ž:

class AuditLog(Base):
    __tablename__ = 'audit_logs'
    
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'))
    action = Column(String(50))  # 'create', 'update', 'delete'
    table_name = Column(String(50))
    record_id = Column(Integer)
    old_value = Column(Text)  # JSON ํ˜•ํƒœ๋กœ ์ €์žฅ
    new_value = Column(Text)
    created_at = Column(DateTime, default=datetime.utcnow)
    ip_address = Column(String(45))

def log_action(user_id, action, table_name, record_id, old_value=None, new_value=None):
    """์ž‘์—… ๋กœ๊ทธ ๊ธฐ๋ก"""
    log = AuditLog(
        user_id=user_id,
        action=action,
        table_name=table_name,
        record_id=record_id,
        old_value=json.dumps(old_value) if old_value else None,
        new_value=json.dumps(new_value) if new_value else None
    )
    session.add(log)
    session.commit()

# ์‚ฌ์šฉ
user = session.query(User).get(1)
old_email = user.email
user.email = 'newemail@example.com'
session.commit()

log_action(
    user_id=current_user.id,
    action='update',
    table_name='users',
    record_id=user.id,
    old_value={'email': old_email},
    new_value={'email': user.email}
)

๐Ÿ”’ ๋ฐ์ดํ„ฐ ์•”ํ˜ธํ™”

๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋Š” ์•”ํ˜ธํ™”ํ•ด์„œ ์ €์žฅ:

from cryptography.fernet import Fernet
import os

# ์•”ํ˜ธํ™” ํ‚ค ์ƒ์„ฑ (ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌ)
ENCRYPTION_KEY = os.getenv('ENCRYPTION_KEY', Fernet.generate_key())
cipher_suite = Fernet(ENCRYPTION_KEY)

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    _ssn = Column('ssn', LargeBinary)  # ์ฃผ๋ฏผ๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ์ €์žฅ
    
    @property
    def ssn(self):
        """๋ณตํ˜ธํ™”ํ•ด์„œ ๋ฐ˜ํ™˜"""
        if self._ssn:
            return cipher_suite.decrypt(self._ssn).decode()
        return None
    
    @ssn.setter
    def ssn(self, value):
        """์•”ํ˜ธํ™”ํ•ด์„œ ์ €์žฅ"""
        if value:
            self._ssn = cipher_suite.encrypt(value.encode())
        else:
            self._ssn = None

# ์‚ฌ์šฉ
user = User(username='john')
user.ssn = '123-45-6789'  # ์ž๋™์œผ๋กœ ์•”ํ˜ธํ™”๋˜์–ด ์ €์žฅ
session.add(user)
session.commit()

# ์กฐํšŒ
user = session.query(User).first()
print(user.ssn)  # ์ž๋™์œผ๋กœ ๋ณตํ˜ธํ™”๋˜์–ด ์ถœ๋ ฅ
โš ๏ธ ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ:
  • โœ… ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ ˆ๋Œ€ ํ‰๋ฌธ ์ €์žฅ ๊ธˆ์ง€
  • โœ… SQL Injection ๋ฐฉ์ง€ (ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ ์‚ฌ์šฉ)
  • โœ… ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋Š” ์•”ํ˜ธํ™”
  • โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘์† ์ •๋ณด๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌ
  • โœ… ์ตœ์†Œ ๊ถŒํ•œ ์›์น™ ์ ์šฉ
  • โœ… ์ •๊ธฐ์ ์ธ ๋ฐฑ์—…
  • โœ… ๊ฐ์‚ฌ ๋กœ๊ทธ ๊ธฐ๋ก
  • โœ… SSL/TLS๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ

๐Ÿ”„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ - Alembic ํ™œ์šฉ

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ๊ฐ€ ๊ณ„์† ๋ณ€๊ฒฝ๋ผ. ์ด๊ฑธ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒŒ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด์•ผ! ๐Ÿ”ง

๐ŸŽฏ Alembic์ด๋ž€?

Alembic์€ SQLAlchemy๋ฅผ ์œ„ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋„๊ตฌ์•ผ. Git์ฒ˜๋Ÿผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ์˜ ๋ฒ„์ „์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด!

# Alembic ์„ค์น˜
pip install alembic

# ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐํ™”
alembic init alembic

# ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ
# myproject/
#   โ”œโ”€โ”€ alembic/
#   โ”‚   โ”œโ”€โ”€ versions/  # ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ๋“ค
#   โ”‚   โ”œโ”€โ”€ env.py
#   โ”‚   โ””โ”€โ”€ script.py.mako
#   โ”œโ”€โ”€ alembic.ini
#   โ””โ”€โ”€ models.py

โš™๏ธ Alembic ์„ค์ •

alembic.ini ํŒŒ์ผ์„ ์ˆ˜์ •ํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์„ค์ •:

# alembic.ini
sqlalchemy.url = sqlite:///./mydb.db
# ๋˜๋Š”
sqlalchemy.url = mysql+pymysql://user:pass@localhost/mydb

# env.py์—์„œ ๋ชจ๋ธ import
from myapp.models import Base

target_metadata = Base.metadata

๐Ÿ“ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ

๋ชจ๋ธ์„ ๋ณ€๊ฒฝํ–ˆ์œผ๋ฉด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด:

# ์ž๋™์œผ๋กœ ๋ณ€๊ฒฝ์‚ฌํ•ญ ๊ฐ์ง€ํ•ด์„œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ
alembic revision --autogenerate -m "Add user table"

# ์ˆ˜๋™์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ
alembic revision -m "Add custom index"

# ์ƒ์„ฑ๋œ ํŒŒ์ผ ์˜ˆ์‹œ (versions/xxxx_add_user_table.py)
def upgrade():
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('username', sa.String(50), nullable=False),
        sa.Column('email', sa.String(100), nullable=False),
        sa.PrimaryKeyConstraint('id')
    )
    op.create_index('idx_username', 'users', ['username'])

def downgrade():
    op.drop_index('idx_username', 'users')
    op.drop_table('users')

๐Ÿš€ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰

# ์ตœ์‹  ๋ฒ„์ „์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ
alembic upgrade head

# ํŠน์ • ๋ฒ„์ „์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ
alembic upgrade ae1027a6acf

# ํ•œ ๋‹จ๊ณ„ ์—…๊ทธ๋ ˆ์ด๋“œ
alembic upgrade +1

# ๋‹ค์šด๊ทธ๋ ˆ์ด๋“œ (๋กค๋ฐฑ)
alembic downgrade -1
alembic downgrade base  # ์ฒ˜์Œ์œผ๋กœ

# ํ˜„์žฌ ๋ฒ„์ „ ํ™•์ธ
alembic current

# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํžˆ์Šคํ† ๋ฆฌ ๋ณด๊ธฐ
alembic history

๐Ÿ”ง ์‹ค์ „ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์˜ˆ์ œ

# ์ปฌ๋Ÿผ ์ถ”๊ฐ€
def upgrade():
    op.add_column('users', sa.Column('phone', sa.String(20), nullable=True))

def downgrade():
    op.drop_column('users', 'phone')

# ์ปฌ๋Ÿผ ํƒ€์ž… ๋ณ€๊ฒฝ
def upgrade():
    op.alter_column('users', 'bio',
                    existing_type=sa.String(255),
                    type_=sa.Text(),
                    existing_nullable=True)

# ์ธ๋ฑ์Šค ์ถ”๊ฐ€
def upgrade():
    op.create_index('idx_email', 'users', ['email'], unique=True)

def downgrade():
    op.drop_index('idx_email', 'users')

# ์™ธ๋ž˜ ํ‚ค ์ถ”๊ฐ€
def upgrade():
    op.create_foreign_key(
        'fk_posts_author',
        'posts', 'users',
        ['author_id'], ['id'],
        ondelete='CASCADE'
    )

# ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
def upgrade():
    # ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
    op.execute("UPDATE users SET is_active = true WHERE is_active IS NULL")
    
    # ์ƒˆ ์ปฌ๋Ÿผ์„ NOT NULL๋กœ ๋ณ€๊ฒฝ
    op.alter_column('users', 'is_active', nullable=False)
๐Ÿ’ก ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค:

1. ํ•ญ์ƒ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ๋จผ์ € ์‹คํ–‰
2. ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์ „ ๋ฐฑ์—… ํ•„์ˆ˜
3. ๋‹ค์šด๊ทธ๋ ˆ์ด๋“œ ์Šคํฌ๋ฆฝํŠธ๋„ ๊ผญ ํ…Œ์ŠคํŠธ
4. ํฐ ๋ณ€๊ฒฝ์€ ์—ฌ๋Ÿฌ ๋‹จ๊ณ„๋กœ ๋‚˜๋ˆ ์„œ
5. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์€ ๋ฒ„์ „ ๊ด€๋ฆฌ(Git)์— ํฌํ•จ

๐ŸŒ ์‹ค์ „ ํ”„๋กœ์ ํŠธ ํŒจํ„ด

์ด์ œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ์–ด๋–ป๊ฒŒ ๊ตฌ์กฐํ™”ํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž! ๊น”๋”ํ•œ ์ฝ”๋“œ ๊ตฌ์กฐ๋Š” ์œ ์ง€๋ณด์ˆ˜๋ฅผ ํ›จ์”ฌ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ค˜. ๐Ÿ—๏ธ

๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

myproject/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ database.py      # DB ์—ฐ๊ฒฐ ์„ค์ •
โ”‚   โ”œโ”€โ”€ models/          # ๋ชจ๋ธ ์ •์˜
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ user.py
โ”‚   โ”‚   โ”œโ”€โ”€ post.py
โ”‚   โ”‚   โ””โ”€โ”€ comment.py
โ”‚   โ”œโ”€โ”€ schemas/         # Pydantic ์Šคํ‚ค๋งˆ (FastAPI ๋“ฑ)
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ user.py
โ”‚   โ”‚   โ””โ”€โ”€ post.py
โ”‚   โ”œโ”€โ”€ crud/            # CRUD ์ž‘์—…
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ user.py
โ”‚   โ”‚   โ””โ”€โ”€ post.py
โ”‚   โ”œโ”€โ”€ api/             # API ์—”๋“œํฌ์ธํŠธ
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ users.py
โ”‚   โ”‚   โ””โ”€โ”€ posts.py
โ”‚   โ””โ”€โ”€ core/            # ์„ค์ •, ์œ ํ‹ธ๋ฆฌํ‹ฐ
โ”‚       โ”œโ”€โ”€ __init__.py
โ”‚       โ”œโ”€โ”€ config.py
โ”‚       โ””โ”€โ”€ security.py
โ”œโ”€โ”€ alembic/             # ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
โ”œโ”€โ”€ tests/               # ํ…Œ์ŠคํŠธ
โ”œโ”€โ”€ .env                 # ํ™˜๊ฒฝ ๋ณ€์ˆ˜
โ”œโ”€โ”€ alembic.ini
โ””โ”€โ”€ main.py              # ์•ฑ ์ง„์ž…์ 

๐Ÿ”Œ database.py - ์—ฐ๊ฒฐ ๊ด€๋ฆฌ

# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os

# ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ DB URL ๊ฐ€์ ธ์˜ค๊ธฐ
DATABASE_URL = os.getenv(
    'DATABASE_URL',
    'sqlite:///./mydb.db'
)

# ์—”์ง„ ์ƒ์„ฑ
engine = create_engine(
    DATABASE_URL,
    pool_pre_ping=True,
    pool_size=10,
    max_overflow=20
)

# ์„ธ์…˜ ํŒฉํ† ๋ฆฌ
SessionLocal = sessionmaker(
    autocommit=False,
    autoflush=False,
    bind=engine
)

# Base ํด๋ž˜์Šค
Base = declarative_base()

# ์˜์กด์„ฑ ์ฃผ์ž…์šฉ ํ•จ์ˆ˜ (FastAPI ๋“ฑ์—์„œ ์‚ฌ์šฉ)
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

๐Ÿ“ฆ models/user.py - ๋ชจ๋ธ ์ •์˜

# app/models/user.py
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.orm import relationship
from datetime import datetime
from app.database import Base

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String(50), unique=True, index=True, nullable=False)
    email = Column(String(100), unique=True, index=True, nullable=False)
    hashed_password = Column(String(255), nullable=False)
    is_active = Column(Boolean, default=True)
    is_superuser = Column(Boolean, default=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, onupdate=datetime.utcnow)
    
    # ๊ด€๊ณ„
    posts = relationship('Post', back_populates='author', cascade='all, delete-orphan')
    
    def __repr__(self):
        return f"<User(id={self.id}, username='{self.username}')>"

๐Ÿ”จ crud/user.py - CRUD ์ž‘์—… ๋ถ„๋ฆฌ

# app/crud/user.py
from sqlalchemy.orm import Session
from app.models.user import User
from app.core.security import get_password_hash, verify_password
from typing import Optional, List

class UserCRUD:
    @staticmethod
    def create(db: Session, username: str, email: str, password: str) -> User:
        """์ƒˆ ์‚ฌ์šฉ์ž ์ƒ์„ฑ"""
        hashed_password = get_password_hash(password)
        user = User(
            username=username,
            email=email,
            hashed_password=hashed_password
        )
        db.add(user)
        db.commit()
        db.refresh(user)
        return user
    
    @staticmethod
    def get_by_id(db: Session, user_id: int) -> Optional[User]:
        """ID๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ"""
        return db.query(User).filter(User.id == user_id).first()
    
    @staticmethod
    def get_by_username(db: Session, username: str) -> Optional[User]:
        """์‚ฌ์šฉ์ž๋ช…์œผ๋กœ ์กฐํšŒ"""
        return db.query(User).filter(User.username == username).first()
    
    @staticmethod
    def get_by_email(db: Session, email: str) -> Optional[User]:
        """์ด๋ฉ”์ผ๋กœ ์กฐํšŒ"""
        return db.query(User).filter(User.email == email).first()
    
    @staticmethod
    def get_all(db: Session, skip: int = 0, limit: int = 100) -> List[User]:
        """๋ชจ๋“  ์‚ฌ์šฉ์ž ์กฐํšŒ (ํŽ˜์ด์ง€๋„ค์ด์…˜)"""
        return db.query(User).offset(skip).limit(limit).all()
    
    @staticmethod
    def update(db: Session, user_id: int, **kwargs) -> Optional[User]:
        """์‚ฌ์šฉ์ž ์ •๋ณด ์—…๋ฐ์ดํŠธ"""
        user = db.query(User).filter(User.id == user_id).first()
        if not user:
            return None
        
        for key, value in kwargs.items():
            if hasattr(user, key):
                setattr(user, key, value)
        
        db.commit()
        db.refresh(user)
        return user
    
    @staticmethod
    def delete(db: Session, user_id: int) -> bool:
        """์‚ฌ์šฉ์ž ์‚ญ์ œ"""
        user = db.query(User).filter(User.id == user_id).first()
        if not user:
            return False
        
        db.delete(user)
        db.commit()
        return True
    
    @staticmethod
    def authenticate(db: Session, username: str, password: str) -> Optional[User]:
        """์‚ฌ์šฉ์ž ์ธ์ฆ"""
        user = UserCRUD.get_by_username(db, username)
        if not user:
            return None
        if not verify_password(password, user.hashed_password):
            return None
        return user

# ์‹ฑ๊ธ€ํ†ค ์ธ์Šคํ„ด์Šค
user_crud = UserCRUD()

๐ŸŒ FastAPI์™€ ํ†ตํ•ฉ ์˜ˆ์ œ

# app/api/users.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
from app.database import get_db
from app.crud.user import user_crud
from app.schemas.user import UserCreate, UserResponse

router = APIRouter(prefix='/users', tags=['users'])

@router.post('/', response_model=UserResponse)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
    """์ƒˆ ์‚ฌ์šฉ์ž ์ƒ์„ฑ"""
    # ์ค‘๋ณต ์ฒดํฌ
    if user_crud.get_by_username(db, user.username):
        raise HTTPException(status_code=400, detail="Username already exists")
    if user_crud.get_by_email(db, user.email):
        raise HTTPException(status_code=400, detail="Email already exists")
    
    return user_crud.create(db, user.username, user.email, user.password)

@router.get('/{user_id}', response_model=UserResponse)
def get_user(user_id: int, db: Session = Depends(get_db)):
    """์‚ฌ์šฉ์ž ์กฐํšŒ"""
    user = user_crud.get_by_id(db, user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

@router.get('/', response_model=List[UserResponse])
def list_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    """์‚ฌ์šฉ์ž ๋ชฉ๋ก"""
    return user_crud.get_all(db, skip, limit)

@router.put('/{user_id}', response_model=UserResponse)
def update_user(user_id: int, user_update: UserUpdate, db: Session = Depends(get_db)):
    """์‚ฌ์šฉ์ž ์ •๋ณด ์ˆ˜์ •"""
    user = user_crud.update(db, user_id, **user_update.dict(exclude_unset=True))
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

@router.delete('/{user_id}')
def delete_user(user_id: int, db: Session = Depends(get_db)):
    """์‚ฌ์šฉ์ž ์‚ญ์ œ"""
    if not user_crud.delete(db, user_id):
        raise HTTPException(status_code=404, detail="User not found")
    return {"message": "User deleted successfully"}

๐Ÿงช ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

# tests/test_user_crud.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base
from app.models.user import User
from app.crud.user import user_crud

# ํ…Œ์ŠคํŠธ์šฉ ์ธ๋ฉ”๋ชจ๋ฆฌ DB
TEST_DATABASE_URL = "sqlite:///:memory:"
engine = create_engine(TEST_DATABASE_URL)
TestingSessionLocal = sessionmaker(bind=engine)

@pytest.fixture
def db():
    """ํ…Œ์ŠคํŠธ์šฉ DB ์„ธ์…˜"""
    Base.metadata.create_all(bind=engine)
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()
        Base.metadata.drop_all(bind=engine)

def test_create_user(db):
    """์‚ฌ์šฉ์ž ์ƒ์„ฑ ํ…Œ์ŠคํŠธ"""
    user = user_crud.create(db, "testuser", "test@example.com", "password123")
    assert user.id is not None
    assert user.username == "testuser"
    assert user.email == "test@example.com"

def test_get_user_by_username(db):
    """์‚ฌ์šฉ์ž๋ช…์œผ๋กœ ์กฐํšŒ ํ…Œ์ŠคํŠธ"""
    user_crud.create(db, "testuser", "test@example.com", "password123")
    user = user_crud.get_by_username(db, "testuser")
    assert user is not None
    assert user.username == "testuser"

def test_authenticate_user(db):
    """์ธ์ฆ ํ…Œ์ŠคํŠธ"""
    user_crud.create(db, "testuser", "test@example.com", "password123")
    
    # ์˜ฌ๋ฐ”๋ฅธ ๋น„๋ฐ€๋ฒˆํ˜ธ
    user = user_crud.authenticate(db, "testuser", "password123")
    assert user is not None
    
    # ํ‹€๋ฆฐ ๋น„๋ฐ€๋ฒˆํ˜ธ
    user = user_crud.authenticate(db, "testuser", "wrongpassword")
    assert user is None

๐Ÿ’ก ์‹ค๋ฌด ํŒ๊ณผ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

์‹ค์ œ๋กœ ๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์—ฌ๋Ÿฌ ๋ฌธ์ œ์— ๋ถ€๋”ชํžˆ๊ฒŒ ๋ผ. ์ž์ฃผ ๋งŒ๋‚˜๋Š” ์ด์Šˆ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ•ด๋ดค์–ด! ๐Ÿ”ง

๐Ÿ› ์ž์ฃผ ๋งŒ๋‚˜๋Š” ์—๋Ÿฌ๋“ค

1. DetachedInstanceError ์„ธ์…˜์ด ๋‹ซํžŒ ํ›„ ๊ฐ์ฒด์— ์ ‘๊ทผํ•˜๋ ค๊ณ  ํ•  ๋•Œ ๋ฐœ์ƒ

ํ•ด๊ฒฐ:
- ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์„ธ์…˜์ด ์—ด๋ ค์žˆ์„ ๋•Œ ๋ฏธ๋ฆฌ ๋กœ๋“œ
- expire_on_commit=False ์„ค์ •
- ๊ด€๊ณ„๋ฅผ eager loading์œผ๋กœ ๋กœ๋“œ

2. IntegrityError
UNIQUE, NOT NULL, FOREIGN KEY ์ œ์•ฝ ์œ„๋ฐ˜

ํ•ด๊ฒฐ:
- ๋ฐ์ดํ„ฐ ์‚ฝ์ž… ์ „ ์ค‘๋ณต ์ฒดํฌ
- try-except๋กœ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
- ์ œ์•ฝ ์กฐ๊ฑด ํ™•์ธ

3. PendingRollbackError
์ด์ „ ํŠธ๋žœ์žญ์…˜์ด ์‹คํŒจํ•œ ํ›„ ๋กค๋ฐฑ ์—†์ด ์ƒˆ ์ž‘์—… ์‹œ๋„

ํ•ด๊ฒฐ:
- ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋ฐ˜๋“œ์‹œ rollback() ํ˜ธ์ถœ
- ์ปจํ…์ŠคํŠธ ๋งค๋‹ˆ์ € ์‚ฌ์šฉ

4. CircularDependencyError
๋ชจ๋ธ ๊ฐ„ ์ˆœํ™˜ ์ฐธ์กฐ

ํ•ด๊ฒฐ:
- relationship()์—์„œ ๋ฌธ์ž์—ด๋กœ ๋ชจ๋ธ๋ช… ์ง€์ •
- ๋ชจ๋ธ์„ ๋ณ„๋„ ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌ

๐Ÿ” ๋””๋ฒ„๊น… ํŒ

# 1. SQL ์ฟผ๋ฆฌ ๋กœ๊น…
import logging
logging.basicConfig()
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)

# 2. ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด ํ™•์ธ
query = session.query(User).filter(User.username == 'john')
print(str(query))  # ์‹ค์ œ SQL ์ถœ๋ ฅ

# 3. ์ฟผ๋ฆฌ ์‹คํ–‰ ๊ณ„ํš ํ™•์ธ
from sqlalchemy import text
result = session.execute(text("EXPLAIN " + str(query)))
for row in result:
    print(row)

# 4. ์„ธ์…˜ ์ƒํƒœ ํ™•์ธ
print(session.new)  # ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ๊ฐ์ฒด๋“ค
print(session.dirty)  # ์ˆ˜์ •๋œ ๊ฐ์ฒด๋“ค
print(session.deleted)  # ์‚ญ์ œ๋œ ๊ฐ์ฒด๋“ค

# 5. ๊ฐ์ฒด ์ƒํƒœ ํ™•์ธ
from sqlalchemy import inspect
insp = inspect(user)
print(insp.persistent)  # DB์— ์ €์žฅ๋˜์–ด ์žˆ๋Š”์ง€
print(insp.pending)  # ์ €์žฅ ๋Œ€๊ธฐ ์ค‘์ธ์ง€
print(insp.detached)  # ์„ธ์…˜์—์„œ ๋ถ„๋ฆฌ๋˜์—ˆ๋Š”์ง€

โšก ์„ฑ๋Šฅ ๋ฌธ์ œ ํ•ด๊ฒฐ

# 1. N+1 ์ฟผ๋ฆฌ ์ฐพ๊ธฐ
from sqlalchemy import event

@event.listens_for(Engine, "before_cursor_execute")
def receive_before_cursor_execute(conn, cursor, statement, params, context, executemany):
    print(f"Query: {statement[:100]}")

# 2. ์Šฌ๋กœ์šฐ ์ฟผ๋ฆฌ ๋กœ๊น…
import time

@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    conn.info.setdefault('query_start_time', []).append(time.time())

@event.listens_for(Engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    total = time.time() - conn.info['query_start_time'].pop(-1)
    if total > 1.0:  # 1์ดˆ ์ด์ƒ
        print(f"SLOW QUERY ({total:.2f}s): {statement[:200]}")

# 3. ์—ฐ๊ฒฐ ํ’€ ๋ชจ๋‹ˆํ„ฐ๋ง
print(f"Pool size: {engine.pool.size()}")
print(f"Checked out: {engine.pool.checkedout()}")
print(f"Overflow: {engine.pool.overflow()}")

๐ŸŽฏ ์žฌ๋Šฅ๋„ท์—์„œ ๋ฐฐ์šฐ๊ธฐ

SQLAlchemy๋‚˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ จํ•ด์„œ ๋” ๊นŠ์ด ๋ฐฐ์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท(https://www.jaenung.net)์—์„œ ์ „๋ฌธ๊ฐ€๋“ค์˜ ๋„์›€์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด! ์‹ค๋ฌด ๊ฒฝํ—˜์ด ํ’๋ถ€ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์ด 1:1๋กœ ๋ฉ˜ํ† ๋ง๋„ ํ•ด์ฃผ๊ณ , ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋„ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฑฐ๋“ . ํŠนํžˆ ์‹ค์ „ ํ”„๋กœ์ ํŠธ์—์„œ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์„ ๋•Œ ์ •๋ง ์œ ์šฉํ•ด. ๐ŸŽ“

๐Ÿ“š ์ถ”๊ฐ€ ํ•™์Šต ์ž๋ฃŒ

๊ณต์‹ ๋ฌธ์„œ: - SQLAlchemy ๊ณต์‹ ๋ฌธ์„œ: https://docs.sqlalchemy.org/
- Alembic ๋ฌธ์„œ: https://alembic.sqlalchemy.org/

์ถ”์ฒœ ๋„์„œ:
- "Essential SQLAlchemy" by Jason Myers
- "SQLAlchemy ๋งˆ์Šคํ„ฐํ•˜๊ธฐ" (ํ•œ๊ธ€ ๋ฒˆ์—ญ์„œ)

์˜จ๋ผ์ธ ๊ฐ•์˜:
- Udemy: "Complete SQLAlchemy Bootcamp"
- YouTube: Corey Schafer์˜ SQLAlchemy ํŠœํ† ๋ฆฌ์–ผ
- ์žฌ๋Šฅ๋„ท์˜ Python/๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ•์˜๋“ค

๐ŸŽ“ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด. SQLAlchemy์™€ ORM์— ๋Œ€ํ•ด ๊ธฐ์ดˆ๋ถ€ํ„ฐ ์‹ค์ „ ํ™œ์šฉ๊นŒ์ง€ ๋ชจ๋‘ ๋‹ค๋ค„๋ดค์–ด. ๐ŸŽ‰

์ฒ˜์Œ์—๋Š” ๋ณต์žกํ•ด ๋ณด์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ๋”ฐ๋ผํ•˜๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ๋Šฅ์ˆ™ํ•˜๊ฒŒ ๋‹ค๋ฃจ๊ณ  ์žˆ๋Š” ์ž์‹ ์„ ๋ฐœ๊ฒฌํ•˜๊ฒŒ ๋  ๊ฑฐ์•ผ. ๋‚˜๋„ ์ฒ˜์Œ์—” "์ด๊ฒŒ ๋ญ์•ผ, SQL ์ง์ ‘ ์“ฐ๋Š” ๊ฒŒ ๋” ์‰ฌ์šด๋ฐ?"๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์—ˆ๊ฑฐ๋“ . ํ•˜์ง€๋งŒ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๊ณ  ๋ณต์žกํ•ด์งˆ์ˆ˜๋ก ORM์˜ ์ง„๊ฐ€๋ฅผ ๋А๋ผ๊ฒŒ ๋์–ด.

ํ•ต์‹ฌ ํฌ์ธํŠธ๋ฅผ ๋‹ค์‹œ ์ •๋ฆฌํ•˜๋ฉด:

โœ… ORM์€ ๊ฐ์ฒด์™€ DB๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ๋‹ค๋ฆฌ Python ํด๋ž˜์Šค๋กœ ํ…Œ์ด๋ธ”์„ ์ •์˜ํ•˜๊ณ , ๊ฐ์ฒด๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์–ด

โœ… SQLAlchemy๋Š” ๊ฐ•๋ ฅํ•˜๊ณ  ์œ ์—ฐํ•ด
Core์™€ ORM ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹์„ ์ œ๊ณตํ•˜๋ฉฐ, ๊ฑฐ์˜ ๋ชจ๋“  DB๋ฅผ ์ง€์›ํ•ด

โœ… ๊ด€๊ณ„ ์„ค์ •์ด ํ•ต์‹ฌ
One-to-Many, Many-to-Many ๊ด€๊ณ„๋ฅผ ์ž˜ ์„ค์ •ํ•˜๋ฉด ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋„ ๊ฐ„๋‹จํ•ด์ ธ

โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”๋Š” ํ•„์ˆ˜
N+1 ๋ฌธ์ œ, ์ธ๋ฑ์Šค, ์—ฐ๊ฒฐ ํ’€ ๋“ฑ์„ ์‹ ๊ฒฝ ์จ์•ผ ํ•ด

โœ… ๋ณด์•ˆ์€ ํƒ€ํ˜‘ ๋ถˆ๊ฐ€
SQL Injection ๋ฐฉ์ง€, ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ, ๋ฐ์ดํ„ฐ ์•”ํ˜ธํ™”๋Š” ๊ธฐ๋ณธ

โœ… ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์œผ๋กœ ์Šคํ‚ค๋งˆ ๊ด€๋ฆฌ
Alembic์„ ์‚ฌ์šฉํ•˜๋ฉด DB ๋ณ€๊ฒฝ์„ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด

โœ… ๊น”๋”ํ•œ ์ฝ”๋“œ ๊ตฌ์กฐ
๋ชจ๋ธ, CRUD, API๋ฅผ ๋ถ„๋ฆฌํ•˜๋ฉด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์‰ฌ์›Œ์ ธ

์‹ค์ „ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด, ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜๊ฑฐ๋‚˜ ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ๊ทธ๋ฆฌ๊ณ  ์•ž์„œ ๋งํ–ˆ๋“ฏ์ด ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ๋น ๋ฅธ ์„ฑ์žฅ์˜ ์ง€๋ฆ„๊ธธ์ด ๋  ์ˆ˜ ์žˆ์–ด!

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ๊ฑฐ์˜ ๋ชจ๋“  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ•ต์‹ฌ์ด์•ผ. SQLAlchemy๋ฅผ ๋งˆ์Šคํ„ฐํ•˜๋ฉด ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž๋กœ์„œ ํ•œ ๋‹จ๊ณ„ ๋” ์„ฑ์žฅํ•  ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ. ์ฒ˜์Œ์—” ์–ด๋ ต๋”๋ผ๋„ ํฌ๊ธฐํ•˜์ง€ ๋ง๊ณ  ๊ณ„์† ์—ฐ์Šตํ•ด๋ด. ์ž‘์€ ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์  ๋ณต์žกํ•œ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๊ฐ€๋ฉด์„œ ์‹ค๋ ฅ์„ ์Œ“์•„๊ฐ€๋Š” ๊ฑฐ์ง€! ๐Ÿ’ช

์ž, ์ด์ œ ๋‹น์‹  ์ฐจ๋ก€์•ผ! ์ง์ ‘ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ , ์—๋Ÿฌ๋ฅผ ๋งŒ๋‚˜๊ณ , ํ•ด๊ฒฐํ•˜๋ฉด์„œ ์ง„์งœ ์‹ค๋ ฅ์„ ํ‚ค์›Œ๋‚˜๊ฐ€์ž. ํ™”์ดํŒ…! ๐Ÿš€

๐Ÿ’ก ๋‹ค์Œ ๋‹จ๊ณ„ ์ถ”์ฒœ:

1. ๊ฐ„๋‹จํ•œ ๋ธ”๋กœ๊ทธ๋‚˜ TODO ์•ฑ์„ SQLAlchemy๋กœ ๋งŒ๋“ค์–ด๋ณด๊ธฐ
2. FastAPI๋‚˜ Flask์™€ ํ†ตํ•ฉํ•ด์„œ REST API ๋งŒ๋“ค๊ธฐ
3. ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ๋ฐฐํฌํ•ด๋ณด๊ธฐ
4. ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์ตœ์ ํ™”ํ•ด๋ณด๊ธฐ
5. ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ์— ๊ธฐ์—ฌํ•˜๋ฉด์„œ ๋” ๋ฐฐ์šฐ๊ธฐ

ํ–‰๋ณตํ•œ ์ฝ”๋”ฉ ๋˜๊ธธ ๋ฐ”๋ผ! ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ์œผ๋ฉด ์–ธ์ œ๋“  ์ฐพ์•„๋ด. ์šฐ๋ฆฌ ๋ชจ๋‘ ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์ž! ๐ŸŒŸ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.