FastAPI 써 본 후기

고은연·2022년 1월 7일
13

기술 이야기

목록 보기
4/6

FastAPI?

파이썬을 이용한 웹 개발 세계에서는 FastAPI 가 최근 기술로 각광받고 있는 중입니다.


물론 전통의 풀스택 프레임워크 Django 나 마이크로 프레임워크 Flask에 비할 바는 아닙니다만 프레임워크 개발이 2018년 12월부터 시작되었다는 점을 감안하면 꽤나 빨리 뜨거운 감자가 된 셈이죠.
그래서 뱉은 말과 다르게 한번 API 문서를 둘러보고 직접 사용해 보았습니다.

엔드포인트 매핑은 플라스크 방식으로

이전에도 이런 방식이 있었는지는 잘 모르겠습니다만, 저는 플라스크때 처음으로 데코레이터를 이용해 엔드포인트를 정의하는 방식을 봤습니다.

@app.get("/hello")
def hello() :

이런 방식은 장점도 있고 단점도 있는데요. 장점은 직관적으로 어떤 엔드포인트가 호출되면 어느 함수가 호출될 지 알 수 있다는 겁니다.
단점은 엔드포인트를 찾으려면 파일 하나에서 찾는 대신 ctrl + H(전체검색) 가 필요하다는 정도죠.
여튼 저는 이 방식이 익숙하기 때문에 특별한 불만은 없습니다.

FastAPI는 전체적인 아키텍쳐가 Flask의 영향을 많이 받았는지 개발 결과물의 코딩 스타일이 Flask와 전반적으로 비슷합니다. Flask에는 익숙한 개발자들이 많으므로 스타일이 비슷하다고 해서 특별히 문제가 될만한 것은 없습니다.

타입을 적극적으로 이용하는 프레임워크

파이썬 3.5 버전에서 타입 힌트라는 게 생겼습니다. 동적 자료형 언어에서 무슨 타입인가 싶었지만, 있으면 IDE 자동완성 등 편리한 기능도 있는 것이 사실입니다. name : str = "koeunyeon" 이런 식으로 씁니다.
FastAPI는 타입에 많이 의존합니다. 공식 홈페이지의 소개에 적혀 있는 아래 문구는 모두 타입 의존성으로 인해 얻은 효과입니다.

적은 버그: 사람(개발자)에 의한 에러 약 40% 감소. *
직관적: 훌륭한 편집기 지원. 모든 곳에서 자동완성. 적은 디버깅 시간.

FastAPI는 그냥 파이썬의 타입 힌트만 사용하지는 않습니다. 타입 힌트를 지원하는 라이브러리 pydantic 과 강하게 묶여 있습니다. 즉 프레임워크 자체가 pydantic에 의존합니다.
pydantic은 파이썬에서 데이터 모델을 제공하기 위한 라이브러리입니다. 멤버 변수의 자료형과 Optional 여부로 자동으로 데이터의 유효성을 검증해주는 편리한 기능을 가지고 있습니다.

Java 혹은 C# 등의 정적 언어를 사용해서 웹 개발을 해 보신 분들은 웹 요청 파라미터를 처리하기 위해 혹은 DB의 데이터를 담기 위한 컨테이너로 DTO(Data Transfer Object)를 만들어서 사용하실 텐데요. FastAPI에서는 pydantic이 DTO 객체의 역할을 한다고 생각하면 편합니다.

fastapi에서 pydantic을 사용하는 예제는 아래와 같습니다.

from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float
    is_offer: Optional[bool] = None

@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
    return {"item_name": item.name, "item_id": item_id}    

타입을 사용해서 개발 안정성을 높이는 것은 좋은 아이디어라고 생각합니다. 특별한 경우가 아닌 이상 변수의 자료형이 바뀌는 경우는 많지 않으니까요.
FastAPI에서 내세우는 pydantic 타입 사용의 장점입니다.

  • 자동완성.
  • 타입 검사.
  • 데이터 검증:
  • 데이터가 유효하지 않을 때 자동으로 생성하는 명확한 에러.
  • 중첩된 JSON 객체에 대한 유효성 검사.
  • JSON 읽기
  • 경로(path) 매개변수 읽기.
  • 쿼리(query string) 매개변수 읽기.
  • 쿠키 읽기.
  • 헤더 읽기.
  • 폼(Forms) 읽기
  • 파일 읽기.
  • 파이썬 타입을 json으로 변환 (str, int, float, bool, list, 등).
  • datetime 객체도 json으로 변환. (내장 json 모듈은 datetime 모듈을 변환하지 못합니다.)
  • UUID 객체 json 변환.
  • pydantic 타입의 데이터베이스 모델 변환.
  • Swagger UI.
  • ReDoc.

