[WAFFLE] 01-6 Dependency Injection

6 minute read

Published:

In this post, basic concepts about Dependency Injection is introduced.

Dependency Injection

  • 의존성 주입은 객체 지향 프로그래밍에서 사용되는 디자인 패턴이다. 객체의 생성을 외부에 위임하여, 객체 간의 의존 관계를 느슨하게 만든다.
  • 인터페이스에 의존하는 설계로 객체 간의 결합도를 낮추어 유지보수성, 테스트 용이성을 높인다.

우리가 만드는 코드는 하나의 파일에 있지 않다. 여러 파일에 존재하고 그러다보면 클래스나 함수 문법 등으로 코드를 분리하고 격리하는 방식을 많이 사용한다. 만약에 우리가 mySQL DB를 사용해서 mySQLUserRepository 라는 것을 만들어서 사용하고 있다고 생각하자. 나중에 어떤 이유로 mySQL을 postgreSQL이라는 DB로 옮겼고 postgreSQLUserRepository 라는 클래스를 새로 만들어서 변경하면 mySQLUserRepository 클래스를 사용하는 모든 곳에서 이를 postgreSQLUserRepository로 바꿔주어야 하고 인자 같은게 바뀐게 있다면 수정해야 하는 등 상위 계층에 영향을 주게 된다. 이때 의존성 주입은 이러한 하위계층의 변화를 상위계층으로 전파시키지 않는 그런 기법이다.

Dependcy Injection in FastAPI

FastAPI에서 의존성이란 다음과 같이 정의할 수 있다.

  • dependable 한 것은 2가지로 나뉘는데, primitive_dependency와 callable_dependency가 있다.
  • primitive_dependency : Request 계층, Path 파라미터, Query 파라미터, Header 등 FastAPI에서 기본으로 제공해주는 것들.
  • callable_dependency : 개발자가 자신의 dependency를 정의할 수 있음 dependable 한 것이 함수일 수도 있고 클래스일 수도 있다. python은 객체를 생성할 때 new 키워드를 사용하지 않고 함수를 호출하는 것과 동일한 문법을 사용하므로 그래서 함수냐 클래스냐를 구분하지 않고 callable이라고 합쳐서 이야기한다.
dependable ::= primitive dependency | callable dependency
primitive_dependency ::= Request | Response | Path | Query | ...
callable_dependency ::= Callable[...dependable, Any]

쉽게 말해, 의존성 주입은 코드가 다른 객체나 함수를 직접 생성하지 않고 외부에서 주입받는 방식.

primitive dependency

from fastapi immport FastAPI, Request, Path, Query, Header

@app.get("/items/{item_id}")
def read_item(
    request: Request,
    item_id: int = Path(...),
    min_price: int = Query(0),
    user_agent: str = Header(None),
):
    ...
  • 이 의존성들은 별로로 정의해줄 필요 없이 바로 사용 가능. FastAPI가 알아서 의존성 주입.

callable dependency

from fastapi import Depends

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

@app.get("/items")
def read_items(db: DBSession = Depends(get_db)):
    ...
  • 자신만의 의존성을 만들 때 사용.
  • Depends 라는 함수를 사용하여 의존성을 주입하는 방법(함수 혹은 클래스 생성자)을 Depends 안에 감싸서 넘겨주면 FastAPI가 요청이 들어왔을 때 자동으로 저 함수를 호출해서 의존성을 주입해줌.
  • 위에서 callable dependency의 반환 값은 Any이지만 인자로는 …dependable 인 것은 callable_dependency 안에서도 다른 dependency 들을 중첩해서 사용할 수 있다는 의미. 이를 통해 의존성을 계층적으로 관리할 수 있고, 재사용성을 높일 수 있음.
from fastapi import Depends

def get_db():
    db = DBSession()
    try:
        yield db
    finally:
        db.close()
    
    def get_current_user(db: DBSession = Depends(get_db))
        user = db.query(User).filter(User.id == 1).first()
        return user
    
    @app.get("/items")
    def read_items(user: User = Depends(get_current_user)):
        ...
  • Depends 안에 인자가 없다면 타입 자체가 dependable(callable 즉, 실행가능) 해야 함
    • Depends 안에 특정 의존성 함수(예: get_db)를 전달하지 않을 경우, FastAPI는 해당 매개변수의 타입 힌트(type hint)를 기반으로 의존성을 찾아야 한다는 의미. 즉, 타입 힌트로 지정된 타입이 FastAPI가 실행 가능한(= dependable) 객체여야 한다는 뜻.
        @app.get("/items")
        def read_items(db: DBSession = Depends()):
        ...
      

      FastAPI는 매개변수(db)의 타입 힌트(DBSession)를 기반으로 의존성을 찾아야 함함. 이때, 타입 힌트로 지정된 DBSession 자체가 “의존 가능(dependable)” 해야 함

  • 위 코드에서 의존성 주입
    • Depends(get_db)를 통해 get_db 함수의 반환값(데이터베이스 세션)을 db 매개변수로 주입.
    • read_items 함수는 주입된 db를 사용해 데이터를 처리.
    • Depends의 역할은 특정 함수(get_db)를 호출해 그 반환값을 다른 함수(read_items)에 주입. 주입되는 값은 FastAPI가 내부적으로 관리.
  • 요청 처리 흐름
    1. 클라이언트가 /items로 요청을 보냄.
    2. FastAPI는 Depends(get_db)를 호출하여 get_db의 반환값(db)을 준비.
    3. read_items 함수는 db를 매개변수로 받아 작업을 수행.
  • 의존성 주입의 장점
    • 코드 간결화: 의존성 생성 및 정리 코드를 직접 작성할 필요가 없으므로 코드를 단순화할 수 있음
    • 중앙 집중 관리: FastAPI가 의존성을 중앙에서 관리하므로, 재사용성과 유지보수성이 높아짐
    • 자동화: 의존성 주입과 관련된 복잡한 로직(생성, 전달, 정리 등)을 개발자가 신경 쓰지 않아도 됨

