카테고리 없음

[FastAPI] Request Method Logic / GET, POST, PUT, DELETE 메소드에 관해 공부하기

우당탕탕코딩일기 2024. 3. 5. 20:04

유데미 FastAPI - The Complete Course 2023 강의를 듣고 정리한 내용입니다.

WHAT FAST API IS

Fast API 란 API 를 빌딩하기 위한 파이썬 웹 프레임워크이다.

 
 

API 는 "Application Programming Interface"의 약자로, 소프트웨어 애플리케이션들이 서로 상호작용하기 위한 규약이나 인터페이스를 의미한다.
우리는 코드를 짤 때 하나의 프로그램만을 사용하지 않는다. 외부 서비스의 기능을 활용하기도 하고,내부 시스템들 간 연결해서 사용하기도 한다.

 
 
 

예를 들어 웹 페이지를 html 로 만들었다고 할 때, 생기는 페이지는 정적인 페이지이다. 이는 user1, user2 가 입장했을 때 같은 화면을 보게 됨을 의미한다. 그러나 실제로는 user1 에 대한 프로필과 user2 에 대한 프로필 화면은 다를 것이다. 이때 사용자들의 정보를 저장하는 서버가 필요하게 되고, 서버와 웹페이지간의 소통할 수 있는 규칙들을 API 로 작성하는 것이다.
 
 
 

웹 프레임워크는 웹 애플리케이션을 개발하기 위한 도구나 라이브러리의 집합이다. 이러한 프레임워크들은 웹 애플리케이션을 구축하는 데 필요한 기본 구조, 기능, 도구들을 제공하여 개발자들이 보다 쉽게 웹 애플리케이션을 만들 수 있도록 도와준다. 그렇다면, "웹 프레임워크를 왜 사용할까요? 내가 직접 개발하면 되지 않나요?" 라는 질문이 나올 수 있다. 미리 개발되어 있는 웹 프레임워크를 사용하면 빠른 개발을 위한 단순화된 방법을 이용할 수 있다. 이것은 수년에 걸친 개발의 결과이고, 이를 따르는 편이 효율적이기 때문에 사용한다.

 

그럼 API 프레임워크 중 우리가 공부할 Fast API 의 장점을 알아보자.

  • Fast API 에서 Fast 가 의미하는 것은 Performance(수행능력)이 빠르다는 의미와 Development (개발)이 빠르다는 의미를 포함한다.
  • Fast API 사용시 버그가 줄어든다.
  • Fast API 는 빠르고 쉽다.
  • Fast API 는 안정성과 유연성을 제공한다.
  • Fast API 는 개발 표준이 존재한다. 오픈 API 표준과 JSON 스키마를 사용한다.

 

관련 문서: https://fastapi.tiangolo.com

 

다음은 웹페이지와 FastAPi 서버가 소통하는 과정을 대략적으로 알아보자.

웹 페이지는 유저와 상호작용을 한다. 이때, FastAPI 서버가 웹 페이지를 위한 비즈니스 로직을 처리하게 된다.
이해하기 쉽게 설명해면, 사용자가 웹페이지와 상호 작용할 때 우리는 웹 서버에 데이터를 "요청" 혹은 "입력"하게 된다. (내 프로필 페이지를 요청하거나, 회원가입을 통해 내 데이터를 입력하게 됨)
이 때 웹 페이지와 상호작용할 때 사용자가 올바른 데이터를 확보할 수 있도록 하는 것이 Fast API 가 하는 일 이다.
이렇게 Fast API 는 비즈니스 로직을 처리하게 되는데, 그 뿐 아니라 웹 페이지도 렌더링한다.

Fast API 는 추가적 도구를 활용하여 풀 스택 응용 프로그램을 만들 수도 있다.

 
 
 
그렇다면 대체 누가 Fast API 를 이용할까?

바로 넷플릭스, 우버, 마이크로소프트와 같은 세계적 기업이 Fast API 를 이용하고 있다.

 
 
 

Fast API Setting

Fast Api 실습 진행을 위해 가상 환경 세팅이 필요하다.
파이썬 가상 환경이란 내 기기(맥북)에 있는 다른 파이썬 환경들과는 분리되어있는 환경을 의미한다.

아래 명령 python3 -m venv fastapienv 으로 가상환경을 생성할 수 있는데 아직 활성화과 된 상태는 아니다. 활성화를 위해선 활성화 명령어를 통한 활성화가 필요하다.