이렇게 얻을 수 있는 수많은 장점에도 불구하고 저는 동적 스크립트 언어에서 객체를 통한 요청 처리 방식을 그다지 좋아하지 않습니다. 이것은 초기 개발 후 요구사항 변경으로 필드가 변경될 때 문제가 있다는 판단 때문인데요.
정적 언어에서는 DTO 객체의 필드명이 바뀌면 빌드 자체가 안되므로 쉽게 DTO 객체의 정의를 변경할 수 있는 반면 동적 언어는 꾸준히 검색의 힘을 빌어야 합니다. 즉, 스크립트 언어의 특성상 (컴파일 타임이 없으니까) 런타임에나 오류가 발생할 텐데 그럴꺼면 굳이 뭔가를 미리 정의해야 하는가 싶은 의구심이 드는 거죠.

swagger(openapi) 혹은 redoc 제공은 정말 편리하다.

실은 이것 때문에 FastAPI를 고려해서 테스트해봤던 거였는데요.
저는 회사에서 백엔드 파트를 담당하고 있고, 백엔드는 결국 클라이언트에서 쓸 API를 만드는 일이 대부분이기 때문에 클라이언트 개발자들이 쉽게 볼 수 있는 문서화가 반드시 필요합니다.
따라서 절대 문서를 만들기 귀찮아서가 아니라 자동화된 문서화를 위하여 스웨거를 제공하려고 했던 겁니다.

정말 너무 좋았습니다.

Depends는 훌륭하다.

의존성 주입(Dependency Injection)은 불편하지만 꽤나 유용한 기법입니다.
우리가 개발을 하다 보면 다른 객체를 내부에 가지고 사용하는 경우가 꽤 있는데요. 소위 합성이라고 불리는 형태죠. 이렇게 내부에 가지고 있는 다른 객체의 인스턴스를 클래스 외부에서 생성하고 객체 안으로 밀어넣는 기법을 의존성 주입이라고 부릅니다.

class Inner:
    def __init__(self):
        self.name = "i am inner class"

    def run(self):
        print("inner run. name :" , self.name)

class Composit:
    inner : Inner
    def __init__(self, inner):
        self.inner = inner
    
    def run(self):
        self.inner.run()

inner = Inner()
composit = Composit(inner)
composit.run()

대충 이런 식입니다. 스프링은 객체를 생성하는 과정을 자동화해주고 자랑스러워하고 있습니다. 그리고 이 DI를 통해 스프링이 널리 사용될 수 있었죠.

# 여기가 없어도 됨.
# inner = Inner()
# composit = Composit(inner)
# composit.run()

FastAPI에서는 클래스 단위로 의존성을 주입하기보다는 함수 레벨에서 의존성을 주입시킬 수 있습니다.

async def verify_token(x_token: str = Header(...)):
    ...

async def verify_key(x_key: str = Header(...)):
    ...

@app.get("/items/", x_key=Depends(verify_key), dependencies=[Depends(verify_token)])
async def read_items():
    print(x_key)

이렇게요.

의존성 주입은 이전에는 데코레이터를 통해 검사하고, 값을 객체 멤버변수를 통해 넣어주던 것을 훨씬 간단하게 해 주었습니다. 예전에는 이렇게 해야 했어요.

class Items:
    @verify_token
    @verify_key
    async def read_items(self):
        x_key = self.x_key # self.x_key는 @verify_key가 객체에 접근해서 넣어준 멤버변수

정말 훌륭한 기능이라고 생각해요.

비동기는 생각보다 불편하더라.

