Ver3.0 ๐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋๊ณผ SQLAlchemy๋ฅผ ํ์ฉํ ORM ๊ธฐ๋ฐ ๊ฐ๋ฐ

๐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋๊ณผ SQLAlchemy๋ฅผ ํ์ฉํ ORM ๊ธฐ๋ฐ ๊ฐ๋ฐ
์๋ ! ์ค๋์ Python ๊ฐ๋ฐ์๋ผ๋ฉด ๊ผญ ์์์ผ ํ SQLAlchemy์ ORM์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ ๊ฑฐ์ผ. ๐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ค๋ฃจ๋ ๊ฒ ์ฒ์์ ์ข ์ด๋ ต๊ฒ ๋๊ปด์ง ์ ์๋๋ฐ, ์ฒ์ฒํ ๋ฐ๋ผ์ค๋ค ๋ณด๋ฉด "์, ์ด๊ฒ ์ด๋ ๊ฒ ํธํ ๊ฑฐ์๊ตฌ๋!" ํ๊ณ ๋ฌด๋ฆ์ ํ ์น๊ฒ ๋ ๊ฑฐ์ผ. ๋ง์น ์์ ๊ฑฐ ํ๋ ๋ฒ์ ๋ฐฐ์ฐ๋ ๊ฒ์ฒ๋ผ ๋ง์ด์ผ!
์์ฆ ์น ๊ฐ๋ฐ์ด๋ ๋ฐ์ดํฐ ๋ถ์ ํ๋ก์ ํธ๋ฅผ ํ๋ค ๋ณด๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ํ์์์? ๊ทธ๋ฐ๋ฐ SQL ์ฟผ๋ฆฌ๋ฅผ ์ง์ ์์ฑํ๋ ๊ฑด ์ค์ํ๊ธฐ๋ ์ฝ๊ณ , ์ฝ๋ ๊ด๋ฆฌ๋ ๋ณต์กํด์ง๋๋ผ๊ณ . ๊ทธ๋์ ๋ฑ์ฅํ ๊ฒ ๋ฐ๋ก ORM(Object-Relational Mapping)์ด์ผ! ๐ฏ
๐ช 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))
์ด ๋ฐฉ์์ ๋ฌธ์ ์ ์ ๋ญ๊น? ๐คทโโ๏ธ
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์ ๋งค๋ ฅ์ด์ผ!
๐ SQLAlchemy ์๊ฐ - Python์ ์ต๊ฐ ORM
Python์๋ ์ฌ๋ฌ ORM ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์์ง๋ง, ๊ทธ์ค์์๋ SQLAlchemy๋ ๊ฐ์ฅ ๊ฐ๋ ฅํ๊ณ ์ ์ฐํ ์ ํ์ง์ผ. Django ORM๋ ์ ๋ช ํ์ง๋ง, SQLAlchemy๋ Django ํ๋ ์์ํฌ์ ์ข ์๋์ง ์๊ณ ๋ ๋ฆฝ์ ์ผ๋ก ์ฌ์ฉํ ์ ์๋ค๋ ๊ฒ ํฐ ์ฅ์ ์ด์ง! ๐ฏ
๐ SQLAlchemy์ ํน์ง
- 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๋ฅผ ์ฌ์ฉํด๋ณด์! ์ฒซ ๋จ๊ณ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฐ๊ฒฐํ๋ ๊ฑฐ์ผ. 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)์ฒ๋ผ! ๐โโ๏ธ
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')
์! ์ด์ ์ฌ์ฉ์, ํฌ์คํธ, ๋๊ธ, ํ๊ทธ๊ฐ ๋ชจ๋ ์ฐ๊ฒฐ๋ ์์ ํ ๋ธ๋ก๊ทธ ์์คํ ์ด ์์ฑ๋์ด! ๐
- One-to-Many: User โ Post (ํ ์ฌ์ฉ์๊ฐ ์ฌ๋ฌ ํฌ์คํธ)
- Many-to-One: Post โ User (์ฌ๋ฌ ํฌ์คํธ๊ฐ ํ ์ฌ์ฉ์์๊ฒ)
- Many-to-Many: Post โ Tag (ํฌ์คํธ์ ํ๊ทธ๋ ์๋ก ์ฌ๋ฌ ๊ฐ)
relationship()์ back_populates๋ ์๋ฐฉํฅ ๊ด๊ณ๋ฅผ ์ค์ ํด์ค. ์ด๋ฌ๋ฉด user.posts๋ก๋ ์ ๊ทผํ๊ณ , post.author๋ก๋ ์ ๊ทผํ ์ ์์ด!
โ๏ธ 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)}๋ช
์ ์ฌ์ฉ์ ์ถ๊ฐ ์๋ฃ!")
๐ 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
๊ด๊ณ๋ ๋ฐ์ดํฐ๋ฅผ ์ธ์ ๊ฐ์ ธ์ฌ์ง ๊ฒฐ์ ํ๋ ๊ฒ ์ค์ํด. ์ด๊ฒ ์ฑ๋ฅ์ ํฐ ์ํฅ์ ๋ฏธ์น๊ฑฐ๋ !
- ๊ด๊ณ๋ ๋ฐ์ดํฐ๋ฅผ ์ค์ ๋ก ์ ๊ทผํ ๋ ๊ฐ์ ธ์ด
- ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ ์ด์ง๋ง, 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()
# ๊ฐ ํฌ์คํธ์ ์์ฑ์, ๋๊ธ(+๋๊ธ ์์ฑ์), ํ๊ทธ๋ฅผ ๋ชจ๋ ๋ฏธ๋ฆฌ ๋ก๋!
์ฌ์ฉ์ 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')
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๋ฅผ ์ ์ฐ๋ ๊ฒ๋ ์ค์ํ์ง๋ง, ๋น ๋ฅด๊ฒ ์ฐ๋ ๊ฒ๋ ์ค์ํด! ์ฑ๋ฅ ์ต์ ํ ๋ฐฉ๋ฒ๋ค์ ์์๋ณด์. ๐
๐ ์ฟผ๋ฆฌ ์ต์ ํ
- 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
๐ก ์ค๋ฌด ํ๊ณผ ํธ๋ฌ๋ธ์ํ
์ค์ ๋ก ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์ฌ๋ฌ ๋ฌธ์ ์ ๋ถ๋ชํ๊ฒ ๋ผ. ์์ฃผ ๋ง๋๋ ์ด์๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ ๋ฆฌํด๋ดค์ด! ๐ง
๐ ์์ฃผ ๋ง๋๋ ์๋ฌ๋ค
ํด๊ฒฐ:
- ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ธ์ ์ด ์ด๋ ค์์ ๋ ๋ฏธ๋ฆฌ ๋ก๋
- 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๋ก ๋ฉํ ๋ง๋ ํด์ฃผ๊ณ , ํ๋ก์ ํธ ์ฝ๋ ๋ฆฌ๋ทฐ๋ ๋ฐ์ ์ ์๊ฑฐ๋ . ํนํ ์ค์ ํ๋ก์ ํธ์์ ๋งํ๋ ๋ถ๋ถ์ด ์์ ๋ ์ ๋ง ์ ์ฉํด. ๐
๐ ์ถ๊ฐ ํ์ต ์๋ฃ
- Alembic ๋ฌธ์: https://alembic.sqlalchemy.org/
์ถ์ฒ ๋์:
- "Essential SQLAlchemy" by Jason Myers
- "SQLAlchemy ๋ง์คํฐํ๊ธฐ" (ํ๊ธ ๋ฒ์ญ์)
์จ๋ผ์ธ ๊ฐ์:
- Udemy: "Complete SQLAlchemy Bootcamp"
- YouTube: Corey Schafer์ SQLAlchemy ํํ ๋ฆฌ์ผ
- ์ฌ๋ฅ๋ท์ Python/๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์๋ค
๐ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด. SQLAlchemy์ ORM์ ๋ํด ๊ธฐ์ด๋ถํฐ ์ค์ ํ์ฉ๊น์ง ๋ชจ๋ ๋ค๋ค๋ดค์ด. ๐
์ฒ์์๋ ๋ณต์กํด ๋ณด์ผ ์ ์์ง๋ง, ํ๋์ฉ ๋ฐ๋ผํ๋ค ๋ณด๋ฉด ์ด๋์ ๋ฅ์ํ๊ฒ ๋ค๋ฃจ๊ณ ์๋ ์์ ์ ๋ฐ๊ฒฌํ๊ฒ ๋ ๊ฑฐ์ผ. ๋๋ ์ฒ์์ "์ด๊ฒ ๋ญ์ผ, SQL ์ง์ ์ฐ๋ ๊ฒ ๋ ์ฌ์ด๋ฐ?"๋ผ๊ณ ์๊ฐํ์๊ฑฐ๋ . ํ์ง๋ง ํ๋ก์ ํธ๊ฐ ์ปค์ง๊ณ ๋ณต์กํด์ง์๋ก ORM์ ์ง๊ฐ๋ฅผ ๋๋ผ๊ฒ ๋์ด.
ํต์ฌ ํฌ์ธํธ๋ฅผ ๋ค์ ์ ๋ฆฌํ๋ฉด:
โ 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
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.