본문 바로가기
Fast API/fastapi배우기

Fast API 배우기 19부 - Dependency Injection

by 붕어사랑 티스토리 2021. 11. 3.
반응형

Dependency Injection

Dependency Injection이란 코드의 재활용을 위해 제공해주는 fastapi의 기능이다.

 

가령 두개의 path operation이 있다고 하자

 

path1, path2 두개 모두 덧셈을 하는 로직이 필요하다.

그리고 두 path operation에 둘다 덧셈을 해주는 로직을 각각 작성하였다.

똑같은 로직인데 비효율적이지 않은가?

 

 

이에대한 해결책은 덧셈을 하는 로직을 하나 만들고 두개의 path operation에 dependency 를 걸어주면 되는것이다!

 

 

from typing import Optional

from fastapi import Depends, FastAPI

app = FastAPI()



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



@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

 

 

 

사용법은 아래와 같다.

 

재활용될 dependency 코드를 작성한다.

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

Depends를 import한다

from fastapi import Depends, FastAPI

dependency를 걸어준다

 

async def read_items(commons: dict = Depends(common_parameters)):
async def read_users(commons: dict = Depends(common_parameters)):

 

 

 

자 여기서 주목할 몇가지를 보자

 

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

 

디펜던시가 걸린 함수의 파라미터를 보면 path operation의 파라미터와 유사하다.

 

즉 dependency가 걸린 함수는 데코레이터(@app.get 같은거) 없는 path operation이라고 생각하면 된다!

 

다음은 Depends에 파라미터이다.

 

Depends에 올 파라미터는 함수처럼 사용이 가능한 파라미터가 와야하며, single 파라미터이여야 한다.

async def read_items(commons: dict = Depends(common_parameters)):
async def read_users(commons: dict = Depends(common_parameters)):

 

 

함수처럼 사용이 가능하다는 말은 꼭 함수가 아니여도 된다는것!

 

 

 

 

이제 docs를 확인해보자. 이전에 일반적으로 작성한 path operation 처럼 나온다!

 

 

Class Dependency

 

앞서 Depends의 파라미터는 함수처럼 쓸 수 있는 파라미터 하나가 와야한다고 했다.

 

그 말인 즉슨 클래스의 생성자는 함수처럼 사용이 가능하니 클래스도 가능하다는 얘기이다!

 

 

from typing import Optional

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]



class CommonQueryParams:
    def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit



@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

 

 

클래스 디펜던시는 앞선 함수 파라미터에 비해 아래와 같은 장점을 가진다.

 

 

  • 클래스 안에 여러가지 기능을 추가해서 리턴해 줄 수 있다
  • 클래스의 멤벼변수가 key값이 되니 IDE에서 코드 작성할 때 자동완성에 이점이 있다

앞선 예제에서는 함수가 dict만 리턴해 주었다. 이를 사용할 때 q, skip, limit 같은 key값들은 ide에서 자동완성이 안되니 내가 수기로 입력해줘야 하는 불편함이 있다.

허나 클래스로 디펜던시를 사용하면 멤버변수이니 key값을 내가 찾지 않아도 자동완성 된다.

 

 

반응형

 

 

Depends의 Type annotation

 

허나 위 코드에 한가지 단점이 있다.

commons: CommonQueryParams = Depends(CommonQueryParams)

CommonQueryParams가 두번이나 반복되었다.

 

사실 fastapi는 = 뒤에있는 코드만 call을 한다.

... = Depends(CommonQueryParams)

 

그러므로 사실 아래처럼만 적어줘도 문제가 없다.

commons = Depends(CommonQueryParams)

 

 

그런데 한가지 문제가 있다.

 

commons의 타입을 명시해주지 않아서 IDE에서 자동완성이 안된다.

그래서 어쩔수 없이 개발의 편의성을 위해 아래처럼 적어주는게 낫다.

 

commons: CommonQueryParams = Depends(CommonQueryParams)

 

 

