FastAPI를 Spring처럼 사용해보기 (Annotation)

Python의 wraps 사용해보기 (feat. Fast API)

2024-12-26

나는 웹 프레임워크를 사용할 때 거의 Python의 Fast API를 사용한다. 사내에서 많이 쓰기 때문이다.

그러나 아무래도 국내에서는 스프링을 많이 사용하는데, 스프링에서 많이 사용되는 어노테이션을 FastAPI에 적용한 사례가 있어 공유하고자 한다.

예시

만약 아래와 같은 스마트 스토어 서비스가 있다고 가정해보자.

이 스마트 스토어는 편의점,약국,백화점,서점등 각종 서비스의 아이템을 하나의 웹 사이트에서 다 이용할 수 있는 서비스이다.

사용자 입장에서?

사용자는 하나의 웹 사이트에서 각종 마켓에 있는 물품 구매할 수 있다. 사용자 입장에서는 불편하게 다른 사이트를 왔다갔다 할 필요 없이 하나의 서비스에서 모든 것을 상호작용 할 수 있다.

개발자 입장에서?

개발자 입장에서는 난감할 수 있다. 서버에 등록된 서비스가 많을경우 구현 & 관리해야할 엔드포인트가 많아질 수 있다.

이 때, 개발자의 생산성을 늘리기 위해 각 API마다 내부에 중복된 기능들을 어노테이션으로 관리할 수 있다.

아래는 어노테이션처럼 쓸 수 있는 파이썬 데코레이터(wraps)를 FastAPI와 함께 사용하는 예시이다.

1. 데코레이터 사용 예시

모든 API의 요청은 맨 최초에 의존하는 DB 및 서비스의 상태를 먼저 체크하고 이후 모든 상호작용이 이루어져야 한다고 가정한다.

from fastapi import FastAPI
from functools import wraps
from service_checker import DB_상태_체크, 서비스_상태_체크

app = FastAPI()

# 서버 상태 체크 데코레이터
def dependency_service_health_check(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
    await DB_상태_체크()
    await 서비스_상태_체크()
    # ... 이하 다른 것도 추가 가능
    return func

@app.get("/items")
@dependency_service_health_check
async def get_items():
    # @dependency_service_health_check 내부 메소드가 먼저 작동된다.
    # 이후 상호작용들
    return {}
  • dependency_service_health_check(func) 부분이 Spring@어노테이션 역할을 한다.
  • 실제로는 파이썬의 데코레이터 이며, 어노테이션과 거의 동일한 개념으로 이해하셔도 무방하다.
  • wrapper 내부에서 필요한 작업(로그, 권한검사, 공통 전처리/후처리 등)을 수행할 수 있다.

2. 추가 데코레이터 예시 (인증/인가)

아래 예시에서는 인증/인가 처리를 위한 데코레이터를 간단히 구현해보았다. 서비스 호출 시 유효한 토큰인지 판별하고, 권한을 확인하는 과정을 공통 로직으로 처리할 수 있다.

from fastapi import FastAPI, HTTPException, Request

def auth_required(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        request: Request = kwargs["request"]  # FastAPI에서는 request 객체를 인자로 받을 수 있음
        token = request.headers.get("Authorization")

        if not token or token != "valid_token":
            raise HTTPException(status_code=401, detail="Unauthorized")

        # 권한 체크
        user_role = "admin"
        if user_role not in ["admin", "manager"]:
            raise HTTPException(status_code=403, detail="Forbidden")

        return await func(*args, **kwargs)
    return wrapper

@app.get("/secret-data")
@auth_required
async def get_secret_data(request: Request):
    return {"secret": "data"}

위와 같이 @auth_required 데코레이터를 통해 인증/인가(권한) 로직을 중앙 집중적으로 관리할 수 있다.

3. 데코레이터의 확장 (파라미터 받기)

어노테이션(데코레이터) 사용 중, 상황에 따라 파라미터를 데코레이터에 넘겨야 할 경우가 생길 수 있다.
예를 들어, 특정 그룹이나 롤(Role)만 접근 가능하도록 설정하고 싶을 때가 그렇다.

def role_required(role_name: str):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            request: Request = kwargs["request"]
            user_role = request.headers.get("User-Role", None)

            if user_role != role_name:
                raise HTTPException(status_code=403, detail="Forbidden")

            return await func(*args, **kwargs)
        return wrapper
    return decorator

@app.get("/admin-only")
@role_required("admin")
async def admin_only_api(request: Request):
    return {"message": "관리자 전용 API인데?"}
  • 파라미터를 받는 데코레이터의 구조:
    • 바깥에 파라미터를 받은 후( role_required("admin") ),
    • 내부에서 실제 작업을 수행하는 wrapper 함수를 구성한다.

4. 마치며

  • 파이썬의 데코레이터Spring의 어노테이션과 유사한 목적(공통 로직 관리, 가독성 향상, 중복 코드 제거)으로 사용 가능하다.
  • 데코레이터 내부에서 전처리/후처리, 예외 처리, 인증/인가, 로깅 등 필요한 작업을 일괄 관리하면, 유지보수가 더욱 용이해진다.
  • 규모가 큰 프로젝트에서도 이처럼 데코레이터를 적극 활용하면, 협업 시에도 로직이 분산되지 않고 체계적이며 일관된 방식으로 관리할 수 있다.

추가로, 데코레이터 작성 시에는 비동기 함수(async)를 사용하는 FastAPI 특성을 고려해 await 키워드를 적절히 활용해야 한다는 점에 유의 ! ! !