티스토리 뷰

파이썬은 인공지능, 빅데이터 분석/처리 트렌드와 함께 엄청난 인기를 얻었다. 전부터 마니아층이 두꺼웠지만, 본격적인 인기는 두 개의 트렌드와 함께했다. 파이썬이 없었다면 이 모든게 가능했을까 싶은 정도니까. 하지만 백엔드 개발 분야는 Spring Framework의 아성에 도전할 수 없을 정도로 레퍼런스가 밀리는 게 현실이고 Node.js, Go는 뛰어난 성능을 무기로 파이썬을 위협한다. "파이썬? 인공지능이나 데이터 분석할 때 적합하지! 백엔드? 글쎄....?" 그렇게 파이썬의 백엔드 겨울이 계속되던 중에 FastAPI가 등장했다. 

FastAPI는 파이썬으로 API를 빌드하기 위한 web framework이다. 공식 블로그에 안내된 fastapi의 장점 중에 현실로 와닿는 부분은 아래와 같다.

- Node.js, Go와 비슷한 수준으로 빠른 성능을 자랑한다.
- 개발 속도가 200%~300% 빨라진다.
- 코드 버그가 40% 감소한다.
- 쉽게 사용할 수 있다.


다른 장점도 많지만 가장 돋보이는 건 성능 부분이다. Go는 태생부터 C언어의 퍼포먼스를 위협했던 언어인데 그런 Go에게 python이 도전한다고? 어떻게 그게 가능할까? fastapi는 web micro framework인 Starlette를 사용하기 때문에 이미 거인의 어깨 위에 올라탄 격이다. Starlette이 뭐길래?

Starlette는 다른 파이썬 웹 프레임워크, 예를 들면 Sanic, Flask, Django 등과 비교하면 가볍고 강력한 ASGI(Asynchronous Server Gateway Interface) 프레임워크/툴킷이다. 한편 fastapi는 Starlette를 한번 감싸기 때문에 Stalette를 직접 사용하는 것보다는 성능이 떨어질 수밖에 없다. 하지만 개발 속도를 폭증시켜주는 등 fastapi의 장점은 이 모든 것을 상쇄시킬 수 있다. 

Starlette이 강력한 성능을 보장하는 이유는 내부적으로 uvicorn을 사용하고 있기 때문이다. uvicorn은 uvloops와 httptools를 사용하는 초고속 ASGI 서버이다. uvloop의 성능 비밀은 libuv과 Cython에 있다. fastapi가 Starlette 보다 성능이 떨어지는 것처럼 Starlette도 uvicorn을 직접 사용하는 것보다 더 많은 코드가 실행되기 때문에 더 느릴 수밖에 없다. 하지만 PATH 기반의 라우팅 등으로 간단한 웹 애플리케이션을 빌드하는 도구를 제공하기 때문에 생산성 측면에서 훨씬 더 도움이 된다.


한편 libuv는 Node.js가 동작하는 환경인데 이렇게 보면 fastapi의 근간이 거기까지 내려간다니 성능이 안 좋을 수가 없다. 파이썬 백엔드 겨울을 fastapi로 지금 바로 끝내보자. 터미널에서 관련 모듈을 아래와 같이 설치한다.

pip install fastapi
pip install uvicorn[standard]

uvicorn을 [standard]로 설치 시 Cython 기반의 디펜던시가 설치된다. 또한, 이벤트 루프인 uvloop가 사용되고 http 프로토콜은 httptools로 처리된다. standrard로 설치하지 않는 경우 uvloop가 설치되지 않고 대신 event loop로 asyncio를 사용한다. 이 경우에는 성능이 더 떨어지게 된다.

설치가 끝나면 공식 홈페이지에 있는 예제로 시작해보자

from typing import Optional

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: Optional[str] = None):
    return {"item_id": item_id, "q": q}

프로그래밍을 해본 적 있는 사람이라면 위에 코드는 어렵지 않게 읽힌다. 두 개의 API가 모두 GET 메소드로 구현되어있고 첫 번째 API는 루트( / )로 접속하면 JSON 형태의 데이터를 리턴한다. 다른 API는 path variable을 입력받고 query parameter를 옵셔널로 처리한다. 이런 API 스펙 문서는 개발과 동시에 정리되어야 한다.

