Python/FastAPI

Dependency와 FastAPI에서의 Dependency Injection

emhaki 2023. 12. 22. 22:54
728x90
반응형
SMALL

 

의존성(Dependency)이란?

코드에서 의존성은 두 모듈간의 연결을 의미한다. class A가 다른 class B를 사용할 때 A는 B에 의존한다고 볼 수 있다. 즉 한 객체의 코드에서 다른 객체를 생성하거나 다른 객체의 메서드를 호출할 때, 또는 파라미터로 객체를 전달받아 사용할 때 의존성이 발생한다고 할 수 있다. 이렇게 되면 A는 B 없이는 작동할 수 없고 B를 재사용하지 않으면 A또한 재사용할 수 없으며 하나를 수정하면 다른 클래스에도 영향을 끼치게 된다. 강한 결합은 강한 의존성을 만들게 되고 이는 유지보수를 힘들게 하며 재사용을 어렵게 만든다.

의존성 주입(Dependency Injection)이란?

흔히 DI(Dependency Injection)라고 불리는 의존성 주입은 위와 같은 의존성의 위험을 해소하기 위해 사용되는 패턴이다. 사용하는 객체가 아닌 외부의 독립 객체가 인스턴스를 생성한 뒤 이를 전달해 의존성을 해결한다. FastAPI에서의 의존성 주입은 Depends를 통해 할 수 있다.

from typing import Annotated
from fastapi import Depends, FastAPI

app = FastAPI()

async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons

@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons

위와 같이 read_items와 read_users 함수 파라미터로 Depends를 사용하여 common_parameters의 역할을 전달할 수 있다. 만약 read_users 함수에서 Depends 없이 내부에서 코드를 작성했다면 common_parameters의 return값이 바뀌었을 때 read_users 함수의 내용도 바뀌었을 것이다. 이를 방지하고자 Depends를 사용하여 구현체에만 의존하게끔 의존성을 주입시킨 경우이다. 이렇게 하면 유지보수도 용이하고 재사용 가능성이 커진다는 장점이 있다.

 

Depends를 이용하면 인가가 필요한 router에서도 쉽게 기능을 적용할 수 있다.

login = FastAPI()

login_router = APIRouter(
    prefix="/login", tags=["Login"], dependencies=[Depends(auth_token)]
)

@login_router.post(
  path="",
  summary="로그인"
)
async def login():
  return "Depends"

위와 같이 router에 dependencies를 추가하면 auth_token의 역할을 router단에서도 사용 가능하다. 물론 parameter로 User의 인가를 확인할 수도 있다.

# 토큰생성
def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

# 토큰 검증 함수
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except JWTError:
        raise credentials_exception
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        raise credentials_exception
    return user

# 토큰 검증 API
@app.get("/users/me/", response_model=User)
async def read_users_me(
    current_user: Annotated[User, Depends(get_current_user)]
):
    return current_user

토큰을 생성하는 메서드(create_access_token)가 있고, 토큰을 검증하는 메서드(get_current_user)가 있다고 할 때 /users/me API를 호출하게 되면 parameter의 Depends를 통해 get_current_user가 실행된다. get_current_user가 메서드 내의 구현된 jwt.decode를 통해 유효성을 체크하고 토큰이 valid하다면 current user를 반환하게 되고 토큰이 invalid하다면 HTTP error를 불러온다.

그리고 보안 관련해서 토큰을 cookie에 넣는 경우도 있는데 FastAPI에서는 Response를 통해 쉽게 토큰을 cookie에 삽입할 수 있다.

from fastapi import Response

async def login(
        response: Response
):
  response.set_cookie(key="refresh_token", value=refresh_token)

reseponse.set_cookie(key, value)를 하게 되면 아래와 같이 cookie에 토큰 값이 담긴걸 확인할 수 있다.

 

의존성 주입을 하면 뭐가 좋은데?

코드의 강한 결합성은 자유도를 크게 떨어뜨린다. 작은 규모의 프로젝트라면 의존성에 대해 크게 신경쓰지 않아도 되겠지만, 일반적인 상용 애플리케이션의 경우 지속해서 유지보수를 해야하는 경우가 많다. 의존성 주입을 하게 되면 모듈간 의존성이 줄어들게 되고, 이는 유지보수와 재사용성 증가를 이끌어내게 된다. 강하게 결합되어 있는 코드를 수정하는 일이 생긴다면... 끔찍해서 생각하고 싶지 않다.

 

728x90
반응형