[Swagger] Swagger 알아보고 기존 프로젝트에 적용해보자.

3월에 팀프로젝트를 시작하는데, 처음으로 백엔드로 참여를 해볼 생각이다.

그런데 기본적인 강의를 제외하고는 준비하고 있는게 없어서, 실질적으로 프로젝트에 도움이 될만한 것을 개인적으로 더 공부해야겠다는 생각이 들었다. 

 

백엔드 하면 API 명세가 가장 중요하고, 모든 프로젝트의 시작이라는 생각에 예전에 프로젝트에서 백엔드 분들이 보여주셨던 Swagger를 공부해보면 갈피가 잡힐 것같아 정리해보았다. 이를 바탕으로 개발스터디 발표도 진행하였다. 

 


✨ Swagger

: REST API를 설계, 빌드, 문서화, 소비하기 위한 오픈 소스 소프트웨어 프레임워크

 

OAS (OpenAPI Specification)

: RESTful API가 어떻게 동작하는지 설명하는 표준 명세서 - YAML, JSON 파일로 작성

→ 💡 이를 시각화해주거나, 코드를 생성해주는 구현체 도구들의 모음이 Swagger!

 

🤔 왜 사용하는거지?

  1. api 명세를 별도의 설치 없이 직관적인 UI로 확인할 수 있다.
  2. Controller에 몇가지 어노테이션을 달기만 해도 API 문서가 만들어진다.
  3. 실제로 서버에 test packet을 보내서 응답을 확인해볼 수 있다
  Swagger Postman
목적 문서화 요청/테스트
사용 시점  설계 단계 + 문서 공유 개발/디버깅
협업 API 계약서 역할  개인/팀 테스트
자동 문서화 X
실제 호출 테스트  가능 더 강력
  - 코드 기반
- Git에 포함
- 서버와 함께 버전관리됨
- 자동 문서 생성
- 앱 기반
- 컬렉션 export로 Git에 올릴 수 있음
- 팀 Workspace 협업 가능
- 코드와 묶여있지 않음


하나의 프로젝트 파일 안에서 코드 형태로 작성하고, URL로 바로 접속이 가능한다는 점에서 Postman보다 압도적으로 협업에 편리하겠다는 생각이 들었다. 


실습

Swagger를 이용하는 방식 두가지

1️⃣ 특정 서버 플랫폼에 종속적으로 사용하는 방식

  • Express 코드 위에 @swagger 주석으로 명세를 작성하는 방식
  • 동기화가 쉬움
  • 서버 언어에 따라 달라짐
  • 코드와 문서가 한 파일에 있음. → 길어지면 파일이 무거워짐!!
const express = require('express');
const router = express.Router();
const conn = require('../mariadb'); 
const {
   join,
   login,
   passwordResetRequest,
   passwordReset
} = require('../controller/UserController');  

router.use(express.json()); 

/**
 * @swagger
 * /users/join:
 * post:
 * summary: 회원가입
 * tags: [Users]
 * description: 새로운 사용자를 등록합니다.
 * requestBody:
 * required: true
 * content:
 * application/json:
 * schema:
 * type: object
 * required:
 * - email
 * - password
 * properties:
 * email:
 * type: string
 * description: 사용자 이메일
 * example: test@gmail.com
 * password:
 * type: string
 * description: 사용자 비밀번호
 * example: password123
 * responses:
 * 201:
 * description: 회원가입 성공
 * 400:
 * description: 잘못된 요청
 */
router.post('/join', join); // 회원가입

/**
 * @swagger
 * /users/login:
 * post:
 * summary: 로그인
 * tags: [Users]
 * description: 이메일과 비밀번호로 로그인합니다.
 * requestBody:
 * required: true
 * content:
 * application/json:
 * schema:
 * type: object
 * properties:
 * email:
 * type: string
 * password:
 * type: string
 * responses:
 * 200:
 * description: 로그인 성공
 * 401:
 * description: 인증 실패
 */
router.post('/login', login); // 로그인

// ... 나머지 라우트들 ...
router.post('/reset', passwordResetRequest); 
router.put('/reset', passwordReset); 

module.exports = router;

 

✔️ 2️⃣ 서버 프로그래밍 언어를 전혀 거치지 않고, swagger api 문서(yml)을 완전히 독립적으로 빼내서 사용하는 방식 ✔️

  • 서버 코드와 문서 완전 분리
  • 서버 언어에 독립적
  • 문서 중심 설계 가능(API 명세 → 서버 개발)
  • 대규모 협업에 유리 - 백엔드/프론트/기획이 문서로 협의 가능
  • 문서와 코드 불일치 가능성

보통 당연시하게 2️⃣ 번의 방법을 사용하는 것같았다. 

파일 하나에 router별로 @swagger주석을 다는 건 실질적으로,, 상식적으로 생각해도 정말 비효율적이고 가독성이 떨어지는 느낌이 있다.

그리고 무엇보다 파일을 따로 나누면 프론트엔드 측에서도 편하게 확인하고 수정할 수 있는 부분에서 훨씬 효율적인 것같아 2번의 방법으로 공부를 해나가는 것을 선택했다. 


