본문으로 바로가기
728x90
반응형

목차

     

     

    2023.10.06 - [Frame Work/FastAPI] - [FastAPI] DI는 bootstrapping을 생각해보기

    개요 

    FastAPI에서 DI를 적용하기 위해, FastAPI의 Depends 함수가 아닌 dependency-injector 라이브러리를 활용했다. 이를 통해 DB Session을 Singleton으로 설정하고, Repository에서 Session을 주입받아 처리하는 시도를 했다.

     

    그러나 이 과정에서 여러 Repository 간에 수행되는 쿼리에 대한 Transaction이 올바르게 묶이지 않는 문제가 발생했다

     

    이번 글은 이러한 문제의 배경과 함께 어떻게 이를 해결했는지에 대한 기록을 담는다.

     

     

    1. Transaction 처리의 발단

    FastAPI에서 Request 단위의 DB Transaction을 처리를 고민한 게 된 건, 여러 조건을 만족해야 하는 로직을 작성해야 할 때였다. 이러한 로직은 단일 Repository보다는 여러 Repository에 걸쳐 데이터를 가져오는 로직을 수행하게된다.

     

    정말 단순한 예제로다가 member를 조회하고 특정 circle에서 member를  탈퇴시키는 로직의 코드는 다음과 같이 작성할 수 있을 것이다.

    # Example..
    def method(member_id:str):
        member = member_repository.get_by_member_id(member_id=member_id)
        if member is None:
            raise Exception("Not Exist Member")
        
        circle_repository.delete_by_id(circle_id, member_id)

    위 예제에서는 Repository의 메서드 단위로 Transaction을 잡는 것보다 method() 함수를 실행할 때 Transaction을 잡는 방법을 생각해볼 수 있다.

     

     

    2. 함수 실행 시점에 Transaction을 처리하는 데코레이터

    앞서 언급한 Transaction 처리를 위해 처음엔 특정 함수의 실행시 Transaction을 처리하는 데코레이터를 만들었다.

    @Transactional()
    def method():
    	...

    그러나 Transactional 데코레이터를 사용하는 것 또한 문제가 생겼다.  예를 들어 다음과 같은 코드를 사용할 때이다.

    @Transactional()
    def find_member(): ...
    
    @Transactional()
    def leave_member():
    	find_member()

    leaver_member는 미리 Transactional 데코레이터로 처리된 find_member를 호출하게된다 이런 경우 leave_member를 호출하게 되면 별것 아닌 한 번의 Request에 Transaction이 두 번 발생하게되는 걸 확인했다.

     

     

    3. Request 단위의 Transaction 잡기

    FastAPI의 Request 단위로 Transactional을 잡기 위해 처음엔 단순히 APIRouter 위에 Transactional 데코레이터를 사용하는 시도를 했다.

    from fastapi import APIRouter
    
    member_router = APIRouter()
    
    @Transactional()
    @member_router.get()
    def member_api():
    	...

    그러나 위와 같은 코드는 member_router.get() 때문에 Transactional 데코레이터를 실행시키지 못했다. 이는 APIRouter의 원리를 살펴봐야 해결할 수 있을테지만 그러기엔 목표 대비 시간 소모가 너무 컸다. 결국 Request 단위로 Transaction을 잡기 위해 여러 조사를 해본 결과 FastAPI의 APIRoute 클래스를 사용했다.

    # https://fastapi.tiangolo.com/how-to/custom-request-and-route/
    class GzipRoute(APIRoute):
        def get_route_handler(self) -> Callable:
            original_route_handler = super().get_route_handler()
    
            async def custom_route_handler(request: Request) -> Response:
                request = GzipRequest(request.scope, request.receive)
                return await original_route_handler(request)
    
            return custom_route_handler

    위 코드는 FastaAPI 문서에 나온 APIRoute 클래스의 예제이다. request 전과 후로 동작시킬 수 있는 행위를 정의할 수 있다. 어떤 예제에서는 Request 전후로 동작키는 코드를 FastAPI의 middleware를 사용한다 소개되어있지만 내가 마주한 문제를 해결하진 못했다.

     

    다시 돌아와 FastAPI의 APIRoute 예제를 보고 다음과 같이 코드를 작성했다.

    class TransactionRoute(APIRoute):
        session = Provide[DataBaseContainer.session]
    
        def get_route_handler(self) -> Callable:
            original_route_handler = super().get_route_handler()
    
            async def custom_route_handler(request: Request) -> Response:
                try:
                    response: Response = await original_route_handler(request)
                    self.session.flush()
                    self.session.commit()
                    return response
                except Exception as e:
                    self.session.rollback()
                    raise e
                finally:
                    self.session.close()
    
            return custom_route_handler
    
    
    # 사용 부분
    api_router = APIRouter(tags=[''], prefix="", route_class=TransactionRoute)

     

     

    마치며

    Transaction 처리는 본문에서 소개한 내용보다 더 높은 난이도의 주제인듯하다. 본문에서는 이러한 내용은 담지 않았는데 아직 복잡한 Transaction 처리를 시도해야될 정도의 케이스를 보지 못해서 그런 듯하다. 또한 스스로 도전적인 코드를 짜다보니 이러한 사례를 찾는 것이 쉽지 않다.

     

    그리고 이런 코드가 실제 프로덕션에 수용가능할만한 코드인지도 불분명하지만 "실패로부터 배운다"라는 말을 되새기며 이외에도 스스로의 생각이 더해진 다양한 코드를 작성해야겠다.

    728x90
    반응형