REST API의 의미를 알아보기 위해선 우선 각 단어의 뜻을 알아야한다.
REST란 Representational State Transfer의 약자로 Representational (표현), State (상태), Transfer (전송)의 의미다. 전송은 클라이언트와 서버 사이의 전송을 말한다.
API는 Application Programming Interface의 약자로 프로그램간의 상호작용을 뜻으로 데이터 교환이라고 생각하면 된다.
RESTful한 API는 자원(Resource, 데이터) 중심으로 설계되며, HTTP 프로토콜의 메소드(GET, POST, PUT, DELETE 등)을 사용하여 해당 자원에 대한 CRUD(Create, Read, Update, Delete) 작업을 수행한다.
자원에 대한 작업을 좀 더 풀어서 설명하자면
/users/{userId}
URL를 통해 식별된다.GET/users/{userId}
요청을 사용하고, 사용자를 생성하기 위해 POST/users
요청을 사용한다.REST API는 다양해진 서비스의 형태(모바일 앱, 웹, 와치 앱, 티비 앱)에 대응하기 위해 등장했다. (안드로이드용, IOS 서버용, Homepage용 서버를 따로 구축하지 않아도 된다는 것이다!)
이를 위해 클라이언트의 형태와 상관없이 문자열 기반의 XML, JSON과 같은 데이터를 주고 받게 되었다. 기존에는 서비스가 대부분 웹의 형태였기 때문에 ASP, JSP, PHP 등을 사용했었다.
/users
라는 URL로 표현된다. 특정 사용자를 나타내려면, /users/123
과 같이 고유 식별자(ID)를 URL에 추가해야한다.상태 없음 (Statelessness)
: '상태 없음'은 서버가 클라이언트의 상테 (로그인 상태나 이전 페이지 기록 등)을 기억하지 않는다는 것을 의미한다. 그래서 각 API 요청을 다른 요청과 독립적으로 처리되기 때문에 이전 요청에 대한 정보는 기억하지 않고 모든 요청은 필요한 모든 정보 (사용자 인정 정보와 같은)를 포함해야 한다.
1. 독립적인 요청
2. 서버의 상태 저장 없음.
3. 세션 관리의 부재
표준화된 메소드 사용
: REST API는 HTTP 표준 메소드를 사용하여 자원에 대한 다양한 작업을 수행한다. 여기서 표준 메소드라는 것은 GET, POST, PUT, DELETE를 의미한다. (GET - 조회, POST - 생성, PUT - 업데이트, DELETE - 삭제)
통신을 위한 표현 (Representation)
: REST API는 클라이언트와 서버 간에 데이터를 주고 받을 때, 특정 형식을 사용한다. 이 형식은 API가 '표현'하는 방법을 결정한다.
REST API의 이러한 원칙들은 인터넷 상의 자원에 대해 일관되고 효율적인 접근 방식을 제공한다. 이를 통해 다양한 클라이언트에서 웹 서버와 효과적으로 소통할 수 있게 된다.
자원 (Resource) - URI (/feeds/:feed_id)
: 클라이언트는 위와 같은 URI 형식을 통해 서버에 데이터를 요청한다.
행위 (method, status) - HTTP Method
: POST, GET, PUT, DELETE (HTTP 프로토콜 메서드)
→ CRUD
표현 (Representation)
: 서버에서 클라이언트로의 데이터 전달 방법이다.
ex) XML, JSON, TEXT, RSS
HTTP Methon에 따라 분류해보았다.
/users/{username}
/posts
/posts/{post_id}/comments
/posts
/posts/{post_id}/comments
/users/{username}/follow
/posts/{post_id}
/posts/{post_id}
/users/{username}/follow
jsonify 함수는 Python 데이터를 JSON 형식으로 변환하여 HTTP 응답으로 반환하는 간편한 방법을 제공한다.
Python의 기본 데이터 타입(dict, list 등)을 JSON 문자열로 변환한다. 반환된 JSON 응답에는 기본적으로 application/json
이라는 Content-Type 헤더가 포함된다.
Flask 1.1.x이후 버전에서는 jsonify를 사용하지 않고도 return 문에서 딕셔너리를 직접 반환할 수 있으며, Flask가 자동으로 JSON 응답으로 변환해 준다. 즉, jsonify는 기본적으로 사용되는 로직으로 변경되었기 때문에 사용성의 의미가 없어졌다. 단, 리스트나 객체의 형태 반환은 자동 변환이 안되는 것에 주의한다!
앞에서 계속 얘기한 JSON은 무엇인가?
JSON은 JavaScript Object Notation의 약자로 컴퓨터 간 상호작용을 할 때 자주 사용하는 형식으로 {key:value} 형태로 이루어진 데이터 포맷이다. (lik dict!)
{
"glossary": {
"title": "example glossary",
"GlossDiv": {
"title": "S",
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"para": "A meta-markup language, used to create markup languages such as DocBook.",
"GlossSeeAlso": ["GML", "XML"]
},
"GlossSee": "markup"
}
}
}
}
}
# 필요한 라이브러리 불러오기.
from flask import Flask # 기본
from flask import request
from flask import jsonify
# 1. 전체 게시물을 불러오는 API
@app.route('/api/v1/feeds', methods=['GET'])
def show_all_feeds():
# return jsonify({'result':'success', 'data': {"feed1":"data", "feed2":"data2"}})
return {'result':'success', 'data': {"feed1":"data", "feed2":"data2"}}
# 2. 특정 게시물을 불러오는 API
@app.route('/api/v1/feeds/<int:feed_id>', methods=['GET'])
def show_one_feed(feed_id):
print(feed_id)
return jsonify({'result':'success', 'data': {"feed1":"data"}})
@app.route('/api/v1/feeds', methods=['POST'])
def create_one_feed():
name = request.form['name']
age = request.form['age']
print(name, age)
return jsonify({'result':'success'})
코드를 위처럼 작성한 뒤 우린 아직 실제로 데이터를 가지고 있는 것이 없기 때문에 postman이라는 프로그램을 통해 코드가 작동되는 지만 확인해보려고 한다.
처음 postman 프로그램을 사용하는 거라면 프로그램 설치, 로그인 (회원가입), 새 Workspaces, Collections 생성한 뒤 실행하면
위 사진과 같은 결과를 얻을 수 있다.
datas = [{"items": [{"name": "item1", "price": 10}]}]
@app.route("/api/v1/datas", methods=['GET'])
def get_datas():
return {'datas':datas}
@app.route("/api/v1/datas", methods=['POST'])
def create_data():
request_data = request.get_json()
new_data = {"items": request_data.get("items", [])}
datas.append(new_data)
return new_data, 201
코드 마지막에 사용된 201은 response status code 인데 201 이 외에도 다양한 code는 HTTP response status codes - MDN 사이트에서 확인할 수 있다.
flask-restful api의 경우 외부에서 가져오는 모듈이기 때문에 아래 코드를 실행하여 설치할 수 있다.
> pip install flask-restful
또한 관련 설명이나 예제는 Flask-RESTful사이트를 통해 확인할 수 있다.
from flask_restful import Api, Resource
api = Api(app) # app 넘겨주기 (initializing)
위 코드로 라이브러리를 불러와 flask-restful api를 사용할 수 있다.
from flask import Flask, request
from flask_restful import Resource, Api
app = Flask(__name__)
api = Api(app)
items = [] # 간단한 데이터베이스 역할을 하는 리스트
# Item 리소스관련 코드
class Item(Resource):
# 특정 아이템 조회
def get(self, name):
for item in items:
if item['name'] == name:
return item
return {'message': 'Item not found'}, 404
# 새 아이템 추가
def post(self, name):
for item in items:
if item['name'] == name:
return {'message': f"An item with name '{name}' already exists."}, 400
data = request.get_json()
item = {'name': name, 'price': data['price']}
items.append(item)
return item, 201
# 아이템 업데이트
def put(self, name):
data = request.get_json()
for item in items:
if item['name'] == name:
item['price'] = data['price']
return item
# 아이템이 존재하지 않으면 새로운 아이템을 추가
new_item = {'name': name, 'price': data['price']}
items.append(new_item)
return new_item
# 아이템 삭제
def delete(self, name):
global items
items = [item for item in items if item['name'] != name]
return {'message': 'Item deleted'}
# Item 리소스 등록 → 경로 추가
api.add_resource(Item, '/item/<string:name>')
if __name__ == '__main__':
app.run(debug=True)
이 코드는 한 파일 안에 여러 CRUD 기능을 다 넣은 것이다.
기능별로 파일을 분리해보자!
from flask import Flask
from flask_restful import Api
from resources.item import Item
app = Flask(__name__)
api = Api(app)
api.add_resource(Item, '/item/<string:name>')
if __name__ == '__main__':
app.run(debug=True)
from flask_restful import Resource, request
items = []
class Item(Resource):
# 특정 아이템 조회
def get(self, name):
for item in items:
if item['name'] == name:
return item
return {'message': 'Item not found'}, 404
# 새 아이템 추가
def post(self, name):
for item in items:
if item['name'] == name:
return {'message': f"An item with name '{name}' already exists."}, 400
data = request.get_json()
item = {'name': name, 'price': data['price']}
items.append(item)
return item, 201
# 아이템 업데이트
def put(self, name):
data = request.get_json()
for item in items:
if item['name'] == name:
item['price'] = data['price']
return item
# 아이템이 존재하지 않으면 새로운 아이템을 추가
new_item = {'name': name, 'price': data['price']}
items.append(new_item)
return new_item
# 아이템 삭제
def delete(self, name):
global items
items = [item for item in items if item['name'] != name]
return {'message': 'Item deleted'}
flask-smorest란 REST API를 쉽게 작성할 수 있도록 도와주는 Flask 라이브러리다. Flask-RESTful보다 더 많은 기능과 OpenAPI(Swagger) 문서 자동 생성 기능을 제공한다.
설치는 flaks-restful과 동일하게 외부에서 가져오는 모듈이기 때문에 코드를 실행하여 설치한다.
> pip install flask-smorest
해당 모듈에 대한 설명 아래 사이트들에서 확인할 수 있다.
smorest를 설정을 하려면 몇가지를 해야한다.
from flask import Flask
from flask_smorest import Api
from api import blp
app = Flask(__name__)
# OpenAPI 관련 설정
app.config["API_TITLE"] = "My API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.1.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(blp)
if __name__ == "__main__":
app.run(debug=True)
from marshmallow import Schema, fields
class ItemSchema(Schema):
# id 필드가 직렬화(즉, Python 객체에서 JSON으로 변환) 과정에서만 사용되고, (서버->클라)
# 역직렬화(즉, JSON에서 Python 객체로 변환) 과정에서는 무시된다 (클라->서버)
# 즉, id 값은 서버에서 관리하겠다는 뜻
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
description = fields.Str()
데이터의 구조가 단순할 때는 스키마를 만드는 작업이 귀찮게 느껴질 수도 있다. 하지만 데이터 구조가 복잡해지면 스키마 구조화 덕분에 불필요한 오류가 발생하는 것을 방지할 수 있다.
>pip install marshmallow
Blueprint는 애플리케이션의 특정 기능별로 라우팅, 뷰 함수, 템플릿, 정적 파일 등의 관리가 가능하게 해주는 함수이다. API가 복잡해질수록 관리의 필요성이 증가한다.
from flask import Flask, Blueprint, render_template, request
app = Flask(__name__)
# 첫 번째 블루프린트
my_blueprint = Blueprint('my_blueprint', __name__)
@my_blueprint.route('/hello')
def hello():
return "Hello from my blueprint!"
@my_blueprint.route('/greet/<name>')
def greet(name):
return f"Hello, {name}!"
# 두 번째 블루프린트
another_blueprint = Blueprint('another_blueprint', __name__, url_prefix='/another')
# /another/world
@another_blueprint.route('/world')
def world():
return "Hello, world, from another blueprint!"
# /another/echo
@another_blueprint.route('/echo', methods=['POST'])
def echo():
data = request.json
return f"Received: {data}"
# 블루프린트에 템플릿을 사용하는 예제
@another_blueprint.route('/template')
def using_template():
return render_template('example.html')
# 세 번째 블루프린트
third_blueprint = Blueprint('third_blueprint', __name__, url_prefix='/third')
@third_blueprint.route('/bye')
def goodbye():
return "Goodbye from the third blueprint!"
# 애플리케이션에 블루프린트 등록
app.register_blueprint(my_blueprint)
app.register_blueprint(another_blueprint)
app.register_blueprint(third_blueprint)
if __name__ == "__main__":
app.run(debug=True)
Abort 함수는 API 개발 과정에서 오류 처리를 위해 사용한다. Abort를 사용하여 클라이언트에 오류 상태와 메시지를 전달 가능하다.
Flask와 Flask-Smorest에서 abort 함수는 요청 처리 중에 오류가 발생했을 때 사용된다. 이 함수를 호출하면 특정 HTTP 상태 코드와 함께 응답이 클라이언트로 전송되며, 선택적으로 오류 메시지나 추가 정보를 포함시킬 수 있다.
기본 사용법
: abort 함수는 flask_smorest에서 가져올 수 있으며, 주로 HTTP 상태 코드와 메시지를 인자로 받는다.
from flask_smorest import abort
# 오류 상황에서 abort 호출
abort(404, message="Resource not found")
abort를 사용한 오류 처리
from flask import Flask, abort
app = Flask(__name__)
@app.route('/example')
def example():
# 어떠한 조건에서 오류를 발생시키고 처리
error_condition = True
if error_condition:
abort(500, description="An error occurred while processing the request.")
# 정상적인 응답
return "Success!"
abort를 사용하지 않은 오류 처리
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/example')
def example():
# 어떠한 조건에서 오류를 발생시키고 처리
error_condition = True
if error_condition:
# abort를 사용했을 때랑 다른 부분
error_response = jsonify({"error": "An error occurred while processing the request."})
error_response.status_code = 500
return error_response
# 정상적인 응답
return "Success!"
주요 차이점 및 이유
abort
를 사용하면 함수에서 바로 HTTP 상태 코드를 지정할 수 있다. 반면에 jsonify
와 같은 방법을 사용하면 상태 코드를 별도로 설정해야한다.abort
를 사용하면 오류 메시지를 description
매개변수를 통해 쉽게 전달할 수 있다. jsonify
를 사용하면 응답 본문에 메시지를 추가해야 한다.abort
를 사용하면 이러한 표준을 쉽게 따를 수 있다.abort
를 사용하면 오류 처리 부분이 더 간결하고 가독성이 높아진다. 코드가 명확하게 오류 상황을 처리하고 해당 상태 코드를 클라이언트에게 반환한다.abort 함수 사용 시 주의사항
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from schemas import ItemSchema
# 블루프린트 생성: 'items'라는 이름으로, URL 접두사는 '/items'
blp = Blueprint("items", "items", url_prefix="/items", description="Operations on items")
# 간단한 데이터 저장소 역할을 하는 리스트
items = []
# 'ItemList' 클래스 - GET 및 POST 요청을 처리
@blp.route("/")
class ItemList(MethodView):
@blp.response(200)
def get(self):
# 모든 아이템을 반환하는 GET 요청 처리
return items
@blp.arguments(ItemSchema)
@blp.response(201, description="Item added")
def post(self, new_data):
# 새 아이템을 추가하는 POST 요청 처리
items.append(new_data)
return new_data
# 'Item' 클래스 - GET, PUT, DELETE 요청을 처리
@blp.route("/<int:item_id>")
class Item(MethodView):
@blp.response(200)
def get(self, item_id):
# 특정 ID를 가진 아이템을 반환하는 GET 요청 처리
# next() => 반복문에서 값이 있으면 값을 반환하고 없으면 None을 반환
# next는 조건을 만족하는 첫 번째 아이템을 반환하고, 그 이후의 아이템은 무시합니다.
item = next((item for item in items if item["id"] == item_id), None)
if item is None:
abort(404, message="Item not found")
return item
@blp.arguments(ItemSchema)
@blp.response(200, description="Item updated")
def put(self, new_data, item_id):
# 특정 ID를 가진 아이템을 업데이트하는 PUT 요청 처리
item = next((item for item in items if item["id"] == item_id), None)
if item is None:
abort(404, message="Item not found")
item.update(new_data)
return item
@blp.response(204, description="Item deleted")
def delete(self, item_id):
# 특정 ID를 가진 아이템을 삭제하는 DELETE 요청 처리
global items
if not any(item for item in items if item["id"] == item_id):
abort(404, message="Item not found")
items = [item for item in items if item["id"] != item_id]
return ''
/items/
/items/
/items/<int:item_id>
/items/<int:item_id>
/items/<int:item_id>
http://localhost:5000/items/
주소에 접속하여 API를 테스트.
Swagger UI 문서는 http://localhost:5000/api-docs/
에서 확인할 수 있다.
OAS (Open Api Specification) - Swagger UI
OAS(OpenAPI Specification)는 RESTful 웹서비스를 약속된 규칙에 따라 약속된 규칙에 맞게 API 스펙을 json과 yaml 형식으로 표현한다. 이를 통해, 직접 소스코드를 보거나 추가 문서 필요없이 서비스를 이해할 수 있다.
Swagger는 OpenAPI 스펙을 맞춘 api-docs를 이용하여 html 페이지로 문서화해주는 프레임워크로, RESTful API의 설계 및 문서화에 매우 도움을 준다.
이러한 특징들은 API의 전체 수명 주기를 지원하며, API를 효율적으로 디자인하고 관리하는 데 도움이 된다. Swagger 또는 OpenAPI Specification을 사용하면 API 개발 및 문서화의 표준을 확립하고 팀간 협업을 촉진할 수 있다.
/swagger-ui
를 추가하여 접속한다.http://localhost:5000/swagger-ui
로 접속하면 Swagger UI를 볼 수 있다.Swagger UI는 API 개발 및 테스트를 보다 쉽고 효율적으로 만들어 주는 강력한 툴이다. Flask와 Flask-Smorest를 사용하여 구축한 API에 대한 문서를 자동으로 생성하고, 이를 인터랙티브한 방식으로 탐색할 수 있게 해준다.
책 정보를 관리하는 간단한 RESTful API를 만들어 본다. 이 API는 책의 목록을 보여주고, 새로운 책을 추가하며, 특정 책의 정보를 업데이트하고 삭제할 수 있어야한다.
app.py
)schemas.py
)api.py
)app.py
, schemas.py
, api.py
)에 필요한 코드를 작성한다.app.py - 애플리케이션 설정
: Flask 애플리케이션과 Flask-Smorest API의 기본 설정을 포함한다. Swagger UI 경로도 설정된다.
from flask import Flask
from flask_smorest import Api
from api import book_blp
app = Flask(__name__)
app.config['API_TITLE'] = 'Book API'
app.config['API_VERSION'] = 'v1'
app.config['OPENAPI_VERSION'] = '3.0.2'
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(book_blp)
if __name__ == '__main__':
app.run(debug=True)
schemas.py - 책 스키마 정의
: 책 정보를 위한 스키마를 정의한다. 여기서는 책의 제목과 저자가 필요하며, 책의 고유 ID는 자동으로 설정된다.
from marshmallow import Schema, fields
class BookSchema(Schema):
id = fields.Int(dump_only=True)
title = fields.String(required=True)
author = fields.String(required=True)
api.py - API 엔드 포인트 구현
: 책 목록을 관리하는 API 엔드포인트를 구현한다.
GET, POST, PUT, DELETE 메소드를 사용하여 책 목록을 조회, 추가, 수정, 삭제할 수 있다.
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from schemas import BookSchema
book_blp = Blueprint('books', 'books', url_prefix='/books', description='Operations on books')
books = []
@book_blp.route('/')
class BookList(MethodView):
@book_blp.response(200, BookSchema(many=True))
def get(self):
return books
@book_blp.arguments(BookSchema)
@book_blp.response(201, BookSchema)
def post(self, new_data):
new_data['id'] = len(books) + 1
books.append(new_data)
return new_data
@book_blp.route('/<int:book_id>')
class Book(MethodView):
@book_blp.response(200, BookSchema)
def get(self, book_id):
book = next((book for book in books if book['id'] == book_id), None)
if book is None:
abort(404, message="Book not found.")
return book
@book_blp.arguments(BookSchema)
@book_blp.response(200, BookSchema)
def put(self, new_data, book_id):
book = next((book for book in books if book['id'] == book_id), None)
if book is None:
abort(404, message="Book not found.")
book.update(new_data)
return book
@book_blp.response(204)
def delete(self, book_id):
global books
book = next((book for book in books if book['id'] == book_id), None)
if book is None:
abort(404, message="Book not found.")
books = [book for book in books if book['id'] != book_id]
return ''
/books/
/books/
/books/<book_id>
/books/<book_id>
/books/<book_id>
코드를 복붙해서 하다보니 완벽하게 이해하지는 못하지만 뭔가 살이 더 붙는게 느껴진다...
(아파서 하루동안 아무것도 못했더니 1일치 공부가 밀린 이 부담감🥲)