스터디 자료 2018_01_15

RESTful 파이썬 웹 서비스 제작

스터디 자료

플라스크를 이용한 레스트풀 API 개발

간단한 리스트에서 CRUD 작업을 수행하는 레스트풀 API를 만들기 위해 다음의 과정이 필요합니다.

  • 플라스크 레스트풀 확장을 사용해 플라스크에서 CRUD 연산을 수행하는 레스트풀 API 설계
  • 각 HTTP 메서드가 수행하는 작업 이해
  • 플라스크와 플라스크 레스트풀 확장을 사용해 가상 환경 설정하기
  • 응답용 상태 코드 선언
  • 자원을 나타내는 모델의 생성
  • 딕셔너리를 저장소로 사용
  • 직렬화된 응답에 대한 출력 필드 구성
  • 플라스크 플러거블 뷰의 맨 위에서 풍부한 라우팅으로 작업
  • 자원 라우팅과 엔드포인트에 대한 구성
  • 플라스크 API에 대한 HTTP 요청 작성
  • 플라스크 API와 대화하는 명령 행 도구에서의 작업

간단한 데이터 소스와 대화하는 레스트풀 API 설계

IoT(사물 인터넷)장치에 유선으로 연결된 OLED 디스플레이에 나타낼 메시지를 구성해야 한다고 상상해봅시다. IoT 장치에서는 파이썬 3.5, 플라스크 및 기타 파이썬 패키지를 실행할 수 있다고 합시다. 딕셔너리에서 문자열 메시지를 얻고 이를 IoT 장치에 연결된 OLED 디스플레이에 나타내는 코드를 작성하는 팀이 있을 것입니다. 문자열 메시지로 CRUD 작업을 수행하는 레스트풀 API와 대화할 웹 사이트와 모바일 웹에 대한 작업을 시작해야합니다.

먼저, 주 자원인 메시지에 대한 요구사항을 지정해야 합니다. 메시지에 대해서는 다음 속성 또는 필드가 필요합니다.

  • 정수 식별자
  • 문자열 메시지
  • 메시지가 OLED 디스플레이에 표시돼야 하는 시간을 나타내는 초 단위의 지속 시간
  • 생성 날짜 및 시간 - 컬렉션에 새 메시지를 추가할 때 타임 스탬프가 자동으로 추가될 것이다.
  • “경고”및 “정보”와 같은 메시지 카테고리 설명
  • 메시지가 OLED 디스플레이에 표시된 시간을 나타내는 정수 카운터
  • 메시지가 적어도 한 번 OLED 디스플레이에 표시 됐는지 나타내는 bool 값

다음 표에서는 첫 번째 API 버전에서 지원해야하는 HTTP, 동사, 범위, 의미를 보여줍니다. 각 메서드는 HTTP 동사와 범위로 구성되며 모든 메서드는 모든 메시지와 컬렉션에 대해 잘 정의된 의미를 갖습니다. API에서는 각 메시지마다 고유한 URL이 있습니다.

HTTP 동사 범위 의미
GET 메시지 컬렉션 컬렉션 내에 저장된 모든 메시지를 얻어 이름의 오름차순으로 정렬합니다.
GET 메시지 단일 메시지를 받아옵니다.
POST 메시지 컬렉션 컬렉션 내에 새 메시지를 생성합니다.
PATCH 메시지 기존 메시지의 필드를 업데이트 합니다.
DELETE 메시지 기존 메시지를 삭제합니다.

각 HTTP 메서드가 수행하는 작업 이해

http://localhost:5000/api/messages/가 메시지 컬렉션의 URL이라고 합시다. 이 URL에 숫자를 추가하면 지정된 숫자 값과 같은 id의 특정 메시지를 식별합니다. 예를 들어, http://localhost:5000/api/messages/6 id가 6인 메시지를 식별합니다.

POST Method

다음과 같이 POST HTTP 동사와 http://localhost:5000/api/messages/라는 요청 URL을 사용해서 HTTP 요청을 작성해 보내면 새 메시지를 작성할 수 있습니다. 또한 JSON 키-값 쌍에 필드 이름과 값을 제공해 새 메시지를 만들어야 합니다. 요청의 결과로 서버는 필드에 대해 제공된 값의 유효성을 검사하고 유효한 메시지인지 확인하고나서 메시지 딕셔너리에 이를 유지합니다.

