Becker
Becker ์˜ TIL
Becker
  • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (30)
    • Computer Science (15)
      • Python (7)
      • Java (1)
      • Algorithm (0)
      • Database (3)
      • Network (1)
      • Openstack (2)
      • ETC (1)
    • ์ž๋™ํ™” (3)
      • Github Action (1)
      • Airflow (0)
      • Docker (2)
    • ๋…ผ๋ฌธ ๋ฆฌ๋ทฐ (1)
    • ์„œํ‰ (3)
    • ๋„์ ๋„์  (8)
์ „์ฒด ๋ฐฉ๋ฌธ์ž
์˜ค๋Š˜
์–ด์ œ

์ธ๊ธฐ ๊ธ€

ํ‹ฐ์Šคํ† ๋ฆฌ

hELLO ยท Designed By ์ •์ƒ์šฐ.
Becker

Becker ์˜ TIL

[Python] FastAPI ๋ฏธ๋“ค์›จ์–ด๋กœ Sqlalchemy Session ๊ด€๋ฆฌํ•˜๊ธฐ
Computer Science/Python

[Python] FastAPI ๋ฏธ๋“ค์›จ์–ด๋กœ Sqlalchemy Session ๊ด€๋ฆฌํ•˜๊ธฐ

2024. 11. 24. 16:19

 

๐ŸŒŸ ๋“ค์–ด๊ฐ€๋ฉฐ

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์„ ๋‹ค๋ฃฐ๋•Œ ํ•„์š”ํ•œ ์กฐ๊ฑด์ž…๋‹ˆ๋‹ค.

 

  1. Session ๊ฐ์ฒด๊ฐ€ Task ๋งˆ๋‹ค ํ•˜๋‚˜์”ฉ ๋งŒ๋“ค์–ด์ ธ์•ผํ•œ๋‹ค.(Thread-Safe)
  2. ํŠน์ • ๋กœ์ง ํ๋ฆ„ ๋‚ด์—์„œ Session ๊ฐ์ฒด๋ฅผ ์—ด๊ณ  ๋‹ซ๋Š” ๋กœ์ง์ด ์—†์–ด์•ผ ํ•œ๋‹ค.
  3. ์‚ฌ์šฉ์ž ์š”์ฒญ๋ถ€ํ„ฐ ์‘๋‹ต๊นŒ์ง€ ์œ ํšจํ•œ 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 ๊ฐ์ฒด์˜ ๋ผ์ดํ”„์‚ฌ์ดํด์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ดค์Šต๋‹ˆ๋‹ค. ๊ธฐ๋Šฅ ๊ตฌํ˜„์„ ์œ„ํ•ด ๊ฐ ๊ฐœ๋…์„ ์ •๋ง ๊ฐ„๋‹จํžˆ ์†Œ๊ฐœ๋“œ๋ ธ์Šต๋‹ˆ๋‹ค... ๐Ÿ˜… ์•„๋ž˜ ์ฐธ๊ณ ํ–ˆ๋˜ ๋ธ”๋กœ๊ทธ ์ž๋ฃŒ๋ฅผ ์ฒจ๋ถ€ํ•˜๋‹ˆ ์ž์„ธํ•œ ๋‚ด์šฉ์ด ๊ถ๊ธˆํ•˜์‹œ๋ฉด ์ฐธ๊ณ ํ•ด๋ณด์„ธ์š”! ์ด๋งŒ ๋งˆ๋ฌด๋ฆฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“š ์ฐธ๊ณ  ์ž๋ฃŒ

  • [FastAPI] 10. Middleware๋ฅผ ์ด์šฉํ•œ ์ „ํ›„ ์ฒ˜๋ฆฌ
  • FastAPI์—์„œ SQLAlchemy Session ๋‹ค๋ฃจ๋Š” ๋ฐฉ๋ฒ•
  • Python threading.local ์™€ ContextVar ๋น„๊ต
  • Python contextvar ๊ด€๋ จ ๊ณต์‹๋ฌธ์„œ

'Computer Science > Python' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[Python] FastAPI์™€ Dependency Injector  (3) 2024.10.27
[Python] ๋น„๋™๊ธฐ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋ ค๋ฉด? (a.k.a pytest-asyncio)  (3) 2024.04.28
[Python] UoW(Unit of Work) ํŒจํ„ด์„ ์•Œ์•„๋ณด์ž  (0) 2024.04.14
[Python] SqlAlchemy 1.4 -> 2.0 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ  (1) 2024.03.31
[Python] Tox๋กœ ์—ฌ๋Ÿฌ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธํ•˜๊ธฐ  (1) 2023.07.02
    'Computer Science/Python' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
    • [Python] FastAPI์™€ Dependency Injector
    • [Python] ๋น„๋™๊ธฐ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋ ค๋ฉด? (a.k.a pytest-asyncio)
    • [Python] UoW(Unit of Work) ํŒจํ„ด์„ ์•Œ์•„๋ณด์ž
    • [Python] SqlAlchemy 1.4 -> 2.0 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ
    Becker
    Becker

    ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”