FastAPI + Pydantic을 이용해 API 문서와 데이터 검증까지 한 번에
Background
백엔드 엔지니어 입장에서 API 개발보다 중요시 여겨지는 것은 바로 API를 사용할 수 있도록 만든 문서 작업이다. 하지만 엔지니어가 요구 사항에 따른 기능 개발과 테스트 코드를 통한 품질 관리에 주력을 두다보니 문서 작업에서 소홀해지기 마련이다.
이렇게 되면 이를 사용하는 프론트 엔지니어들은 버그가 고쳐지기 이전이나 요구 사항의 변경으로 인해 바뀐 API 때문에 사용에 혼란을 겪게 된다.
버즈니 경영전략 스쿼드에서는 기존 프레임워크로 Flask를 사용하고 있었다. 마이크로서비스를 지향하는 버즈니의 입장에서 Flask는 마이크로프레임워크로는 나은 선택이지만 요청/응답 등 데이터 검증의 처리에 있어서 백엔드 엔지니어의 일을 가중시킨다.
또 자주 변화하는 코드에 일일이 문서를 변경하는 수작업에서 불리하다는 단점이 있다.
위 그림에서 보다시피 백엔드 엔지니어가 형상 관리 시스템에 변경된 소스 코드를 반영해 서버 배포를 자동화 했다. 하지만 API 문서까지 최신화가 되지 않기 때문에 같이 협업을 하는 프론트 엔지니어가 기존에 받은 문서에 의존을 하게 돼 업무 이슈 발생도가 증가하게 된다.
FastAPI의 채택
백엔드 엔지니어 입장에서 위와 같은 이슈를 해결하기 위해 문서의 자동화가 필요했다.
코드의 변화에 따라 발생하는 요청/응답 스키마를 자동으로 문서화해 제공하고, 이를 프론트 엔지니어가 백엔드 애플리케이션의 코드 변화에도 즉각 대응할 수 있도록 한다면 위와 같은 문제를 해소할 수 있을 것이라고 판단했다.
FastAPI는 이러한 파이프라인을 제공해줌으로써 백엔드 엔지니어가 문서 작업에 할애하는 시간을 줄이고, 오직 코드에만 집중하도록 해 업무 효율을 증진시킬 수 있다.
백엔드 엔지니어가 FastAPI를 이용해 애플리케이션을 개발하면 기본적으로 엔지니어가 만든 EndPoint를 제외하고, 아래의 3가지 EndPoint가 생성된다.
- /docs
- /redoc
- /openapi.json
위 3가지 엔드포인트는 API에 대한 문서를 위한 것이다. 기본적으로 FastAPI는 OpenAPI를 채택하고 있으며 이를 기반으로 자신의 코드를 읽어 OpenAPI 규격에 맞는 json 파일을 만들어준다.
이를 기반으로 FastAPI에 내장돼 있는 Swagger, Redoc이 문서를 시각화 해줌으로써 백엔드 엔지니어가 문서 작업에 할애하는 시간을 대폭 줄일 수 있다.
어떻게 사용할 수 있을까?
먼저 간단한 FastAPI 애플리케이션을 정의해보겠다.
프레임워크 개발에 필요한 fastapi와 애플리케이션 실행에 필요한 uvicorn을 먼저 설치해준다.
FastAPI로 애플리케이션을 개발할 때는 FastAPI 인스턴스를 사용하고, 만든 인스턴스를 uvicorn의 run 함수의 파라미터로 넣어 실행할 수 있다. 서버를 실행한 뒤 브라우저를 켜보면…
주어준대로 OK를 출력하고 있다. 그러면 이제 redoc을 들어가보면…
이렇게 코드에 맞춰진대로 description이 문서에 들어가 있음을 알 수 있다. 기본적으로 status_code를 지정해주지 않으면 문서에서는 200 Success 코드를 나타내며 이와 관련된 사항은 FastAPI 인스턴스의 데코레이터에서 설정할 수 있다.
상태 코드는 int형 값을 사용할 수도 있고, FastAPI에서 제공하는 constant pool을 이용할 수도 있다. 더욱이 아까 위 문서에서 Text Response가 application/json으로 타입이 나온 것을 볼 수 있는데, 이를 정정하려면 response_class를 통해 해당 API가 json 응답이 아닌 text 응답이라는 것을 문서에 명시할 수도 있다.
Request / Response (JSON)
이번에는 요청과 응답을 JSON으로 받는 API를 정의하고 이를 문서화 해보도록 하겠다. FastAPI에서는 pydantic을 사용해 Request, Response Model을 정의할 수 있다.
사용자가 서비스에 가입하는 요청 API를 만든다고 가정하겠다. 그러기 위해서는 클라이언트가 서버에 자신의 정보를 알려줘야 한다. 닉네임과 이메일 주소, 휴대전화 번호로 회원을 받는 서비스 API를 만들고, 추가로 자기소개를 선택할 수 있는 값으로 만들었다.
기본적으로 pydantic에서 주어지는 멤버 변수는 required이며 필수가 아닌 변수로 정하고 싶다면 typing의 Optional을 이용하면 된다.
먼저 클라이언트로부터 받은 회원 정보를 그대로 반환하는 API로 만들어보겠다(직접 데이터베이스를 사용하는 것은 아래에서 좀 더 자세히 다룰 것).
문서를 띄우면 코드에서 정의한대로 샘플이 나오고 그 밑으로 422 Validation Error가 눈에 보인다. 이는 코드에서 정의하지 않았는데, 어떻게 나온 것일까?
기본적으로 pydantic은 데이터 검증 기능을 제공한다. 클라이언트의 요청 값이나 DTO로 변환하면서 데이터가 누락되는 부분을 자동으로 잡아주는 기능으로 위와 같이 required 값이 누락되거나 패턴과 일치 하지 않는 잘못된 값을 입력하는 경우 pydantic이 직접 오류를 반환한다.
예를 들어, 위와 같이 유효하지 않는 휴대전화 번호를 입력하면 422 코드를 반환하게 된다. 위에서 잘못된 원인은 국내 휴대전화 식별 번호가 010, 011, 016, 017, 018, 019가 있는데, 013을 넣었기 때문에 오류를 반환한 것이다.
Response Model
위에서 반환 모델을 문서화 할 때 decorator에 example을 넣었다. 그런데, 간단한 모델이라면 상관없겠지만 많은 정보를 줘야 하는 모델인 경우에는 데코레이터 내에 모든 변수를 쓰고 그에 따른 예시를 작성해줘야 한다. 그렇게 되면 API 부분에 코드가 방대하게 쌓이게 되고, 코드 리딩이 어려워질 수 있다.
Request 모델과 마찬가지로 Response 모델도 DTO로 정의해 사용할 수 있다.
Request 모델과 동일하게 값을 반환하므로 모델의 변수 또한 똑같이 만든다.
이 때는 response_class가 아닌 response_model을 사용해야 한다. 둘의 차이는 response_class는 REST API에서 제공하는 content-type에 기반한 인스턴스를 넣어야 하고, response_model은 pydantic으로 정의한 클래스가 들어간다는 것이다.
그런데, Response Model로 pydantic을 사용하니 샘플 코드가 조금 부정확하다. 이럴 때는 Config 클래스를 이용해서 샘플 모델을 정의할 수 있다.
Config 클래스에서는 해당 모델에 대한 다양한 설정을 참조할 수 있다. 이는 pydantic 공식 문서에서 더 상세하게 알 수 있다.
우리가 원하는 샘플 예시를 넣기 위해서는 schema_extra를 사용해 example을 정의하면 된다.
이는 Request Model에도 동일하게 적용할 수 있다.
SQLAlchemy와 같이 사용하는 방법
버즈니에서는 백엔드 애플리케이션에서 데이터베이스 처리를 위해 ORM을 사용하며 그 중에서도 SQLAlchemy를 사용한다. FastAPI에 내장돼 있는 Pydantic Mapper 모듈은 SQLAlchemy의 Entity 모델을 각 API 요구 사항에 맞는 응답인 DTO 모델로 변환해 Overfetch(오버패치)를 줄여주고 거기에 문서화까지 더해준다.
Overfetch는 클라이언트가 요구하지 않은 데이터를 포함해 서버에서 가져오는 현상을 말한다.
가입한 사용자를 저장하는 users 라는 테이블을 User 라는 이름의 Entity로 생성했다.
Request Model은 그대로 두 되, Response Model에서 사용자를 구분하는 id와 사용자가 입력한 이메일 주소만을 반환하는 모델로 정의하고, Config 클래스에 orm_mode를 True로 설정한다.
DB에 직접 추가하는 코드를 작성하고 문서를 열어본다.
문서에는 반환 모델이 id와 email로 축소된 것을 볼 수 있다.
그리고 API 결과도 이와 같이 축소됐다.
그런데, 분명 코드에서는 별다른 처리 없이 Entity 모델을 그대로 반환했다. 그렇다면 id ~ description까지 전부 나와야 하는데, 어떻게 id와 email만 출력된 것일까?
FastAPI가 요청과 응답을 파싱하는 원리
본래 FastAPI는 Python에서 기본적으로 제공하는 json 모듈의 loads를 이용해 JSON 모델로 반환한다. 이를 support 해주는 것이 Pydantic인데, FastAPI 인스턴스에서 제공하는 데코레이터에 이와 관련된 로직이 존재한다.
실제로 이 데코레이터는 문서화에도 영향을 미친다. 모델을 지정함으로써 문서에 어떻게 반영되는 지에 따라 달라진다. 그러나 실제로 FastAPI가 값을 반환하거나 받는 데도 그 영향이 있다.
데코레이터의 코드대로라면, response_model을 지정함으로써 문서에 반영되는 것과 동시에 APIRoute 클래스가 반환하기 전, response_model에 지정된 인스턴스를 호출해 엔지니어가 return 한 데이터를 인스턴스에 주입한다.
여기서부터는 pydantic이 작동하는데, pydantic으로 만든 Model에서 Config으로 orm_mode가 주어졌다면 from_orm 메소드를 사용해 ORM 모델에 맞춰서 파싱해주고, 그렇지 않으면 기본적으로 parse_obj 메소드를 사용해 dict의 값으로 가정하고 DTO 모델로 변환하게 된다.
앞서 사용했던 코드를 잠깐 보면...
이렇게 단순히 req의 모델을 dict로 변환하는 메소드를 사용해 return 해줘도 response_model에 맞춰서 나오는 이유가 바로 여기에 있는 것이다.
보통이라면 이렇게 했을 때 모델이 바뀌더라도 반영되는 것이 맞게 된다. 하지만 response_model과 return을 같이 주게 되는 경우 인스턴스가 중복돼서 반영이 되므로 주의해야 한다(특히, custom validation을 통해 데이터 타입을 변환하는 경우, 검증 과정을 두 번 거치기 때문에 오류가 발생한다).
(부록) Microservice
마이크로서비스에서는 각 도메인별로 API가 분리돼 있다. 따라서 FastAPI로 문서화를 진행했을 때, 모놀리식인 경우에는 한 서버에서 모든 API에 대한 문서가 나오게 되지만 마이크로서비스는 각 API 별로 문서 주소가 따로 분배된다.
이렇게 되면 API를 사용하려는 개발자가 각 API를 사용하기 위해 각 API 주소를 직접 찾아서 문서 페이지에 접속해야 한다. API가 적다면 큰 문제가 되지 않겠지만 API가 점점 늘어난다면 그 주소가 늘어나고, 그만큼 관리가 힘들어진다.
FastAPI에서는 Redoc, Swagger와 같은 내장된 Interactive Document System을 사용하기 위해 OpenAPI 3.x 표준에 맞춰 json 형태로 API 스펙을 작성한다. 해당 스펙은 각 API에 포함돼 있기 때문에 OpenAPI를 지원하는 Interactive Document System 애플리케이션을 Docker 컨테이너 등으로 생성해 사용하는 것도 하나의 방법이 될 수 있다.
OpenAPI는 애플리케이션 개발자 외 타 개발자 등 누구나 사용할 수 있도록 공개된 API를 말한다.
OpenAPI를 지원하는 문서 시스템은 아래의 4가지가 있다.
- Swagger
- Redoc
- DapperBox
- WidderShins
버즈니 경영전략 스쿼드에서는 Redoc을 사용하고 있고, 하나의 Redoc Application을 이용하여 스쿼드 내에서 운영하는 모든 API 문서에 액세스할 수 있도록 돼 있다.
하나의 문서 시스템에서 모든 API에 접근하도록 함으로써 API를 사용하려는 엔지니어가 각 API의 문서를 보기 위해 각 주소를 알아야 할 필요 없이 문서 시스템의 주소만 가지고 API 문서에 접근할 수 있어 접근성을 향상시킬 수 있다.
(부록) Postman, Insomnia 등의 API 테스트 도구 사용
Swagger에 비해 Redoc이 문서의 모습이 깔끔한 편이지만 Swagger에서 사용했던 테스트 도구가 Redoc에서 없다는 것이 아쉽다면 Postman 등의 API 테스트 도구를 사용할 수 있다.
Postman과 Insomnia는 OpenAPI를 지원하고 있어 FastAPI에서 만들어진 openapi.json 파일을 로드해 API 테스트 목록을 만들고 쉽게 테스트 할 수 있다.
Insomnia나 Postman에서 URL import를 클릭해 FastAPI의 openapi.json 링크를 입력해준다.
테스트 할 때는 base_url의 변수를 자신의 서버에 맞는 주소로만 설정해준다면 쉽게 테스트 해 볼 수 있다.
마치며..
이 외에도 많은 내용들이 있지만 가장 많이 사용하는 JSON request, response를 중심으로 FastAPI로 API를 개발하고 문서화 하는 간단한 방법에 대해 살펴봤다. FastAPI가 가진 문서 자동화의 메리트는 내장된 문서 시스템에도 그 의의가 있지만 OpenAPI 규격을 사용함으로써 다른 플랫폼에서도 호환성 있게 작동한다는 점이다.
여기에 typing에 있는 데이터 타입을 잘 이용한다면 데이터 검증의 로직을 백엔드 엔지니어가 고려하지 않아도 되며 문서화와 데이터 검증이라는 두마리 토끼를 잡을 수 있는 이득을 볼 수 있게 된다.