서버는 최근에 추가된 메시지와 함께 201 Created 상태 코드와 JSON으로 직렬화된 JSON 본문을 반환하는데, 이때 메시지 객체에는 서버가 자동으로 생성한 할당 ID가 들어갑니다.

POST http://localhost:5000/api/messages/
GET Method

다음과 같이 GET HTTP 동사와 http://localhost:5000/api/messages/{id} 요청 URL을 사용해서 HTTP 요청을 작성해 보내면 id가 {id}의 자리에 지정된 숫자 값과 일치하는 메시지를 받을 수 있습니다. 예를 들어, 요청 URL인 http://localhost:5000/api/messages/82를 사용하면 서버는 id가 82와 일치하는 게임을 검색합니다. 이 요청의 결과로 서버는 딕셔너리에서 지정된 id를 가진 메시지를 찾을 것입니다.

메시지가 발견되면 서버는 메시지 객체를 JSON으로 직렬화하고 200 OK 상태 코드와 직렬화된 메시지 객체가 들어간 JSON 본문을 반환할 것입니다. 지정된 id 또는 기본 키와 일치하는 메시지가 없으면 서버는 404 Not Found 상태를 반환합니다.

GET http://localhost:5000/api/messages/{id}
PATCH Method

다음과 같이 PATCH HTTP 동사와 http://localhost:5000/api/messages/{id} 요청 URL을 사용해서 HTTP 요청을 작성해 보내면 id가 {id}의 자리에 지정된 숫자 값과 일치하는 메시지의 필드를 하나 이상 업데이트할 수 있습니다. 또한 JSON 키-값 쌍에 업데이트할 필드 이름과 새 값을 제공해야 합니다. 이 요청의 결과로 서버는 필드에 대해 제공된 값의 유효성을 검사하고 지정된 id와 일치하는 메시지에서 이 필드를 업데이트할 것이며 유효한 메시지라면 딕셔너리에 있는 해당 메시지를 업데이트합니다.

서버는 200 OK 상태 코드와 최근 업데이트된 게임이 JSON으로 직렬화된 JSON 본문을 반환할 것입니다. 업데이트할 필드에 유효하지 않은 데이터를 제공하면 서버는 400 Bad Request 상태 코드를 반환합니다. 서버가 지정된 id를 가진 메시지를 찾지 못하면 서버는 404 Not Found 상태만 반환합니다.

PATCH http://localhost:5000/api/messages/{id}
DELETE Method

다음과 같이 DELETE HTTP 동사와 http://localhost:5000/api/messages/{id} 요청 URL을 사용해서 HTTP 요청을 작성해 보내면 {id}의 자리에 지정된 숫자 값과 일치하는 id의 메시지를 제거할 수 있습니다. 예를 들어, http://localhost:5000/api/messages/15라는 요청 URL을 사용하면 서버는 id가 15와 일치하는 메시지를 삭제할 것입니다. 이 요청의 결과로 서버는 딕셔너리에서 지정된 id를 가진 메시지를 찾을 것입니다.

메시지가 있으면 서버는 딕셔너리에게 이 메시지 객체와 관련된 항목을 삭제할 것을 요청하고 204 No Content 상태 코드를 반환합니다. 지정된 id와 일치하는 메시지가 없으면 서버는 404 Not Found 상태를 반환합니다.

DELETE http://localhost:5000/api/messages/{id}

플라스크와 플라스크 레스트풀 확장을 사용해 가상 환경 설정하기

다음의 명령을 실행하고 가상 환경을 만듭니다.

python3 -m venv ~/PythonREST/Flask01

그리고 나서 다음의 명령을 실행하여 가상 환경을 활성화 시켜야합니다.

source ~/PythonREST/Flask01/bin/activate

이렇게 가상환경을 만들고 활성화 시켰다면 pip를 사용해서 다음과 같이 설치해야 합니다.

pip install flask-restful

응답용 상태 코드 선언

from flask import jsonify
from app.exceptions import ValidationError
from . import api


def bad_request(message):
    response = jsonify({'error': 'bad request', 'message': message})
    response.status_code = 400
    return response


def unauthorized(message):
    response = jsonify({'error': 'unauthorized', 'message': message})
    response.status_code = 401
    return response