mkdir fastapi-tutorial     // fastapi-tutorial 폴더를 생성한다.
python3 -m venv fastapienv  // fastapienv 라는 이름의 가상환경을 생성한다.

 

활성화를 해보자. 활성화를 위해선

 source [폴더]/bin/activate

명령어를 통해 fastapi 환경을 활성화 시켜야한다.

 
비활성화를 하려면 활성화된 가상 환경에서 deactivate 를 입력하면 된다.
 

이제 가상환경에 Fast API를 설치해보자. 가상환경에 진입한 후, pip install fastapi 를 입력하여 설치하면 된다.

pip install fastapi

그리고 나중 웹 서버를 열기 위해선 uvicorn 을 설치해야한다.

  pip install "uvicorn[standard]"

를 입력한다. 표준 버전의 유비콘을 설치한다는 의미이다.

Fast API Request Method Logic

Fast API 요청 메소드 로직을 공부하기 위해 하나의 작은 프로젝트로 공부를 진행할 예정이다.

BOOK PROGECT

BOOKS 책 목록들은 key : value 값을 부여받는다. title 은 책의 제목을 의미하는 카테고리이고, value 에 실제 인스턴스의 책 제목 value 가 들어간다. author 는 작가를 의미하는 카테고리이고 value 에는 실제 인스턴스의 작가 value 가 들어간다. category 는 책의 장르를 의미하는 카테고리이고, value 에는 실제 인스턴스의 카테고리 value 가 들어간다. 이것을 하나로 묶어서 하나의 책 요소가 되고 이 책들이 모여있는게 BOOKS(책 리스트)이다.

이 책들에 대해 CRUD 연산을 진행할 것이다.
C : Create
R : Read
U : Update
D : Delete
를 의미한다.

웹 페이지는 Fast API 와 상호작용한다. 웹페이지가 FastAPI 에 요청을 보내면 FastAPI 가 응답하게 된다. 예를 들어 웹 페이지가 Fast API 에 Book 2 에 대한 정보를 요청하면 Fast API 가 해당 정보를 전송한다.
 
웹 페이지가 Fast API 에 정보를 요청할 때는 HTTP Request Method 를 사용하여 요청하게 된다.
HTTP 요청 메서드는 웹 페이지가 서버에 통신할 수 있는 방법이다.
 
CRUD 작업에 대해 Fast API 에서는 스웨거 UI 가 이미 구현하고 있다.

각각의 CRUD 작업에 대해 매치되는 HTTP 리퀘스트를 표로 나타내면 이렇다.

CRUD HTTP Request
Create POST
Read GET
Update PUT
Delete DELETE

1. GET Request Method

  • books.py 작성
from fastapi import FastAPI

  app = FastAPI()

  @app.get('/api-endpoint')
  async def first_api():
      return {'message' : 'Hello Eric!'}

 

우리는 api-endpoint 에 접속하면 해당하는 데이터에 접근가능하도록 하고싶다. 엔드포인트는 네트워크에서 흔히 호스트의 의미로 여기서는 서버에 접속한 고객으로 생각하면 된다. 이를 위해 비동기 함수 first_api() 를 작성한다. 우리가 만드는 모든 함수에는 명시적으로 async 를 사용할 것이다.
비동기 함수란 함수를 호출했을 때 실행이 완료 되지 않더라도 호출자에게 리턴, 즉, 제어권을 넘기고 자기 혼자 백그라운드로 작업을 계속 한다. 그리고 어느 순간 작업이 완료 되면 호출자에게 작업이 완료 되었음을 '통보' 해주는 함수이다. 지금은 그냥 비동기 함수를 사용하는 구나 하고 이해하고 넘어가자.
 
 
파이썬에서는 데이터를 Read 하기 위한 Get 함수을 포함한다.이건 HTTP 요청 메소드로 간주된다. 이 함수는 API 엔드포인트를 추가해야한다.함수가 실행될 경로를 지정해야한다는 것을 의미한다.
경로 지정은 함수 위에 데코레이터를 추가해서 할 수 있다.
@app.get('/api-endpoint')
다음 같은 형식을 데코레이터라고 한다. 이렇게 데코레이터를 추가하여 어플리케이션을 시작하면
fast API 로 서버를 연 후, url 127.0.0.1:8000/api-endpoint 를 실행하면 fastAPI 로 부터 first_api() 함수에 대한 응답을 받게 된다.
 

 
우리가 작성한 FastAPI 서버를 열기 위해선 우선 터미널을 연 후, uvicorn 을 실행해야한다. uvicorn 실행을 위해선 아래 명령어를 터미널에 입력한다.