path operation decorator

의존성을 주입하는 두 번째 다른 방법은 함수의 인자로 전달하는 것이 아니라 PATH decorator에 dependencies 라는 파라미터로 전달을 해주는 것임. 어떤 API가 있는데 이 API는 로그인한 유저만 사용할 수 있다고 가정하자. 근데 로그인한 유저를 가지고 무언가 하지는 않음. 그런 경우에는 굳이 함수 인자로 적어두는 것보다는 decorator에 적어주면 조금 더 깔끔하게 적용할 수 있음. 이것은 취향차이라 dependency는 전부 함수 인자에 있는 게 좋을 거 같다고 생각하면 그렇게 해도 됨.

from fastapi import Depends

@app.get("/some_api", dependencies=[Depends(get_current_user)])
def some_api_required_auth():
    ...

Global Dependency

의존성을 주입하는 세 번째 방법은 global dependency이다. 우리가 만든 모든 API가 다 로그인이 필요하다고 가정하자. 그러면 매번 의존성을 주입하는 것이 코드가 중복 되는 일. 그런 경우에는 FastAPI app을 만들 때 아래와 같이 dependency를 넘겨주면 됨.

from fastapi import FastAPI, Depends

app = FastAPI(dependencies=[Depends(get_current_user)])
  • 나중에 app이 커지게 되면 Router 클래스를 이용해서 FastAPI app을 쪼개게 되는데 이 때 특정 router에만 dependency를 넣을 수도 있음.

Dependcy Generator

의존성을 주입하는 네 번째 방법은 generator를 사용하는 것이다.

  • iterable은 순회할 수 있는 객체 즉, iter 함수에 넣었을 때 iterator가 되는 객체이다. ex) list, dictionary
  • iterator는 한 번 순회할 수 있는 객체이다. next라는 함수를 통해서 계속 다음 원소를 가져올 수 있는 객체.

  • generator는 iterator의 한 종류인데, yield라는 문법을 이용해서 만든다. 순회가능한 객체를 만들고 싶은데 그 객체가 바로 만들어지는 게 아니라 next를 할 때마다 만들어지게(lazy evaluation) 하고 싶을 때 사용할 수 있다.
    def gen(n):
      for i in range(i):
          yield i
    
    >>> g = gen(10)
    >>> next(g)
    0
    >>> next(g)
    1
    >>> next(g)
    2
    

    위 코드에서 gen은 generator를 만드는 함수이고 g는 generator이다.

from fastapi import Depends

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

위 코드에서 yield를 한 번만 해주고 있다. 그 말은 get_db를 이용해서 generator를 만든 다음에 next를 한 번만 하면 db가 반환이 되고 한 번 더 next를 하면 StopIteration이 발생함. 이렇게 하나의 yield만 있는 generator를 dependency로 사용할 때 FastAPI는 자동으로 첫 번째 yield에 해당하는 결과를 종속성으로 전달해줌. finally는 try에서 어떤 일이 일어나든 무조건 수행되는데 즉, db를 사용한 다음엔 무조건 db connection이 끊어져야 하는데 generator 문법을 이용하면 return을 이용하는 것보다 훨씬 간편하게 코드를 짤 수 있다.

def get_A():
    try:
        yield make_A()
    finally:
        ...  # clean up A after return response

def get_B(a: A = Depends(get_A)):
    try:
        yield make_B(a)
    finally:
        ...  # clean up B after clean up A

def get_C(b: B = Depends(get_B)):
    try:
        yield make_C(b)
    finally:
        ...  # clean up C after clean up B

@app.get("/items")
def read_items(c: C = Depends(get_C)):
    ...

요청이 들어오면 요청의 인자에 get_C를 통해 종속성을 받아오란 코드가 가장 아래에 있음. get_C를 받아오기 위해선 B, A가 있어야 하기 때문에 get_A의 yield 부분이 먼저 실행됨. A가 생성이 되면 get_B의 yield가 실행되고 B가 생성이 되면 get_C의 yield가 실행되어 C가 생성됨. 그렇게 만들어진 C라는 종속성을 전달해주면 이 요청에 대한 처리가 시작이 됨. JSONResponse를 반환하든, 에러를 일으키든 요청이 끝났을 때, FastAPI는 순서대로 돌아가면서 만들었던 종속성들의 자원을 해제함. finally 문을 역순으로 돌아가면서 C-B-A 순서로 자원을 해제함.

Leave a Comment