23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
FastAPI는 빠르고 효율적인 웹 프레임워크로, Python을 사용해 REST API를 구축하는 데 매우 유용합니다.
이 프레임워크는 또한 의존성 주입(Dependency Injection)을 쉽게 구현할 수 있는 강력한 기능을 제공하여, 더 유연하고 확장 가능한 서비스 구조를 설계하는 데 도움을 줍니다.
이번 글에서는 의존성 주입이 무엇인지, 그리고 FastAPI에서 이를 어떻게 적용할 수 있는지 살펴보겠습니다.
SOLID 원칙은 객체지향 설계의 다섯 가지 주요 원칙을 의미하며, 소프트웨어 개발에서 유연성과 유지보수성을 높이는 데 중점을 둡니다.
그중에서도 DIP(Dependency Inversion Principle, 의존성 역전 원칙)은 실무에서 특히 많이 활용되는 원칙 중 하나입니다.
DIP는 고수준 모듈(비즈니스 로직)이 저수준 모듈(구체적인 구현체)에 의존하지 않고, 추상화에 의존하도록 설계함으로써 시스템의 결합도를 낮추고 유연성을 높이는 것을 목표로 합니다.
아래는 이 구조를 시각적으로 표현한 다이어그램입니다.
다이어그램에서 볼 수 있듯이, 고수준 모듈은 추상화된 인터페이스에만 의존하고, 구체적인 구현체는 추상화된 인터페이스를 구현하는 방식으로 설계됩니다.
DIP는 쉽게 말해 구체적인 구현 대신 추상화에 의존함으로써, 코드의 수정 없이도 구현체를 교체할 수 있는 구조를 만드는 것을 의미합니다.
이로 인해 비즈니스 로직은 데이터 접근 방식의 변화에 영향을 받지 않으며, 시스템의 유지보수성과 확장성을 크게 개선할 수 있습니다.
의존성 주입(Dependency Injection, DI)은 의존성 역전 원칙(DIP)을 구현하는 방법 중 하나입니다.
DI는 객체가 직접적으로 다른 객체를 생성하거나 참조하지 않고, 외부에서 필요한 객체를 주입받도록 함으로써 DIP를 실현합니다.
이를 통해 시스템의 결합도를 낮추고, 구현체를 쉽게 교체하며, 테스트 용이성을 높일 수 있습니다.
이번 글에서는 이러한 의존성 주입을 FastAPI에서 어떻게 활용할 수 있는지, 구체적인 예시를 통해 살펴보겠습니다.
의존성 주입은 프로그램의 각 구성 요소가 서로 강하게 결합되는 것을 피하기 위해 사용하는 디자인 패턴입니다.
보통 서비스 로직은 데이터 접근 로직과 같은 특정 모듈에 직접 의존하게 되는데, 이는 시스템의 유지보수성을 떨어뜨리고 확장 가능성을 제한합니다.
이를 해결하기 위해 의존성 주입은 구체적인 구현체 대신 추상화된 인터페이스에 의존하도록 하여 유연성을 높입니다.
간단히 말해, 의존성 주입은 특정 객체가 직접적으로 다른 객체를 생성하거나 의존하지 않도록 하고, 외부에서 필요한 객체를 주입함으로써 시스템의 결합도를 낮추는 방법입니다.
이를 통해 구현체 변경이 쉽고, 테스트 시 모의 객체(mock)를 주입하여 독립적인 테스트를 수행할 수 있습니다.
FastAPI에서는 의존성 주입을 사용해 비즈니스 로직과 데이터 접근 로직 간의 결합도를 낮추고, 유연하게 서비스 구조를 변경할 수 있는 장점을 제공합니다.
만약 데이터베이스를 MySQL에서 MongoDB로 변경하고 싶다면, 의존성 주입을 사용하여 서비스 로직을 변경하지 않고도 쉽게 교체할 수 있습니다.
FastAPI의 Depends() 는 이러한 의존성 주입을 매우 간단하고 효과적으로 구현할 수 있게 해줍니다.
비교 항목 | 의존성 주입을 사용하지 않는 경우 | 의존성 주입을 사용하는 경우 |
|---|---|---|
의존성 | 비즈니스 로직이 특정 구현체에 직접 의존 | 비즈니스 로직이 인터페이스에만 의존 |
확장성 | 데이터베이스 변경 시 서비스 코드 수정 필요 | 데이터베이스 변경 시 의존성 주입만 수정 |
테스트 용이성 | 모의 객체를 사용하기 어려워 테스트가 복잡함 | 모의 객체를 쉽게 주입 가능, 독립적 테스트 가능 |
결합도 | 높음 (구현체와 강하게 결합) | 낮음 (인터페이스를 통한 느슨한 결합) |
유지보수성 | 구현체 변경 시 서비스 로직 수정 필요 | 구현체 변경 시 주입된 인스턴스만 변경하면 됨 |
이제 FastAPI에서 의존성 주입을 어떻게 사용하는지 구체적인 예시를 통해 알아보겠습니다.
우리는 사용자 정보를 조회하는 간단한 API를 만들어 보겠습니다.
이 API는 데이터베이스 접근을 위해 UserRepository를 사용하며, 이 저장소(repository)는 의존성 주입을 통해 비즈니스 로직에 주입됩니다.
먼저 데이터 접근 로직의 추상화된 인터페이스를 정의합니다.
# 추상화된 인터페이스 정의
class UserRepositoryInterface:
def get_user(self, user_id: int):
pass이 인터페이스는 get_user 메서드를 포함하고 있으며, 실제 구현체에서 이 메서드를 구현하게 됩니다.
이제 MySQL 데이터베이스에 접근하는 구체적인 저장소를 구현합니다.
# MySQL 저장소 구현
class MySQLUserRepository(UserRepositoryInterface):
def get_user(self, user_id: int):
# MySQL 데이터베이스에서 사용자 조회
return {"id": user_id, "name": "MySQL User"}MySQLUserRepository는 UserRepositoryInterface를 구현하며, MySQL 데이터베이스에서 사용자 정보를 조회하는 로직을 포함하고 있습니다.
비즈니스 로직에서는 UserRepositoryInterface에 의존하게 하고, 구체적인 구현체는 의존성 주입을 통해 런타임에 전달합니다.
from fastapi import Depends
def get_user_service(repo: UserRepositoryInterface = Depends(MySQLUserRepository)):
def get_user(user_id: int):
return repo.get_user(user_id)
return get_user여기서 get_user_service 함수는 UserRepositoryInterface에 의존하며, Depends()를 사용하여 구체적인 구현체(MySQLUserRepository)를 주입받습니다.
이로써 서비스 로직은 구체적인 저장소에 대해 알 필요가 없으며, 오직 추상화된 인터페이스에만 의존하게 됩니다.
이제 FastAPI 엔드포인트를 정의하여 사용자 정보를 조회할 수 있습니다.
from fastapi import FastAPI
app = FastAPI()
@app.get("/user/{user_id}")
async def get_user(user_id: int, service=Depends(get_user_service)):
return service(user_id)get_user 엔드포인트는 get_user_service를 의존성으로 주입받고, 주어진 user_id로 사용자 정보를 조회합니다.
만약 MySQL에서 MongoDB로 데이터베이스를 변경하고 싶다면, 단순히 Depends() 안의 구현체를 MongoDBUserRepository로 변경하기만 하면 됩니다.
# MongoDB 저장소로 교체
def get_user_service(repo: UserRepositoryInterface = Depends(MongoDBUserRepository)):
# 이제 MongoDB를 사용하여 사용자 정보를 조회합니다.
...유연성: 데이터 접근 방식이 변경되더라도 서비스 로직을 수정할 필요가 없습니다.
낮은 결합도: 비즈니스 로직이 특정 구현체에 의존하지 않으므로, 더 유연하고 확장 가능한 구조를 가집니다.
테스트 용이성: 테스트 시 모의 객체(mock)를 주입하여 독립적이고 신뢰성 있는 테스트가 가능합니다.
의존성 관리: DI를 적용하면 구체적인 구현체가 아닌 인터페이스에 의존하도록 하여, 코드의 결합도를 낮출 수 있습니다.
서비스 호출: FastAPI의Depends()를 활용하여 유연하게 구현체를 주입받을 수 있습니다.
테스트 용이성: DI를 사용하면 Mock 객체를 주입할 수 있어, 독립적인 단위 테스트가 가능해집니다.
확장성: 데이터베이스를 교체할 때 서비스 코드를 수정하지 않고 의존성 주입만 변경하면 됩니다.
FastAPI 라우터 설정: Depends()를 사용하여 라우터에서 동적으로 의존성을 주입받아 코드의 유연성과 재사용성을 극대화합니다.
의존성 역전 원칙(DIP)은 고수준 모듈이 저수준 모듈의 구체적인 구현에 의존하지 않고 추상화에 의존하도록 설계하여 유연성과 확장성을 높이는 데 중점을 둡니다.
이를 구현하는 방법 중 하나인 의존성 주입(DI)은 FastAPI에서 매우 효과적으로 사용될 수 있으며, 이를 통해 구현체 변경이 용이한 구조를 만들 수 있습니다.
의존성 주입은 FastAPI를 사용하여 유연하고 확장 가능한 애플리케이션을 설계하는 데 있어 매우 중요한 디자인 패턴입니다.
FastAPI의 Depends()를 활용하면 구현체를 쉽게 교체하고 테스트할 수 있는 구조를 만들 수 있습니다.
이를 통해 비즈니스 로직은 구체적인 데이터 접근 방식과 독립적으로 유지되며, 시스템의 유지보수성과 확장성이 크게 향상됩니다.
FastAPI에서 의존성 주입을 활용하여 더 나은 설계를 해보세요! 여러분의 프로젝트가 더 견고하고 유연해질 것입니다.
graph TD
A[FastAPI Application] -->|의존| B[UserRepositoryInterface]
B -.->|구현| C[MySQLUserRepository]
B -.->|구현| D[MongoDBUserRepository]
E[get_user_service] -->|의존| B
F["/user/{user_id} Endpoint"] -->|호출| E
G[Depends] -->|런타임 의존성 주입| C
G -->|런타임 의존성 주입| D
H[Runtime Injection] -.-> Gfrom sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def create_tables():
# Import models here to ensure they're registered with Base
from app.models import User # Make sure this import exists
Base.metadata.create_all(bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()from sqlalchemy import Column, Integer, String
from app.database import Base
from pydantic import BaseModel
# SQLAlchemy Model
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
email = Column(String, unique=True, index=True)
full_name = Column(String)
hashed_password = Column(String)
# Pydantic Models
class UserBase(BaseModel):
username: str
email: str
full_name: str
class UserCreate(UserBase):
password: str
class UserResponse(UserBase):
id: int
class Config:
from_attributes = Truefrom sqlalchemy.orm import Session
from typing import List, Optional
from app.models import User, UserCreate
# Repository Interface
class UserRepositoryInterface:
def get_user_by_username(self, db: Session, username: str) -> Optional[User]:
pass
def get_user_by_email(self, db: Session, email: str) -> Optional[User]:
pass
def get_users(self, db: Session, skip: int = 0, limit: int = 10) -> List[User]:
pass
def create_user(self, db: Session, user: UserCreate) -> User:
pass
# UserRepository 구현체
class UserRepository(UserRepositoryInterface):
def get_user_by_username(self, db: Session, username: str) -> Optional[User]:
return db.query(User).filter(User.username == username).first()
def get_user_by_email(self, db: Session, email: str) -> Optional[User]:
return db.query(User).filter(User.email == email).first()
def get_users(self, db: Session, skip: int = 0, limit: int = 10) -> List[User]:
return db.query(User).offset(skip).limit(limit).all()
def create_user(self, db: Session, user: UserCreate) -> User:
db_user = User(
username=user.username,
email=user.email,
full_name=user.full_name
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_userfrom sqlalchemy.orm import Session
from app.user_repository import UserRepositoryInterface
from app.models import UserCreate, User
from fastapi import HTTPException
from typing import List
class UserService:
def __init__(self, repository: UserRepositoryInterface):
self.repository = repository
def create_user(self, db: Session, user: UserCreate) -> User:
db_user = self.repository.get_user_by_email(db, user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return self.repository.create_user(db, user)
def get_users(self, db: Session, skip: int = 0, limit: int = 10) -> List[User]:
return self.repository.get_users(db, skip, limit)
def get_user_by_username(self, db: Session, username: str) -> User:
user = self.repository.get_user_by_username(db, username)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return userfrom fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from app.database import get_db, create_tables
from app.models import UserCreate, UserResponse
from app.user_service import UserService
from app.user_repository import UserRepository
from typing import List
# Create tables before the app starts
create_tables()
app = FastAPI()
# Repository와 Service를 의존성 주입으로 설정
def get_user_service(repo=Depends(UserRepository)):
return UserService(repo)
# Create User API Endpoint
@app.post("/users/", response_model=UserResponse)
def create_user_endpoint(
user: UserCreate,
db: Session = Depends(get_db),
service: UserService = Depends(get_user_service)
):
return service.create_user(db, user)
# Get Users API Endpoint
@app.get("/users/", response_model=List[UserResponse])
def read_users(
skip: int = 0,
limit: int = 10,
db: Session = Depends(get_db),
service: UserService = Depends(get_user_service)
):
return service.get_users(db, skip, limit)
# Get User by Username API Endpoint
@app.get("/users/{username}", response_model=UserResponse)
def read_user(
username: str,
db: Session = Depends(get_db),
service: UserService = Depends(get_user_service)
):
return service.get_user_by_username(db, username)
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.