uvicorn books:app --reload 

uvicorn 은 fastAPI 을 설치할 웹 서버를 의미한다. 여기서 books 는 파이썬 파일을 의미한다. 우리가 해당 파일을 books.py 로 지정했기 때문에 books 이지 만약 main.py 로 지정한다면 main:app 으로 했을 것이다. 그 다음 나오는 app 은 FastAPI 이다. app = FastAPI() 와 같은 코드를 위에서 봤을 것이다. 그때 생성된 FastAPI 를 의미한다. 이것 또한 만약 app 이 아니라 merong = FastAPI() 로 생성했다면 uvicorn books:merong --reload 의 형식으로 서버를 열면 된다. 뒤에 붙는 옵션 --reload 는 코드 변화가 있을 때마다 앱을 재부팅하는 것을 의미한다.

이 명령어를 입력하면 URL : 127.0.0.0:8000 으로 서버가 열린다.

 
다음은 모든 책의 정보를 볼 수 있도록 코드를 작성해보자.
모든 책의 정보를 보려면 방금처럼 endpoint 로 url 에 접속해서는 안될 것이다. 예를들어 고객1이 모든 고객의 정보를 볼 수 있다면 아주 큰 문제가 될 것이다. 따라서 데코레이터로 경로를 수정한다.
127.0.0.0:8000/books 에 접속하면
모든 책 리스트를 볼 수 있도록 다음같은 함수를 추가하였다.
 

@app.get('/books')
 async def read_all_books():
      return BOOKS

--reload 모드로 서버를 열었으므로 재구동할 필요 없이 바로 사이트에서 확인할 수 있다.

 

Swagger UI

FastAPI는 기본적으로 Swagger UI를 자동으로 제공하여 개발자가 API를 쉽게 이해하고 테스트할 수 있는 환경을 제공한다.
스웨거 UI 에 진입하면 생성한 모든 API 의 endpoint 를 볼 수 있게 해준다.
 
스웨거 UI 에는 url 에서 endpoint 를 지우고 /docs 로 입장하면 볼 수 있다.

 

Path Parameters

경로 매개 변수란 위치에 근거한 정보를 찾는 방법으로 정의된다.
 
만약 우리가 폴더에서 /Users/codingwithroby/Document/python/fastapi/section1 의 경로에 간다면 섹션1 폴더를 보게 될 것이다.
 
만약 아래 함수가 있다고 가정하자.

@app.get('/books')
 async def read_all_books():
      return BOOKS

웹페이지에 127.0.01:8000/books 과 같은 요청이 있을 때,
우리가 데코레이터를 통해 입력한 경로의 위치에 해당하는 함수를 실행하게 된다.
 
이때 이 /books 와 같은 경로를 정적(static) 경로라고 한다. FastAPI 애플리케이션 내의 책들은 변경될 수 없기 때문이다.
 
 
하지만 FastAPI 에는 동적(dynamic) 경로 매개변수를 만들 수 있다.
예를 들어 127.0.0.1:8000/books/book_one 과 같은 요청이 있을 때,
book_one 과 같은 입력은 사용자가 입력하는 것이다. 우리는 사용자가 입력하는 모든 book1,2,3,4,5, 에 대해 endpoint 를 만들지 않는다.
이를 위해선 사용자가 전달하는 모든 정보를 리턴하는 동적 경로 매개 변수를 사용할 수 있어야 한다. => 이 말은 즉슨, 사용자가 어떤 걸 요청할 지 미리 알 수 없고, 요청하면 해당 데이터를 리턴해주는 동적 경로 매개 변수를 (요청 데이터에 따라 반응하는) 정의해야한다는 것을 의미한다.
 
 
이론으로는 이해하기 어려우니 아래 예시를 살펴보자.
동적 경로 매개 변수 정의를 위해 아래처럼 코드를 작성한다.

@app.get('/books/{dynamic_param}')
async def read_all_books(dynamic_param):
      return {'dynamic_param': dynamic_param}

 
{dynamic_param}가 만약 book_one 로 설정된다면 '/books/book_one' 을 요청하게 될 것이다. 이때 dynamic_param 의 부분에 book_one 이 들어간다면 book_one 에 대한 함수가 호출되고 이에 따라 book_one 정보를 리턴할 수 있을 것이다.
book_two, book_three 여도 마찬가지다.

 

