Ayden's journal

07. Swagger를 사용한 백엔드 문서화

Swagger는 주로 RESTful 웹 서비스의 개발 및 관리를 지원하는 오픈 소스 프레임워크이다. API와 상호 작용하는 방법을 직관적으로 제공하며, 클라이언트가 API를 테스트하고 이해하기 쉽게 만드는 데 도움을 준다. 프론트엔드 입장에서는 일일이 fetch 해보지 않아도 되기 때문에 굉장히 편리하다고 생각하고 있었다.

나는 프로젝트에서 swagger-jsdoc과 swagger-ui-express를 사용하여 swagger 문서를 제공하고 있다.

 

기본 설정

먼저 swagger 문서의 설정을 options 객체에 담아주고, 이를 swaggerJsdocs에 넣어 swaggerSpec을 뽑아내야 한다. securitySchemes 객체를 통해 인가 과정에서 JWT를 사용하겠다는 것을 옵션에 넣어줄 수 있다. 나는 openapi를 통해 swagger 문서를 작성하는데, apis에 opneapi를 작성할 파일을 지정해주면 라이브러리가 자동으로 변경 사항을 추적하여 반영해준다.

import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
import { version } from '../../package.json';

const options: swaggerJsdoc.Options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'Panda Market API Docs',
      version,
    },
    components: {
      securitySchemes: {
        bearerAuth: {
          type: 'http',
          scheme: 'bearer',
          bearerFormat: 'JWT',
        },
      },
    },
  },
    apis: [`./src/module/**/*.controller.ts`, './src/swagger/*.ts'],
};

const swaggerSpec = swaggerJsdoc(options);

그리고 스웨거 문서로 접근할 수 있도록 라우터를 만들어주어야 한다. 위에서 만들었던 sweggerSpec은 setup의 아규먼트로 사용된다. 이렇게 하면 localhost:3000/docs로 swagger 문서에 접근할 수 있다.

// Swagger/swagger.ts
export const swaggerDocsRoute = Router();

swaggerDocsRoute.use('/', swaggerUi.serve, swaggerUi.setup(swaggerSpec));

// app.ts
app.use('/docs', swaggerDocsRoute);

 

openAPI

OpenAPI는 RESTful API를 정의하고 설명하는 표준 사양으로써, 원래 이름은 Swagger Specification이었다. 주로 API의 구조와 동작을 기술하는 데 사용되며, JSON 또는 YAML 형식으로 작성된다. 나는 우선 가장 기본적인 형태에 대한 스키마를 작성하고, 이를 모아서 각각의 도메인에 대한 스키마를 작성하며, 마지막으로 각 컨트롤러에서 API를 정의하고 있다.

 

basicSchema

우선은 가장 기본이 되는 데이터에 대한 스키마를 작성한다. 그리고 이걸 기반으로 조금 더 복잡한 형태의 ─ 그러나 여전히 여러 곳에서 반복되어 사용되는 ─ 스키마를 만들어나간다.

/**
 * @openapi
 * components:
 *   schemas:
 *     Id:
 *       type: integer
 *       format: int32
 *       minimum: 1
 * 
 *     Uuid:
 *       type: string
 *       format: uuid
 *       example: 'e0e72e5e-8a42-4005-9c56-e6bdee91f149'
 * 
 *     Nickname:
 *       type: string
 *       example: 닉네임
 *       minLength: 1
 *       maxLength: 20
 * 
 *     UrlType:
 *       type: string
 *       format: url
 *       example: 'https://example.com/...'
 *       pattern: ^https?://.*
 * 
 *     Password:
 *       type: string
 *       example: password
 *       minLength: 8
 *       pattern: ^([a-zA-Z0-9!@#$%^&*])+$
 */

BoardWriter 스키마는 nickname과 Uuid 스키마를 조합하여 만들어졌고, CommentWriter 스키마는 allOf 를 사용해 BoardWriter 스키마를 그대로 가져올 수 있었다. 이렇게 작은 스키마부터 만들고 이를 조합하여 조금 더 큰 스키마를 만드는 식으로 중복되는 코드를 효과적으로 줄일 수 있다.

