FastAPI 리팩토링: Repository & Service Layer 패턴 적용

034179·2025년 3월 10일

배경

최근 진행 중인 FastAPI 프로젝트에서 데이터 처리 로직과 비즈니스 로직이 뒤섞여 유지보수성이 떨어지는 문제가 발생했다.

새로운 기능을 추가하는 과정에서 구조적인 개선이 필요하다는 점을 인지했고, 적절한 디자인 패턴을 적용하여 코드의 가독성과 유지보수성을 향상시키고자 한다.

이번 글에서는 리팩토링을 위해 학습한 Repository PatternService Layer Pattern에 대해 알아보고, FastAPI에서 어떻게 적용할 수 있는지 실용적인 코드 예제와 함께 설명하겠다.


Repository Pattern: 데이터 접근을 분리하자

데이터 접근(DB 쿼리)과 비즈니스 로직을 분리하는 패턴

Repository 패턴은 데이터베이스 접근 로직을 별도 파일로 분리하여 서비스 계층과의 결합도를 낮추는 방식이다. 이를 통해 데이터베이스 접근 방식의 변경이 필요한 경우, 서비스 로직을 수정하지 않고도 변경을 쉽게 적용할 수 있다.

  • 데이터베이스와의 직접적인 상호작용을 캡슐화하여 API나 서비스 계층이 직접 데이터베이스와 연결되지 않도록 함
  • 이를 위해 데이터 저장, 조회, 필터링 등의 작업을 Repository Layer에서 담당하며, 데이터베이스 쿼리(SQLAlchemy 같은 ORM 코드)를 직접 서비스 로직에 작성하지 않고, 별도의 Repository 클래스에서 처리
  • 적용 시 데이터 저장소 변경이 쉬워지며, 테스트 용이성 증가 (ex. Mock 데이터를 활용한 단위 테스트가 가능해짐.)

적용 전 예시 코드 (Bad Case)

API 엔드포인트에서 직접 DB 접근을 수행하는 경우

from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Item
from app.schemas import ItemCreate

app = FastAPI()

@app.post("/items/")
def create_item(item_data: ItemCreate, db: Session = Depends(get_db)):
    new_item = Item(**item_data.dict())
    db.add(new_item)
    db.commit()
    db.refresh(new_item)
    return new_item
  • 문제점
    • API 엔드포인트가 데이터베이스와 직접 연결
    • DB 변경 시 모든 API 코드를 수정해야 함
    • 비즈니스 로직과 데이터 접근 로직이 분리되지 않음

적용 후 코드 (Repository Pattern 적용)

Repository 클래스를 만들어 API 엔드포인트에서 DB 접근을 분리

from sqlalchemy.orm import Session
from app.models import Item
from app.schemas import ItemCreate

class ItemRepository:
    """아이템 관련 DB 작업을 처리하는 Repository"""
    
    def __init__(self, db: Session):
        self.db = db
    
    def create_item(self, item_data: ItemCreate):
        db_item = Item(**item_data.dict())
        self.db.add(db_item)
        self.db.commit()
        self.db.refresh(db_item)
        return db_item

    def get_item_by_id(self, item_id: int):
        return self.db.query(Item).filter(Item.id == item_id).first()

FastAPI 엔드포인트에서 Repository 사용

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.repositories.item_repository import ItemRepository
from app.schemas import ItemCreate

router = APIRouter()

@router.post("/items/")
def create_item(item_data: ItemCreate, db: Session = Depends(get_db)):
    repo = ItemRepository(db)
    return repo.create_item(item_data)
  • API 엔드포인트가 데이터베이스에 직접 접근하지 않음
  • 데이터베이스 변경이 쉬워짐 (Repository만 수정하면 됨)
  • 테스트가 용이 (Mock Repository를 활용하여 테스트 가능)

Service Layer Pattern: 비즈니스 로직을 분리하자

비즈니스 로직을 한 곳에 모아 관리하는 패턴

  • 데이터베이스와의 직접적인 상호작용을 Repository Layer에 맡기고, 비즈니스 로직은 Service Layer에서 담당.
  • Service Layer는 여러 개의 Repository를 조합하여 하나의 기능을 처리하는 역할을 한다.
  • API 엔드포인트에서는 비즈니스 로직을 직접 처리하지 않고, Service Layer를 호출하여 기능을 수행.

적용 시 장점

  • 코드 재사용성 증가 → 여러 엔드포인트에서 같은 로직을 반복하지 않도록 함.
  • 비즈니스 로직과 데이터 접근을 분리 → Repository는 DB 접근만 담당하고, Service Layer는 비즈니스 규칙을 관리.
  • 유지보수성과 확장성 증가 → 변경 사항을 하나의 Service Layer에서만 수정하면 됨.

적용 전 예시 코드 (Bad Case)

