Dependency Injection 이해하기(with FastAPI)

hodu·2023년 1월 23일
0

클린아키텍쳐

목록 보기
2/5

Dependency Injection

의존성 주입이란?

디자인 패턴으로 모듈이나 컴포넌트의 의존성을 런타임에 주입할 수 있도록 한다. 의존성 주입의 궁극적인 목적으로는 "관심사 분리"에 있다

조금 더 쉽게 말해보자면...

A라는 코드는 반드시 B라는 코드를 실행해야만 동작이 된다.
이런 관계에서는 A 👉 B 흐름으로 의존 관계가 형성된다.
이런 코드가 현실 세계에서는 굉장히 많을 수도 있다.

하지만 이 코드를 지양해야 하는 이유는 B코드가 수정되면 반드시 A에 영향을 주게된다.

class Database:
    def __init__(self):
        self._connection = None
    
    def connect(self):
        self._connection = "DB가 연결되었습니다."
        print(self._connection)

    def get_connection(self):
        return self._connection
    
class UserService:
    def __init__(self):
        self._db = Database()  # UserService에서 직접 Database 객체를 생성
    
    def get_user(self, user_id: int) -> dict:
        self._db.connect()  # 매번 연결을 생성
        return {'user_id': user_id, 'name': '안뇽'}

user_service = UserService()
user = user_service.get_user(1)
print(user)
  • 이 코드에서는 UserService가 Database객체를 직접 생성하고 있음
  • Database 객체의 생명주기가 UserService에 종속됨
  • get_user 메서드를 호출할 때마다 새로운 데이터베이스 연결을 생성해줌
  • UserService와 관련된 테스트코드를 작성해주려면 직접 Database를 연결시켜 주어야 함.

이런 흐름을 역전시키는 원칙이 SOLID 원칙 중 DIP(Dependency Injection Principle)이다.

  • 상위 모듈은 하위 모듈에 의존해서는 안된다. 둘다 추상화에 의존해야 한다.
  • 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.

이 두 가지 원칙을 지키면 두 코드 모두 의존하지 않고, 영향을 받지 않게된다.

그러면 의존성 주입은 무엇일까?

하드코딩 된 A코드와 B코드 사이의 관계를 인자로 전달하여 종속성을 관리하는 것을 의미한다.

아까 코드를 수정해보자.

class Database:
    def __init__(self):
        self._connection = None
    
    def connect(self):
        self._connection = "DB가 연결되었습니다."
        print(self._connection)

    def get_connection(self):
        return self._connection
    
class UserService:
    def __init__(self, db: Database):
        self._db = db
    
    def get_user(self, user_id: int) -> dict:
        self._db.connect()
        return {'user_id': user_id, 'name': '안뇽'}

database = Database()
user_service = UserService(database)
user = user_service.get_user(1)
print(user)

마지막줄을 보면

database = Database()
user_service = UserService(database)

Database 클래스의 인스턴스 databaseUserService의 인자로 전달된다.
이를 통해 UserService는 데이터베이스 객체를 사용하여 데이터베이스에 연결하고 사용자를 검색할 수 있다.

또한 이렇게 구현하면 UserService 클래스를 수정하지 않고 데이터베이스 클래스의 구현을 변경하거나 다른 데이터베이스를 사용할 수 있다.

UserService 클래스는 데이터베이스 클래스가 연결을 만드는 방법을 알 필요가 없으며 connect() 메서드만 존재한다.
이런식으로 구현하면 UserService 클래스가 Database 클래스와 분리되어 보다 유연하고 쉽게 테스트할 수 있다.

Python의 다양한 DI 툴

Django, DRF에서는 특별한 내장 DI 시스템을 가지고 있지 않고 개발자가 직접 구현을 해주어야 한다.

Django에서 의존성 주입을 구현하는 일반적인 방법들은 다음과 같다.

  • 함수 기반 뷰에서 인자를 통한 주입
  • 클래스 기반 뷰에서 생성자나 메서드를 통한 주입
  • 미들웨어를 사용한 주입
  • 서드파티 라이브러리 사용 (예: django-injector)

fastAPI에서는 Depends를 이용하여 의존성 주입을 사용한다.

fastapi의 Depends의 다양한 사용법

Depends는 의존성 주입 외에도 다양한 이유로 사용한다.

  1. 인증, 권한 부여
    OAuth2PasswordBearer를 이용하여 유저를 인증할 수 있다.

    ...
    oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
    
    async def get_current_user(token: str = Depends(oauth2_scheme)):
        user = authenticate_user(token)
    	...(생략)...
        return user
    
    @app.get("/items/{item_id}")
    async def read_item(item_id: int, current_user=Depends(get_current_user)):
        return {"item_id": item_id, "owner": current_user.username}
  2. 공통 기능 최적화(코드 최적화)

  3. 추상화

  4. 데이터 유효성 검사

    # 데코레이터를 이용하여 사용한다.
    # Depends를 이용하여 유효성 검사가 가능하다.
    from fastapi import FastAPI, Depends, Query
    
    app = FastAPI()
    
    @app.get("/items/{item_id}")
    async def read_item(item_id: int, limit: int = Depends(validate_query_param)):
        return {"item_id": item_id, "limit": limit}
    
    async def validate_query_param(limit: int = Query(..., gt=0)):
        return limit
  5. 의존성 주입

추가 정보

  • 의존을 의존할 수 있다.(원하는 만큼 의존에 의존에 의존을 더할 수 있다.)

  • 특정 값을 리턴하지 않지만 실행은 해야하는 경우 데코레이터에 리스트 형태로도 작성할 수 있다.

    class Item(BaseModel):
        name: str
        description: str
    
    def validate_item(item: Item):
        if item.name != "valid":
            raise ValueError("Invalid item")
        return item
    
    @app.post("/items/", dependencies=[Depends(validate_item)])
    async def create_item(item: Item):
        return {"item": item}
    

참고 링크

profile
안녕 세계!

0개의 댓글