저는 노인성 개발 신기술 불감증을 앓고있는 환자이기 때문에 신기술을 받아들이는 것을 좋아하지 않습니다.
아얘 Node.js 처럼 비동기가 기본이어서 모든 라이브러리가 비동기를 전제로 하고 있다면 오히려 개발은 쉬울 꺼에요.
하지만 기본적으로 파이썬은 동기로 동작하고, 파이썬 3.4 버전이 되어서야 asyncio 를 통한 비동기 처리를 지원하기 시작했어요. 심지어는 파이썬 3.8 버전에는 비동기 관련 이슈가 있어서 사실상 활성화는 파이썬 3.9 버전부터 되었어요.
게다가 파이썬은 GIL(Global Interpreter Lock) 때문에 쓰레드의 사용도 권장되지 않았기 때문에 파이썬 개발자들이나 라이브러리들은 동시성 개발 따위는 전혀 신경도 안쓰고 만들어진 경우가 많죠. 그래서 동시성 처리를 위한 모듈이 없는 경우가 많아요.
단적인 예로 파이썬 ORM계에서는 탑 레벨인 SQLAlchemy도 1.4 버전이 되어서야 비동기를 지원하기 시작했어요.

비동기와 동기 방식이 혼용이 되면서 모듈이 비동기를 지원하게 되었더라도 async 키워드와 await 키워드만으로는 동작하지 않는 경우가 많이 생기더군요. 외부에서 pip를 통해 설치한 모듈이야 단위테스트를 거쳤겠지만, 제가 개인적으로 만드는 라이브러리는 "동기와 비동기를 모두 고려"해서 만들어야 한다는 뜻이 되더라고요.
게다가 개발이 종료된 모듈의 경우 비동기 자체를 지원하지 않는 경우도 많아 더욱 곤란한 상황이 몇몇 있었습니다.

데이터베이스 연동은 너무너무너무너무 번거롭다.

API 서버가 데이터베이스 연동을 하지 않는 일은 거의 없을 거에요. 그런만큼 데이터베이스 연동은 가장 쉬운 일이어야 한다고 생각합니다.
하지만 공식 홈페이지의 문서 (Peewee를 이용한 동기 방식, SQLAlchemy를 이용한 비동기 방식)를 보면 알 수 있듯이 파이썬에서 데이터베이스를 연동하는 건 그다지 쉬운 일이 아닙니다. 특히 ORM을 연동하는 건요.
물론 복사 붙여넣기로 동작하게 하는 것 자체는 쉽습니다만 다수의 데이터베이스 서버 연동이라거나, active record 방식이 아닌 entity와 repository manager를 이용한 방식 등을 사용하기에는 너무 불편함이 많았어요.
저는 이 글을 읽으시는 분들이 제가 실력 없이 말만 많다는 것을 알고 계실꺼라 생각하기 때문에 "개발을 못하니까 어렵지" 라는 팩트는 절대 뱉지 않을 겁니다.

de facto standard (사실상의 표준) 방식의 문제점

사실상의 표준(de facto standard)은 "무조건 이렇게 만들어야 동작한다는 지침은 없고 A 방법으로 만드는 것을 권유해" 정도를 말합니다.
다시 말하면 개발을 할 때 "코딩 스탠다드"가 없거나 약해서 개발자에 따라 스타일이 굉장히 많이 갈리게 되죠.

사실상의 표준은 좋게 말하면 자유로운 것이고, 나쁘게 말하면 무책임한 것입니다.

잔소리쟁이 자바 월드의 웹 개발 표준 스프링 프레임워크는 터무니없이 복잡한 규칙을 가지고 있어서 누구나 특정 목적을 위해서는 거의 비슷한 형태의 코드를 작성해야 합니다.
자유로운 영혼의 vanilla PHP로 웹 개발을 하게 된다면 규칙이라고 부를만한 것이 거의 없어서 특정 목적을 달성하는 방법이 천개도 넘을 꺼에요.
이러한 상반된 특성은 특정 우위를 가진다기보다는 개발자의 특성과 프로젝트의 특성에 맞물려 있습니다. 각각 장점과 단점이 있고 서로의 장점이 반대편의 단점이 될 겁니다.

그런데 FastAPI는 자바처럼 빡빡한 규칙을 가지지만 FastAPI 개발진에서 예상하지 않은 특정 문제에 대해서는 vanilla php처럼 (혹은 python zen처럼) 능력껏 빠져나가야 합니다. 두가지의 특성이 하나의 프로젝트에 공존한다는 의미이고, 언제 충돌을 일으켜도 이상하지 않다는 뜻이 됩니다.

