적용한 이유나 장단점이 뭐였어?
Fastapi로 바꾸게 된 이유?
- 백엔드 개발에 도움을 주는 프레임워크는 언어별로 정말 많이 존재하고 정말 각자의 장,단점이 존재한다고 생각한다.
- 기존에는 Sanic을 사용했었지만, Python 버전이 업되면서 생태계가 활발하고 Swagger가 기본으로 제공되는 Fastapi로 전환을 위한 공부를 하고 있다.
- 퍼포먼스적인 측면보다는 별도의 문서 관리를 줄이기 위함이 컸다. (퍼포먼스는 거기서 거기)
Fastapi 장단점은
- 개인적으로 느낀 Fastapi의 장단점은 다음과 같다.
- 장점
- Swagger가 기본으로 지원되고, Pydantic 지원을 통해 타입 처리에 용이하다
- starlette 기반이라 처리 속도가 준수하고, 비동기 처리를 지원한다.
- 트렌디한 프레임워크라 레퍼런스가 그래도 존재한다. (Sanic 대비..)
- 단점
- Path 파라미터의 타입을 통한 라우팅 분기가 되지 않는다.
- 아직도 1버전이 안 나온 불안정한 버저닝.. (0.108.0??)
- 래핑에 래핑에 래핑으로 인한 챙길 부분이 많음 (fastapi < starlette < anyio < ..)
- 도큐먼트를 잘 읽지 않으면 알 수 없는 서드파티 모듈 사용 정보
어떤 문제가 있었고 어떻게 해결했어?
1. Pydantic에러 발생 위치 및 정보 차이로 인한 문제
- Fastapi에서는 사용자에게 리퀘스트가 오면 아래와 같이 처리한다 (거의 대부분 비슷하겠지만)
- Fastapi App 접근 (이미 DB 커넥션 등은 모두 설정된 상태, 앞에 uvicorn 같은 asgi는 지나서)
- Fastapi Router 접근
- Fastapi Router 내 자신의 Method 및 Path 접근 (위에서 부터 찾음)
- 실제 함수가 실행되기 전에 Depend 동작 (결국 데코레이터 붙인 것과 차이는 없음)
- Pydantic으로 설정한 path, query 정보에 대해 형 타입 확인 > 여기서 문제가 있으면 422에러 발생
- 함수 접근 및 로직 실행
- 리스폰스 반환
- 에러와 관련된 커스텀 리스폰스 및 코드를 정말 열심히 관리하고 있기 때문에 (에러 발생 시 빠른 판단 및 대응을 위한), 해당 정보를 동일하게 반환하는 것이 문제였다.
- 문제는 아래가 문제였다.
- 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)
- 이러한 케이스에 대해서는, 분기를 못 쳐주는 것이었다.
- 해결 방법
- 답은 starlette routing에서 알 수 있었다. (https://www.starlette.io/routing/)
- 해당 링크 방식으로 {item_id:int}, {item_name:str}으로 변경하고 수행해야 한다.
- 참고자료는 아래 링크를 보면 된다.
- https://dev.to/gyudoza/how-to-divide-router-by-datatype-in-path-parameter-fastapi-endpoint-advanced-tip-1646
- Fastapi 쉽다쉽다 말하는 사람이 많은 것 같다... 하지만 결국 봐야하는 범위는 넓은 것 같다는 생각이 여기서 많이 들었다.
3. 도큐먼트에서 나오지 않는 정보들 (비동기 모듈)
- 일전에 이야기 한 것 처럼 Fastapi는 비동기 처리를 지원하는 프레임워크이다 따라서 내부적으로 Task, 코루틴, 이벤트루프 생성 및 처리를 위해 비동기 모듈을 사용한다.
- 그 기반을 쭈욱 살펴보면 다음과 같다.
- 비동기 처리 모듈들 (asyncio, trio, uvloop) (이것의 차이도 나중에...)
- anyio 모듈이 어떤 것을 사용할 수 있는 확인 및 선택
- starllet이 anyio모듈을 통해서 내부 이벤트루프 및 태스크 생성 및 처리
- 또 동기냐 비동기냐 따라서 사용하는 스레드와 이벤트루프가 다른 데, 이것은 나중에..
- 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개발자 형님의 자료를 우선 참고해보자 (역시..)
- https://hyperconnect.github.io/2023/05/30/Python-Performance-Tips.html
- 하지만 난 pydantic을 뺄 수 없었는 몸이.. (v2로 오면서 퍼포먼스가 좋아졌다 하니 믿어보자)
- gunicorn > uvicorn > fastapi worker를 사용하는 구조가 좋은 것 같다.
- fastapi 및 uvicorn에서는 request의 max size를 Config 값으로 지정하는 기능이 없다.
- 개인적으로 필요한 기능이 제공하는 구조를 채택 후, 그 중에서 베스트 퍼포먼스가 나오는 것을 선택하는 것이 맞다고 생각한다.
- 워커 레벨까지 내려와서 그것을 처리한다면 메모리 효율이 나빠질 수 밖에 없다.
- gunicorn은 uvicorn대비 제공하는 config기능들이 많다. (위 max size limit도 가능하다)
2. 디렉토리 구조?
- 개인적으로 개발을 잘 못하기 때문에 간단한 구조가 좋았다. 아래 참고자료 링크를 첨부한다.
3. 모니터링 등 운영에 필요한 방법은?
- opentelemetry, prometheus, loki를 사용한다면 아래의 링크를 봐보자
- https://github.com/blueswen/fastapi-observability
- 미들웨어 등록을 통해 trace-id 등을 부과하고 모니터링하기 딱 좋은 예시다.
- 메모리릭도 확인할 수 있는 지표도 제공한다.
- 여기서 다시 데이터독 짱짱맨을 외칩니다.
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 |