[FastAPI] 1. GET, POST, PUT, DELETE 등 기본 API 만들고 문서 자동화 해보기

지난 글에 이어서 FastAPI를 이용해서 기본적인 REST API 개발 이야기 두 번째를 이어가보도록 하겠습니다. 

 

우리가 REST API를 개발하면 주요 메소드 종류 5가지가 있는데요. 바로 GET, POST, PUT, DELETE, PATCH 이렇게 5가지 입니다. 물론 이 외에도 OPTIONS가 있지만 이 글에서는 다루지 않겠습니다.

 

 

 

기본 API 만들기 (GET)

REST API의 기본 API는 먼저 GET 메소드부터 시작됩니다. GET 메소드는 서버로부터 리소스를 가져올 때 쓰는 READ API 입니다. FastAPI에서는 아래의 두 가지 방법으로 작성할 수 있습니다.

from fastapi import FastAPI

app = FastAPI()


@app.route('/health')
async def health_check():
    return "OK"
import uuid
from fastapi import FastAPI
from starlette.responses import JSONResponse

app = FastAPI()


@app.get("/{name}")
async def generate_id_for_name(name: str):
    return JSONResponse({
        'id': str(uuid.uuid4()),
        'name': name
    })

첫 번쨰 방법은 route 메소드를 이용하는 방법입니다. route의 메소드를 이용할 경우 오직 String에 해당하는 데이터 타입만을 반환 시킬 수 있습니다. 물론 dict 형도 반환은 가능하지만 dict처럼 Wrapper 자료형에는 반드시 String만 사용해야 합니다. String을 반환하는 메소드나 함수 또한 사용할 수 없습니다.

 

따라서 서버가 제대로 동작하는지 간단히 확인할 수 있는 Health Check용에 적합합니다.

 

GET 메소드에서는 다양한 자료형을 반환할 수 있습니다. JSONResponse 객체를 이용하여 dict 자료형을 JSON으로 변환할 수 있으며 이 때는 json.dumps 메소드가 사용됩니다. 

 

URI에 변수를 사용하고자 하는 경우 중괄호로 매핑해주시면 됩니다.

 

 

 

 

기본 API 만들기 (POST)

POST 메소드는 CREATE에 해당합니다. 서버에 데이터를 저장하고자 하는 경우 해당 메소드를 사용할 수 있는데요. FastAPI에서는 아래의 메소드로 사용하여 구현할 수 있습니다.

from fastapi import FastAPI
from pydantic import BaseModel
from starlette.responses import JSONResponse

app = FastAPI()


class Item(BaseModel):
    user_id: str
    password: str


@app.post("/register")
async def register_item(item: Item):
    dicted_item = dict(item)
    dicted_item['success'] = True

    return JSONResponse(dicted_item)

FastAPI에서 POST 메소드를 작성할 때 사용하는 스키마 디펜던시가 있습니다. 바로 pydantic인데요. 이전에 저도 Python에서 웹 개발했을 때는 스키마 체킹으로 marshmallow를 많이 사용했었습니다. 최근에 Pydantic으로 갈아타면서부터 성능 향상을 많이 체험하였는데요.

 

FastAPI에서는 이를 기본적으로 채택하기 때문에 Pydantic에서 제공하는 BaseModel 상속 없이는 POST 메소드를 작성할 수 없습니다. 

 

BaseModel 상속 없이 구현할 경우 위와 같이 Item 클래스가 pydantic field type이 아니라는 오류를 내뱉습니다.

 

 

 

 

기본 API 만들기 (PUT, PATCH)

PUT과 PATCH 메소드는 UPDATE에 해당하는 메소드입니다. 두 개의 차이는 모든 내용을 바꾸냐(PUT), 아니면 일부의 내용을 바꾸냐(PATCH)의 차이인데요. 어떤식으로 구현할 수 있는지 알아보도록 하겠습니다.