API 엔드포인트에서 직접 비즈니스 로직을 처리하는 경우

  • 비즈니스 로직은 프로젝트마다 다르게 적용되며 복잡할 수 있지만, 이번 예시에서는 단순하게 "같은 이름의 아이템이 존재하면 생성할 수 없다"는 조건을 적용하여 설명하겠다.
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Item
from app.schemas import ItemCreate

app = FastAPI()

@app.post("/items/")
def create_item(item_data: ItemCreate, db: Session = Depends(get_db)):
    """API 엔드포인트에서 직접 비즈니스 로직을 처리"""

    # 같은 이름의 아이템이 존재하면 생성 불가 (비즈니스 로직)
    existing_item = db.query(Item).filter(Item.name == item_data.name).first()
    if existing_item:
        raise HTTPException(status_code=400, detail="이미 존재하는 아이템입니다.")

    new_item = Item(**item_data.dict())
    db.add(new_item)
    db.commit()
    db.refresh(new_item)
    
    return new_item

문제점

  • 비즈니스 로직이 API 엔드포인트 내부에 존재하여 코드 중복이 발생할 가능성이 높음
  • API 엔드포인트가 데이터 검증, 예외 처리, DB 접근까지 담당하여 역할이 모호해짐
  • 테스트 작성이 어려움 (비즈니스 로직과 API가 결합되어 단위 테스트가 힘들어짐)

적용 후 코드 (Service Layer Pattern 적용)

Service Layer를 추가하여 API에서 비즈니스 로직을 분리

from sqlalchemy.orm import Session
from app.repositories.item_repository import ItemRepository
from app.schemas import ItemCreate
from fastapi import HTTPException

class ItemService:
    """비즈니스 로직을 담당하는 Service Layer"""

    def __init__(self, db: Session):
        self.db = db
        self.repo = ItemRepository(db)
    
    def create_item(self, item_data: ItemCreate):
        """비즈니스 로직: 같은 이름의 아이템이 존재하면 생성 불가"""
        existing_item = self.repo.get_item_by_name(item_data.name)
        if existing_item:
            raise HTTPException(status_code=400, detail="이미 존재하는 아이템입니다.")
        return self.repo.create_item(item_data)

FastAPI 엔드포인트에서 Service Layer 사용

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.services.item_service import ItemService
from app.schemas import ItemCreate

router = APIRouter()

@router.post("/items/")
def create_item(item_data: ItemCreate, db: Session = Depends(get_db)):
    service = ItemService(db)
    return service.create_item(item_data)
  • API 엔드포인트가 비즈니스 로직을 직접 처리하지 않음
  • 서비스 로직과 데이터 로직이 분리되어 유지보수성 증가
  • 여러 Repository를 조합하여 복잡한 로직도 깔끔하게 정리 가능

결론

이번 글에서는 Repository PatternService Layer Pattern을 적용하여 FastAPI 프로젝트의 유지보수성과 확장성을 높이는 방법을 살펴보았다.

  • Repository Pattern을 적용하면 데이터베이스 접근 로직을 분리하여 API 엔드포인트와의 결합도를 낮출 수 있다. 이를 통해 데이터베이스 변경이 용이해지고, 테스트 작성이 쉬워지는 장점이 있다.
  • Service Layer Pattern을 적용하면 비즈니스 로직을 API 엔드포인트에서 분리할 수 있다. 이를 통해 코드 중복을 줄이고, 비즈니스 로직을 체계적으로 관리할 수 있다.

이 두 가지 패턴을 활용하면 API 엔드포인트가 단순해지고, 데이터 처리, 비즈니스 로직, API 요청 처리를 각각의 계층에서 관리할 수 있다.

결과적으로 코드의 가독성이 향상되고, 새로운 기능을 추가하거나 수정할 때 변경해야 할 범위가 줄어들어 유지보수가 쉬워진다.
디자인 패턴을 적용하는 것은 프로젝트의 복잡도에 따라 선택적으로 고려해야 하지만, 코드의 역할을 명확히 분리하는 것은 장기적인 관점에서 개발 생산성을 높이는 중요한 요소이다.

과거에는 "디자인 패턴이란 프로그램을 설계할 때 발생하는 문제를 객체 간의 상호 관계를 이용해 해결할 수 있도록 정리한 ‘규약’이다." 라는 정의가 명확하게 와닿지 않았다.
하지만 프로젝트를 개발하면서 코드에 대한 답답함과 유지보수가 어렵다는 막연한 불편함을 느꼈다. 무엇이 문제인지 명확히 정의하기 어려웠고 어떻게 개선해야 할지도 막막했는데, 디자인 패턴을 학습하며 고민들을 해결할 수 있었다. 역시 개발자 부지런히 공부해야….

1개의 댓글

comment-user-thumbnail
2025년 7월 18일

감사합니다 !

답글 달기