/**
 * @openapi
 * components:
 *   schemas:
 *     BoardWriter:
 *       type: object
 *       properties:
 *         nickname:
 *           $ref: '#/components/schemas/Nickname'
 *         id:
 *           $ref: '#/components/schemas/Uuid'
 * 
 *     CommentWriter:
 *       allOf:
 *         - type: object
 *           properties:
 *             image:
 *               type: string
 *         - $ref: '#/components/schemas/BoardWriter'
 */

쿼리와 같은 스키마도 아래와 같이 만들어놓을 수 있다.

/**
 * @openapi
 * components:
 *   schemas:
 *     Number:
 *       type: number
 *       format: double
 * 
 *     SearchPageQuery:
 *       in: query
 *       name: page
 *       default: 1
 *       schema:
 *         $ref: '#/components/schemas/Number'
 * 
 *     SearchPageSizeQuery:
 *       in: query
 *       name: pageSize
 *       default: 10
 *       schema:
 *         $ref: '#/components/schemas/Number'
 * 
 *     SearchBoardsOrderByQuery:
 *       in: query
 *       name: orderBy
 *       default: recent
 *       schema:
 *         type: string
 *         enum: [like, recent]
 */

 

boardSchema

이렇게 만들어진 기본적인 스키마를 사용하여 각 도메인이 필요로 하는 더 큰 형태의 스키마를 작성해주어야 한다. 나는 요청과 응답에서 중복되어 나타나는 값들을 모아서 BaseResponse와 BaseRequest 스키마를 만들고, allOf 등을 사용해 덧붙이는 식으로 코드 중복을 줄이려 하고 있다.

가령 board 도메인에 관련된 응답 스키마들은 아래와 같은 방식으로 만들어질 수 있다

/**
 * @openapi
 * components:
 *   schemas:
 *     BoardBaseResponse:
 *       type: object
 *       properties:
 *         createdAt:
 *           $ref: '#/components/schemas/DateTime'
 *         updatedAt:
 *           $ref: '#/components/schemas/DateTime'
 *         likeCount:
 *           $ref: '#/components/schemas/Number'
 *         writer:
 *           $ref: '#/components/schemas/BoardWriter'
 *         images:
 *           $ref: '#/components/schemas/UrlType'
 *         content:
 *           $ref: '#/components/schemas/BoardContent'
 *         title:
 *           $ref: '#/components/schemas/BoardTitle'
 *         id:
 *           $ref: '#/components/schemas/Uuid'
 * 
 *     SearchBoardAll:
 *       type: object
 *       properties:
 *         totalCount:
 *           type: number
 *           default: 0
 *         list:
 *           type: array
 *           items:
 *             $ref: '#/components/schemas/BoardBaseResponse'
 *      
 *     SearchBoardSome:
 *       allOf:
 *         - $ref: '#/components/schemas/BoardBaseResponse'
 *         - type: object
 *           properties:
 *             isFavorite:
 *               type: boolean
 *               default: true 
 */

 

컨트롤러

이렇게 스키마들이 다 준비되었다면 컨트롤러에서 각 API 라우터에 대한 openAPI를 작성해주어야 한다. 첫 줄은 라우팅 경로를 선언하며, 어떤 메소드에 반응하는지, 어떤 태그로 묶여있는지 등을 이어서 작성해준다.

/**
 * @openapi
 * '/boards':
 *   get:
 *     tags:
 *     - boards
 *     description: 상품 목록 조회
 *     parameters:
 *       - $ref: '#/components/schemas/SearchPageQuery'
 *       - $ref: '#/components/schemas/SearchPageSizeQuery'
 *       - $ref: '#/components/schemas/SearchBoardsOrderByQuery'
 *       - $ref: '#/components/schemas/SearchKeywordQuery'
 *     responses:
 *       200:
 *         description: Ok
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SearchBoardAll'
 */

만약 다이나믹 라우트라면 중괄호를 사용하여 표기하고, 인가 로직이 필요한 경우라면 security 옵션을 설정해주면 된다. 쿼리 스트링의 경우 in: query로 표기했는데, 다이나믹 라우트의 경로에 사용되는 값은 in: path로 표기해준다.

/**
 * @openapi
 * '/boards/{boardId}':
 *   patch:
 *     tags:
 *     - boards
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - $ref: '#/components/schemas/SearchBoardBoardIdPath'
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *             schema:
 *               $ref: '#/components/schemas/BoardBaseRequest'
 */

 

 

 

이렇게 열심히 엔드포인트마다 작성해주면 아래와 같은 결과를 얻을 수 있다.

 

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기