def forbidden(message):
    response = jsonify({'error': 'forbidden', 'message': message})
    response.status_code = 403
    return response


@api.errorhandler(ValidationError)
def validation_error(e):
    return bad_request(e.args[0])
  • 플라스크나 플라스크 레스트풀 모두에는 각 HTTP 상태 코드에 대한 변수 선언이 들어 있지 않습니다. 위의 예는 flasky(플라스크 기본서의 예제코드)의 HTTP 상태 코드입니다.

모델의 생성

이제, 메시지를 나타내는 데 사용할 간단한 MessageModel 클래스를 만들어야합니다. api 폴더에 새 models.py 파일을 생성합니다.

class MessageModel:
	def __init__(self, message, duration, creation_date, message_category):
	# We will automatically generate the new id
	self.id = 0
	self.message = message
	self.duration = duration 
	self.creation_date = creation_date
	self.message_category = message_category
	self.printed_times = 0
	self.printed_once = False

MessageModel클래스는 생성자, 즉, __init__ 메서드를 선언합니다. 이 메서드는 많은 인자를 받아 이를 사용해 message, duration, creation_date, message_category와 같은 이름의 속성을 초기화 합니다. id 속성은 0, printed_times도 0, print_once는 False로 설정됩니다. API 호출로 발생된 각 새 메시지의 식별자는 자동으로 증가할 것 입니다.

딕셔너리를 저장소로 사용

이제, MessageModel 인스턴스를 메모리 내장 딕셔너리에 유지하는데 사용할 MessageManager 클래스를 생성합니다. API 메서드는 MessageModel 인스턴스를 얻기, 삽입, 업데이트, 삭제하기 위해 MessageManager 클래스의 메서드를 호출합니다.

from flask import Flask
from flask_restful import abort, Api, fields, marshal_with, reqparse, Resource
from datetime import datetime
from models import MessageModel
import status
from pytz import utc


class MessageManager():
    last_id = 0
    def __init__(self):
        self.messages = {}

    def insert_message(self, message):
        self.__class__  .last_id += 1
        message.id = self.__class__.last_id
        self.messages[self.__class__.last_id] = message

    def get_message(self, id):
        return self.messages[id]

    def delete_message(self, id):
        del self.messages[id]

MessageManager 클래스는 last_id 클래스 속성을 선언하고 0으로 초기화합니다. 이 클래스 속성은 딕셔너리에 저장된 MessageModel 인스턴스에 대해 생성돼 할당된 최근 id를 저장합니다. 생성자, 즉, __init__메서드는 message 속성을 생성하고 빈 딕셔너리로 초기화합니다.

  • insert_message: 이 메서드는 message 인자에서 최근에 생성된 MessageModel 인스턴스를 받습니다. 이 코드는 last_id 클래스 속성의 값을 증가시키고 나서 그 결과 값을 수신된 메시지의 id에 대입합니다. 이 코드는 self.__class__를 사용해 현재 인스턴의 타입을 참조합니다. 마지막으로, message를 self.message 딕셔너리에 생성된 id인 last_id로 식별되는 키에 값으로 추가합니다.

  • get_message: 이 메서드는 self.message 딕셔너리에서 검색해야 하는 메시지의 id를 수신합니다. 이 코드는 데이터 소스로 사용 중인 self.message 딕셔너리에서 수신 id와 일치하는 키와 관련된 값을 반환합니다.

  • delete_message: 이 메서드는 self.message 딕셔너리에서 제거해야 하는 메시지의 id를 수신합니다. 이 코드는 데이터 소스로 사용 중인 self.message 딕셔너리에서 수신 id와 일치하는 키-값 쌍을 삭제합니다.

출력 필드 구성

message_fields = {
    'id': fields.Integer,
    'uri': fields.Url('message_endpoint'),
    'message': fields.String,
    'duration': fields.Integer,
    'creation_date': fields.DateTime,
    'message_category': fields.String,
    'printed_times': fields.Integer,
    'printed_once': fields.Boolean
}


message_manager = MessageManager()	

