본문 바로가기

개발 공부/FastAPI

[FastAPI] FastAPI를 적용하며

적용한 이유나 장단점이 뭐였어?

Fastapi로 바꾸게 된 이유?

  • 백엔드 개발에 도움을 주는 프레임워크는 언어별로 정말 많이 존재하고 정말 각자의 장,단점이 존재한다고 생각한다.
  • 기존에는 Sanic을 사용했었지만, Python 버전이 업되면서 생태계가 활발하고 Swagger가 기본으로 제공되는 Fastapi로 전환을 위한 공부를 하고 있다.
  • 퍼포먼스적인 측면보다는 별도의 문서 관리를 줄이기 위함이 컸다. (퍼포먼스는 거기서 거기)

Fastapi 장단점은

  • 개인적으로 느낀 Fastapi의 장단점은 다음과 같다.
  • 장점
    • Swagger가 기본으로 지원되고, Pydantic 지원을 통해 타입 처리에 용이하다
    • starlette 기반이라 처리 속도가 준수하고, 비동기 처리를 지원한다.
    • 트렌디한 프레임워크라 레퍼런스가 그래도 존재한다. (Sanic 대비..)
  • 단점
    • Path 파라미터의 타입을 통한 라우팅 분기가 되지 않는다.
    • 아직도 1버전이 안 나온 불안정한 버저닝.. (0.108.0??)
    • 래핑에 래핑에 래핑으로 인한 챙길 부분이 많음 (fastapi < starlette < anyio < ..)
    • 도큐먼트를 잘 읽지 않으면 알 수 없는 서드파티 모듈 사용 정보

어떤 문제가 있었고 어떻게 해결했어?