아래 사진을 보고 결과를 예측해보자. 만약 /books/mybook 의 경로로 들어가서 요청한다면 어떻게 될까? http://127.0.0.1:8000/books/mybook 로 들어가면 {'book_title': 'My Faborite Book'} 이 아닌, {'dynamic_param': mybook} 이 리턴되는 것을 알 수 있다.
그 이유로는 우선 /books/mybook 의 경로에 대한 요청은 첫번째 /books/{dynamic_param} 에 포함되므로 만약 mybook 으로 요청을 하더라도 두번째 함수가 호출될 일은 없는 것이다 (첫번째 함수에서 다 처리되기 때문!)
 
그렇기 때문에 항상 정적(static) 또는 작은 API 를 앞에 둬야한다. (큰것에 포함되면 안되게!)

 
 
그렇다면 /books/mybook 과 /books/{dynamic_param} 의 위치를 변경하면 어떤 결과로 나타날까?
아래 사진은 위치를 바꾼 후 mybook 으로 접속했을 때의 화면이다. 우리가 원하는 결과대로 출력된 것을 확인할 수 있다. 이것이 가능한 이유는 만약 위치를 바꾼다면 mybook 에 대한 요청은 위 함수로 먼저 처리된 후, 아래 {dynamic_param} 에 대한 요청은 mybook 이 아닌 요청에 대해 처리할 수 있게 되기 때문이다. ( 정적 매개 변수가 먼저 요청을 잡아냄 )

Query Parameters

쿼리 매개 변수는 물음표 다음에 추가된 매개 변수를 요청하는 것이다.
쿼리 매개 변수는 이름=value 페어의 형태를 띈다.
예를 들어 127.0.0.1:8000/books/?category=math 라는 url 이 있을 때,
books 중 category=math 인 애들을 요청할 수 있다.

만약 127.0.0.1:8000/books/author%20four/?category=science 라는 url 이 있다면
과학 카테고리에 속하는 저자를 찾는 요청이다.
코드로 이해해보자.
/books/ 에 대해 원하는 카테고리의 데이터만 출력하기 위해선, 만약 book에서 'category'의 값이 내가 요청한 쿼리의 category: str 값과 일치하면 배열에 해당 book을 담은 후 한번에 리턴하게 된다.

@app.get('/books/')
async def read_category_by_query(category: str):
    books_to_return = []
    for book in BOOKS:
        if book.get('category').casefold() == category.casefold():
            books_to_return.append(book)
        return books_to_return

작성한 코드를 스웨거 UI 에서 실행하면 다음같은 출력이 나온다. 'books/?category-science' URL 에 대한 GET 요청을 보내서 카테고리 중 science 인 애들에 대해 필터링을 맞게 하는 것을 확인할 수 있다.

 
 

Query Parameters + Path Parameters