위의 코드는 flask_restful.fields 모듈에 선언된 문자열과 클래스의 키-값 쌍을 가진 message_fields 딕셔너리(dict)를 선언한 것입니다.

  • field.Integer: 정수 값을 출력합니다.

  • fields.Url: URL의 문자열 표현을 생성합니다. 기본적으로 이 클래스는 요청되는 자원에 대해 상대 URI를 생성합니다. 이 코드는 endpoint 인자에 ‘message_endpoint’를 지정합니다. 그러면 클래스는 지정된 엔드포인트 이름을 사용할 것입니다.

  • fields.DateTime: UTC로 형식화된 datetime 문자열을 출력하는데 기본 RFC 822 형식입니다.

  • fields.Boolean: bool 값의 문자열 표현을 생성합니다.

플라스크 플러거블 뷰의 맨 위에서 풍부한 라우팅으로 작업

플라스크 레스트풀은 플라스크 플러거블 뷰(Flask pluggable views) 위에 빌드된 자원을 레스트풀 API의 주요 구성 블록으로 사용합니다. 우리는 flask_restful.Resource 클래스의 서브 클래스를 만들어 지원되는 각 HTTP 동사의 메서드를 선언하면 됩니다. flask_restful.Resource의 서브 클래스는 레스트풀 자원을 나타내므로 메시지 컬렉션을 나타내는 클래스 하나와 메시지 자원을 나타내는 클래스를 선언해야 합니다.

class Message(Resource):
    def abort_if_message_doesnt_exist(self, id):
        if id not in message_manager.messages:
            abort(
                status.HTTP_404_NOT_FOUND, 
                message="Message {0} doesn't exist".format(id))

    @marshal_with(message_fields)
    def get(self, id):
        self.abort_if_message_doesnt_exist(id)
        return message_manager.get_message(id)

    def delete(self, id):
        self.abort_if_message_doesnt_exist(id)
        message_manager.delete_message(id)
        return '', status.HTTP_204_NO_CONTENT

    @marshal_with(message_fields)
    def patch(self, id):
        self.abort_if_message_doesnt_exist(id)
        message = message_manager.get_message(id)
        parser = reqparse.RequestParser()
        parser.add_argument('message', type=str)
        parser.add_argument('duration', type=int)
        parser.add_argument('printed_times', type=int)
        parser.add_argument('printed_once', type=bool)
        args = parser.parse_args()
        if 'message' in args:
            message.message = args['message']
        if 'duration' in args:
            message.duration = args['duration']
        if 'printed_times' in args:
            message.printed_times = args['printed_times']
        if 'printed_once' in args:
            message.printed_once = args['printed_once']
        return message
  • get: 이 메서드는 id 인자로 얻어야 하는 메시지의 id를 수신합니다. 요청된 id를 가진 메시지가 없는 경우에는 self.abort_if_message_doesnt_exist 메서드를 호출해 중단시킵니다. 해당 메시지가 존재하는 경우에는 message_manager.get_message 메서드가 반환한 id와 일치하는 id를 갖는 MessageModel 인스턴스를 반환합니다. get 메서드는 message_fields가 인자로 들어간 @marshal_with 데커레이터를 사용합니다. 이 데커레이터는 MessageModel 인스턴스를 가져와서 message_fields에 지정된 필드 필터링 및 출력 형식을 적용합니다.

  • delete: 이 메서드는 id 인자로 삭제해야 하는 메시지의 id를 받습니다. 요청된 id를 가진 메서드가 없는 경우에는 self.abort_if_message_doesnt_exist 메서드를 호출해 중단시킵니다. 해당 메시지가 존재하는 경우에는 수신된 id를 인자로해서 message_manager.delete_message 메서드를 호출해 데이터 저장소에서 MessageModel 인스턴스를 제거합니다. 그리고 나서 빈 응답 본문과 204 Not Content 상태 코드를 반환합니다.

  • patch: 이 메서드는 id 인자로 업데이트하거나 패치해야 하는 메시지의 id를 받습니다. 요청 된 id의 메시지가 없는 경우에는 self.abort_if_message_doesnt_exist 메서드를 호출해 중단시킵니다. 해당 메시지가 존재하는 경우에는 message_manager.get_message 메서드가 반환한 id와 일치하는 id를 가진 MessageModel 인스턴스를 메시지 변수에 저장합니다. 그 다음 행에서는 parser라는 이름의 flask_restful.reqparse.RequestParser 인스턴스를 생성합니다. RequestParser 인스턴스를 사용하면 이름과 타입을 인자로. 추가하고 나서 요청과 함께 받은 인자를쉽게 파싱할 수 있습니다. 이 코드는 파싱할 이름과 타입을 인자로 해서 parser.add_argument를 4번 호출합니다. 그러고 나서 parser.parse_args 메서드를 호출해 요청의 모든 인자를 파싱하고 반환된 딕셔너리를 args 변수에 저장합니다. 이 코드는 MessageModel 인스턴스의 args 딕셔너리에 새 값을 갖는 모든 속성을 업데이트 합니다. 요청에 특정 필드의 값이 포함되지 않은 경우에는 realted 속성을 변경하지 않습니다. 요청에는 값으로 업데이트할 수 있는 4개의 필드가 포함될 필요가 없습니다. 이 코드는 업데이트 된 message를 반환합니다. patch 메서드는 message_fields를 인자로 가진 @marshal_with 데커레이터를 사용합니다. 이 데커레이터는 MessageModel 인스턴스인 message를 가져와서 message_fields에 지정된 필드 필터링 및 출력 형식을 적용합니다.