이문제를 어떻게 해결 할 수 있을가?

 

 

 

Shortcut

 

fastapi는 이를 해결해주기 위해 아래와 같은 shortcut을 제공한다.

 

commons: CommonQueryParams = Depends()

 

그러면 알아서 Depends가 CommonQueryParams 인스턴스를 만들어 리턴해준다.

 

그럼 코드중복도 해결하고 리턴타입이 명시되어있으니 IDE의 자동완성문제도 해결되었다!

 

from typing import Optional

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends()):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

 

 

 

Sub Dependency

depency의 dependency를 만들 수 있다.

 

원하는 만큼 deep하게 만들수 있다.

 

from typing import Optional

from fastapi import Cookie, Depends, FastAPI

app = FastAPI()


def query_extractor(q: Optional[str] = None):
    return q


def query_or_cookie_extractor(
    q: str = Depends(query_extractor), last_query: Optional[str] = Cookie(None)
):
    if not q:
        return last_query
    return q


@app.get("/items/")
async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)):
    return {"q_or_cookie": query_or_default}

 

 

위코드를 설명하자면

 

디펜던시의 디펜던시를 만든다.

def query_extractor(q: Optional[str] = None):
    return q

 

디펜던시의 디펜던시가 될 함수를 디펜던시 함수에 연결시켜준다

 

def query_or_cookie_extractor(
    q: str = Depends(query_extractor), last_query: Optional[str] = Cookie(None)
):
    if not q:
        return last_query
    return q

 

마지막으로 path operation에 연결시켜준다

async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)):

 

 

Path Operation Decorator's Dependency

만약에 디펜던시가 리턴하는 값이 필요하지 않고 실행은 꼭 해야한다면

path operation decorator에 디펜던시를 달 수 있다.

 

사용방법은 데코레이터에 dependencies 파라미터에 Depends를 list형태로 넘겨준다.

 

from fastapi import Depends, FastAPI, Header, HTTPException

app = FastAPI()


async def verify_token(x_token: str = Header(...)):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def verify_key(x_key: str = Header(...)):
    if x_key != "fake-super-secret-key":
        raise HTTPException(status_code=400, detail="X-Key header invalid")
    return x_key



@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
    return [{"item": "Foo"}, {"item": "Bar"}]
@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])

 

 

 

여기서 주목할점은 verify_key 디펜던시 이다. 값을 리턴하고 있다.

 

async def verify_key(x_key: str = Header(...)):
    if x_key != "fake-super-secret-key":
        raise HTTPException(status_code=400, detail="X-Key header invalid")
    return x_key

하지만 저 리턴값은 path operation에 전달되지 않는다.

 

 

 

Global Dependency

개발을 하면서 어플리케이션 전체에 dependency를 걸어주고 싶다면 global dependency를 이용하면 된다

 

from fastapi import Depends, FastAPI, Header, HTTPException


async def verify_token(x_token: str = Header(...)):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def verify_key(x_key: str = Header(...)):
    if x_key != "fake-super-secret-key":
        raise HTTPException(status_code=400, detail="X-Key header invalid")
    return x_key



app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])



@app.get("/items/")
async def read_items():
    return [{"item": "Portal Gun"}, {"item": "Plumbus"}]


@app.get("/users/")
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]
app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])

사용법은 위와 같다.

 

 

 

 

Dependency with yield

FastAPI에서는 yield를 사용하여 디펜던시가 끝나면 몇가지 작업을 수행할 수 있다.

 

 

데이터베이스 디펜던시 with yield

대표적인 예제가, 데이터베이스를 생성한뒤 사용하고 사용이 끝났으면 데이터베이스를 종료하는 작업이 있다.

 

async def get_db():
    db = DBSession()
    try:
        yield db
    finally:
        db.close()

 

response를 보내기전에 yield 까지의 코드만이 실행된다.

 

그리고 response가 전달되면 마지막으로 finally 이후 코드가 수행된다.

반응형

댓글