최근들어 FastAPI에 대해 학습하고 있다. FastAPI를 학습하다 보면 다음과 같은 구조를 자주 보게 된다.
router
schema
service
repository
model
Plain Text
복사
대략적으로 이 구조를 왜 사용하는지 알고는 있지만 완벽하게 이해하지 못하고 있고 각 계층이 무슨 역할을 하는지 궁금하여 정리하고자 이 글을 작성하게 되었다.
많은 자료에서 다음과 같은 흐름으로 FastAPI 를 구현한다.
이 글에서 설명하는 구조는 FastAPI 뿐만 아니라 많은 백엔드 및 개발 과정에서 적용하는 구조인 것 같은데, 현재 FastAPI 학습 중에 있어 FastAPI 구조에 대한 언급이 많다.
Router -> Service -> Repository -> Model
Plain Text
복사
하지만 개발을 하다 보면 이 흐름이 항상 고정된 순서로만 동작하는 것은 아니다.
예를 들어, schema는 요청/응답 데이터 검증을 담당하고, model은 데이터베이스 구조를 정의하는 역할을 하기 때문에 단순히 하나의 직선적인 흐름으로 이해하기보다는 각 계층의 역할을 중심으로 이해하는 것이 도움이 될 것이다. 이 글에서는 위에서 언급한 것과 같이 FastAPI에서 자주 사용하는 다섯 계층의 구조를 정리해보고 각 계층이 어떤 역할을 하는지, 요청이 어떻게 처리되는지를 살펴보고자 한다.
보통 FastAPI 프로젝트를 도메인 단위로 구성하면 다음과 같은 구조를 가진다.
여기서 말하는 도메인은 서비스를 구성하는 큰 단위의 개체라고 가볍게 이해하자. 더해서, 처음에는 FastAPI 기초를 알기 위해 도메인 단위로도 구조를 잡지 않고 무작정 파일을 생성했는데, 도메인 단위로 프로젝트를 구성하고 구현하면 개발하는데에 용이하다.
app/
├── domains/
│ └── user/
│ ├── router.py
│ ├── schema.py
│ ├── service.py
│ ├── repository.py
│ └── model.py
Plain Text
복사
각 파일의 역할은 다음과 같다.
구성 요소 | 역할 |
router | API 요청을 받는 진입점 |
schema | 요청/응답 데이터 검증 및 구조 정의 |
service | 비즈니스 로직 처리 |
repository | 데이터베이스 접근 로직 |
model | 데이터베이스 테이블 구조 정의 |
Router
Router는 API 요청을 처리하는 진입점이다. 클라이언트에서 HTTP 요청이 들어오면 가장 먼저 Router가 이를 받는다.
router = APIRouter(prefix="/users")
@router.post("/")
def create_user(payload: UserCreate):
return user_service.create_user(payload)
Python
복사
Router의 역할은 다음과 같다.
•
HTTP end-point 정의
•
요청 데이터 받기
•
schema를 통한 요청 검증
•
service 호출
•
응답 반환
즉, Router는 요청을 받아 적절한 로직으로 전달하는 역할을 한다.
Schema
Schema는 요청(Request)과 응답(Response)의 데이터 구조를 정의하는 역할을 한다. FastAPI에서는 Pydantic을 사용하여 schema를 정의한다.
class UserCreate(BaseModel):
name: str
email: EmailStr
Python
복사
Schema의 주요 역할은 다음과 같다.
•
요청 데이터 검증
•
데이터 타입 검사
•
직렬화(Serialization) / 역직렬화(Deserialization)
•
응답 데이터 구조 정의
예를 들어 클라이언트가 다음과 같은 데이터를 보내면
{
"name": "James",
"email": "example@test.com"
}
Python
복사
FastAPI는 이를 자동으로 다음과 같은 Python 객체로 변환한다.
UserCreate(name="James", email="example@test.com")
Python
복사
이 과정을 역직렬화(Deserialization) 라고 한다. 반대로, Python 객체를 JSON으로 변환하여 응답으로 보내는 과정은 직렬화(Serialization) 라고한다.
Service
Service는 비즈니스 로직을 담당하는 계층이다.
예를 들어 사용자 생성 로직을 생각해보자. 사용자를 생성할 때 단순히 데이터를 저장하는 것 전에 다음과 같응ㄴ 작업이 필요할 수 있다.
•
이메일 중복 검사
•
비밀번호 해싱
•
기본 권한 설정
•
로그 기록
•
기타 데이터 검증
이러한 것은 비즈니스 로직으로 Router가 아니라 Service에서 처리하는 것이 좋다.
비즈니스 로직은 서비스 성격에 따라 있을수도 있고 없을수도 있다.
아래는 Service 계층에서 사용하는 예시이다.
def create_user(db: Session, payload: UserCreate):
existing_user = repository.get_user_by_email(db, payload.email)
if existing_user:
# 아래는 에러 처리
raise HTTPException(status_code=400, detail="이미 존재하는 이메일입니다.")
return repository.create_user(db, payload)
Python
복사
Repository
Repository는 데이터베이스 접근을 담당하는 계층이다. 데이터 조회, 저장, 수정, 삭제와 같은 CRUD 작업을 수행한다.
def get_user_by_email(db: Session, email: str):
return db.query(User).filter(User.email == email).first()
Python
복사
Repository의 역할은 다음과 같다.
•
데이터 조회
•
데이터 저장
•
데이터 수정
•
데이터 삭제
•
ORM 쿼리 관리
즉, Repository는 데이터베이스와 가장 가까운 계층이다.
Model
Model은 데이터베이스 테이블 구조를 정의하는 계층이다. FastAPI에서는 보통 SQLAlchemy ORM 모델을 사용한다.
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
Python
복사
이 모델은 실제 데이터베이스의 users 테이블과 매핑된다.
Model은 보통 다음을 정의한다.
•
테이블 이름
•
컬럼 구조
•
데이터 타입
•
제약 조건
즉, Model은 데이터베이스 구조를 표현하는 객체라고 볼 수 있다.
지금까지 각 계층이 존재하는 이유와 어느 역할을 담당하는지에 대해 작성 해보았다. 작성 순서대로 사용자의 요청을 처리하는데 아래는 FastAPI에서 요청이 처리되는 일반적인 흐름을 나타냈다.
(위에서 아래로 가면서 요청을 처리함)
Client
|
Router
|
Schema (요청 검증)
|
Service (비즈니스 로직)
|
Repository (DB 접근 및 데이터 입력)
|
Model (DB 테이블 매핑)
|
Database
Plain Text
복사
그리고 응답은 반대로 반환된다.
하지만 중요한 점은 이 흐름이 항상 고정된 순서로만 동작하는 것은 아니라는 것이다. 따라서 이 구조를 단순한 실행 순서라기보다 역할 중심 구조로 이해하는 것이 더 좋다.
지금까지 FastAPI에서 자주 사용하는 도메인 구조에 대해 알아보았는데 이러한 구조를 사용하는 이유는 크게 다음과 같다.
•
코드 책임 분리
•
유지보수 용이
•
테스트 용이
•
코드 재사용성 향상
사실상 프로젝트를 깔끔하게 하고 유지보수가 용이하게 하는 것이 주된 목적이다. 처음에는 복잡하게 느껴질 수 있지만, 프로젝트 규모가 커질수록 이러한 계층 분리가 큰 도움이 된다.