API 문서는 서버팀-클라이언트팀처럼 유관부서 간에 커뮤니케이션할 때 사용되는 개발 산출물이다. 다른 기업이나 일반인에게 서비스를 제공하는 경우 API 문서를 외부에 오픈하기도 한다. 구글 인증 API, 카카오 알림톡 API, PayPal 결제 API, twillo 문자 발송 API, 국내 미세먼지, 날씨와 같은 정보를 제공하는 오픈 API 등이 여기 포함된다. 아무튼, 이 문서를 쓰는 것도 보통 일이 아니다. 그런데 FastAPI는 자동으로 swagger와 ReDoc 형식의 문서를 제공한다. 문서작업에 따로 시간을 들일 필요가 없다는 거다. 곧 살펴보겠지만 위에 코드만 있어도 브라우저에서 API 문서가 확인된다.


자, 그럼 다시 본론으로 돌아와서 터미널에서 uvicorn으로 위에 코드를 실행해보자. 뒤쪽에 붙은 --reload 옵션은 hot reloading 기능으로 소스 코드가 수정되면 서버를 바로 재시작하는 기능이다. 개발 환경에서 활용하면 좋다.

uvicorn main:app --reload


부라우저에 http://localhost:8000 으로 접속해서 결과를 확인해보자. 루트( / )로 접속했으니 JSON 데이터가 출력된다.

앞서 이야기한 API 문서를 살펴보자. 먼저 swagger UI를 통해 규격을 살펴보고 API를 테스트해 보자. 브라우저를 통해 localhost:8000/docs로 접속하면 된다. docs라니, 참 직관적이다. 


API를 클릭하면 안이 내용이 펼쳐진다(expand all). Try it out을 누르고 Excuse로 실행해보자. 복잡한 시나리오를 품고 있는 API가 아니라면 스웨거 환경에서 충분히 테스트할 수 있다.


ReDoc도 확인할 수 있다. localhost:8000/redoc 으로 접속해보자.


다시 코드로 돌아와서 비동기처리가 필요하면 async와 await를 기억하자. 비동기로 호출이 필요한 함수를 def가 아닌 async def로 처리해주고 함수 호출하는 곳에 await를 걸어주면 된다. 데이터베이스 조회나 I/O를 처리해야 하는 곳에 사용해주면 된다. 위에 예제코드에서 def 부분만 async def로 변경했다. (이것도 공식 페이지의 예제)

from typing import Optional

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
async def read_item(item_id: int, q: Optional[str] = None):
    return {"item_id": item_id, "q": q}

FastAPI는 uvicorn에서 제공되는 비동기 이벤트 루프를 사용한다. 또한 run_in_executor 안에서 threadpool을 사용해서 동기 함수를 처리한다. 즉, 이미 threadpool을 갖고 있다. 따라서 gunicorn을 사용하는 경우 퍼포먼스를 끌어올리고자 thread를 활성화 시켜서는 안된다. 오히려 성능이 저하되고 최악의 경우에는 컨텍스트에 안전하지 않은 코드가 있는 경우 문제가 발생할 수 있다고 한다

이벤트 루프 기반의 async가 빠른 건 자명한 사실인데 JMeter를 통해 대략 2분 동안 트래픽을 흘린 결과는 아래와 같다. API를 호출한 횟수와 Throughput 부분을 주목하면 되는데 대략 1.5배 가까이 차이가 나는 것을 확인할 수 있다. 터미널에서 실행은 명시적으로 event loop를 uvloop, http 핸들러는 httptools를 사용하도록 해줬다. (stdout으로 출력되는 로그도 성능에 영향을 주기 때문에 껐다)

uvicorn main:app --no-access-log --loop uvloop --http httptools