제가 우려하는 "사실상의 표준" 문제는 Flask에서도 동일하게 나오는데요.
다만 Flask는 원래부터 Micro Framework를 지향하고 있었고, 메타 프레임워크로 사용하라는 것이 모토인만큼 (일부러 그러나 싶을 정도로) 메뉴얼이 어렵고 복잡하게 작성되어 있죠.
하지만 FastAPI는 그렇지 않아요. 공식 메뉴얼은 정말 자세하게 적혀 있고 따라하면 무조건 결과가 나오게 되어 있습니다. 책으로 출판해도 될 만큼 한 줄 한 줄 코드를 설명해 줍니다.
이렇게 하나씩 다 알려주고 이대로 하라고 지침을 주지만 메뉴얼에 없는 예외의 상황에서는 어떻게 해야 할 지 표준이 없다는 게 문제입니다.

저도 API 를 하나 만들다가 도저히 풀기 어려운 문제에 부딛히는 바람에.. FastAPI의 사용을 포기하게 되었습니다.
그냥 처음부터 지저분했으면 지저분하면 어때..라고 구현을 할 텐데, 다른 공식 지원 스타일 코드는 깔끔한데 내가 만든 코드는 지저분한 건 잘 못참겠더라고요.

이럴꺼면 스프링이나 ASP.NET Core MVC로 만드는 게 훨씬 낫지 않나?

FastAPI 문서를 둘러보고 직접 작은 API 몇개를 개발해 보고 난 후 이런 생각이 들었습니다.

스프링 부트의 파이썬 버전인가?

타입 시스템 도입으로 인한 빡빡한 기준, DDD를 하고싶어지는 논리 레이어, 정작 필요할 때는 없는 de facto ..

그렇다고 Ruby on rails 처럼 극적으로 코드량이 줄어드는 것도 아닙니다. 샘플 예제에서는 간결해 보이지만 실제로 비즈니스 로직이 들어가는 순간부터는 복잡도가 급격히 상승해 버립니다.

그냥 Flask 처럼 코드를 작성하고 pydantic 부분만 살려서 갈 까도 계속 고민하고 코드를 바꿔 보았는데, 우아한 프레임워크 코드에 참 안어울려서 그만두었습니다.

저는 "스크립트 언어는 그 특성을 살려서 가능한 동적으로 기능을 구성하고 코드량을 적게 가져가는 것"이 가장 큰 장점이라고 생각합니다. 반면 "정적 언어는 코드를 복잡하게 작성하더라도 누가 작성해도 비슷한 코드를 안정적으로 쓰는 것" 이 그 지향점이라고 생각하고요.
하지만 FastAPI로 작성한 제 코드는 이것도 저것도 아닌, 마치 오토바이인 척 하는 자전거 같다는 생각이 계속 들어서 결국 FastAPI 도입은 안하기로 했습니다.

(내 소중한 2주..)

대안 찾아보기

가장 찾기 쉬운 대안은 당연히 Flask였습니다. 거의 비슷하거든요.

하지만 뭔가 더 좋은 방안이 있을 거라는 생각에 하루를 헤메다AWS CHALICE를 발견했습니다. AWS LAMBDA 위에서 돌아가는 파이썬 웹 프레임워크입니다.
사용법도 간단하고, 그다지 프레임워크 레벨에서 강요하는 것도 없습니다. 저는 역시 우아한 프로그래밍보다는 실전 지향 코딩을 더 좋아하는 것 같습니다.
람다 위에서 실행되므로 서버의 스케일 아웃같은 귀찮은 일을 하지 않아도 되고 사용하지 않으면 돈도 안 냅니다.
물론 일반 서버에 직접 배포해서 실행하는 것은 안되지만 (되기야 하겠지만 AWS에서 권고할 일은 없겠죠.) 반대로 직접 서버를 관리하지 않아도 됩니다. 로그는 클라우드워치에서 보면 되는 거고 서버의 용량이나 트래픽 같은 것도 걱정할 필요 없어요.
그리고 아무리 생각해도 AWS가 망하거나 LAMBDA 서비스를 내릴 확률보다는 제가 개발자를 그만둘 확률이 훨씬 더 높을 것 같아서 그냥 사용하기로 했습니다.
그리고 한달쯤 지났는데 (잘 안되는 것도 있지만 그래도 뭐 이정도면 쓸만은 하네 싶어서) 꽤나 만족하면서 사용하고 있습니다.

스웨거는 지원하지 않지만, 그냥 손으로 API 문서 작성하던가 하면 됩니다. 기능이 동작 안하는 건 큰 문제지만 문서화는 수작업으로도 가능하니까요.

profile
중년 아저씨. 10 + n년차 웹 백엔드 개발자. 자바 스프링 (혹은 부트), 파이썬 플라스크, PHP를 주로 다룹니다.

0개의 댓글