from fastapi import FastAPI
from pydantic import BaseModel
from starlette.responses import JSONResponse

app = FastAPI()


class Item(BaseModel):
    user_id: str
    password: str


@app.post("/register")
async def register_item(item: Item):
    global dicted_item
    dicted_item = dict(item)
    dicted_item['success'] = True

    return JSONResponse(dicted_item)


@app.put("/update")
async def update_item(item: Item):
    dicted_item = {k:v for k, v in dict(item).items()}
    dicted_item['success'] = True
    
    return JSONResponse(dicted_item)

PUT 메소드는 위와 같이 구현하면 쉽게 구현할 수 있습니다. register 포인트를 통해 등록한 회원 정보를 update 포인트를 호출해 요청하면 전부 변경됩니다.

from fastapi import FastAPI
from pydantic import BaseModel
from starlette.responses import JSONResponse

app = FastAPI()


class Item(BaseModel):
    user_id: str
    password: str


@app.post("/register")
async def register_item(item: Item):
    global dicted_item
    dicted_item = dict(item)
    dicted_item['success'] = True

    return JSONResponse(dicted_item)


@app.put("/update")
async def update_item(item: Item):
    dicted_item = {k:v for k, v in dict(item).items()}
    dicted_item['success'] = True
    
    return JSONResponse(dicted_item)


@app.patch("/update")
async def update_item_sub(item: Item):
    for k, v in dict(item).items():
        dicted_item[k] = v
    dicted_item['success'] = True

    return JSONResponse(dicted_item)

자 그럼 PATCH API는 어떨까요? 이렇게 구현하면 될 것 같아 보이지만...

 

불행하게도 위와 같은 422 오류를 내뿜습니다. 왜 그럴까요? PATCH 메소드는 일부를 변경한다고 하여 Password만 변경한다면 이런식으로 오류를 나타내게 됩니다.

 

원인은 바로 Pydantic으로 구현한 Item 클래스에 있습니다.

class Item(BaseModel):
    user_id: str
    password: str

 

해당 클래스를 위와 같이 구현하면 각 멤버 변수는 Required 형태가 됩니다. 그렇게 될 경우 반드시 멤버 변수에 해당하는 값이 반드시 존재해야 하는데, PATCH 메소드에서는 user_id를 입력하지 않았기 때문에 오류가 나타나는 것입니다.

class PatchItem(BaseModel):
    user_id: Optional[str]
    password: Optional[str]
    

@app.patch("/update")
async def update_item_sub(item: PatchItem):
    for k, v in dict(item).items():
        if v:
            dicted_item[k] = v
    dicted_item['success'] = True

    return JSONResponse(dicted_item)

다소 번거롭지만 이런 경우는 PATCH를 위한 클래스를 하나 더 생성해야 합니다. 여기에 각 멤버들에게 Optional 타입을 붙여줌으로써 Pydantic이 해당 값 중 어떤 하나라도 입력되지 않은 경우에라도 API가 허용될 수 있도록 구현해줘야 합니다.

 

 

 

 

 

기본 API 만들기 (DELETE)

삭제 API는 delete 메소드를 이용해서 쉽게 구현할 수 있습니다.

@app.delete("/delete")
async def delete_item():
    dicted_item = None
    return Response(status_code=HTTP_204_NO_CONTENT)

삭제 메소드 사용시에는 보통 반환 값으로 EmptyResponse를 사용합니다. 그러나 Starlette에서는 EmptyResponse 객체를 별도로 제공해주지 않으므로 Response 객체에 204 코드를 사용하여 구현할 수 있습니다.

 

 

 

 

 

API 문서 만들고 다듬기

위의 코드를 작성하고, http://127.0.0.1:8000/redoc 혹은 http://127.0.0.1:8000/docs 로 접속하면 아래와 같이 어느 정도의 문서가 만들어 졌음을 알 수 있습니다.

 