1. Pydantic에러 발생 위치 및 정보 차이로 인한 문제

  • Fastapi에서는 사용자에게 리퀘스트가 오면 아래와 같이 처리한다 (거의 대부분 비슷하겠지만)
    1. Fastapi App 접근 (이미 DB 커넥션 등은 모두 설정된 상태, 앞에 uvicorn 같은 asgi는 지나서)
    2. Fastapi Router 접근
    3. Fastapi Router 내 자신의 Method 및 Path 접근 (위에서 부터 찾음)
    4. 실제 함수가 실행되기 전에 Depend 동작 (결국 데코레이터 붙인 것과 차이는 없음)
    5. Pydantic으로 설정한 path, query 정보에 대해 형 타입 확인 > 여기서 문제가 있으면 422에러 발생
    6. 함수 접근 및 로직 실행
    7. 리스폰스 반환
  • 에러와 관련된 커스텀 리스폰스 및 코드를 정말 열심히 관리하고 있기 때문에 (에러 발생 시 빠른 판단 및 대응을 위한), 해당 정보를 동일하게 반환하는 것이 문제였다.
  • 문제는 아래가 문제였다.
    • Depends 에서 수행되는 함수 내부에서 발생되는 pydantic 에러는 Fastapi에서 관리가 되지 않았고(개인적으로도 안할 수도 있다고 생각한다.), 그로 인해 리스폰스가 달라지는 문제가 발생되었다.
    • 당연히 프레임워크가 달라지면서 발생하는 문제니까, 넘어갈 수 있는 거 아니야? 라고 생각할 수 있지만, 타 서비스에서 사용하고 있기 때문에 API 호환성을 보장해야 할 필요가 있었다.
  • 해결방법은 생각보다 간단했다.
    • fastapi handling erros[https://fastapi.tiangolo.com/tutorial/handling-errors/] 를 보게 되면 RequestValidationError와 그 상위 클래스인 ValidationError가 존재한다.
    • ValidationError은 pydantic에 존재하는 클래스로 fastapi는 해당 클래스를 상속받아 에러를 처리하는 방식이었다.
    • 따라서 except_handller에서 ValidationError에 대해서 처리를 수행하고 미들웨어에 등록하면, pydantic에러는 한 곳에서 선 처리해버리면서 에러코드를 정의할 수 있었다.
  • 추가적으로 Fastapi에서 에러가 발생하면 Fastapi Request 내부의 error변수에서 에러에 대한 정보를 들고 있다. 해당 정보가지고 분기를 쳐서 커스텀 핸들러를 만들어서 사용하면 좋다.

2. Path 파라미터 타입의 기능 미흡

  • Fastapi는 Path 파라미터와 Query 파라미터를 통해 Get 메소드 내부의 정보를 처리한다.
  • API 구조에 대해 이것이 딱 맞다라는 답은 없다고 생각하지만 같은 Path에서 타입이 달라도 분기가 되지 않는 문제가 있었다. (메소드 타입을 통한 분기는 잘 작동한다.)
  • 구체적으로는 아래와 같은 케이스가 문제였다.
      api/items/{item_id}(int)
      api/items/{item_name}(str)
    • 이러한 케이스에 대해서는, 분기를 못 쳐주는 것이었다.
  • 해결 방법
  • Fastapi 쉽다쉽다 말하는 사람이 많은 것 같다... 하지만 결국 봐야하는 범위는 넓은 것 같다는 생각이 여기서 많이 들었다.

3. 도큐먼트에서 나오지 않는 정보들 (비동기 모듈)

  • 일전에 이야기 한 것 처럼 Fastapi는 비동기 처리를 지원하는 프레임워크이다 따라서 내부적으로 Task, 코루틴, 이벤트루프 생성 및 처리를 위해 비동기 모듈을 사용한다.
  • 그 기반을 쭈욱 살펴보면 다음과 같다.
    1. 비동기 처리 모듈들 (asyncio, trio, uvloop) (이것의 차이도 나중에...)
    2. anyio 모듈이 어떤 것을 사용할 수 있는 확인 및 선택
    3. starllet이 anyio모듈을 통해서 내부 이벤트루프 및 태스크 생성 및 처리
      • 또 동기냐 비동기냐 따라서 사용하는 스레드와 이벤트루프가 다른 데, 이것은 나중에..
    4. fastapi는 starlette을 래핑하여, 엔드포인트 제공
  • anyio 도큐먼트를 보면, 다음과 같이 backend를 어느 것을 사용할지 https://anyio.readthedocs.io/en/stable/basics.html 선택하는 것이 있다...
  • 결론 아무것도 설치 안하고 쓰면 asyncio인 파이썬 기본 모듈을 사용하고, uvloop를 설치하면 uvloop를 통해 비동기 작업을 처리한다.
  • uvloop는 libuv를 cpython으로 포팅한 형태로 asyncio보다 퍼포먼스가 좋다. (퍼포먼스가 좋은 nodejs 기반인 v8엔진도 libuv를 사용한다.)
    • uvicorn도 처음에는 uvloop 설치가 기본이었으나, 현재는 설치된 것이 없으면 asyncio를 사용한다. (Python 3.9 정도 부터)
    • hypercorn은 trio를 기본으로 사용하는 것 같은 데 이것도 나중에..

잘 쓰기 위한 팁이나, 고려사항은 뭐가 있어?

1. 퍼포먼스 관련?

  • 하이퍼커넥트 ML개발자 형님의 자료를 우선 참고해보자 (역시..)
  • gunicorn > uvicorn > fastapi worker를 사용하는 구조가 좋은 것 같다.
    • fastapi 및 uvicorn에서는 request의 max size를 Config 값으로 지정하는 기능이 없다.
    • 개인적으로 필요한 기능이 제공하는 구조를 채택 후, 그 중에서 베스트 퍼포먼스가 나오는 것을 선택하는 것이 맞다고 생각한다.
    • 워커 레벨까지 내려와서 그것을 처리한다면 메모리 효율이 나빠질 수 밖에 없다.
    • gunicorn은 uvicorn대비 제공하는 config기능들이 많다. (위 max size limit도 가능하다)

2. 디렉토리 구조?

3. 모니터링 등 운영에 필요한 방법은?

  • opentelemetry, prometheus, loki를 사용한다면 아래의 링크를 봐보자
  • 여기서 다시 데이터독 짱짱맨을 외칩니다.

4. 기타?

  • Depends를 잘 활용할 수 있는 것이 중요 포인트라 생각한다.
    • Depends에서 사용하는 함수는 변수를 집어넣기 위해서 ()를 넣고 돌리는 순간, callable 인스턴스가 아니게 된다는 에러메시지를 뱉는다
    • 그래도 원하는 파라미터를 집어넣는 방식이 있다.
    • 그것은 app또는 환경 변수로 설정된 변수를 Depend로 또 넣어주는 것이다. (나중에..)
    • https://fastapi.tiangolo.com/advanced/advanced-dependencies/

다르게 배운 것도 있어?

1. Sqlalchemy에 대해서 조금 더 알게된 부분도 있어

  • flush, commit,, 등등에 대한 개념들
  • db에서 제공하는 stored_procedure기능을 사용할 때, db cursor를 반환을 안해주는 케이스를 확인하고 해결 한 것
  • 결국 Sqlalchemy 내부적으론 동기적으로 처리하는 부분이 있다는 것
  • 그리고 Sqlalchemy 도큐먼트를 보면서 가독성에 대한 고찰을 하게 됨 (더 읽기 어렵게 만든 느낌)

2. Starllet과 파이썬의 비동기 처리에 대해 이해한 것

  • 대부분 Flask에서 Fastapi를 넘어가면서 고려하는 것이 많을 것 같다. 왜냐하면 동기에서 비동기로 변경하는 부분이기 때문이다. 그래도 어느정도는 비동기에 대해 알아먹을수 있게 된 것?

앞으로 더 수정해야 하는 것?

1. 아직 uvicorn의 처리 방식에 대해 소켓 레벨까지 이해하진 못했다.

  • 결국 외부 request를 받아 소켓을 할당하고 fastapi 워커 호출을 통해 request를 처리하는 것은 uvicorn이 진행해주는 것일텐데, 아직 구조를 제대로 이해하진 못했다.

2. pyTest 적용에 대해 db 접근을 분기치고 롤백하는 것 테스트

  • 일반적인 pytest 관련 내용들을 보면, DB까지 실제로 쳐서 결과값을 확인하는 것이 아닌 정말 기초적인 Stateless한 케이스만 쓱 보여주고 마는 것 같다.
  • Fastapi app 인스턴스는 dependencies를 오버라이딩 하는 기능을 제공한다.
    • db 세션을 받아오는 것을 depends로 구현해놓았다면, 그것을 test db에 접근하도록 변경을 할 수 있는 것이다.
    • 각 작업마다 db 실행 정보를 rollback하거나 초기값을 넣어주는 작업을 하게 되면, 테스트마다의 격리를 보장해주는 것이 더 간편해지고, 추가적으로 pytest-loop 등을 통해 스트레스 테스트나 오랫동안 돌려보아, 메모리릭을 확인할 수 있는 지표 제공을 용이하게 할 수 있을 것 같다.
    • https://fastapi.tiangolo.com/advanced/testing-dependencies/
728x90
반응형

'개발 공부 > FastAPI' 카테고리의 다른 글

[Python] Memory Leak 이해  (0) 2023.01.13
[Python] Python 리스트 사용법 정리  (0) 2022.03.07
파이썬 개인 공부 정리  (0) 2020.05.18