데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      FastAPI에서의 의존성 주입: 유연하고 확장 가능한 서비스 구조 만들기

      broccoli 25.02.26
      4,917 12 1
      DEVOTEE 요약
      FastAPI는 Python을 사용하여 REST API를 구축하는 데 매우 유용한 웹 프레임워크로, 의존성 주입을 쉽게 구현할 수 있는 기능을 제공합니다. 의존성 주입은 프로그램의 결합도를 낮추고 유연성을 높여, 쉽게 구현체를 교체하고 독립적인 테스트를 가능하게 해주는 중요한 디자인 패턴입니다. 이러한 원리를 바탕으로, FastAPI에서 의존성 주입을 활용하여 더 유연하고 확장 가능한 애플리케이션을 설계할 수 있으며, FastAPI의 Depends()를 통해 구체적인 구현체를 쉽게 관리할 수 있습니다.
      DEVOTEE 추천 블로그

      시작하기

      FastAPI는 빠르고 효율적인 웹 프레임워크로, Python을 사용해 REST API를 구축하는 데 매우 유용합니다.

      이 프레임워크는 또한 의존성 주입(Dependency Injection)을 쉽게 구현할 수 있는 강력한 기능을 제공하여, 더 유연하고 확장 가능한 서비스 구조를 설계하는 데 도움을 줍니다.

      이번 글에서는 의존성 주입이 무엇인지, 그리고 FastAPI에서 이를 어떻게 적용할 수 있는지 살펴보겠습니다.


      SOLID 원칙과 의존성 역전 원칙 (DIP)

      SOLID 원칙은 객체지향 설계의 다섯 가지 주요 원칙을 의미하며, 소프트웨어 개발에서 유연성과 유지보수성을 높이는 데 중점을 둡니다.

      그중에서도 DIP(Dependency Inversion Principle, 의존성 역전 원칙)은 실무에서 특히 많이 활용되는 원칙 중 하나입니다.

      DIP는 고수준 모듈(비즈니스 로직)이 저수준 모듈(구체적인 구현체)에 의존하지 않고, 추상화에 의존하도록 설계함으로써 시스템의 결합도를 낮추고 유연성을 높이는 것을 목표로 합니다.

      아래는 이 구조를 시각적으로 표현한 다이어그램입니다.

      image.png

      다이어그램에서 볼 수 있듯이, 고수준 모듈은 추상화된 인터페이스에만 의존하고, 구체적인 구현체는 추상화된 인터페이스를 구현하는 방식으로 설계됩니다.

      DIP는 쉽게 말해 구체적인 구현 대신 추상화에 의존함으로써, 코드의 수정 없이도 구현체를 교체할 수 있는 구조를 만드는 것을 의미합니다.

      이로 인해 비즈니스 로직은 데이터 접근 방식의 변화에 영향을 받지 않으며, 시스템의 유지보수성과 확장성을 크게 개선할 수 있습니다.


      DIP와 의존성 주입 (DI)

      의존성 주입(Dependency Injection, DI)은 의존성 역전 원칙(DIP)을 구현하는 방법 중 하나입니다.

      DI는 객체가 직접적으로 다른 객체를 생성하거나 참조하지 않고, 외부에서 필요한 객체를 주입받도록 함으로써 DIP를 실현합니다.

      이를 통해 시스템의 결합도를 낮추고, 구현체를 쉽게 교체하며, 테스트 용이성을 높일 수 있습니다.

      이번 글에서는 이러한 의존성 주입을 FastAPI에서 어떻게 활용할 수 있는지, 구체적인 예시를 통해 살펴보겠습니다.


      의존성 주입(Dependency Injection)이란?

      의존성 주입은 프로그램의 각 구성 요소가 서로 강하게 결합되는 것을 피하기 위해 사용하는 디자인 패턴입니다.

      보통 서비스 로직은 데이터 접근 로직과 같은 특정 모듈에 직접 의존하게 되는데, 이는 시스템의 유지보수성을 떨어뜨리고 확장 가능성을 제한합니다.

      이를 해결하기 위해 의존성 주입은 구체적인 구현체 대신 추상화된 인터페이스에 의존하도록 하여 유연성을 높입니다.

      간단히 말해, 의존성 주입은 특정 객체가 직접적으로 다른 객체를 생성하거나 의존하지 않도록 하고, 외부에서 필요한 객체를 주입함으로써 시스템의 결합도를 낮추는 방법입니다.

      이를 통해 구현체 변경이 쉽고, 테스트 시 모의 객체(mock)를 주입하여 독립적인 테스트를 수행할 수 있습니다.


      FastAPI에서 의존성 주입의 필요성

      FastAPI에서는 의존성 주입을 사용해 비즈니스 로직과 데이터 접근 로직 간의 결합도를 낮추고, 유연하게 서비스 구조를 변경할 수 있는 장점을 제공합니다.

      만약 데이터베이스를 MySQL에서 MongoDB로 변경하고 싶다면, 의존성 주입을 사용하여 서비스 로직을 변경하지 않고도 쉽게 교체할 수 있습니다.

      FastAPI의 Depends() 는 이러한 의존성 주입을 매우 간단하고 효과적으로 구현할 수 있게 해줍니다.

      비교 항목

      의존성 주입을 사용하지 않는 경우

      의존성 주입을 사용하는 경우

      의존성

      비즈니스 로직이 특정 구현체에 직접 의존

      비즈니스 로직이 인터페이스에만 의존

      확장성

      데이터베이스 변경 시 서비스 코드 수정 필요

      데이터베이스 변경 시 의존성 주입만 수정

      테스트 용이성

      모의 객체를 사용하기 어려워 테스트가 복잡함

      모의 객체를 쉽게 주입 가능, 독립적 테스트 가능

      결합도

      높음 (구현체와 강하게 결합)

      낮음 (인터페이스를 통한 느슨한 결합)

      유지보수성

      구현체 변경 시 서비스 로직 수정 필요

      구현체 변경 시 주입된 인스턴스만 변경하면 됨


      FastAPI에서 의존성 주입 사용하기

      이제 FastAPI에서 의존성 주입을 어떻게 사용하는지 구체적인 예시를 통해 알아보겠습니다.

      예시: 사용자 정보 조회 API

      우리는 사용자 정보를 조회하는 간단한 API를 만들어 보겠습니다.

      이 API는 데이터베이스 접근을 위해 UserRepository를 사용하며, 이 저장소(repository)는 의존성 주입을 통해 비즈니스 로직에 주입됩니다.

      Step 1: 추상화된 인터페이스 정의하기

      먼저 데이터 접근 로직의 추상화된 인터페이스를 정의합니다.

      # 추상화된 인터페이스 정의
      class UserRepositoryInterface:
          def get_user(self, user_id: int):
              pass

      이 인터페이스는 get_user 메서드를 포함하고 있으며, 실제 구현체에서 이 메서드를 구현하게 됩니다.

      Step 2: 구체적인 저장소 구현하기

      이제 MySQL 데이터베이스에 접근하는 구체적인 저장소를 구현합니다.

      # MySQL 저장소 구현
      class MySQLUserRepository(UserRepositoryInterface):
          def get_user(self, user_id: int):
              # MySQL 데이터베이스에서 사용자 조회
              return {"id": user_id, "name": "MySQL User"}

      MySQLUserRepository는 UserRepositoryInterface를 구현하며, MySQL 데이터베이스에서 사용자 정보를 조회하는 로직을 포함하고 있습니다.

      Step 3: 비즈니스 로직에 의존성 주입하기

      비즈니스 로직에서는 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)를 주입받습니다.

      이로써 서비스 로직은 구체적인 저장소에 대해 알 필요가 없으며, 오직 추상화된 인터페이스에만 의존하게 됩니다.

      Step 4: FastAPI 엔드포인트 정의하기

      이제 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)를 주입하여 독립적이고 신뢰성 있는 테스트가 가능합니다.


      비교 코드

      image.pngimage.png

      • 의존성 관리: DI를 적용하면 구체적인 구현체가 아닌 인터페이스에 의존하도록 하여, 코드의 결합도를 낮출 수 있습니다.

      • 서비스 호출: FastAPI의Depends()를 활용하여 유연하게 구현체를 주입받을 수 있습니다.

      • 테스트 용이성: DI를 사용하면 Mock 객체를 주입할 수 있어, 독립적인 단위 테스트가 가능해집니다.

      • 확장성: 데이터베이스를 교체할 때 서비스 코드를 수정하지 않고 의존성 주입만 변경하면 됩니다.

      • FastAPI 라우터 설정: Depends()를 사용하여 라우터에서 동적으로 의존성을 주입받아 코드의 유연성과 재사용성을 극대화합니다.


      결론

      의존성 역전 원칙(DIP)은 고수준 모듈이 저수준 모듈의 구체적인 구현에 의존하지 않고 추상화에 의존하도록 설계하여 유연성과 확장성을 높이는 데 중점을 둡니다.

      이를 구현하는 방법 중 하나인 의존성 주입(DI)은 FastAPI에서 매우 효과적으로 사용될 수 있으며, 이를 통해 구현체 변경이 용이한 구조를 만들 수 있습니다.

      의존성 주입은 FastAPI를 사용하여 유연하고 확장 가능한 애플리케이션을 설계하는 데 있어 매우 중요한 디자인 패턴입니다.

      FastAPI의 Depends()를 활용하면 구현체를 쉽게 교체하고 테스트할 수 있는 구조를 만들 수 있습니다.

      이를 통해 비즈니스 로직은 구체적인 데이터 접근 방식과 독립적으로 유지되며, 시스템의 유지보수성과 확장성이 크게 향상됩니다.

      FastAPI에서 의존성 주입을 활용하여 더 나은 설계를 해보세요! 여러분의 프로젝트가 더 견고하고 유연해질 것입니다.


      부록

      1. DIP 예시 다이어그램의 mermaid code

      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] -.-> G

      2. DI 적용 코드 예시

      app/database.py

      from 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()

      app/models.py

      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 = True

      app/user_repository.py

      from 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_user

      app/user_service.py

      from 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 user

      main.py

      from 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)

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      broccoli 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기