1️⃣ 필수 라이브러리 설치

npm install swagger-ui-express yamljs swagger-cli 

2️⃣ app.js에 Swagger 연결

const swaggerUi = require('swagger-ui-express');
const yaml = require('yamljs');
//아까 install했던 것들
const path = require('path');
//node.js 기본 내장 모듈, 파일 경로 찾을 때 에러 방지하고 현재 폴더 위치 정확하게 찍어줌 (윈도우 \, 맥리눅스 / 기호 다른 것 통일해줌)

const swaggerSpec = yaml.load(path.join(__dirname, './swagger.yaml'));
// __dirname : 현재 이 파일이 있는 현재 폴더 위치
// path.join(...) : 현재 위치와 ./swagger.yaml 파일 이름 합쳐서 정확한 절대경로 만들어줌

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
//api-docs로 서버를 들어오면 이것을 보여줘!
//swaggerUi.serve : UI 그리기 위한 핵심 파일들 준비
//swaggerUi.setup(swaggerSpec)) : 준비된 화면에 아까 읽어온 내용(swaggerSpec) 읽어와서 펼쳐주는 함수 

3️⃣ Swagger 폴더 구조 만들기

book-shop
 ├─ app.js
 ├─ swagger/
 │   ├─ index.yaml
 │   ├─ paths/
 │   │   └─ users.yaml
 │   └─ components/
 │       └─ schemas.yaml

 

4️⃣ components/schemas.yaml 작성 - 데이터 구조 정의

components:
  schemas:
    User:
      type: object
      required:
        - email
        - password
      properties:
        email:
          type: string
          example: "test@gmail.com"
          description: "사용자 이메일"
        password:
          type: string
          example: "password123"
          description: "사용자 비밀번호"
    EmailOnly:
      type: object
      required:
        - email
      properties:
        email:
          type: string
          example: "test@gmail.com"
          description : "비밀번호 초기화 요청"
    TokenResponse:
      type: object
      properties:
        email:
          type: string
          example: "test@gmail.com"
          description : "로그인 성공 응답 구조"

5️⃣ paths/users.yaml 작성 - API 명세 정의

paths:
  /users/join:
    post:
      summary: "회원가입"
      description: "새로운 사용자를 등록합니다."
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '../components/schemas.yaml#/components/schemas/User'
      responses:
        201:
          description: "회원가입 성공"
        400:
          description: "잘못된 요청"

  /users/login:
    post:
      summary: "로그인"
      description: "이메일과 비밀번호로 로그인합니다."
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '../components/schemas.yaml#/components/schemas/User'
      responses:
        200:
          description: "로그인 성공 (쿠키에 JWT 저장)"
          content:
            application/json:
              schema:
                $ref: '../components/schemas.yaml#/components/schemas/TokenResponse'
        401:
          description: "로그인 실패"
        400:
          description: "잘못된 요청"

  /users/reset:
    post:
      summary: "비밀번호 초기화 요청"
      description: "이메일이 존재하는지 확인합니다."
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '../components/schemas.yaml#/components/schemas/EmailOnly'
      responses:
        200:
          description: "이메일 존재 확인"
          content:
            application/json:
              schema:
                $ref: '../components/schemas.yaml#/components/schemas/EmailOnly'
        401:
          description: "이메일 없음"
        400:
          description: "잘못된 요청"
    put:
      summary: "비밀번호 재설정"
      description: "새 비밀번호로 변경합니다."
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '../components/schemas.yaml#/components/schemas/User'
      responses:
        200:
          description: "비밀번호 변경 성공"
        400:
          description: "변경 실패"

6️⃣ index.yaml 작성 - 전체 문서의 시작점

openapi: 3.0.0

info:
  title: Book Shop API
  version: 1.0.0
  description: "Book Shop 프로젝트 API 명세서입니다."

paths:
  $ref: './paths/users.yaml#/paths'

components:
  schemas:
    $ref: './components/schemas.yaml#/components'

7️⃣ 여러 파일을 하나로 합치기 - 번들화

지금 구조로는 $ref 로 파일을 나눠놨기 때문에 Swagger UI에서 안정적으로 읽으려면 하나의 파일로 합쳐야 합니다.

package.json에 추가

  "scripts": {
    "start": "node app.js",
    "api-docs": "swagger-cli bundle ./swagger/index.yaml --outfile ./swagger.yaml --type yaml",
    "prestart": "npm run api-docs"
  }

여러 파일로 나뉜 OpenAPI 문서를 하나의 완성된 swagger.yaml 파일로 자동으로 합쳐줌

8️⃣ 서버 실행

npm start

http://localhost:9999/api-docs 으로 Swagger UI 화면 접속

 

 

 

 

 

 


 

[spring] API 문서 자동화 : Spring rest docs + Open API Specification(OAS)

api를 문서화하는 방법은 매우 다양하다. 지금하고 있는 프로젝트의 api 명세 방법으로는 postman을 사용해 왔다. 그런데 쓰면서도 굉장히 불편함을 느꼈다. api를 등록하기 위해 일일히 url을 다 작

velog.io