우측 상단에서 확인 가능한데 스레드는 엔드포인트 별로 50개 설정해줬다. MacOS에서 돌린 테스트라 위에 나온 Throughput이 FastAPI의 궁극적인 성능을 이야기하지는 않는다. 더욱이 JMeter GUI로 생성할 수 있는 최대 트래픽도 높지가 않다. 그러니까 sync, async 차이가 어느 정도인지만 확인하면 되겠다.


다른 언어와 성능을 비교해보면 어떨까? FastAPI는 정말 공식 문서처럼 Node.js, Go만큼의 성능을 보여줄까? 만만한 게 Node.js다. 바로 비교해보자. 코드는 아래처럼 심플하다. express 같은 프레임워크는 사용하지 않았다. FastAPI 문서에서 비교하고 있는 건 순수 Node.js 자체니까.

const http = require('http');

const server = http.createServer();
server.on('request', async (req, res) => {
  const data = {"Hello": "World"}
  res.end(JSON.stringify(data));
});

server.listen(3000);

- JSON.stringify를 쓰지 말았어야 더 정확한 테스트였을 텐데... 스크린 캡처 다 찍고 나서 발견(...) 눈 감아 달라.

비율 비교를 위해 앞서 만든 sync, async FastAPI에도 같이 쐈다. JMeter로 한 번에 많은 트래픽을 발생시키다 보니 FastAPI 쪽이 앞에서보다 성능이 떨어진 것처럼 보이지만 역시 비율만 보면 되겠다. 

node의 throughput은 대략 19,900/sec, FastAPI async는 3,300/sec이다. 대략 5.7배 차이가 난다... (API는 node와 비교를 위해 http://localhost:8000/ GET으로 호출했다)


당장 내가 진행한 테스트가 가장 정확한 잣대일 거라고 생각하지만 다른 사람들 생각은 어떨까? 사람들이 참여해서 함께 만들어가는 벤치마크 결과를 살펴봤다. 적당히 인지도가 있는 프레임워크들만 놓고 비교를 했는데 아래 내용을 살펴보자. (개인들이 작성한 코드를 올려서 테스트 되는 방식이라 퀄리티가 가지각색이다. 고로 이 내용이 100% 맞다고 보긴 어렵다. 역시 이것도 참고만 하자)

https://www.techempower.com/benchmarks/#section=test&runid=646af5ea-0fae-448d-b77d-bc27b13d73e5&hw=ph&test=composite&a=2&f=zik0vz-zg24fz-z7xy3j-zik0zj-zijunz-zijxtr-zik0zj-zik0lb-zik0xb-zik0zi-zik0zj-sf

- JSON : JSON serialization - {"message":"Hello, World!"}
- 1-query : DB에 단일 행 조회 후 JSON 응답 - {"id":3217,"randomNumber":2149}
- 20-query : DB에 복수 행 조회 후 JSON 응답 - 동시성과 관련된 조금 더 복잡한 조건이 있음
- Fortunes : DB에서 Unix fortune cookie 메시지가 포함된 모든 행(12개)을 가져와서 HTML 템플릿으로 응답
- Updates : DB에 복수 행을 업데이트 - 행을 가져와서 메모리에 올리고 객체를 수정, 동시성 등 세부 조건 있음
- Plaintext : 일반 텍스트로 응답 - Hello, World


여기 데이터를 봐도 FastAPI는 확실히 다른 파이썬 프레임워크 django, flask보다 뛰어난 면모를 보이지만 Go의 웹 프레임워크인 gin이나 node.js와 직접 비교하기엔 아직 갈 길이 멀다. 뭐 물론 Node.js의 express와 비교한다면 월등하다. 


또한, 앞에서 이야기한 대로 uvicorn 기반으로 동작하는 starlette가 uvicorn을 직접 쓰는 것보다 느리고, starlette를 품고 있는 fastapi는 역시 조금 더 느린 것을 확인할 수 있다. 여기서 오히려 주목해야 하는 부분은 sanic(python web framework)이다. fastapi와 동일하게 uvloop를 사용하고 있으며 성능 부분에서도 우위를 선점했다(sanic의 Plain text 부분은 뭔가 테스트가 잘못된 게 아닌가 하는 의심을). 하지만 현재(2021-01-26) 기준으로 fastapi는 깃헙에 star 수가 26.3k, sanic이 14.5k이다.  이 수치가 말해주는 건 사람들의 관심인데 여기에는 단순 성능 이외에도 프레임워크가 사용자(개발자)에게 어필될 수 있는 포인트가 숨겨져 있다. 공식 문서의 퀄리티나, 위에서 언급한 FastAPI의 장점 등을 생각하면 이 인기가 무리도 아니다.


spring의 경우 multiple query와 data update 부분에서 다른 프레임워크와 말도 안 되는 차이가 나기 때문에 Go보다 높은 순위에 오른 것을 확인할 수 있다.


# 마무리

시작에서 거창하게 파이썬 백엔드의 봄을 이야기했지만 사실 아직 갈 길은 멀다. 파이썬 백엔드로 서비스하는 곳도 많고 Node.js, Go를 이용하는 곳도 많다. 하지만 아직 국내 시장에서 spring의 아성을 넘지 못한다. 스프링의 인기는 위에 프레임워크 벤치마크 스코어에서 확인한 것처럼 단순히 레퍼런스 차이 때문만은 아니다. 엔터프라이즈급 서비스에서 아직 자바의 위치는 향후 몇 년은 굳건할 것으로 보인다. 그렇지만 마이크로 서비스 아키텍처가 유행처럼 번지고 있는 시점에 부서 단위, 팀 단위, 기능 단위로 서비스가 쪼개지면 자연스럽게 서버 프레임워크는 개발자의 취향에 많은 영향을 받게 될 거다. 그때 우리에게 주어지는 또 하나의 선택지가 바로 FastAPI가 되겠다. 빠른 개발, 문서 자동 생성을 통해 허물어지는 협업 장벽, ... 하지만 이런 부분을 스프링 앞에서 내밀긴 좀 그렇다. 그쪽도 이미 아주 빨라서. 하지만 IDE에서 Run 버튼을 누르자마자 실행되는 서버는 스프링을 하다가 파이썬으로 다시 넘어온 내게는 그야말로 10년 묵은 체증이 내려가는 느낌. 여러분도 함께 느껴보시길 바란다. :-)