이번에는 {book_author} 중에서 category 가 science 인 book 만 출력해보자.(쿼리매개변수
'/books/' 에서의 쿼리 요청 함수를 작성하는 것이 아닌 '/books/{book_author}/' 의 동적 패쓰에서 쿼리 요청을 해야 한다.따라서 아까 동적 매개 변수 함수 작성했듯이 URL 을 설정하고
함수에는 매개변수로 book_author: str 과 category: str 을 전달하면 된다.

@app.get('/books/{book_author}/')
async def read_author_category_by_query(book_author: str, category: str):
    books_to_return = []
    for book in BOOKS:
        if book.get('author').casefold() == book_author.casefold() and \
        book.get('category').casefold() == category.casefold():
            books_to_return.append(book)
    return books_to_return

스웨거 UI 에서 실행하면 다음과 같다.

2. POST Request Method

데이터를 생성하기 위한 요청이다. POST 방식은 GET 은 가지지 않은 추가적인 정보인 body 를 가질 수 있다. 만약 사용자가 Body 를 전송하면 해당 데이터의 생성이 가능하게 한다.

우리의 book 프로젝트에서 바디는 
{'title':'Title Seven' , 'author':'Author Two', 'category':'math} 의 형태를 가진다.

127.0.0.1:8000/books/create_book 과 같은 요청이 있을 때

처리하기 위한 함수로 create_book(new_book=Body()) 를 정의한다.
데코레이터에는 /books/create_book 의 경로를 적는다.
그럼 body 값을 BOOKS 에 append(추가) 할 수 있다.
이 때 주의할 점은 위에 
from fastapi import Body 
코드를 추가하여 Body 를 fastapi 에서 import 해와야한다.
@app.post('/books/create_book')
    async def create_book(new_book=Body()):
        BOOKS.append(new_book)


스웨커 UI 에서 실행하면 이렇다.

주의할 점은 GET 은 Body 를 가질 수 없다. 예를 들어 아래 코드를 실행하면 오류가 뜨게 된다.

@app.get('/books/{book_title}')
async def read_book(book_title: str, new_book=Body()):
    for book in BOOKS:
        if book.get('title').casefold()==book_title.casefold():
            return book

3. PUT Request Method

PUT 메소드는 데이터를 업데이트하는데 사용된다.
PUT 은 추가적 정보인 바디를 가진다.

코드로 살펴보면 예를들어 127.0.0.1:8000/books/update_book 요청이 있을 때
아래 함수에선 body 의 정보를 받아와서 title 이 같은 BOOKS 요소에 대해 해당 바디값으로 책 정보를 변경(업데이트)하는 것을 의미한다.

@app.get('/books/update_book')
async def update_book(updated_book=Body()):
    for i in range(len(BOOKS)):
        if BOOKS[i].get('title').casefold() == updated_book.get('title').casefold():
            BOOKS[i] = updated_book

스웨거 UI 에서 실행해보았다. PUT 메소드에서 Title Six 의 카테고리를 math 에서 history 로 바꿔서 실행하였다. 그 결과 모든 책을 GET 했을 때 변경돼서 나오는 것을 알 수 있다.

4. DELETE Request Method

DELETE 명령어는 data 를 삭제하는데 사용된다.
코드로는 다음처럼 표현한다.
127.0.0.1:8000/books/delete_book/{book_title} 의 URL 로 접근하면 {book_title} 의 book 이 삭제된다.

@app.delete('/books/delete_book/{book_title}')
async def delete_book(book_title:str):
    for i in range(len(BOOKS)):
        if BOOKS[i].get('title').casefold() == book_title.casefold():
            BOOKS.pop(i)
            break

스웨거 UI 에서 실행해보면,

정상적으로 삭제된 것을 확인할 수 있다.

전체 코드

이렇게 기본 메소드 GET, PUT, POST, DELETE 들을 가벼운 book 프로젝트로 익히는 시간을 가졌다.

GET : 데이터 요청
PUT : 데이터 업데이트
POST : 데이터 생성
DELETE : 데이터 삭제

전체 코드는 이렇다.

from fastapi import Body,FastAPI

app = FastAPI()

BOOKS = [
    {'title' : 'Title one', 'author': 'Author One', 'category':'science'},
{'title' : 'Title Two', 'author': 'Author Two', 'category':'science'},
{'title' : 'Title Three', 'author': 'Author Three', 'category':'history'},
{'title' : 'Title Four', 'author': 'Author Four', 'category':'math'},
{'title' : 'Title Five', 'author': 'Author Five', 'category':'math'},
{'title' : 'Title Six', 'author': 'Author Six', 'category':'math'},
]
@app.get('/books')
async def Read_All_Books():
    return BOOKS


@app.get('/books/{book_title}')
async def read_book(book_title: str, new_book=Body()):
    for book in BOOKS:
        if book.get('title').casefold()==book_title.casefold():
            return book

#쿼리 매개변수는 데이터에 대해 필터링을 하는 것.
@app.get('/books/')
async def read_category_by_query(category: str):
    books_to_return = []
    for book in BOOKS:
        if book.get('category').casefold() == category.casefold():
            books_to_return.append(book)
        return books_to_return


@app.get('/books/{book_author}/')
async def read_author_category_by_query(book_author: str, category: str):
    books_to_return = []
    for book in BOOKS:
        if book.get('author').casefold() == book_author.casefold() and \
        book.get('category').casefold() == category.casefold():
            books_to_return.append(book)
    return books_to_return

@app.post('/books/create_book')
async def create_book(new_book=Body()):
    BOOKS.append(new_book)


@app.put('/books/update_book')
async def update_book(updated_book=Body()):
    for i in range(len(BOOKS)):
        if BOOKS[i].get('title').casefold() == updated_book.get('title').casefold():
            BOOKS[i] = updated_book

@app.delete('/books/delete_book/{book_title}')
async def delete_book(book_title: str):
    for i in range(len(BOOKS)):
        if BOOKS[i].get('title').casefold() == book_title.casefold():
            BOOKS.pop(i)
            break
728x90