본문 바로가기
Swagger

Flask에서 Swagger사용

by shulk 2024. 12. 30.

1. Swagger 란?

* RESTful API를 설계, 빌드, 문서화 및 소비하는 데 도움이 되는 오픈 소스 도구 집합, API의 명세와 문서화를 위한 도구이다

* API 문서화를 직접 문서에 적고 하는 것은 매우 귀찮고, API가 수정될때마다 문서를 같이 수정하는 것은 더욱 귀찮아서 이러한 불편함을 줄여주기 위해 사용하고, 프로젝트를 모르는 사람이 프로젝트 테스트 해볼시 이해하기 쉽게 해주는 용도다.

 

2. 설치하고 시작전 알아보기

일단 swagger를 아예 처음 사용해보는거라 완벽한 답은 아직 잘 모르겠는데,ChatGpt도 그렇고 Gemini도 그렇고 구글 블로그들 봐도 대부분 테스트 코드가 아닌 실제 API로 테스트 해보는거로 나와있고 답변해서, Swagger로 실제 API 호출해서 테스트 해볼거다.

 

 (1) swagger 라이브러리 여러 종류가 있겠지만, 일단 flask_restx의 내부에 있는 swagger 사용해본다.

# flask-restx 설치
pip install flask-restx

 

(2) swagger 하기전 이전에는 Blueprints(Flask 애플리케이션을 여러 모듈로 나누어 구성)를 적용했는데 이제 swagger 사용하면 namespace 로 적용해본다.

[Blueprints 쉽게 말하면 app.py인 한파일에 프로젝트 모든 코드 하면 당연 유지보수도 힘들고 길어지고 지져분해니 스프링 MVC처럼]

# app.py의 create_app()메소드에 이전Blueprints 이용했을때 코드
# Blueprints 등록
  from controller.FileController import fileController
  app.register_blueprint(fileController, url_prefix='/api')

 

(3) [Blueprint VS namespace ]를 이용한 프로젝트에 flask-restx 적용 차이  

사진은 namespace 를 했을때 인데 저렇게 엔드포인트가 Namespace 설정한 단위로 구분하기 쉽게 보여지고

만약 Blueprint  했을시 프로젝트에 모든 컨트롤러에 있는 엔드포인트가 한줄로 다 나올테니, 어떤 API 기능 테스트 해보고 싶을때 찾기가 힘들거다.

 

3. 시작하기

(1) Flask-RESTX를 사용하여 API와 네임스페이스를 설정 (appTest.py)

from flask import Flask
from dotenv import load_dotenv
from flask_restx import Api

# .env 파일 로드 (Swagger UI에 환경 변수 표시에 필요)
load_dotenv()

# Flask 애플리케이션 초기화 함수
def create_app():
  app = Flask(__name__)

  # Flask-RESTX 초기화 (Swagger UI 경로 수정)
  api = Api(app, version='1.0', title='My API', description='A simple API', doc='/swagger/')
  app.config['api'] = api

  # Namespace 등록 (예시로fileControllerTest.py 하는거로 app.py랑 동일한 위치에 있다)
  from fileControllerTest import file_namespace
  api.add_namespace(file_namespace, path='/api')

  return app

# Flask 애플리케이션 실행
if __name__ == '__main__':
  app = create_app()
  app.run(debug=True)

 

API 설정 부분 보면 title 이랑 descrition은 스웨거 페이지에 저렇게 제목이랑 설명등 나오게 하는거고 doc는 스웨거 페이지 경로 설정하는거다. 즉 flask이니  http://127.0.0.1:5000/swagger/  하면 스웨거 페이지에 들어가게 된다.

 

(2) 네임스페이스에 리소스 추가

*  기초적인 Flask 예제에서 하는 것처럼 function 형식으로 route시킬 수 없고, Resource에서 파생된 클래스만 이용할 수 있다. 따라서 기존 function은 class로 변경해야 한다.

[즉 이전에는 FileController.py 보면 이렇게 메소드 형식이였는데 이제 각 api들을 class 형식으로 변경해야한다. ]

@fileController.route('/video/split',methods=['POST'])
def videoSplit():

 

예시 FileControllerTest.py 전체 코드

from flask import request, jsonify
import os
from werkzeug.datastructures import FileStorage
from service.FileService import FileService
from flask_restx import Namespace, Resource, fields, reqparse

# File 네임스페이스  
file_namespace = Namespace('File', description='File operations', path='/video')


# /split API 요청 파서 정의
video_split_parser = reqparse.RequestParser()
video_split_parser.add_argument('video', location='files', type=FileStorage, required=True, help='비디오 파일을 업로드 해야합니다')


@file_namespace.route('/split')
class VideoSplitResource(Resource):
  @file_namespace.expect(video_split_parser)
  @file_namespace.doc(description="업로드한 비디오를 분리 합니다")
  def post(self):
    """Splits an uploaded video into segments."""
    args = video_split_parser.parse_args() # 파서로 args 가져오기
    videoFile = args['video'] # FileStorage 객체 가져오기

    # videoFile = request.files['video']

    segments = {
      "test": "성공입니다"
    }
    response = {
      "message": "Video split successfully!",
      "segments": segments
    }

    return response


# ====================  /echo ============================
# 요청 및 응답 모델 정의 (응답 모델은 Swagger UI 표시에만 사용)
echo_request_model = file_namespace.model('EchoRequest', {
  'contents': fields.String(description='게시글 내용을 적어주세요', required=True)
})