이제 메시지 컬렉션을 나타내는 데 사용할 MessageList 클래스를 만들어봅시다.

class MessageList(Resource):
    @marshal_with(message_fields)
    def get(self):
        return [v for v in message_manager.messages.values()]

    @marshal_with(message_fields)
    def post(self):
        parser = reqparse.RequestParser()
        parser.add_argument('message', type=str, required=True, help='Message cannot be blank!')
        parser.add_argument('duration', type=int, required=True, help='Duration cannot be blank!')
        parser.add_argument('message_category', type=str, required=True, help='Message category cannot be blank!')
        args = parser.parse_args()
        message = MessageModel(
            message=args['message'],
            duration=args['duration'],
            creation_date=datetime.now(utc),
            message_category=args['message_category']
            )
        message_manager.insert_message(message) 
        return message, status.HTTP_201_CREATED
  • get: 이 메서드는 message_manager.messages 딕셔너리에 저장된 모든 MessageModel 인스턴스가 있는 리스트를 반환합니다. get 메서드는 message_fields를 인자로 가진 @marshal_with 데커레이터를 사용합니다. 이 데커레이터는 반환된 리스트의 각 MessageModel 인스턴스를 가져와서 message_fields에 지정된 필드 필터링 및 출력 형식을 적용합니다.

  • post: 이 메서드는 parser라는 flask_restful.reqparse.RequestParser 인스턴스를 생성합니다. RequestParser 인스턴스를 사용하면 이름과 타입을 인자로 추가하고 나서 POST 요청으로 받은 인자를 쉽게 파싱해 새 MessageModel 인스턴스를 만들 수 있습니다. 이 코드는 파싱할 3개의 인자로 이름과 타입을 넣어 parser.add_argument를 3번 호출합니다. 그러고 나서 parser.parse_args 메서드를 호출해 요청의 모든 인자를 파싱하고 반환된 딕셔너리를 args 변수에 저장합니다. 이 코드는 딕셔너리의 파싱된 인자를 사용해 message, duration, message_category 속성 값을 지정해 새 MessageModel 인스턴스를 생성하고 message 변수에 저장합니다. creation_date 인자의 값은 시간대 정보가 있는 현재 datetime으로 설정되므로 요청에서 파싱되지 않습니다. 그러고 나서 새 MessageModel 인스턴스(message)로 message_manager.insert_message 메서드를 호출해 이 새 인스턴스를 딕셔너리에 추가합니다. post 메서드는 message_fields를 인자로 가진 @marshal_with 데커레이터를 사용합니다. 이 데커레이터는 최근에 생성돼 지정된 MessageModel 인스턴스인 message를 취해 message_fields에 지정된 필드 필터링 및 출력 형식을 적용합니다. 이 코드는 HTTP 201 Created 상태 코드를 반환합니다.

다음의 표는 HTTP 동사와 범위의 각 조합에 대해 실행할 앞서 만든 클래스의 메서드를 보여줍니다.

HTTP 동사 범위 클래스와 메서드
GET 메시지 컬렉션 MessageList.get
GET 메시지 Message.get
POST 메시지 컬렉션 MessageList.post
PATCH 메시지 Message.patch
DELETE 메시지 Message.delete

