들어가며
안녕하세요. 오픈소스컨설팅 Linux 엔지니어 조철진 입니다.
저는 Linux 시스템 엔지니어로 경력을 시작한 지 벌써 만 6년이 되었습니다. 그 동안 다양한 시스템을 구축하고 운영해 보았는데 그렇게 구축한 시스템에 올라가는 자사의 소프트웨어나 벤더사 제품의 API를 사용해보면서 누군가 만들어 놓은 것을 쓸 줄은 알지만 이러한 API는 어떻게 개발하는 것인지 의문을 항상 가져왔습니다.
이후, REST API에 대한 학습를 하고 나서 동작 원리에 대해서는 이해할 수 있었는데 API를 직접 개발해보는 영역까지 공부를 해보다가 “아.. 이것은 개발자의 영역이구나..” 라는 생각으로 항상 도중에 포기하기 십상이었습니다. 참고로 저는 django 프레임워크 학습을 시도해보다 이런 한계를 경험했었습니다.^^
그렇게 학습에 대한 난이도 때문에 개발은 잠시 접어두고 일을 하던 중 FastAPI 라는 프레임워크와 예제 코드를 보게 되었고 이 정도 라면 나도 시도해 볼만 할 것 같은데? 라는 생각이 들어 학습해보고 블로그에도 소개하게 되었습니다!
FastAPI의 특징
FastAPI는 Python을 기반으로 개발된 현대적이고 빠른 웹 프레임워크로, 특히 API 개발에 초점을 맞추어 설계되었고 높은 성능과 직관적인 문법을 제공합니다.
다음은 공식 홈페이지에 소개된 주요 특징입니다.[1]
- Fast: Very high performance, on par with NodeJS and Go (thanks to Starlette and Pydantic). One of the fastest Python frameworks available.
- Fast to code: Increase the speed to develop features by about 200% to 300%. *
- Fewer bugs: Reduce about 40% of human (developer) induced errors. *
- Intuitive: Great editor support. Completion everywhere. Less time debugging.
- Easy: Designed to be easy to use and learn. Less time reading docs.
- Short: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.
- Robust: Get production-ready code. With automatic interactive documentation.
- Standards-based: Based on (and fully compatible with) the open standards for APIs:
FastAPI가 빠른 동작을 보여주는 이유 중 하나는 비동기 식 웹 어플리케이션을 사용하기 때문인데요. [2]
기존의 WSGI(Web Server Gateway Interface)는 파이썬 웹 애플리케이션과 웹 서버 간의 인터페이스로서 사용되었습니다. 하지만 WSGI는 동기적인 프로그래밍 모델을 따르기 때문에 동시 처리에 제약이 있었습니다. Flask나 Django와 같은 기존의 프레임 워크가 WSGI를 기반으로 동기 처리를 지원합니다.
ASGI(Asynchronous Server Gateway Interface)는 비동기 웹 애플리케이션을 위한 파이썬 웹 서버와 웹 프레임워크 간의 인터페이스입니다. ASGI는 비동기 처리와 동시 처리를 지원하여 더욱 향상된 성능을 보여줍니다.
FastAPI와 같은 현대적인 파이썬 웹 프레임워크는 ASGI를 기반으로 비동기 처리를 지원하여 더 빠르고 효율적인 웹 애플리케이션을 개발할 수 있게 해줍니다.
또한, 매력적인 기능 중 하나로 FastAPI는 작성한 API 코드에 대하여 문서와 테스트 자동으로 API 엔드포인트와 파라미터에 대한 샘플을 생성해주는 기능을 제공하는데 이 기능은 Swagger UI 라는 도구와 통합되어 있기 때문에 가능하다고 합니다. Swagger UI는 OpenAPI Specification 이라는 표준을 사용하여 코드를 분석해 API를 시각적으로 문서화하고 테스트할 수 있는 도구 입니다.
FastAPI 설치 및 테스트
pip install fastapi
pip install "uvicorn[standard]"
pip를 통해 fastapi와 uvicorn 웹 서버 패키지를 설치합니다.
uvicorn은 앞서 말씀드린 ASGI를 준수하는 웹 서버입니다.
FastAPI 코드 예시
다음은 FastAPI를 통해 설계해본 API 예시입니다.
코드를 보면 직관적으로 API 엔드포인트에 어떤 함수가 지정되고 결과를 리턴 하는지 확인이 가능합니다.
main.py
from typing import Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float
Pydantic 라이브러리를 사용하여 Item 이라는 데이터 모델을 정의한 부분입니다.
FastAPI는 Pydantic 라이브러리를 활용하여 데이터 유효성 검사와 파싱을 처리하며, 이를 통해 보다 빠르고 효율적으로 개발할 수 있습니다.
@app.get("/") async def read_root(): return "This is root path from MyAPI" @app.get("/items/{item_id}") async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} @app.post("/items/") async def create_item(item: Item): return item @app.put("/items/{item_id}") async def update_item(item_id: int, item: Item): result = {"item_id": item_id, **item.dict()} @app.delete("/items/{item_id}") def delete_item(item_id: int): return {"deleted": item_id}
여기서는 API 엔드포인트와 HTTP 메서드(GET, POST, PUT, DELETE)에 대한 처리를 정의합니다.@app.get("/")
와 같이 데코레이터 라는 문법을 사용하여 해당 URL에 대한 함수를 정의할 수 있습니다.
실행
uvicorn main:app --reload --host 0.0.0.0
위의 코드를 저장한 후, 터미널에서 위의 명령어를 실행하여 FastAPI 애플리케이션을 실행할 수 있습니다.
–reload 옵션을 통해 코드 변경이 일어나면 자동으로 반영되며, –host 옵션으로 사용할 IP를 지정합니다.
API 테스트
곧바로 확인해 볼 수 있는 GET /items/{item_id} API를 브라우저를 테스트해본 결과 입니다.
다른 API 도 테스트 해보기 위해 Swagger UI에 접속해보겠습니다.
브라우저 URL에 /docs 경로를 추가해 접속하면 자동으로 생성된 API 문서를 확인 가능합니다.
POST /items/ API를 테스트 해보겠습니다.
해당 API 탭을 클릭하면 위와 같이 Request의 샘플 형식과, Response의 샘플을 확인 가능합니다.
실행을 위해서 우측 상단의 “Try it out” 버튼을 클릭합니다.
“Try it out” 버튼을 클릭하면 위와 같이 Request body를 생성할 수 있는 UI가 나오며 원하는 파라미터를 구성하고 “Execute” 버튼을 클릭하여 API를 테스트해 볼 수 있습니다.
위에서 구성한 파라미터에 대한 실행 결과가 표시 됩니다.
Responses 메뉴에서는 curl로 샘플을 똑같이 터미널에서 curl로 실행시킬 수 있는 예시도 제공합니다.
FastAPI CRUD
앞서 FastAPI를 통해 어떻게 API를 만드는지 소개해 드렸는데요.
앞선 예시는 실제로 데이터가 저장되고 수정되고 삭제되는 것이 아닌 설계만 해본 것이고, 데이터베이스를 연동하여 CRUD까지 구현해야 실제 개발을 시작할 수 있겠죠. 제가 구현해본 예시를 한 번 소개 드리겠습니다!
pip install sqlalchemy pymysql
pip를 통해 sqlalchemy 와 pymysql 패키지를 설치합니다.
sqlalchemy는 ORM을 지원하기 위한 라이브러리이며,
pymysql은 MySQL 데이터베이스에 접속해 사용할 수 있게 해주는 라이브러리입니다.
프로젝트 구조는 아래와 같이 최상단 app 디렉토리가 있고 그 하위에 소스 파일이 구성되어 있습니다.
MariaDB에서 미리 database를 생성해둡니다.
MariaDB [(none)]> create database myweb;
사용할 테이블의 스키마는 아래와 같으나 app/models.py 파일에서 ORM을 통해 생성됩니다.
MariaDB [(none)]> desc myweb.items; +-------------+-------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------+-------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | name | varchar(30) | YES | | NULL | | | description | varchar(30) | YES | | NULL | | | price | int(11) | YES | | NULL | | +-------------+-------------+------+-----+---------+----------------+ 4 rows in set (0.002 sec)
app/models.py
from sqlalchemy import Column, Integer, String from app.database import Base class Item(Base): __tablename__ = "items" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(30)) description = Column(String(30)) price = Column(Integer)
sqlalchemy를 통해 데이터베이스 모델 정의하는 파일입니다.
app/database.py
from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base DATABASE_URL = "mysql+pymysql://root:1234@localhost:3306/myweb?charset=utf8" engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() def create_tables(): Base.metadata.create_all(bind=engine) print("Tables created successfully")
데이터베이스 연결 및 세션 생성/관리를 담당하는 파일입니다. create_tables()
함수는 데이터베이스 내 테이블을 생성합니다.
app/schema.py
from pydantic import BaseModel class ItemBase(BaseModel): name: str description: str price: int class ItemCreate(ItemBase): pass class Item(ItemBase): id: int class Config: orm_mode = True
Pydantic 스키마를 정의하는 파일로, 데이터 유효성 검사와 요청/응답 데이터 변환을 담당합니다.
여기서 app/models.py 파일에서 이미 데이터베이스 모델을 정의했는데 왜 app/schema.py 에서 유사한 구조를 또 정의하는지 의문이 드실 수 있습니다.
이렇게 하는 이유는 DB를 조회하여 결과를 보여줄 때 필요하지 않은 정보가 포함될 수 있기 때문인데요. 대표적으로 id 와 같은 값은 INSERT 시에는 필요 없지만, 이외의 작업을 할 때 필요하게 됩니다.
Pydantic 모델을 사용하여 응답 데이터에서 필요한 필드만 선택하거나, 필요 없는 필드를 제외하여 클라이언트에게 반환할 정보를 조절하기 위한 의도입니다.
app/crud.py
from sqlalchemy.orm import Session from app.models import Item from app.schema import ItemCreate def get_items(db: Session): return db.query(Item).all() def get_item(db: Session, item_id: int): return db.query(Item).filter(Item.id == item_id).first() def create_item(db: Session, item: ItemCreate): db_item = Item(**item.dict()) db.add(db_item) db.commit() db.refresh(db_item) return db_item def update_item(db: Session, item: Item, updated_item: ItemCreate): for key, value in updated_item.dict().items(): setattr(item, key, value) db.commit() db.refresh(item) return item def delete_item(db: Session, item: Item): db.delete(item) db.commit()
데이터베이스 CRUD 함수들을 포함하고 있습니다. 데이터베이스에 접근하여 item을 생성, 조회, 업데이트, 삭제하는 함수들이 정의되어 있으며 app/main.py 에서 이러한 데이터베이스 조작 함수들을 호출하여 실제 데이터베이스 작업을 수행합니다.
app/main.py
from fastapi import FastAPI, HTTPException, Depends from sqlalchemy.orm import Session from fastapi.responses import RedirectResponse from app import crud, database, models, schema app = FastAPI() def get_db(): db = database.SessionLocal() try: yield db finally: db.close() @app.on_event("startup") def startup_event(): database.create_tables() @app.get("/") async def root(): return RedirectResponse(url="/items/") @app.get("/items/") async def get_items(db: Session = Depends(get_db)): items = crud.get_items(db) return items @app.get("/items/{item_id}") async def get_item(item_id: int, db: Session = Depends(get_db)): item = crud.get_item(db, item_id) if item is None: raise HTTPException(status_code=404, detail="Item not found") return item @app.post("/items/") async def create_item(item: schema.ItemCreate, db: Session = Depends(get_db)): db_item = crud.create_item(db, item) return db_item @app.put("/items/{item_id}") async def update_item(item_id: int, updated_item: schema.ItemCreate, db: Session = Depends(get_db)): db_item = crud.get_item(db, item_id) if db_item is None: raise HTTPException(status_code=404, detail="Item not found") updated_item = crud.update_item(db, db_item, updated_item) return updated_item @app.delete("/items/{item_id}") async def delete_item(item_id: int, db: Session = Depends(get_db)): db_item = crud.get_item(db, item_id) if db_item is None: raise HTTPException(status_code=404, detail="Item not found") crud.delete_item(db, db_item) return {"message": "Item deleted successfully"}
애플리케이션의 주요 코드가 정의되어 있는 파일입니다. FastAPI 애플리케이션을 생성하고, API 엔드포인트 들을 정의하며, 앱 시작 시에 데이터베이스 테이블을 생성하는 역할을 담당합니다.
실행
app 디렉토리의 상위 디렉토리에서 실행합니다.
uvicorn app.main:app --reload --host 0.0.0.0
API 테스트
앞서 테스트 해 본 것처럼 브라우저와 Swagger UI를 통한 테스트가 가능하나 이번엔 curl을 사용하겠습니다.
데이터 저장이 가능해지니 각 메소드에 대하여 데이터를 생성, 조회, 수정, 삭제하는 것을 확인 가능합니다.
POST
curl -X 'POST' \ 'http://192.168.145.211:8000/items/' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "name": "마우스", "description": "초경량 게이밍 마우스", "price": 139000 }'
결과
{“description”:”초경량 게이밍 마우스”,”id”:1,”name”:”마우스”,”price”:139000}
GET
curl -X 'GET' \ 'http://192.168.145.211:8000/items/' \ -H 'accept: application/json'
결과
{“description”:”초경량 게이밍 마우스”,”id”:1,”name”:”마우스”,”price”:139000}
PUT
curl -X 'PUT' \ 'http://192.168.145.211:8000/items/1' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "name": "키보드", "description": "초 고성능 키보드", "price": 200000 }'
결과
{“description”:”초 고성능 키보드”,”id”:1,”name”:”키보드”,”price”:200000}
DELETE
curl -X 'DELETE' \ 'http://192.168.145.211:8000/items/1' \ -H 'accept: application/json'
결과
{ “message”: “Item deleted successfully” }
마무리
FastAPI의 기본 개념과 특징, CRUD 예시를 통해 간단한 REST API 구현 까지 살펴보았습니다.
코드를 작성하면서 막막한 부분은 ChatGPT의 도움을 받기도 했습니다. 하지만, 제가 여기까지 구현해 보았다는 것은 이 글을 보시는 여러분도 충분히 할 수 있다는 것이고 파이썬을 활용한 웹 개발 입문을 고려하는 분들에게 추천 드리고 싶습니다. 이제 FastAPI로 여러분의 프로젝트를 시작해보세요!