@file_namespace.route('/echo')
class EchoResource(Resource):
  @file_namespace.expect(echo_request_model)
  @file_namespace.doc(description="보낸 게시글 내용 그대로 답변 테스트용입니다")
  @file_namespace.response(200, 'Successfully echoed')
  @file_namespace.response(400, 'Validation Error')
  def post(self):
    """보낸 게시글 그대로 답변 API"""
    data = request.get_json().get("contents")
    if not data: # 'contents' not in data 이부분 삭제 data값이 없다면으로 변경.
      return {'message': 'Invalid request: contents field is required.'}, 400

    response = {'result': data}

    return response, 200

 

4. API 테스트 요청해보기

(1) 해당 API 클릭후 Try it out 클릭한다

(2) 이 API의 모델 Request 정보 있으면 참고후 Edit Value에 요청시 보낼 데이터 적고 Excute 클릭한다

 

(3) 결과화면

5.  FileControllerTest.py 해석

이부분은 아까 appTest.py코드에서 네임스페이스 설정한 거기서 이거를 가져와야하니 등록하는거다.

file_namespace = Namespace('File', description='File operations', path='/video')

 

[1] 

@file_namespace.expect()

 API 요청(request)의 구조를 정의하고, Swagger UI에 표시하며,이를 통해 API가 어떤 데이터를 입력받는지 명확하게 나타낼 수 있다. 

이전엔 클라가 보낸 폼타입 데이터를 request.files['video']로 파일 자체 받아왔는데 이제 reqparse를 이용해서 FileStorage 객체로 받아와야한다.

 

  • 프로젝트가 간단하거나 빠르게 개발해야 한다면: reqparse.RequestParser()
  • 유효성 검사가 중요하거나 확장성을 고려한다면: Marshmallow

지금은 빠르게 간단히 해보기 위해 폼데이터를 reqparse.RequestParser() 사용해서 받으나 나중 유효성등 다 따지면 Marshmallow 이용해보기

 

@file_namespace.doc()

이 부분을 보면 이 해당 api 열면 저렇게 설명을 넣을 수 있는 기능이다.

 

def post(self):

""" 업로드한 비디오를 설정한 단위로 분리하는 API"""

이 부분을 보면 이전에는 http 메소드를 밑에 사진처럼 이렇게 설정했으나 스웨거 사용시에는 클래스 이름이 이전 메소드 이름 역활이고,  메소드 이름이 http메소드 설정하는거다.

그리고 """     """ 이 안에 적은 내용이 코드창 밑에 사진보면 해당 API 제목 나오게 하는거다.

video_split_parser = reqparse.RequestParser()
#만약 예시로 다중 이미지 or 비디오 업로드 하고 싶으면 add_argument매개변수에 action='append' 추가 
video_split_parser.add_argument('video', location='files', type=FileStorage, required=True, help='비디오 파일을 업로드 해야합니다')

@file_namespace.route('/split')
class VideoSplitResource(Resource):
  @file_namespace.expect(video_split_parser)
  @file_namespace.doc(description="업로드한 비디오를 분리 합니다")
  def post(self):
    """업로드한 비디오를 설정한 단위로 분리하는 API"""
    args = video_split_parser.parse_args() # 파서로 args 가져오기
    videoFile = args['video'] # FileStorage 객체 가져오기

    # videoFile = request.files['video']

    segments = {
      "test": "성공입니다"
    }
    response = {
      "message": "Video split successfully!",
      "segments": segments
    }

    return response

 

 

[2]

이번에 두번째 api 코드보면

@file_namespace.expect() 

아까는 reqparse.RequestParser() 담았는데 이번엔 file_namespace.model  담는다.

이거는 프로젝트 코드 모르는 사람이 스웨거를 통해 프로젝트 테스트 해보려는데 각 API마다 어떤 데이터 포함해서 요청보내야하는지 모르니, 그 설명을 남겨두는 기능이다.

 

@file_namespace.response(200, '성공시 당신이 보낸 컨텐츠 텍스트 그대로 답변합니다')
@file_namespace.response(400, 'Validation Error')

이거는 응답이 성공했을때, 실패했을때 어떠한 형식 응답 받는지 API 사용자가 API의 동작을 더 잘 이해할 수 있도록  남기는 메세지다.

# ====================  /echo ============================
# 요청 및 응답 모델 정의 (응답 모델은 Swagger UI 표시에만 사용)
echo_request_model = file_namespace.model('EchoRequest', {
  'contents': fields.String(description='게시글 내용을 적어주세요', required=True)
})

@file_namespace.route('/echo')
class EchoResource(Resource):
  @file_namespace.expect(echo_request_model)
  @file_namespace.doc(description="보낸 게시글 내용 그대로 답변 테스트용입니다")
  @file_namespace.response(200, '성공시 당신이 보낸 컨텐츠 텍스트 그대로 답변합니다')
  @file_namespace.response(400, 'Validation Error')
  def post(self):
    """보낸 게시글 그대로 답변 API"""
    data = request.get_json().get("contents")
    if not data: # 'contents' not in data 이부분 삭제 data값이 없다면으로 변경.
      return {'message': 'Invalid request: contents field is required.'}, 400

    response = {'result': data}

    return response, 200