입사 후 몇달간 FastAPI
를 이용하여 백엔드를 구축했으나 이번에 진행하는 새 프로젝트에서는 Typescript
를 사용하기로 됐다. 한동안은 FastAPI를 볼 일이 없을 것 같아 간단하게나마 정리해두고자 한다.
Pydantic
FastAPI에서 python의 pydantic
를 활용해 데이터 요청 객체와 응답 객체를 구성할 수 있었다.
필수값을 지정해줄 수 있고 Swagger랑 호환이 되어 설명도 추가해줄 수 있었다. 타입 힌트를 통해 유효성 검사까지 자동으로 처리된다.
Swagger를 위한 코드는 타입 선언 정도에서 끝나 매우 깔끔했다. 추가적인 필드 설명이 필요하면 스키마에서 별도로 작성해주는 것으로 가능했다.
라우팅 (Routing)
FastAPI는 데코레이터(@app.get
, @app.post
등)를 사용하여 HTTP 메서드별 엔드포인트를 매우 쉽게 정의할 수 있게 해준다. 여기서 app은 FastAPI 애플리케이션 객체를 의미한다.
경로 매개변수, 쿼리 매개변수, 요청 본문 등을 함수의 매개변수로 직접 선언하여 사용할 수 있었다. 헤더 또한 파라미터 명과 일치시킴으로써 가져올 수 있었다.
의존성 주입 (Dependency Injection)
FastAPI의 강력한 기능 중 하나는 의존성 주입 시스템이다.
컨테이너가 존재한다기 보다는 Depends
라는 함수를 사용하여 데이터베이스 연결, 인증 로직 등 공통된 객체를 주입받을 수 있었다.
FastAPI에서 의존성 주입은 경로 작동 함수
(path operation function)를 통해서만 동작한다.
HTTP 컨텍스트 내부에서만 동작한다는 의미이며, HTTP 외부에서의 주입은 FastAPI가 해주지 않아 명시적으로 해줘야한다.
외부에서는 Depends를 통해 의존성 주입을 받으려고하면 필요한 객체가 아닌 Depends 함수 객체가 담긴 채로 존재하게 된다.
SQLAlchemy 연동
SQLAlchemy 2.0 버전 이상은 비동기 작업을 지원해 FastAPI를 온전한 비동기 프레임워크로 사용할 수 있다.
종종 관계 로딩을 누락하여 never await I/O 이슈를 맞이하게 되면서 eager loading
기법에 익숙해졌다.
SQLAlchemy는 주로 eager loading
을 권장한다. 이를 통해 N+1 문제를 해결할 수 있기 때문이다. N+1은 ORM 객체가 매 레코드별로 관계를 맺은 테이블을 조회하게 되면서 발생하는 문제이다.
다른 ORM 같은 경우에는 필요할 때 불러오는 lazy loading이 기본 기능이였지만 SQLAlchemy는 기본 기능이라 보기엔 힘들고 별다른 설정이 필요했다.
단연코 가장 기억에 남는 로딩 기법은 역시 selectinload
이다.
앞선 쿼리의 결과를 기반으로 where 문의 쿼리를 한번 더 날리는 기법으로 join의 오버헤드를 피할 수 있다.
예를 들어, 상품과 결제가 일대일인 관계에서 결제한 상품을 찾는다고 하자. 결제 레코드별로 상품 id를 가질 것이다. 그러면 먼저 결제에서 상품 id를 추출한 뒤, 상품 테이블에서 where에 in clause를 사용하여 추출한 상품 id에 해당하는 상품 정보를 로드해오는 식이다.
일반적인 join을 통한 로딩(joinedload
)은 여러 관계를 join 하게 될수록 테이블 크기가 커져 성능 저하가 발생하나, selectinload는 테이블의 크기가 항상 일정한 상태에서 index를 활용한 조회를 진행할 수 있어 데이터 크기에 따른 성능 저하가 발생하지 않는다.
물론 쿼리 실행 횟수가 늘어나 네트워크 관점에서 손해를 볼 순 있다. Round Trip 횟수가 늘어나는 것이니깐 말이다.
마무리
FastAPI 환경에서도 Layered Architecture를 도입하기 위해 경로 작동 함수로 응용해서 사용하기도 하면서 최대한 짜임새 있는 서버 구조를 지향했다.
그 과정에서 잘못된 설계가 발생해 순환 참조가 발생하기도 했고, 이것을 해결하려 별도의 컨테이너도 구현하면서 Depends의 한계를 명확히 깨달을 수 있었다.
타입 힌팅을 통해 타입 추론 기능을 제공할 수 있는 것은 좋았으나 여전히 동적 언어의 한계는 존재했다.
파라미터 타입 이슈나 순서 이슈는 타입 힌팅과 키워드 전용 인수(keyword-only arguments) 적용으로도 놓치는 경우가 발생했고 런타임에서 발견되어 곤욕을 치뤘다.
Python에서 덕타이핑을 지원하기 위한 Protocol
이란 기술이 존재했는데, 이것을 활용하면 타입 추론을 강력히 지원해주면서 안정성을 더할 수 있지 않을까 싶다.