๐ ๋ค์ด๊ฐ๋ฉฐ
FastAPI๋ฅผ ์ฌ์ฉํด ์น ์ดํ๋ฆฌ์ผ์ด์ ์ ๊ตฌํํ ๋ Sqlalchemy Session๊ฐ์ฒด์ ๋ผ์ดํ ์ฌ์ดํด์ ๊ด๋ฆฌํ๋ ๊ฒ์ ๋งค์ฐ ์ค์ํฉ๋๋ค. ๋ณํ์ฑ์ด ๋ณด์ฅ๋์ด์ผํ๋ ํ๊ฒฝ์์ Session๊ฐ์ฒด๋ ์ค๋ ๋๋ณ๋ก ๋ ๋ฆฝ์ ์ด์ง ์๊ธฐ ๋๋ฌธ์ ๋๋ค. ๋ฐ๋ผ์ ๊ฐ ์ค๋ ๋๋ณ๋ก ๋ ๋ฆฝ์ ์ผ๋ก Session์ ๊ด๋ฆฌํด์ค์ผํฉ๋๋ค. ์ด๋ฅผ ์ํด ๋ฏธ๋ค์จ์ด, ContextVar ๊ทธ๋ฆฌ๊ณ scoped_session์ ํ์ฉํด ๋ณํ์คํ์ด ํ์ํ ํ๊ฒฝํด์ Session์ ์ด๋ป๊ฒ ๊ด๋ฆฌํด์ผํ๋์ง ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๐ ์ด๋ฒ ํฌ์คํ ์์ ๋ค๋ฃจ์ง ์๋๊ฒ
- Sqlalchemy์ Engine, Session์ ๊ฐ๋
- FastAPI Depends
- ๋ฏธ๋ค์จ์ด ๊ฐ๋
- ContextVar์ ๊ฐ๋
๐ Thread local, Thread Safe?
Sqlalchemy๋ BaseModel๊ฐ์ฒด๋ฅผ ๋ค๋ฃจ๊ธฐ ์ํด Session์ ์ ๊ณตํฉ๋๋ค. Session์ ํตํด DB ๋ ์ฝ๋๋ฅผ Python๊ฐ์ฒด๋ก ๋ค๋ฃฐ ์ ์๊ณ ๋ฐ๋๋ก Python ๊ฐ์ฒด๋ฅผ DB ๋ ์ฝ๋๋ก ๋ณ๊ฒฝํ๋ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ ค์ฃผ๊ธฐ๋ ํฉ๋๋ค. ๋ค๋ง ์ด Session ๊ฐ์ฒด๋ฅผ ๋ค๋ฃฐ ๋์๋ ์ฃผ์ ํด์ผํ ๋ถ๋ถ์ด ์์ต๋๋ค.
The concurrency model for SQLAlchemy’s Session and AsyncSession is therefore Session per thread, AsyncSession per task.
Session๊ฐ์ฒด๋ Thread local(์ค๋ ๋ ๋ณ๋ก ํ๋์ฉ ์์ฑ๋์ง ์์)ํ์ง ์์ต๋๋ค. ์ฐ๋ฆฌ๊ฐ ํน๋ณํ ์์
์์ด ์์ฑํ๋ ๋ชจ๋ ๋ณ์, ๊ฐ์ฒด๋ค์ด Thread localํ์ง ์์ต๋๋ค. ์ฌ๋ฌ ์ค๋ ๋์์ ๊ฐ์ฒด๋ฅผ ๊ณต์ ํด์ ์ฌ์ฉํ ์ ์๊ณ , ๋์์ ํด๋น ๊ฐ์ฒด์ ์ ๊ทผํ ๊ฒฝ์ฐ ์์
์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์์ง๊ฐ ์๋ Thread Safe ํ์ง ์์ ๊ฐ์ฒด์ธ๊ฑฐ์ฃ
๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ์ฌ๋ฌ ์ค๋ ๋๋ฅผ ์ฌ์ฉํ๊ฑฐ๋, ๋น๋๊ธฐ ์์
์ ํ๊ฒ๋ ๊ฒฝ์ฐ ๋ค๋ฅธ ์คํํ๋ฆ ๋ด์์ ๊ณต์ ๋๋ Session๊ฐ์ฒด๋ฅผ ๋ซ์๋ฒ๋ฆฌ๋ฉด ๋ค๋ฅธ ์์
์ Session ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ๋์ด๋ฒ๋ฆฝ๋๋ค.
์ด๋ฌํ ์ํฉ์์ Sqlalchemy์์ ์ ์ํ๋ ๊ฐ์ฅ ์ข์ ๋ฐฉ๋ฒ์ ContextManager ํจํด์ ์ ์ฉํด์ Session์ด ํ์ํ ์๊ฐ๋ง Session์ ์ด๊ณ ๋ซ์ผ๋ผ๋ ๊ฒ ์
๋๋ค. ๋ค์๊ณผ ๊ฐ์ ํจํด์ด์ฃ
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
engine = create_engine("postgresql+psycopg2://scott:tiger@localhost/")
# create session and add objects
with Session(engine) as session: # <- Session ์ด๊ธฐ
session.add(some_object)
session.add(some_other_object)
session.commit()
# <- with ๋ธ๋ญ ๋ฐ์์๋ Session์ด ๋ซํ
Sqlalchemy์์ ์ ์ํ๋๋๋ก Context manager๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋ ๊น์? Session๊ฐ์ฒด๋ฅผ ํ์ํ ์๊ฐ๋ง ์์ฑํ๊ณ ๋ซ๊ธฐ ๋๋ฌธ์ Thread Safeํ๊ฒ ๋ง๋ค ์ ์์ต๋๋ค.
session์ ํน์ ๋ธ๋ญ ๋ด๋ถ์์ ์ฌ์ฉํ๊ณ ๋ค์ ORM ๊ฐ์ฒด์ ๊ฐ์ ๋ณ๊ฒฝํ๊ณ ํ๋ ค๊ณ ํ๋ค๋ฉด ์ด๋ค์ผ์ด ์ผ์ด๋ ๊น์?
์์ ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด ๋ธ๋ก๊ทธ์ ํฌ์คํ
(Post)๊ณผ ๋๊ธ(Comment)์ ๊ด๋ จ๋ ํ
์ด๋ธ ๋จผ์ ์ ์ํด๋ณด๊ฒ ์ต๋๋ค.
# Post ๋ชจ๋ธ ์ ์
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False) # ๊ธธ์ด๋ฅผ ์ง์
content = Column(Text, nullable=False)
comments = relationship('Comment', back_populates='post',
cascade='all, delete-orphan')
# Comment ๋ชจ๋ธ ์ ์
class Comment(Base):
__tablename__ = 'comments'
id = Column(Integer, primary_key=True, index=True)
content = Column(Text, nullable=False)
post_id = Column(Integer, ForeignKey('posts.id'), nullable=False)
post = relationship('Post', back_populates='comments')
๊ทธ๋ฆฌ๊ณ with ๋ธ๋ญ ๋ฐ์์ ๊ฐ์ ๋ณ๊ฒฝํ๋ ๊ฒฝ์ฐ๋ฅผ ์์๋ก ๋ง๋ค์ด๋ณด๊ฒ ์ต๋๋ค. ๋ค์๊ณผ ๊ฐ์ด ํฌ์คํ ์ ๋ณด๋ฅผ ๋ณ๊ฒฝํ๋ FastAPI ๋ผ์ฐํฐ๋ฅผ ๋ง๋ค์ด๋ณด๊ฒ ์ต๋๋ค.
@router.put(
"/post/{post_id}",
)
def update_post(
post_id: int,
request: PostSchema = Depends(),
session_maker=Depends(get_session_maker),
):
with session_maker() as session:
post = session.query(Post).filter(Post.id == post_id).first()
post.title = request.title
post.content = request.content
session.commit()
# with ๋ธ๋ญ ๋ฐ์์ session์ด close๋์ด์ ๊ฐ ๋ณ๊ฒฝ์ด ์๋จ
post.title = "์ธ์
๋ซํ ํ ์์ ๋ถ๊ฐ๋ฅ"
์ ์ฝ๋์์ post.title์ ๊ฐ์ ์ด๋ป๊ฒ ๋ ๊น์? with ๋ธ๋ญ ๋ฐ์์๋ Session์ด ์ ํจํ์ง ์๊ธฐ ๋๋ฌธ์ ORM ๊ฐ์ฒด์ ๋ณ๊ฒฝ์ด DB์ ๋ฐ์๋์ง ์์ต๋๋ค. ORM๊ฐ์ฒด๊ฐ DB์ ๊ฐ์ ์ฃผ๊ณ ๋ฐ์ ์ ์๋ ์์ญ์ด with ๋ธ๋ญ ๋ด๋ถ๋ก ์ ํ๋๋ ๊ฒ์ด์ฃ .
๋ ๋ค๋ฅธ ์์๋ฅผ ํ๋ฒ ๋ค์ด๋ณผ๊น์? ๋ง์ผ Post ํ
์ด๋ธ๊ณผ Comment ํ
์ด๋ธ์ด ์ฐ๊ด๊ด๊ณ๊ฐ ์๋ ๊ฒฝ์ฐ with ๋ธ๋ญ ๋ฐ์์ ์ฐ๊ด ๊ฐ์ฒด์ ๊ฐ์ ์กฐํํ๋ ๊ฒฝ์ฐ์ ๋ํ ์ฝ๋๋ฅผ ์์ฑํด๋ณด๊ฒ ์ต๋๋ค.
class PostResponseSchema(BaseModel):
title: str
content: str
comments: list[str] = []
@classmethod
def mapper(cls, post: Post):
return cls(
title=post.title,
content=post.content,
comments=[comment.content for comment in post.comments], # <- ์๋ฌ ๋ฐ์
)
@router.get(
"/post/{post_id}",
)
def get_post(
post_id: int,
session_maker=Depends(get_session_maker),
):
with session_maker() as session:
post = session.query(Post).filter(Post.id == post_id).first()
return PostResponseSchema.mapper(post)
์ ์ฝ๋๋ฅผ ์คํํ๋ฉด PostResponseShchema์ mapper์์ post์ ์ฐ๊ด ๊ฐ์ฒด์ธ comments์ ์ ๊ทผํ๋ ค๊ณ ํ ๋ DetachedInstanceError ์๋ฌ๊ฐ ๋ฐ์ํฉ๋๋ค. ๋ซํ Session์ ์ฌ์ฉํด์๋ ์ฐ๊ด๊ฐ์ฒด์ ์ ๊ทผํ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
raise orm_exc.DetachedInstanceError(
sqlalchemy.orm.exc.DetachedInstanceError: ... is not bound to a Session; lazy load operation of attribute 'comments' cannot proceed (Background on this error at: https://sqlalche.me/e/20/bhk3)
)
๐ Context-local ํ Session
๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ๊ธฐ ์์ Context-local ํ Session๊ฐ์ฒด๊ฐ ๋ฌด์์ธ์ง ์์๋ณด๊ฒ ์ต๋๋ค. Context-local ํ๋ค๋ ์๋ฏธ๋ ํน์ ์คํ context ์์๋ง ๊ฐ์ฒด๊ฐ ์ ํจํ๋ค๋ ์๋ฏธ์
๋๋ค.
Context local์ ๋ํด ์กฐ๊ธ ๋ ์์ธํ๊ฒ ์ค๋ช
ํ๊ธฐ ์ํด ์น ์ดํ๋ฆฌ์ผ์ด์
์ ์์๋ก ๋ค์ด๋ณด๊ฒ ์ต๋๋ค. ์น ์ดํ๋ฆฌ์ผ์ด์
์์ Context์ ๋ฒ์๋ ํ๋์ ์ฌ์ฉ์ ์์ฒญ์
๋๋ค. ์น ์๋น์ค์ ๊ฐ์ด ๋ค์ค ์์ฒญ์ด ๋์์ ์ฒ๋ฆฌ๋๋ ํ๊ฒฝ์ ๊ฐ์ ํด๋ณด์์ ๋ Context-local ํ๊ฒ Session ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋ค๋ฉด, ์์ฒญ๋ง๋ค ๋
๋ฆฝ๋ Session ์ธ์คํด์ค๋ฅผ ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค. ์์์ ๋ฌธ์ ๊ฐ ๋์๋, thread-safe ํ์ง ์์ Session ๊ฐ์ฒด ํ๋๋ฅผ ์ฌ๋ฌ ์์ฒญ์ด ๊ณต์ ํ๋ ๋ฐฉ์์ด ์๋, ์ค์ง ํ๋์ ์ปจํ์คํธ ๋ด๋ถ์์ ๋
๋ฆฝ์ ์ธ Session ๊ฐ์ฒด๋ฅผ ํ์ฉํ ์ ์๊ฒ๋ฉ๋๋ค.
`Context-local` ์ธ์
์ Sqlalchemy์ scoped_session์ ํตํด ๋ง๋ค ์ ์์ต๋๋ค. scoped_session์ ์ ํ๋ฆฌ์ผ์ด์
์ ํ์ฌ ์ค๋ ๋ ํน์ ์ฝ๋ฃจํด์ ์ปจํ
์คํธ์ ๋ฐ๋ผ ๊ฐ๊ธฐ ๋ค๋ฅธ `Session` ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค.
์๋๋ scoped_session์ ์ฌ์ฉํ๋ ์์ ์ฝ๋์ ๋๋ค.
from sqlalchemy.orm import scoped_session, sessionmaker
from context_store import get_request_id
SessionFactory = sessionmaker(bind=some_engine)
# request_id์ ๋
๋ฆฝ์ ์ธ session์ ์์ฑ
SessionLocal = scoped_session(SessionFactory, scoped_func=get_request_id)
# ์ฌ์ฉ ์
session = SessionLocal()
๐ ๏ธ Session ๊ฐ์ฒด์ ๋ฏธ๋ค์จ์ด
์ด๋ฒ ํฌ์คํ ์์ ๋ณด์ฌ๋๋ฆด FastAPI ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํด์ Session์ ๋ค๋ฃฐ๋ ํ์ํ ์กฐ๊ฑด์ ๋๋ค.
- Session ๊ฐ์ฒด๊ฐ Task ๋ง๋ค ํ๋์ฉ ๋ง๋ค์ด์ ธ์ผํ๋ค.(Thread-Safe)
- ํน์ ๋ก์ง ํ๋ฆ ๋ด์์ Session ๊ฐ์ฒด๋ฅผ ์ด๊ณ ๋ซ๋ ๋ก์ง์ด ์์ด์ผ ํ๋ค.
- ์ฌ์ฉ์ ์์ฒญ๋ถํฐ ์๋ต๊น์ง ์ ํจํ ORM ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ ์ ์์ด์ผ ํ๋ค.
์ ์ฒด์ ์ธ ํ๋ฆ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์ ํ๋ฆ์ ์์ ์ฝ๋๋ฅผ ์์ฑํด ๋ณด๊ฒ ์ต๋๋ค.
โ๏ธ ContextVar, ๋ฏธ๋ค์จ์ด ์ฝ๋
Context๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํด ํด๋ผ์ด์ธํธ ์์ฒญ์ `request_id` ๋ฅผ ContextVar๋ฅผ ์ฌ์ฉํด scoped_session ๊น์ง ์ ๋ฌํ๋ ์ฝ๋๋ฅผ ์์ฑํด ๋ณด๊ฒ ์ต๋๋ค.
1. ContextVar
์์ฒญ๋ง๋ค ๋ ๋ฆฝ์ ์ธ request_id๋ฅผ ๊ด๋ฆฌํ๋ context ๋ณ์๋ฅผ ์์ฑํฉ๋๋ค.
# context_store.py
from contextvars import ContextVar
request_id_var: ContextVar[str] = ContextVar("request_id", default="")
๋ค์์ reqeust_id๋ฅผ ์์ฑํ๊ณ , request_id_var์ ์ ์ฅํ๋ XRequestIdMiddleware ๋ฏธ๋ค์จ์ด์ ๋๋ค. ์ด ๋ฏธ๋ค์จ์ด๋ฅผ ํตํด ์ฌ์ฉ์ ์์ฒญ๋ง๋ค request_id๋ฅผ request_id_var์ด๋ผ๋ contextVar์ ์ ์ฅํฉ๋๋ค.
class XRequestIdMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
request_id_var.set(request_id)
response = await call_next(request)
response.headers["X-Request-ID"] = request_id # ์๋ต์ ํค๋ ์ถ๊ฐ
return response
2. scoped session
scoped_session์์๋ request_id_var.get ํจ์๋ฅผ ๊ฐ์ ธ์ scoped_session์ `scopefunc`์ ๋ฃ์ด์ค๋๋ค. ์ด์ request_id ๋ง๋ค ๋ ๋ฆฝ์ ์ธ Session์ scoped session์ผ๋ก ๋ถํฐ ์ป์ ์ ์์ต๋๋ค.
# session.py
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from src.context_store import reqest_id_var
engine = create_engine(settings.database.server_url, pool_pre_ping=True)
session_maker = sessionmaker(bind=engine)
session_factory = scoped_session(session_maker, scopefunc=request_id_var.get)
3. Session์ ๋ผ์ดํ ์ฌ์ดํด์ ๊ด๋ฆฌํ๋ ๋ฏธ๋ค์จ์ด
Session ๊ฐ์ฒด์ ๋ผ์ดํ ์ฌ์ดํด์ ๊ด๋ฆฌํ๋ SessionMiddleware ์ ๋๋ค. ์๋ ๋ฏธ๋ค์จ์ด์์ Session ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ ์์ฒญ์ ๋ค ์ฒ๋ฆฌํ๊ณ ์๋ตํ ๋ Session ๊ฐ์ฒด๋ฅผ ๋ฐ๋ฉํ๊ฒ๋ฉ๋๋ค. ๋ง์ผ ์ค๊ฐ์ ์๋ฌ๊ฐ ๋ฐ์ํด Exception์ด ๋ฐ์ํ ๊ฒฝ์ฐ ๋กค๋ฐฑ ์ฒ๋ฆฌํ ์ ์๊ฒ ๊ตฌํ๋์์ต๋๋ค.
from session import session_factory
class SessionMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
session = session_factory()
try:
response = await call_next(request)
return response
except Exception as e:
session.rollback()
raise e
finally:
session.close()
4. ๋ฏธ๋ค์จ์ด ์ ์ฉ
๋ฏธ๋ค์จ์ด๋ฅผ FastAPI์ ์ ์ฉํด ๋ณด๊ฒ ์ต๋๋ค. ์ปค์คํ ํ๊ฒ ๋ง๋ ๋ฏธ๋ค์จ์ด๋ ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ๋ฒ์ผ๋ก ์ ์ฉํ ์ ์์ต๋๋ค.
def middlewares():
middlewares = []
# X-Request-ID Middleware
middlewares.append(Middleware(XRequestIdMiddleware))
# Session Middleware
middlewares.append(Middleware(SessionMiddleware))
return middlewares
def create_app():
app = FastAPI(
title="API Server",
version="0.1.0",
description="",
middleware=middlewares(), # ๋ฏธ๋ค์จ์ด ์ ์ฉ!
docs_url="/docs",
redoc="/redoc"
)
๐ FastAPI ๋ผ์ฐํฐ
๋ง์ง๋ง์ผ๋ก Session ๋ผ์ดํ์ฌ์ดํด์ ๋ฏธ๋ค์จ์ด๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์ ์ฉํ FastAPI๋ก ๋ง๋ Router ํจ์๋ฅผ ๋ง๋ค์ด๋ณด๊ฒ ์ต๋๋ค.
def get_session():
return session_factory()
class PostCreateSchema(BaseModel):
title: str
content: str
class PostResponseSchema(BaseModel):
class Config:
orm_mode = True
title: str
content: str
comments: list[str] = []
@classmethod
def mapper(cls, post: Post):
return cls(
title=post.title,
content=post.content,
comments=[comment.content for comment in post.comments],
)
@router.post(
"/post",
)
def create_post(
request: PostSchema,
session: Session = Depends(get_session),
):
post = Post(
title=request.title,
content=request.content,
)
session.add(post)
session.commit()
return PostResponseSchema.mapper(post)
@router.get(
"/post/{post_id}",
)
def get_post(
post_id: int,
session: Session = Depends(get_session),
):
post = session.query(Post).filter(Post.id == post_id).first()
return PostResponseSchema.mapper(post)
๋ผ์ฐํฐ ํจ์๋ฅผ ์ดํด๋ณด๋ฉด, Session์ ์ด๊ณ ๋ซ๋ ์ฝ๋๊ฐ ์๋ค๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค. ๋, ๋ผ์ฐํฐ ํจ์๋ฅผ ๋ฒ์ด๋์ PostResponseSchema ํด๋์ค์ mapper ํจ์ ๋ด์์๋ ORM ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค. ๋ํ scoped_session์ ํตํด ์ป์ Session ๊ฐ์ฒด๋ฅผ ํตํด ์์ฒญ Context๋ง๋ค ์ ์ผํ๊ณ ๋ ๋ฆฝ์ ์ธ Session ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
๐ ๋ง์น๋ฉฐ
์ง๊ธ๊น์ง Middleware, ContextVar, scoped_session์ ํ์ฉํด์ Session ๊ฐ์ฒด์ ๋ผ์ดํ์ฌ์ดํด์ ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ดค์ต๋๋ค. ๊ธฐ๋ฅ ๊ตฌํ์ ์ํด ๊ฐ ๊ฐ๋ ์ ์ ๋ง ๊ฐ๋จํ ์๊ฐ๋๋ ธ์ต๋๋ค... ๐ ์๋ ์ฐธ๊ณ ํ๋ ๋ธ๋ก๊ทธ ์๋ฃ๋ฅผ ์ฒจ๋ถํ๋ ์์ธํ ๋ด์ฉ์ด ๊ถ๊ธํ์๋ฉด ์ฐธ๊ณ ํด๋ณด์ธ์! ์ด๋ง ๋ง๋ฌด๋ฆฌํ๊ฒ ์ต๋๋ค.
๐ ์ฐธ๊ณ ์๋ฃ
'Computer Science > Python' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Python] FastAPI์ Dependency Injector (3) | 2024.10.27 |
---|---|
[Python] ๋น๋๊ธฐ ํ ์คํธ๋ฅผ ํ๋ ค๋ฉด? (a.k.a pytest-asyncio) (2) | 2024.04.28 |
[Python] UoW(Unit of Work) ํจํด์ ์์๋ณด์ (0) | 2024.04.14 |
[Python] SqlAlchemy 1.4 -> 2.0 ๋ง์ด๊ทธ๋ ์ด์ ๋จ๊ณ๋ณ ๊ฐ์ด๋ (0) | 2024.03.31 |
[Python] Tox๋ก ์ฌ๋ฌ ํ๊ฒฝ์์ ํ ์คํธํ๊ธฐ (0) | 2023.07.02 |