디자인 패턴으로 모듈이나 컴포넌트의 의존성을 런타임에 주입할 수 있도록 한다. 의존성 주입의 궁극적인 목적으로는 "관심사 분리"에 있다
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)
이런 흐름을 역전시키는 원칙이 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
클래스의 인스턴스 database
가 UserService
의 인자로 전달된다.
이를 통해 UserService
는 데이터베이스 객체를 사용하여 데이터베이스에 연결하고 사용자를 검색할 수 있다.
또한 이렇게 구현하면 UserService
클래스를 수정하지 않고 데이터베이스 클래스의 구현을 변경하거나 다른 데이터베이스를 사용할 수 있다.
UserService
클래스는 데이터베이스 클래스가 연결을 만드는 방법을 알 필요가 없으며 connect()
메서드만 존재한다.
이런식으로 구현하면 UserService
클래스가 Database
클래스와 분리되어 보다 유연하고 쉽게 테스트할 수 있다.
Django, DRF에서는 특별한 내장 DI 시스템을 가지고 있지 않고 개발자가 직접 구현을 해주어야 한다.
Django에서 의존성 주입을 구현하는 일반적인 방법들은 다음과 같다.
fastAPI에서는 Depends를 이용하여 의존성 주입을 사용한다.
Depends는 의존성 주입 외에도 다양한 이유로 사용한다.
인증, 권한 부여
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}
공통 기능 최적화(코드 최적화)
추상화
데이터 유효성 검사
# 데코레이터를 이용하여 사용한다.
# 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
의존성 주입
의존을 의존할 수 있다.(원하는 만큼 의존에 의존에 의존을 더할 수 있다.)
특정 값을 리턴하지 않지만 실행은 해야하는 경우 데코레이터에 리스트 형태로도 작성할 수 있다.
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}