자원 라우팅과 엔드포인트에 대한 구성

적절한 메서드를 호출하고 URL 규칙을 정의해 필요한 모든 인자를 전달하기 위해서는 필요한 자원 라우팅 구성을 만들어야 합니다.

app = Flask(__name__)
api = Api(app)
api.add_resource(MessageList, '/api/messages/')
api.add_resource(Message, '/api/messages/<int:id>', endpoint='message_endpoint')


if __name__ == '__main__':
    app.run(debug=True)

위의 코드는 flask_restful.Api 클래스의 인스턴스를 생성하고 이를 api 변수에 저장합니다. api.add_resource 메서드를 호출할 때마다 자원, 특히 flask_restful.Resource 클래스의 이전에 선언된 서브 클래스 중 하나로 URL 경로가 잡힙니다. API에 대한 요청이 있고 URL이 api.add_resource 메서드에 지정된 URL 중 하나와 일치하면 플라스크는 지정된 클래스에 대한 요청에서 HTTP 동사와 일치하는 메서드를 호출합니다. 이 메서드는 표준 플라스크 라우팅 규칙을 따릅니다.

플라스크 API에 대한 HTTP 요청 작성

명령 행 도구에서의 작업 - curl과 httpie
POST
curl -iX POST -H "Content-Type: application/json" -d '{"message": "Welcome to IoT", "duration": 10, "message_category": "Information"}' :5000/api/messages/

위의 요청은 /api/messages/를 지정하므로 ‘/api/messages/’를 적용해 MessageList, post 메서드를 실행합니다. URL 경로에 매개 변수가 없기 때문에 이 메서드는 인자를 받지 않습니다. 요청에 대한 HTTP 동사가 POST이므로 플라스크는 post 메서드를 호출합니다. 새 MessageModel이 딕셔너리에 성공적으로 유지되면, 이 함수는 HTTP 201 Created 상태 코드와 함께 응답 본문에 JSON으로 직렬화된 최근 유지된 MessageModel을 반환합니다.

GET

이제 모든 메세지를 얻는 HTTP 요청을 작성해서 보내봅시다.

curl -iX GET -H :5000/api/messages/

404 Not Found

이제 존재하지 않는 메세지를 얻을 HTTP 요청을 작성해서 보내봅시다.

curl -iX GET :5000/api/messages/800

서버는 id 인자 값으로 800을 사용해서 Message.get 메서드를 실행합니다. 이 메서드는 인자로 받은 id값과 일치하는 id의 MessageModel 객체를 얻는 코드를 실행하게 됩니다. 하지만 지정된 id값을 가진 메시지가 없기 때문에 MessageList.get 메서드의 첫 번째 행은 딕셔너리 키에서 id를 찾지 못하는 abort_if_message_doesnt_exist 메서드를 호출하고 flask_restful.abort 함수가 실행될 것입니다. 따라서 코드는 HTTP 404 Not Found 상태 코드를 반환합니다.

PATCH

이제 기존 메시지를 업데이트하는 HTTP 요청, 즉 두 필드의 값을 업데이트하라는 메시지를 작성해 보내봅시다.

curl -iX PATCH -H "Content-Type: application/json" -d '{"printed_once": "true", "printed_times": 1}' :5000/api/messages/2

지정된 id를 가진 MessageModel 인스턴스가 존재하고 성공적으로 업데이트된다면, 이 메서드 호출은 HTTP 200 OK 상태 코드와 함께 응답 본문 속에 JSON으로 직렬화시킨 최근 업데이트의 MessageModel 인스턴스를 반환합니다.

DELETE

이제 기존 메시지, 특히 마지막으로 추가한 메시지를 삭제하는 HTTP 요청을 작성해봅시다.

curl -iX DELETE :5000/api/message/2

이 요청은 /api/messages/ 뒤에 숫자가 있으므로, ‘/api/messages/'가 적용돼 Message.delete 메서드, 즉 Message 클래스의 delete 메서드를 실행합니다. 지정된 id를 가진 MessageModel 인스턴스가 존재하고 성공적으로 삭제됐다면, 이 메서드 호출은 HTTP 204 No Content 상태 코드를 반환합니다.