Python FastAPI 개발 방법 정리
- 이번 포스팅에서는 Python 의 FastAPI 에 대해 정리하겠습니다.
프레임워크 사용 개발 방식 설명이므로 기본적인 프로그래밍 지식 및 Python 언어에 대해서는 설명을 생략합니다.
- FastAPI는 Python 3.6+ 버전을 기반으로 만들어진 비동기(Async) 웹 프레임워크입니다.
주요 특징으로는,
단순하고 쉬운 파이썬 언어에서 추가 보완하여 보다 안전한 프로그래밍을 위한 변수 타입 처리, Nullable 제어 등이 가능하며,
내부적으로 Swagger 를 도입하여 API 를 자동으로 문서화해주고,
비동기 프로그래밍을 지원하여 높은 성능을 낼 수 있게 해준다는 강점이 있습니다.
- 저는 springboot 를 중심으로 한 백엔드 개발자로서,
굳이 파이썬의 백엔드 프레임워크를 익힌 이유는 아래와 같습니다.
1. springboot 의 현재 제 개인적 숙련도가 거의 포화 상태라고 느끼기에 기술 확장의 일환입니다.
2. 차세대 기술인 AI 등의 기능을 지원하는 언어가 파이썬이기 때문으로,
AI 뿐 아니라 RPA, Web Crawler, Computer Vision, IoT 등의 기술을 지원해주기 때문에 확장성이 Java 베이스의
Springboot 에 비해 매우 넓다고 생각했습니다.
Javascript 계열 백엔드 프레임워크와 비교했을 때,
JS 기반 풀스택을 기반으로 파이썬 기능 모듈들을 연동하여 사용하는 것 보다,
기능 모듈들을 지원하는 파이썬 백엔드를 기반으로 프론트엔드를 JS 로 떼어두는 것이
통합 테스트 및 관리에 있어 더 합리적이라 생각했기 때문입니다.
3. FastAPI 성능에 대한 호평이 많고, springboot 가 지원하는 기능을 모두 구현 가능하며,
타입 세이프티 등의 안정성도 제공하기에 성능상 문제도 없을 것 같다고 여겼습니다.
아래로는 제가 후보로 두었던 파이썬 언어의 다른 프레임워크와 FastAPI 를 비교하겠습니다.
- 비교표
항목 | FastAPI | Flask | Django |
출시년도 | 2018 | 2010 | 2005 |
성능 | 5 (매우 빠름) | 3 (보통) | 2 (상대적으로 느림) |
비동기처리 | O (async/await) | X (3rd party 필요) | X (제한적) |
문서 자동화 | O (Swagger) | X (수동 작성 필요) | X (수동) |
데이터 검증 | O (Pydantic 기반) | X (수동 검증) | O (Django Forms) |
제가 고려한 후보는 위와 같습니다.
Django 는 제가 경력이 없던 시점에 고려했던 것이고,
Flask 는 제가 처음으로 Python 모듈을 만들어 배포할 때 사용했던 것입니다.
위와 같이 비교하여 보았을 때, Django 는 논외로 쳤으며, Flask 는 단순하고 쉽게 익힐 수 있었지만 불충분 했습니다.
결국 가장 현대적이라 할 수 있는 FastAPI 를 선택하였으며,
이는 제가 기존에 다루던 Springboot 에서 지원하는 모든 기능(Swagger, 데이터 검증 등)을 사용할 수 있었고, 또한 성능 또한 더욱 좋은 선택이라 생각합니다.
- 프레임워크 학습법
새로운 기술을 익히는 것은 어려운 일이지만 백엔드 개발자라면 백엔드 기술을 익히는 것이 처음 다루는 것 보다는 월등히 나을 것입니다.
저 역시 백엔드 관련 경험이 있으므로 이를 적극 활용하였습니다.
한가지 제 프레임워크 학습 방법을 공유해드리자면, 개발 템플릿을 작성하시는 것을 추천드립니다.
https://github.com/RaillyLinker/Kotlin_SpringbootMvc_Template
위 링크는 제 Springboot 프레임워크 개발 템플릿입니다.
HTTP 요청/응답의 모든 기본 형태(Query Param, Path Param, Json Response, Multipart Parameter 등) 에 대한 예시 및 처리 방법, 요청/응답에 대한 자동 로깅 방식, 인증/인가 적용 방식, MSA 적용 방식, Kafka, Redis, Database 응용 방식, TCP/IP 통신 방식 등의 백엔드 개발시 만날 수 있는 여러 상황을 상정하여 가장 근본적인 '기본 형태'를 미리 만들어두고, 추후 관련된 기능을 구현해야 할 때 참고하는 프로젝트입니다.
위와 같은 학습 방식이 유리한 이유는 세가지 입니다.
첫째,
사람의 기억은 아무리 좋아도 천 줄 이상의 코드를 외우기란 어려울 것입니다.
고로 프로젝트를 진행하며 그때그때 검색해서 찾아서 사용하다보면 시간이 걸리는 것은 둘째치고, 여러 코딩 스타일이 뒤섞여 자기 자신도 보기 어려워질 가능성이 있습니다.
평소에 뚜렷하게 잘 정리된 템플릿을 구비해둔다면 그만큼 클린 코드를 유지하기 쉬워질 것입니다.
둘째,
신 기술, 기능 구현시 일어날 수 있는 충돌을 미리 구현하여 방지할 수 있습니다.
예를들어 실무에서 이제껏 구현하지 못한 기능을 구현해야 할 때에는,
실무 프로젝트에 바로 구현하여 적용하는 것이 아닌, 템플릿 프로젝트 안에서 먼저 구현하여 동작을 확인한 후 이식하는 방식으로 개발한다면, 기존 구조와 충돌되지 않는 깔끔한 구조와 더불어 에러에 안전하게 진행이 가능해 집니다.
물론, 이렇게 만들어진 템플릿 코드는 다음 프로젝트에도 동일하게 사용할 수 있으므로 더욱 좋습니다.
마지막으로,
템플릿 프로젝트를 구비해 둔다면, 다른 프레임워크를 학습시에도 도움이 됩니다.
저의 경우 FastAPI 를 학습시 기존에 작성한 Springboot 템플릿에 작성한 모든 기능을 FastAPI 로 구현하는 방식으로 진행중입니다.
신 기술 습득시 무엇을 공부해야 할 지에 대해 고민할 필요 없이,
이전 템플릿에서 구현했던 실용적인 기능들을 직접 구현하는 방식으로 진행하면 실무를 거치기 전에도 실전적인 경험을 얻을 것이며, 이 코드를 기반으로 바로 실무에 적용할 수 있습니다.
제가 작성중인 FastAPI 의 템플릿은
https://github.com/RaillyLinker/Python_FastApi_Template
이 링크이며, 본 게시글에서는 이 내용을 기반으로 설명할 것입니다.
[Fast API 실습 및 설명]
- 백엔드 프레임워크로 개발하려면 적합한 IDE 가 필요합니다.
여러가지를 사용할 수 있을 것인데, 저의 경우는 Intellij 를 만든 Jetbrains 에서 만든 PyCharm 을 사용할 것입니다.
https://www.jetbrains.com/pycharm/download/?section=windows
위 링크에서 파이참 무료(Community) 구조를 다운받아 설치하면 충분합니다.
- IDE 를 열고, 프로젝트를 만들어주겠습니다.
프로젝트명은 Python_Fastapi_Template 입니다.
프로젝트 안에는 "새로만들기 - Python 패키지" 를 눌러, module_sample_api 라는 패키지를 만들고,
.gitignore 파일을 만들어줍니다.
module_dample_api 폴더는 모듈을 의미합니다.
취향이기는 한데, 저는 프로젝트 안에 바로 코드를 만드는 것이 아니라 모듈을 만듭니다.
즉, 하나의 프로젝트 안에 여러 모듈이 있는 것이죠.(추후 module_auth, module_payments 등 여러 모듈 추가)
각 모듈은 하나의 포트를 담당하는 MSA 에서의 마이크로 서비스를 의미합니다.
꼭 MSA 를 구축하지 않더라도, 일단 템플릿만이라도 MSA 를 구현하면 추후 기술 응용에 유리하므로 추천하는 방식입니다.
이유를 설명드리자면,
예전에 싱글 모듈로 모든 기술을 한 모듈에 넣어둔 경험이 있었기 때문입니다.
이렇게 한 모듈에 모든 기능을 몰아넣는다면, 템플릿이 존재해도 기술을 분리하는 것이 어려웠습니다.
예를들어 복합 모듈에서 '결제'관련 기능만 분리하려고 해도, 해당 기능에만 속하는 의존성 라이브러리나 코드가 혼재되어 있기에 파악이 어려웠기 때문에 이식 및 활용에 그만큼의 수고가 듭니다.
하지만 모듈단위 기능을 분리하면 해당 모듈에 꼭 필요한 내용만 넣어 구현하게 되므로, 단위 기능 구현 코드를 파악하는 것도 쉬우며, 응용에 그대로 사용이 가능하다는 장점이 있습니다.
- .gitignore 파일은,
https://www.toptal.com/developers/gitignore
위 링크에서 간단히 Python, FastAPI 에 해당하는 ignore 목록을 얻어와서 작성하면 됩니다.
- 파이참 설정 및 Fast API 기본 라이브러리 설치
파이참에서 파이썬 코드를 실행할 때에는, 어떤 환경으로 실행할지에 대한 설정을 해주어야 합니다.
같은 파이썬 코드를 실행시키더라도 다른 파이썬 버전이나 라이브러리 버전을 사용할 수 있게 하기 위해서입니다.
IDE 의 좌측 상단 햄버거 버튼을 누르고, 파일 - 설정을 누르고, 좌측 부분의 프로젝트: ** 로 시작되는 탭을 눌러서 Python 인터프리터 부분을 선택하면 실행 환경 설정이 나오는데, 이때 Python 인터프리터를 선택하는 것입니다.
선택한적이 없다면 이부분이 선택되지 않았을것이므로 본 프로젝트용 인터프리터를 새로 만들어야 합니다.
인터프리터 추가를 누르고 나온 창에서, Conda 환경을 누르고, 위 캡쳐와 같이 새 환경 설정을 선택하고, 파이썬 버전 등을 선택한 후 확인을 누르고, 이렇게 만들어진 인터프리터를 선택하면 됩니다.
(Conda 환경은 Anaconda 를 설치해야 합니다. Anaconda 의 가상 환경, 즉 이 가상 환경별로 파이썬 버전과 라이브러리 종류를 별도로 둘 수 있습니다.)
라이브러리의 경우,
인터프리터 설정 후 IDE 를 껐다 다시 켜면,
이처럼 아래쪽 IDE 커맨드창의 소괄호 안에 인터프리터 이름이 들어가게 되는데,
이때,
pip install fastapi
pip install unicorn
이렇게 기본적인 라이브러리를 2개 설치합니다.
앞으로 필요한 다른 라이브러리는 위와 같은 방식으로 추가해가면 됩니다.
- IDE 설정돠 더불어 모듈이 준비되었으므로 해당 모듈을 채워넣겠습니다.
위에서 마련한 모듈은, HTTP 기반 RestAPI 를 구현하기 위한 기본 샘플 코드를 모아두는 모듈입니다.
예를들어, FastAPI 에서 query param 을 설정하는 방법, path param 을 설정하는 방법, multipart 로 file 을 받는 방법, 비동기로 요청을 처리하는 방법 등을 모아두는 것입니다.
모듈 내의 기본 구조는,
이렇습니다.
configurations 안에는 모듈의 설정(모듈 포트번호, 모듈 실행 정보 등)에 관한 정보가 있습니다.
controllers 는 MVC 패턴에서의 Controller 역할을 하는 코드들을 모아둔 폴더이고,
Models 는 말 그대로 Model 코드의 모음, Services 는 Controller 의 비즈니스 로직을 처리하는 서비스 코드를 모아둔 것입니다.
모듈의 실행은 main.py 에서 이뤄집니다.
즉, 프로젝트의 시작 경로이고, main 함수나 마찬가집니다.
참고로, __init__.py 는 내용 없이 비워진 파일입니다.
이것이 있어야 해당 폴더가 파이썬 패키지라는 것을 확인할 수 있는 이정표 역할(import 시 이것이 없으면 검색 안됨)이므로 지우지만 않게 조심합시다.
- main.py
main 파일은 본 모듈의 시작점입니다.
모듈별로 시작점이 따로 존재할 수 있으며,
각각의 포트를 점유하며 실행됩니다.
import uvicorn
import fastapi
import importlib
import pkgutil
import os
import module_sample_api.configurations.app_conf as app_conf
import module_sample_api.configurations.swagger_conf as swagger_conf
# [FastAPI 실행 Main]
# 현재 파일이 속한 디렉토리 경로
dir_path = os.path.dirname(os.path.abspath(__file__))
# 디렉토리 경로에서 폴더명만 추출 (main.py 파일은 모듈 폴더 바로 안에 위치 해야 함)
folder_name = os.path.basename(dir_path)
# FastAPI 객체 생성
app = fastapi.FastAPI()
# Swagger 설정 적용
app.openapi = lambda: swagger_conf.SwaggerConf.custom_openapi(app)
# controllers 디렉토리에 있는 모든 라우터 등록
for _, module_name, _ in pkgutil.iter_modules([dir_path + "/" + app_conf.AppConf.controllers_package_name]):
module = importlib.import_module(f"{folder_name}.{app_conf.AppConf.controllers_package_name}.{module_name}")
if hasattr(module, "router") and isinstance(module.router, fastapi.APIRouter):
app.include_router(module.router)
# FastAPI 서버 실행
if __name__ == "__main__":
uvicorn.run(
"main:app",
host=app_conf.AppConf.unicorn_host,
port=app_conf.AppConf.unicorn_port,
reload=app_conf.AppConf.unicorn_reload
)
이렇게 작성하였습니다.
기본적으로는 app 변수에 FastAPI 객체를 할당하고,
unicorn.run 으로 이 app 변수를 실행시키는 것입니다.
실행시에는 main.py 파일을 python 으로 실행시키거나, main.py 를 파이참에서 우클릭해서 실행 버튼을 누르면 되는데,
위 코드에서 if __name__ == "__main__":
부분의 코드를 날려도 무방합니다.
이 경우에는 unicorn 명령어로,
uvicorn main:app --reload
이렇게 실행시켜도 됩니다.
이렇게 판단하자면, FastAPI 는 코드이고, unicorn 이 실행 환경으로,
main 이라는 파일 안의 app 이라는 FastAPI 객체를 실행시킨다는 의미라고 해석할 수 있습니다.
위 코드를 조금 더 살펴봅시다.
import 부분에서 프로젝트 내 파일을 import 하려면 module_sample_api.** 이렇게 프로젝트 경로에서부터 시작해서, 모듈 - 서브폴더들 이런 순서로 작성합니다.
앞서 __init__.py 가 패키지 폴더내에 존재한다고 했는데,
module_sample_api 에도 이 파일이 있고, 그 안의 서브 패키지 안에도 이 파일이 있음으로 해서 위와 같이 import 로 프로젝트 루트에서부터 조회할 수 있습니다.
위 코드의 핵심은, FastAPI 서버가 실행되는 시점에 서버 전체에 대한 설정을 하는 것입니다.
FastAPI 에서 기본 제공하는 API 공유 기능인 Swagger 에 대해서도 설정하고,
핵심적으로는, 서버에서 제공하는 API 를 등록하기도 합니다.
위에서는 모듈 패키지 안에 있는 controllers 폴더 안의 모든 파일들을 파악해서 그 안에 있는 API 목록을 프로젝트에 등록하도록 처리하였습니다.
- configurations
본 패키지는 서버의 설정을 모아두는 위치입니다.
포트번호, 접근 허용, 인증/인가 등의 서버 전체에 적용되는 설정에 대한 코드를 이곳에 응집시켰습니다.
현재는 일단 앱 전체 설정인 app_conf.py, 스웨거 설정인 swagger_conf.py 파일을 생성하였습니다.
app_conf.py 파일은,
import uuid
import time
class AppConf:
# 서버 고유값 (런타임 시에 고정되도록 생성)
server_uuid = f"{int(time.time() * 1000)}/{uuid.uuid4()}"
# controllers 폴더 위치(main.py 기중)
controllers_package_name = "controllers"
# 서버가 어떤 IP 주소에서 접근을 허용할지를 설정하는 옵션
# "127.0.0.1" 또는 "localhost" 로컬 컴퓨터에서만 접근 가능 (외부에서 접근 불가)
# "0.0.0.0" 모든 IP에서 접근 허용 (같은 네트워크의 다른 장치나 외부에서도 접속 가능)
unicorn_host = "0.0.0.0"
# 서버 점유 포트
unicorn_port = 8080
# 코드를 수정 하면 서버를 자동으로 재시작(reload) 해주는 개발용 기능
unicorn_reload = True
위와 같이 main.py 에서 사용한 설정값을 모아둔 곳입니다.
서버 프로세스 구분을 위해 서버 실행시 자동으로 할당되는 server_uuid,
라우터로 등록될 컨트롤러 파일들이 위치할 위치 설정,
포트번호 등의 정보가 있습니다.
다음으로 swagger 설정을 담당하는 swagger_conf.py 는,
import fastapi
import fastapi.openapi.utils as openapi_utils
class SwaggerConf:
@staticmethod
def custom_openapi(app: fastapi.FastAPI):
if app.openapi_schema:
return app.openapi_schema
openapi_schema = (
openapi_utils.get_openapi(
title="SAMPLE-API",
description="API 명세입니다.",
version="1.0.0",
contact={
"name": "Railly Linker",
"url": "https://railly-linker.tistory.com",
"email": "raillylinker@gmail.com",
},
license_info={
"name": "MIT",
"url": "https://opensource.org/licenses/MIT",
},
routes=app.routes
)
)
# 모든 경로에 400, 500 응답을 추가
for path in openapi_schema["paths"].values():
for method in path.values():
responses = method.setdefault("responses", {})
responses.setdefault("400", {"description": "잘못된 요청입니다."})
responses.setdefault("500", {"description": "서버 내부 오류입니다."})
app.openapi_schema = openapi_schema
return app.openapi_schema
이처럼 Swagger 설정 코드를 만들어,
main.py 에서 사용됩니다.
Swagger API 화면에 표시될 문서 정보와 모든 API 에 공통으로 적용될 Response State 를 설정하였습니다.
필요에 따라 추후에 Swagger 설정을 이곳에서 하면 되도록 분리하였습니다.
잠시, Swagger 에 대해 미리 말씀드리자면, API 문서입니다.
백엔드 개발자가 API 를 만들어서
'이러이러한 정보를 이러이러한 형태로 요청하면 이러이러한 데이터를 반환합니다'
이렇게 만들어도 이를 사용하는 측에게 전달하지 않으면 사용할 수 없을 것입니다.
API 명세를 만들어 배포해야 하는데,
수동으로 액셀 파일로 작성할 수도 있겠지만, 코드를 작성하면 자동으로 이를 만들어서 접근하여 확인할 수 있으면 더 좋겠죠?
Swagger 가 이러한 역할을 합니다.
FastAPI 로 API 를 만들면 위와 같이 /docs 라는 위치로 접근하여 api 명세를 볼 수 있습니다.
각 항목을 누르면 해당 api 의 역할이 무엇이고, 해당 api 에 입력해야 하는 데이터와 출력되는 데이터를 알 수 있습니다.
configuration 에서 설정한 설정 정보는 위 화면에서 문서 제목과 문서 설명 글 외의 요소들입니다.
- controllers
본 패키지에는 백엔드 서버의 컨트롤러 역할을 담당하는 파일들이 위치하게 됩니다.
잠시, MVC 패턴에 대해서 간략히 설명하자면,
웹 서버를 Model View Controller 의 세 부분으로 나누어 코드를 분리하는 개발 방식을 의미합니다.
웹 개발을 할 때에 가장 대표적인 기능 분류인데,
Model 은 화면 및 동작을 구성하는 데이터의 형태를 의미합니다.
예를들어 본 게시글 화면에서는,
현재 게시글을 보고 있는 사용자의 회원 정보, 게시글 제목, 게시글 본문, 게시글 작성일, 게시글 수정일, 댓글 목록 등이 있을 것입니다.
기능을 구현하기 위한 변수들의 집합을 Model 이라 합니다.
View 는 일단 여기선 다루지 않습니다.
말 그대로 '웹'을 개발할 때에, 화면을 나타내는 것으로, 게시글이 브라우저에서 보여지는 HTML, CSS 등의 화면 관련 리소스를 의미합니다.
Model 을 기반으로 화면을 구현하여 보여주는 역할로, 백엔드, 프론트엔드로 뚜렷하게 나뉘는 현대에서 View 는 프론트엔드쪽이 담당하고, 백엔드에서는 프론트엔드가 요청하는 정보를 화면 데이터가 아닌 JSON 이나 TXT 등의 raw 데이터로 넘기는데, 정확한 정의는 아니지만, 웹페이지를 넘기지 않는 API 를 RestAPI 라고 생각하면 됩니다.
Controller 는 API 의 입구 역할을 담당합니다.
사용자가 127.0.0.1:8080 이라는 서버측에 요청을 보낼 때, 127.0.0.1:8080/test 이렇게 엔드포인트를 붙이면, 컨트롤러가 이를 파악하여 이 주소에 해당하는 처리 코드에 요청을 전달(라우팅)합니다.
컨트롤러는 이렇듯 사용자의 요청을 받아서 적절한 처리 로직을 수행하며, 그 와중에 Model 이나 View 를 사용하여 결과를 합성하고 사용자에게 응답을 되돌려주는 역할까지를 수행합니다.
본 프로젝트에서의 controllers 는 이를 의미하며,
controllers 안의 controller 는 주소체계 하나를 담당합니다.
import fastapi
import module_sample_api.services.api_test_service as service
import module_sample_api.models.api_test_model as model
# [그룹 컨트롤러]
# Router 설정
router = fastapi.APIRouter(
prefix="/api-test", # 전체 경로 앞에 붙는 prefix
tags=["API 요청 / 응답에 대한 테스트 컨트롤러"] # Swagger 문서 그룹 이름
)
# ----------------------------------------------------------------------------------------------------------------------
# <API 선언 공간>
# ----
@router.post(
"/post-request-application-json",
response_model=model.PostRequestTestWithApplicationJsonTypeRequestBodyOutputVo,
summary="Post 요청 테스트 (application-json)",
description="application-json 형태의 Request Body 를 받는 Post 메소드 요청 테스트"
)
def post_request_test_with_application_json_type_request_body(
request_body: model.PostRequestTestWithApplicationJsonTypeRequestBodyInputVo
):
return service.post_request_test_with_application_json_type_request_body(request_body)
위와 같은 api_test_controller.py 파일 안에서는,
먼저 해당 컨트롤러가 뭔지를 설명하는 router 객체를 볼 수 있습니다.
fastapi 의 APIRouter 클래스는 해당 컨트롤러가 어떤 주소로 시작되고, 스웨거에 어떻게 표시되는지를 보여줍니다.
위 컨트롤러는 /api-test 라는 주소로 시작됩니다.
API 선언 공간 아래에는 해당 컨트롤러에 속하는 api 가 선언됩니다.
@router.post 어노테이션으로 해당 함수가 /post-request-application-json 이라는 엔드포인트에 post 메소드 요청을 보내면 된다고 알려주고 있습니다.
response_model 로는 해당 api 가 응답시 반환하는 데이터 형태(모델)를 설정하고,
함수 파라미터에는 입력값의 데이터 형태(모델)을 설정하며, 나머지는 보시는 바와 같이 Swagger 에 표시될 데이터입니다.
API 하나에 대한 함수는 위와 같이 작성합니다.
컨트롤러 상위 설정의 주소와 합쳐져서, 위 api 에 요청을 보내려면, /api-test/post-request-application-json 이라고 post 요청을 보내면 위에서 작성한 코드대로 동작하는 것이죠.
실제 예시 화면을 보면 아래와 같습니다.
앞서 작성한 컨트롤러 그룹 안에 API 항목이 생성되고,
그에대한 설명은 어노테이션에 적힌 대로 표시됩니다.(참고로 위 API 를 테스트 해보고 싶다면 API 의 Try it out 버튼을 눌러서, 아래에 생성되는 Execute 버튼을 누르면 됩니다. Postman 같은 별도 툴을 사용할 필요가 없습니다.)
이처럼 fastApi 는 코드를 작성하면, 작성한 api 가 문서화되어 표시가 되는 장점이 있으며,
그러한 문서화를 하기 위해서는 위 코드에서 보이는 것처럼 Swagger 에 표시될 내용을 적는 어노테이션 인자값을 설정해야 하는데,
이 역시 장점으로 볼 수 있습니다.
코드는 조금 길어질지언정, 해당 API 가 어떤 역할을 하고 어떤 구조를 갖는지에 대해 개발자 본인이 주석으로 정리할 필요가 없이 자연스럽게 코드 설명이 되므로 알아보기 좋은 코드가 되는 것이죠.
- models
controller 코드 안에서 사용한 model 을 보겠습니다.
입력값과 출력값에 대한 형태 설정의 의미가 있는데,
import typing
import pydantic
# [그룹 모델]
# ----
# (Post 요청 테스트 (application-json))
class PostRequestTestWithApplicationJsonTypeRequestBodyInputVo(pydantic.BaseModel):
request_body_string: str = (
pydantic.Field(
...,
alias="requestBodyString",
description="String Body 파라미터",
examples=["testString"]
)
)
request_body_string_nullable: typing.Optional[str] = (
pydantic.Field(
None,
alias="requestBodyStringNullable",
description="String Nullable Body 파라미터",
examples=["testString"]
)
)
request_body_int: int = (
pydantic.Field(
...,
alias="requestBodyInt",
description="Int Body 파라미터",
examples=[1]
)
)
request_body_int_nullable: typing.Optional[int] = (
pydantic.Field(
None,
alias="requestBodyIntNullable",
description="Int Nullable Body 파라미터",
examples=[1]
)
)
request_body_double: float = (
pydantic.Field(
...,
alias="requestBodyDouble",
description="Double Body 파라미터",
examples=[1.1]
)
)
request_body_double_nullable: typing.Optional[float] = (
pydantic.Field(
None,
alias="requestBodyDoubleNullable",
description="Double Nullable Body 파라미터",
examples=[1.1]
)
)
request_body_boolean: bool = (
pydantic.Field(
...,
alias="requestBodyBoolean",
description="Boolean Body 파라미터",
examples=[True]
)
)
request_body_boolean_nullable: typing.Optional[bool] = (
pydantic.Field(
None,
alias="requestBodyBooleanNullable",
description="Boolean Nullable Body 파라미터",
examples=[True]
)
)
request_body_string_list: typing.List[str] = (
pydantic.Field(
...,
alias="requestBodyStringList",
description="StringList Body 파라미터",
examples=[["testString1", "testString2"]]
)
)
request_body_string_list_nullable: typing.Optional[typing.List[str]] = (
pydantic.Field(
None,
alias="requestBodyStringListNullable",
description="StringList Nullable Body 파라미터",
examples=[["testString1", "testString2"]]
)
)
class Config:
validate_by_name = True
class PostRequestTestWithApplicationJsonTypeRequestBodyOutputVo(pydantic.BaseModel):
request_body_string: str = (
pydantic.Field(
...,
alias="requestBodyString",
description="입력한 String Body 파라미터",
examples=["testString"]
)
)
request_body_string_nullable: typing.Optional[str] = (
pydantic.Field(
None,
alias="requestBodyStringNullable",
description="입력한 String Nullable Body 파라미터",
examples=["testString"]
)
)
request_body_int: int = (
pydantic.Field(
...,
alias="requestBodyInt",
description="입력한 Int Body 파라미터",
examples=[1]
)
)
request_body_int_nullable: typing.Optional[int] = (
pydantic.Field(
None,
alias="requestBodyIntNullable",
description="입력한 Int Nullable Body 파라미터",
examples=[1]
)
)
request_body_double: float = (
pydantic.Field(
...,
alias="requestBodyDouble",
description="입력한 Double Body 파라미터",
examples=[1.1]
)
)
request_body_double_nullable: typing.Optional[float] = (
pydantic.Field(
None,
alias="requestBodyDoubleNullable",
description="입력한 Double Nullable Body 파라미터",
examples=[1.1]
)
)
request_body_boolean: bool = (
pydantic.Field(
...,
alias="requestBodyBoolean",
description="입력한 Boolean Body 파라미터",
examples=[True]
)
)
request_body_boolean_nullable: typing.Optional[bool] = (
pydantic.Field(
None,
alias="requestBodyBooleanNullable",
description="입력한 Boolean Nullable Body 파라미터",
examples=[True]
)
)
request_body_string_list: typing.List[str] = (
pydantic.Field(
...,
alias="requestBodyStringList",
description="입력한 StringList Body 파라미터",
examples=[["testString1", "testString2"]]
)
)
request_body_string_list_nullable: typing.Optional[typing.List[str]] = (
pydantic.Field(
None,
alias="requestBodyStringListNullable",
description="입력한 StringList Nullable Body 파라미터",
examples=[["testString1", "testString2"]]
)
)
class Config:
validate_by_name = True
위와 같습니다.
보신다면 꽤 긴 것을 확인할 수 있습니다.
변수 하나를 설정하더라도 최소 8줄을 소모하는데,
이렇게 한 이유는, 앞서 설명드린 Swagger 문서를 위해서입니다.
위와 같이 Swagger 문서의 API 항목 중 Request 와 Response 부분을 보면 해당 API 를 실행하기 위해 필요한 데이터와, 해당 API 가 반환하는 데이터 형태가 나옵니다.
상세히 확인하기 위해 Schema 탭을 누르면 위와 같이 해당 변수가 Nullable 인지 아닌지, 어떤 의미를 가지고 있고, 어떤 타입의 데이터를 넣어야 하는지에 대해 볼 수 있습니다.
바로 이러한 설명을 입력하기 위한 파라미터를 입력한 것으로,
이 역시 개발자 본인에게는 코드 주석과 같은 도움을 주며, 해당 API 를 사용하는 사용자에게는 명확한 설명을 제공합니다.
models 의 model 파일은 controller 에 따라 무조건 1개가 생성되어야 합니다.
여기서 저만의 개발 방식을 소개드리자면,
저는 모든 API 에 대해 별도의 input, output Object 를 작성합니다.
개발자에 따라서는 Model 을 기반으로 하여 controller 가 따라가는 분이 있으며,
이 경우에는 하나의 Model 을 여러 컨트롤러가 공유해 사용하는데,
저의 경우는 생각이 다릅니다.
코드 재활용성은 물론 중요하지만, 코드 의존도를 낮추고, 코드 응집도를 높이는 것이 클린 코딩의 조건입니다.
고로 저는 API 를 객체지향적으로 보았을 때, 하나의 API 는 함수 기능 + 해당 API 의 입출력 모델로 이루어진다고 봅니다.
즉, 입출력 모델을 합쳐서 하나의 인터페이스가 성립되는 것이기에 API 는 한개당 하나씩의 입출력 모델이 존재해야 한다고 생각합니다.
이렇게 되지 않은 경우,
여러 API 에서 참조중인 모델 클래스가 변경된다면 그 여파가 어디에서 어떻게 퍼질 지 알 수 없는 노릇이며,
때에 따라선 모델 클래스 변경이 불가능해지기 때문에 코드의 유연성이 떨어진다고 판단한 것입니다.
하지만 생각은 다를 수 있기에 강요는 하지 않을 것이며,
어떤 방식을 사용하더라도 위 프로젝트에서는 대응이 가능합니다.
- services
MVC 패턴에 더해서 일반적으로 service 라는 개념을 도입하는 편입니다.
이유는, Controller 가 맡고있는 역할이 다른 역할보다 큰 편이기 때문입니다.
API 의 간판 역할도 하고, Model 과 View 의 조합 역할도 하고, 응답도 보내는 등의 모든 일을 Controller 가 처리하는 것은 이전부터 많은 개발자들이 지적해오던 것입니다.
고로 Controller 는 이름 그대로 API 의 요청을 받고 응답을 받는 역할을 하며,
이에 따른 비즈니스 로직을 처리하는 코드를 떼어내는 것이 기능 분리에 따른 코드 분할에 합리적이라고 할 수 있습니다.
import module_sample_api.models.api_test_model as model
import fastapi
# [그룹 서비스]
# ----
# (Post 요청 테스트 (application-json))
def post_request_test_with_application_json_type_request_body(
request_body: model.PostRequestTestWithApplicationJsonTypeRequestBodyInputVo
):
return fastapi.responses.JSONResponse(
status_code=200,
content=model.PostRequestTestWithApplicationJsonTypeRequestBodyOutputVo(
request_body_string=request_body.request_body_string,
request_body_string_nullable=request_body.request_body_string_nullable,
request_body_int=request_body.request_body_int,
request_body_int_nullable=request_body.request_body_int_nullable,
request_body_double=request_body.request_body_double,
request_body_double_nullable=request_body.request_body_double_nullable,
request_body_boolean=request_body.request_body_boolean,
request_body_boolean_nullable=request_body.request_body_boolean_nullable,
request_body_string_list=request_body.request_body_string_list,
request_body_string_list_nullable=request_body.request_body_string_list_nullable
).model_dump()
)
본 프로젝트에서는 간단히 위와 같이 작성하였습니다.
보다시피 컨트롤러가 받은 요청에 따라 실질적인 처리를 담당하는 역할이며,
컨트롤러는 요청을 받자마자 모든 로직을 서비스에 할당하고, 서비스가 보내는 응답을 그대로 반환합니다.
위 코드에서는 단순히 입력받은 내용을 그대로 반환하는 역할을 수행하지만, 추후 복잡한 로직이 존재한다면 API 명세 역할을 하는 컨트롤러에 작성하는 것 보다, 이 서비스 파일에 작성함으로써 가독성과 코드 응집성을 높일 수 있을 것입니다.
- 이상입니다.
제가 공유드린 Python FastAPI 템플릿의 깃허브 링크로 프로젝트를 확인하실 수 있습니다.
이 설명글에 해당하는 템플릿 프로젝트는 지속적으로 개선중입니다.