API 규격서 서버로 도커 컨테이너와 Swagger 를 이용하기

김세준·2023년 8월 19일
0

project-issue

목록 보기
3/3

1. API 규격서를 Swagger + Rest Doc 으로 정의하기

사내에서 완전 처음 시작하는 프로젝트라 API 규격을 처음부터 만들어야 했습니다. 제가 진행하고 있는 또 다른 프로젝트는 Notion 을 이용해서 규격서를 작성하고 있었는데, Notion 규격서는 이래저래 장점보다는 단점이 많았습니다.

  1. 각 상태값은 Enum 으로 관리하는데, 이것이 하나 추가될 때마다 Notion 페이지에 일일이 추가해줘야 됐습니다. 상황이 이렇다보니 정작 코드에는 있지만 규격서에는 없는 상태 코드가 굉장히 많아서 처음 프로젝트에 투입됐을 때, 이것을 파악하는데 꽤 많은 시간이 걸렸습니다.
  2. 노션은 데이터베이스에 응답 필드가 많아질수록 페이지 자체에서 일부를 숨겨버리는데, 확인할 때마다 "더보기" 버튼을 눌러가면서 모든 필드를 확인하기가 귀찮습니다.
  3. 각 API 에 대한 curl, Request Body, Response Body 예시 코드를 개발자가 직접 작성해야합니다.

상황이 이렇다보니 기존 프로젝트와 새로 들어갈 프로젝트는 Spring Rest Docs 와 Swagger UI 를 결합한 방식을 선택하기로 했습니다. 컨트롤러 테스트 코드를 이용해 규격서를 자동 생성하는 Spring Rest Docs 와 오픈 소스인 epages 를 이용하는 방식입니다.

Swagger 는 기본적으로 컨트롤러에 규격서를 위한 애노테이션이 많이 추가된다는 점과 Spring Rest Docs 는 클라이언트 친화적이지 못하다라는 단점을 지니고 있습니다. 그래서 이것들의 장점을 결합한 형태라고 보시면 됩니다. 적용 방법은 워낙 다른 블로그에서도 많이 소개하고 있기 때문에 생략하겠습니다. 아래는 해당하는 오픈소스 링크입니다. swagger 문서를 만들 때 사용되는 .json 혹은 .yaml 파일을 중간에서 만들어주는 역할을 하는 오픈소스입니다.

https://github.com/ePages-de/restdocs-api-spec

2. Swagger API 를 어느 프로젝트에다 두어야 하는가?

사내에서 하나의 프로젝트라고 해도 그 프로젝트 안에 여러 소규모의 스프링 애플리케이션 있는 경우가 대다수입니다. 당장 제가 하고 있는 서비스만 하더라도 그것을 구성하고 있는 스프링 애플리케이션의 가짓수는 꽤 많았습니다.

  • 사용자가 직접 앱과 상호작용할 때 작동되는 애플리케이션
  • 뒤에서 작동되는 특정 요금제에 대한 애플리케이션 - 1
  • 뒤에서 작동되는 특정 요금제에 대한 애플리케이션 - 2
  • 뒤에서 작동되는 특정 요금제에 대한 애플리케이션 - 3 (신규 요금제)
    1. 해당 신규 요금제에 대해서 하드웨어로부터 이벤트를 받는 애플리케이션 -1
    2. 해당 신규 요금제에 대해서 하드웨어로부터 롱 폴링 처리를 담당하는 애플리케이션 -2
  • 사용자에 대한 여러 통계를 다루거나 앱 전체 푸시와 같은 기능을 담당하는 프로젝트

각 애플리케이션은 담당하는 역할이 다르며, API를 호출하는 IP와 Port도 모두 다릅니다. 이런 상황에서 단순히 API 규격서를 추가하기 위해 특정 애플리케이션에 Swagger API 종속성을 부여하고, 권한 관리를 위해 Spring Security를 적용하는 것은 비합리적이라고 생각했습니다.

그래서 현재 사용되고 있는 EC2 인스턴스에 도커를 이용해서 swagger ui 를 분리해서 관리하는게 좋겠다라는 생각을 했습니다.
각각의 애플리케이션에 대해 규격서를 통합할 때도 ePages 를 이용해 openapi3.json 파일을 뽑아낸 다음, Swagger 컨테이너로 이 파일을 배포하기만 하면 되기 때문입니다.

3. Docker Compose 를 이용한 설정

docker pull swaggerapi/swagger-ui

도커 이미지를 다운로드 받고 docker compose.yml 에 다음과 같이 설정해줬습니다.

services:
  swagger:
    image: swaggerapi/swagger-ui
    container_name: swagger
    ports:
      - "8080:8080"
    environment:
      - URLS_PRIMARY_NAME=agent
      - "URLS=[{ url: 'docs/openapi3.yaml', name: 'agent' }]"
      - SUPPORTED_SUBMIT_METHODS=['get']
    volumes:
      - ./docs:/usr/share/nginx/html/docs

docker compose 를 이용할 때 좋은 점은 Swagger UI 에 적용되는 swagger-initialize.js 파일에 대한 내역을 yaml 파일로 간편하게 관리할 수 있다는 점입니다.

