FastAPI 백엔드를 구축하기 전에 알았으면 좋았을 것들

📌 1. 읽기 전에

📌 해당 글은 저자가 FastAPI를 이용한 백엔드 서버를 구축하며 필요한 개념들을 밀키트(?) 형식으로 작성한 글입니다. 📌 FastAPI로 백엔드 서버를 구축하고자 하는데 어디서부터 시작해야할지 고민중인 분들께 조금이나마 도움이 되기를 바랍니다.

📌 해당 글에서는 다음과 같은 내용들을 다룹니다.
- pydantic 라이브러리를 통한 model 정의
- 프로젝트 폴더 구조


📌 2. Model 정의

어떤 서비스를 개발하든지 데이터를 정의하고 체계화하는 것이 필수라고 생각합니다. FastAPI를 통한 API 서버를 구축할 때도 마찬가지인데요, FastAPI 서버의 request와 response에 사용되는 데이터(Model)를 정의해봅시다.

2.1 pydantic

  • 공식 문서 링크

  • 백엔드 서버라고 한다면 많은 정보들을 조합하고 가공하는 로직이 많을텐데, 이 과정에서 데이터의 유효성이나 형식을 관리하는 것은 매우 번거롭고 복잡할 것입니다. 따라서 pydantic과 같은 data validation 라이브러리를 사용하여 이러한 번거로움을 줄일 수 있습니다.

    엄밀히 말하면 validation을 해주는 라이브러리는 아닙니다. 공식 문서에도 나와있지만 input data가 아닌 output model의 형(type)과 조건(constraint)을 보장해주는 라이브러리라고 할 수 있습니다.

  • 대표적으로 FastAPI와 궁합(?)을 맞춰 사용하고, 이미 다양한 프로젝트들에서 사용할 만큼 활용도가 높습니다.

jupyter notebook의 여러 서브 프로젝트에서도 사용한다고 jupyter notebook의 개발자가 친히(?) pydantic 프로젝트 이슈에 글을 남기기도 했군요.


2.2 Model

pydantic이 무엇인지 대충은 알고 넘어갔으니, 이제는 실제로 model을 정의하는 방식을 살펴봅시다.

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel

class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: Optional[datetime] = None
    friends: List[int] = []

pydantic의 예제 코드를 그대로 가져와봤는데요, 위의 코드를 통해 데이터 모델의 형 선언과 여러 옵션을 살펴볼 수 있습니다.

2.2.1 BaseModel

from pydantic import BaseModel

class User(BaseModel):

pydantic을 통해 object를 정의하기 위해서는 위에서부터 계속 언급한 Model이라는 개념을 사용하는데요, 간단히 말하면 pydantic의 BaseModel을 상속한 클래스입니다.

위의 예시에서는 User라는 Model을 정의하였고, pydantic을 통한 data validation이 가능해지는 object를 생성할 수 있게 되었군요.

2.2.2 field 정의

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel

class User(BaseModel):
    id: int # required
    name = 'John Doe' # not required
    signup_ts: Optional[datetime] = None # not required
    friends: List[int] = [] # not required

여기서부터는 잠시 집중해주실 필요가 있습니다. 앞으로 많은 Model을 정의하시게 되면서 헷갈릴 수 있거든요.

(1) 필수 vs 필수가 아닌 필드 선언

=을 통한 default값 선언이 있느냐 없느냐로 구분한다’ 를 기억합시다. 위의 코드에서 id같은 경우에는 값이 선언되어있지 않기 때문에 필수(required) 필드이고, 나머지 name, signup_ts, friends 필드의 경우 default값이 선언되어있기 때문에 필수 필드가 아닙니다. 즉 다시 말해 User object를 선언할 경우, id 필드의 데이터를 반드시 정의해주어야합니다.

user = User(id='123')

이렇게 user라는 이름의 User object를 선언하게 되면, id값만 선언해주었지만 다른 field의 값은 default 값으로 채워지겠죠?

assert user.name == 'Jane Doe' # True!
(2) 필드 타입 선언
from typing import List, Optional

class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: Optional[datetime] = None
    friends: List[int] = []

위의 코드를 보며 어느정도 유추할 수 있듯이, id같은 경우 int 타입이라는 것을 알 수 있고, signup_ts는 뭔지는 모르겠지만 대충 datetime 타입이라는 것, friends의 경우 int형 리스트라는 것을 아실 수 있으실 겁니다.

특이한 점이 있다면 name 필드같은 경우에는 따로 타입이 선언되어있지 않지만, ‘John Doe’라는 값을 default값으로 선언하여 str(문자열) 타입이라는 것을 선언하였습니다. 이런 식으로 default값을 통해 타입을 선언할 수도 있습니다.

다음으로는 signup_ts, friends 필드에는 각각 Optional, List 타입으로 선언이 되었는데요, 이는 typing 모듈(공식 docs)에서 지원하는 기능입니다. 타입 힌트를 언어 차원에서 지원하기 위해 파이썬 3.5 버전에서부터 스탠다드 라이브러리로 추가되었습니다.


2.2.3 정리

  • 지금까지 기본적인 Model을 정의하는 방법, Model 내의 field에 대해서 필요하다고 생각하는 부분에 대해서만 설명드렸는데, 어떠셨나요? 분명 설명이 부족하겠지만 이런 부분은 앞으로 개발하시며 공식 문서를 읽어가다보면 자연스럽게 덧칠이 되는 느낌을 받으실 수 있으실 겁니다(개인적으로는 공식 문서를 읽는 재미가 있는 것 같았습니다).

  • 마지막으로 실제 사용 예시 코드를 보며 간단히 정리해봅시다.

from typing import List
from pydantic import BaseModel

class Foo(BaseModel):
    count: int
    size: float = None

class Bar(BaseModel):
    apple = 'x'
    banana = 'y'

class Spam(BaseModel):
    foo: Foo
    bars: List[Bar]

3개의 Model이 정의되었군요. 어떻게 알 수 있냐구요? BaseModel을 상속한 클래스가 3개 있거든요!

Foo라는 Model은 int형 required 필드인 count와 default 값이 None으로 선언된 not required 필드인 size로 이루어졌군요.

다음으로 Bar라는 Model은 시크합니다. not required 필드 2개로 ‘x’와 ‘y’값이 default인 필드로 이루어졌군요. 타입이 따로 선언되지 않았지만 default값이 둘 다 문자열(‘ ‘)인 것으로 보아 str(문자열) 타입인 것을 알 수 있습니다.

마지막으로 Spam은 좀 신기합니다. 우리가 아는 타입(str, int 등)이 아닌 우리가 위에서 선언한 Model들을 타입으로 사용했습니다. 따라서 barstypingList 타입을 이용하여 Bar 타입의 리스트인 것을 알 수 있겠습니다(required 필드인 것도 아셨겠죠?!).

이처럼 우리가 정의한 Model은 또 다른 Model의 타입으로 사용될 수 있습니다. 서비스를 개발하시다보면 나도 모르게 데이터들의 구조를 신경쓰며 개발하고있는(?) 본인을 발견하시게 될 겁니다.