# Update

페이스북 Python Korea에 글을 공유 했는데 좋은 댓글이 많이 달렸습니다. 같이 참고해서 보시면 보시기를 권장합니다. 소중한 댓글 남겨주신 모든 분들께 감사드립니다. - 바로가기














댓글
  • 프로필사진 BlogIcon 지나가는 행인 사실 spring이 백엔드 분야에서 주도권을 잡은건 단지 web framework 때문만은 아닙니다.. 파이썬이나 java script같은 동적 타입언어는 팀의 규모가 커지면 커질수록 코드를 관리하기 힘들뿐더러 10~20년 이상 유지되는 서비스에 개발 수단으로는 언어에 특성 자체게 미흡한점이 없지않아 있습니다. 물론 MSA 방향으로 가면서 확실히 전보다는 나아졌습니다. 다만 회사 입장에서는 실력이 우수한 python 백엔드 개발자를 뽑는게 쉽지않아서 인력 수급 문제도 더불어 한몫하죠.. 파이썬으로 개발된 서비스를 운영하는 팀 사람들이 이직한뒤에 그걸 인수인계받을 사람을 구하는게 힘든게 제일큰 문제같습니다.. 2021.01.31 13:57
  • 프로필사진 BlogIcon Jaeyeon Baek 댓글 감사합니다. :)

    맞아요. 인력 수급도 큰 문제라고 생각해요. 그게 스프링이 앞으로 몇 년간 굳건할거라고 생각하는 이유중에 하나 입니다. 현실적으로 내로라하는 회사들 JD를 보면 죄다 스프링입니다. 그러니 계속 스프링으로 사람들이 몰리죠.

    다만 이 글에서는 단지 스프링의 우수한 성능도 무시할 수 없다는것을 이야기하고 싶었습니다. 이런 이야기를 깊이있게 하다보면 글의 주제에서 너무 벗어나서 자제하게 됐습니다. :)
    2021.01.31 22:06 신고
댓글쓰기 폼