url: OpenAPI 대한 경로 또는 URL입니다. 이 경우 Swagger UI Docker 로컬 파일인 docs/openapi3.yaml을 가리킵니다.
name: 사용자가 드롭다운 메뉴에서 볼 수 있는 리스트입니다.
URLS_PRIMARY_NAME=agent: Swagger UI가 로드될 때 기본적으로 표시되어야 하는 API 정의를 지정합니다.
SUPPORTED_SUBMIT_METHODS: 특정 HTTP 메서드에 대해서만 Try it Out 을 적용할 것인지 설정할 수 있습니다.

이렇게 설정해두면 swagger-initialize.js 에 알아서 아래처럼 세팅됩니다.

window.onload = function() {

      //<editor-fold desc="Changeable Configuration Block">
      window.ui = SwaggerUIBundle({
        url: "https://petstore.swagger.io/v2/swagger.json",
        "dom_id": "#swagger-ui",
        deepLinking: true,
        presets: [
          SwaggerUIBundle.presets.apis,
          SwaggerUIStandalonePreset
        ],
        plugins: [
          SwaggerUIBundle.plugins.DownloadUrl
        ],
        layout: "StandaloneLayout",
        queryConfigEnabled: false,
        urls: [{ url: 'docs/openapi3.yaml', name: 'agent' }],
        "urls.primaryName": "agent",
        supportedSubmitMethods: ['get'],
      })

      //</editor-fold>

};

3.1 발생한 문제 1 - 인스턴스와 포트 설정

위 예제에서는 포트가 8080 이지만 제가 사내에서 구축한 포트는 전혀 다른 포트입니다.
그러다보니 도커에서 내, 외부 포트 설정을 해도 인스턴스 자체의 보안 그룹에서 해당 포트를 차단하고 있었기 때문에 접속이 불가능한 상황이이었습니다.
클라우드 인스턴스에서 도커 컨테이너를 사용할 때는 docker-compose.yml 에 있는 포트 설정과 인스턴스에 있는 보안 규칙의 port 가 서로 뚫려있는지 확인하셔야 됩니다.

저는 사내망의 특정 포트를 보안 규칙에 추가함으로써 이 문제를 해결했습니다.

3.2 발생한 문제 2 - CORS 문제

접속까지는 성공했지만 실제 Try it Out 을 실행했을 때 응답 코드가 제대로 나오지 않는 문제였습니다.
openApi3 에 외부 API 설정을 잘 끝마쳤다면 사실상 CORS 문제라고 볼 수 있는데요,
Try It Out 을 눌렀을 때 크롬의 console 을 검사해보시면 빨간색 글씨로 아래처럼 나올 것입니다.

Access to fetch at 'http://xx.xx.xx:8080/id

from origin 'http://xx.xx.xx:8080' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource. 
If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

브라우저에서는 보안적인 이유로 cross-origin HTTP 요청들을 제한합니다.
그래서 cross-origin 요청을 하려면 서버의 동의가 필요한데요, 동의하지 않는다면 브라우저에서 거절합니다.
이러한 것들은 HTTP-header를 이용해서 가능한데, 이를 CORS(Cross-Origin Resource Sharing)라고 부릅니다.

CORS 가 발생하는 이유는 아래와 같습니다.

  1. 서버는 https:// 인데 http:// 에서 요청하는 경우, 프로토콜이 서로 다른 경우
  2. 도메인이 다른 경우
  3. 포트가 다른 경우

제 경우는 3번이었습니다.
예를 들어 제가 요청하고자 하는 스프링 애플리케이션의 컨테이너 포트는 3000 이지만, swagger 컨테이너 포트는 8080 포트라고 가정합니다.
프로토콜과 도메인이 같다고해도 포트가 다르면 당연히 cross-origin 요청을 거부하게 되고 Swagger API 로는 해당 API 를 호출할 수 없게 됩니다.

이것을 해결하려면 스프링 부트에서는 두 가지 방법이 있습니다.
@Controller 위에 @CrossOrigin(origins = "http://x.x.x.x:8080")
처럼 애노테이션을 붙여주는 방법과

WebMvcConfigurer 을 상속받아서 글로벌하게 설정해주는 방법이 있습니다.
저는 API 규격서가 있는 도커 컨테이너에 대한 포트 접근을 사내 IP 로만 지정하고, 각 스프링 부트 애플리케이션은 Swagger 컨테이너에 대해서만 cross origin 을 허용하는 방식이 낫다고 판단되어 글로벌 설정 방식을 적용했습니다.

설정 코드는 단순합니다.

@Configuration
class WebConfig : WebMvcConfigurer {
    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/**")
            .allowedOrigins("http://{원하는 IP}:8080")
            .allowedMethods("GET")
            .allowedHeaders("*")
            .allowCredentials(true)
    }
}

allowedOrigins 에 swagger 컨테이너가 있는 EC2 인스턴스의 퍼블릭 IP와 도커 컨테이너에서 지정한 포트번호만 지정해주시면 끝납니다.
이렇게 하면 API 규격서가 있는 swagger 컨테이너에 대해서만 Cross Origin 을 허용하게 되며, Swagger UI 에서 Try it Out 을 실행할 수 있게 됩니다.
마지막으로 보안을 위해 swagger 컨테이너 포트는 사내망을 제외한 외부 접근을 완전히 차단하는 방식으로 가면 됩니다.

0개의 댓글