위 화면은 redoc의 모습입니다. 우리는 스키마 모델로  Pydantic을 사용하였는데요. FastAPI는 Pydantic에서 제공하는 BaseModel을 기반으로 하여금 Request, Response 타입의 문서를 자동화 시켜줍니다. 

 

그러나 문서가 기본적으로 타입 등 깔끔하게 보여주고 있긴 하지만 조금 설명이 부족한 모습입니다. 문서에 설명 등을 추가하고, 이를 다듬기 위해서 메타데이터를 사용할 수 있습니다.

@app.get("/{name}", description="사용자 이름을 받고 ID를 생성하는 API 입니다.")
async def generate_id_for_name(name: str):
    return JSONResponse({
        'id': str(uuid.uuid4()),
        'name': name
    })

각 decorator에는 description 파라미터를 제공합니다. 이 파라미터에 간단한 설명을 넣으면...

 

이렇게 API 제목 밑에 간단한 설명이 표시됩니다. 만약 제목도 바꾸고 싶다면...

@app.get("/{name}", name="사용자 ID 생성", description="사용자 이름을 받고 ID를 생성하는 API 입니다.")
async def generate_id_for_name(name: str):
    return JSONResponse({
        'id': str(uuid.uuid4()),
        'name': name
    })

name 파라미터를 사용하여 쉽게 바꿀 수 있습니다.

JSON 데이터를 입력하는 메소드의 경우 요청 파라미터는 나와 있지만 응답 파라미터에 대한 상세 사항은 나와 있지 않습니다. 그 이유는 응답 모델에 대한 클래스를 구현하지 않았기 때문입니다. 

class Item(BaseModel):
    user_id: str
    password: str


class ResponseItem(Item):
    success: bool


@app.post("/register", response_model=ResponseItem)
async def register_item(item: Item):
    global dicted_item
    dicted_item = dict(item)
    dicted_item['success'] = True

    return JSONResponse(dicted_item)

위처럼 Response Model을 정의하면 문서에 샘플이 표시됩니다. 그러나 여전히 부족한 점이 있습니다. 각 파라미터가 어떤 걸 나타내는지 한 단어로 표시하기 어려울 경우 간단한 description이 있다면 좋을 것 같습니다.

class Item(BaseModel):
    user_id: str = Field(title='사용자가 사용할 ID')
    password: str = Field(title='사용자가 사용할 Password')


class ResponseItem(Item):
    success: bool = Field(True, "처리 여부/결과")


class PatchItem(BaseModel):
    user_id: Optional[str] = Field(None, title='사용자가 사용할 ID')
    password: Optional[str] = Field(None, title='사용자가 사용할 Password')

이런 경우에는 Pydantic에서 제공하는 Field를 사용해볼 수 있습니다. 이렇게 하면 타입과 함께 간단한 코멘트가 표시됨으로써 프론트엔드 개발자가 좀 더 상세하게 API 문서를 볼 수 있는 장점을 제공합니다. 

 

 

 

 

마치며...

여기까지 아주 간단한 API를 만들어보면서 문서 자동화 기능에 대한 것에 대해 알아봤습니다. Pydantic의 성능을 가져감과 동시에 FastAPI가 제공하는 API 문서 자동화는 REST API 서버를 쉽고 재밌게 개발해주는 플러스 요소가 되었습니다.

 

더욱이 간단한 문법을 통하여 버그와 오류를 줄이는 역할을 하는 것은 아주 좋은 시너지를 보여주었고, 비동기 처리를 제공함으로써 파이썬에서 웹 프레임워크를 보다 쉽고 강력하게 개발할 수 있었다고 생각하는데요.

 

다음 글 부터는 데이터베이스와 연동을 시작으로 ORM을 이용한 FastAPI 프로그래밍에 대해 좀 더 심층적으로 다뤄보도록 하겠습니다.

 

 

 

comments powered by Disqus

Tistory Comments 0