ecsimsw

현재 사용 불가능한 API 의 응답을 자동 생성해주는 라이브러리 본문

현재 사용 불가능한 API 의 응답을 자동 생성해주는 라이브러리

JinHwan Kim 2024. 1. 17. 08:38

미리 보기 

컨트롤러에 @ShutDown 어노테이션을 추가하고 임시 응답을 어떻게 전달할지를 지정해주는 것으로 ShutDown 조건에서 해당 컨트롤러 아래 모든 핸들러의 임시 응답을 자동으로 생성해 준다.

 

 

위 예시에서 DailyCountRepository 타입의 빈이 존재하지 않으면 /api/counts 를 GET 요청하는 경우 아래와 같이 응답한다.

 

HTTP status : 503, SERVICE_UNAVAILABLE
Content type : application/json
Message : This API is currently unavailable.

 

아래 사용 방법이나 버전, 기능은 현재 글을 쓰는 첫 배포 시점을 기준으로 한다.

최신 변경 사항은 https://github.com/ecsimsw/api-shutdown 에서 확인할 수 있다. 

 

제작 배경

애니메이션 명대사를 읊어주는 외국 공개 API 서비스을 보고 한국 애니메이션을 포함한 한국어 버전이 있었으면 재밌겠다는 생각에 한국어 버전 API 를 제작하여 배포했다. (anime-kr)

 

사용자 요청 수를 카운트하고 있는데 공개 5일만에 12만 개, 어제는 만 칠천 개의 요청 들어왔을 정도로 생각보다 요청이 쏠쏠하다. 아마 주변 친구들이 죄다 개발자이고 공개도 Github 에서 했기에 다들 참지 못하고 부하테스트부터 돌려본 게 아닐까 생각해 본다. 이런 상황이다보니 요청 부하 분산, Rate limit 이나 서비스 장애 대비에 더 신경 쓰고 있고 오히려 재밌다.

 

 

배포 구조는 위와 같은데 게이트웨이는 LB 중 WAS가 특정 조건으로 응답을 수신하지 못하게 되면 일정 시간 동안 이 WAS를 unavailable 으로 표시한다. 그리고 모든 WAS가 다운되는 경우 요청을 돌려 Backup server 에서 임시 처리하게 된다. 

 

이때 backup server 는 다른 인프라 없이 단일 WAS 하나로 최소한의 API만 제공한다. 그렇다고 이미 정의된 Docs 의 API 를 포기할 순 없다. Not found 가 아니라 적절한 응답 Status 와 함께 현재 사용 불가능한 상태임을 알려야 한다.

 

가장 쉬운 방법은 Main server 와 동일한 핸들러를 정의하고 둘 중 하나만 빈으로 등록하면 될 것 같다. 그러면 코드도 더러워지고 매 핸들러마다 응답 방법도 반복될 것이다. Main 의 API 가 추가되면 또 이를 관리해줘야 할 것이고 아주 하드 코딩 그 자체다. 

 

@ConditionalOnBean(DailyCountRepository.class)
@RestController
class AccessCountApi {

    private final DailyCountRepository dailyCounts;

    @GetMapping("/api/counts")
    public ResponseEntity<DailyCount> count() {
        var result = dailyCounts.get();
        return ResponseEntity.ok(result);
    }
}

@ConditionalOnMissingBean(DailyCountRepository.class)
@RestController
class AccessCountForBackupApi {

    @GetMapping("/api/counts")
    public ResponseEntity<String> count() {
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("현재 사용 불가");
    }
}

 

ShutDown 이런 상황에서 만들어졌다.

이 문제를 어노테이션 하나로 해결하려 한다.

 

@ShutDown(
    conditionOnMissingBean = DailyCountRepository.class,
    message = "This API is currently unavailable.",
    status = HttpStatus.SERVICE_UNAVAILABLE,
    contentType = MediaType.APPLICATION_JSON_VALUE
)

 

사용 방법 

1. 의존성 추가

 

build.gradle 에 라이브러리를 import 한다. 

Javax 를 사용하는 Spring boot 2.X.X, JDK 17 미만의 경우 javax-X.X.X 를 이용한다. 

 

repositories {
    maven { url 'https://jitpack.io' }
}

dependencies {
    implementation 'com.github.ecsimsw:api-shutdown:0.0.5'
    // implementation 'com.github.ecsimsw:api-shutdown:javax-0.0.5'       // for Versions lower than java17
}

 

 

2. 라이브러리 사용 활성화

 

@EnableShutDown 로 라이브러리 사용을 활성화한다.

 

@EnableShutDown
@SpringBootApplication
public class MyApplication {}

 

 

3. @ShutDown 정의

 

@ShutDown(
    conditionOnActiveProfile = "backup",
    message = "This API is currently unavailable.",
    status = HttpStatus.SERVICE_UNAVAILABLE,
    contentType = MediaType.APPLICATION_JSON_VALUE
)
@RestController
class ShutDownController {

    @GetMapping("/api/shutDownGet")
    public ResponseEntity<String> hi() {
        return ResponseEntity.ok("Hi");
    }

    @PostMapping("/api/shutDownPost")
    public ResponseEntity<String> hey() {
        return ResponseEntity.ok("Hey");
    }
}

 

핸들러는 @GetMapping, @PutMapping, @DeleteMapping, @PatchMapping, @RequestMapping 를 지원하고 여러 UrlPath, HttpMethod 를 지정할 수 있다.

 

@RequestMapping(
    value = {
        TestApis.API_TO_BE_SHUTDOWN_MULTIPLE_MAPPINGS_1,
        TestApis.API_TO_BE_SHUTDOWN_MULTIPLE_MAPPINGS_2
    },
    method = { RequestMethod.GET, RequestMethod.POST }
)
public ResponseEntity<String> api1() {
    return ResponseEntity.ok(DEFAULT_NORMAL_MESSAGE);
}

 

정의된 조건에서 핸들러의 정의대로 API 호출 시 ShutDown 에 표시된 응답이 오는 것을 확인할 수 있다.

 

추가 기능

1. Default 설정 변경 - Global configuration


ShutDownGlobalConfig 빈을 등록하는 것으로 기본값을 설정할 수 있다. @ShutDown 에 직접 값을 입력하는 경우 해당 값이 우선시되고, 값을 입력하지 않는 경우 Global config 에 정의된 값으로 설정된다.

 

@EnableShutDown
@Configuration
public class ShutDownConfig {

    @Bean
    public ShutDownGlobalConfig shutDown() {
        return new ShutDownGlobalConfigBuilder()
            .message("Global config message")
            .status(HttpStatus.TEMPORARY_REDIRECT)
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .build();
    }
}

 

 

2. 설정 가능한 ShutDown 조건들

 

- conditionOnActiveProfile : 정의된 Profile 들이 모두 Activate 되어 있는 경우 ShutDown 된다.
- conditionOnProperties : 정의된 Property 들이 모두 Application property 로 등록되어 있는 경우 ShutDown 된다.
- conditionOnBean : 정의된 Type 들이 모두 빈으로 등록되어 있는 경우 ShutDown 된다.
- conditionOnMissingBean : 정의된 Type 들이 모두 빈으로 등록되어 있지 않은 경우 ShutDown 된다.
- force : 앞선 어떤 조건들과 상관없이 true 면 해당 핸들러들은 모두 ShutDown 된다.

 

public @interface ShutDown {

    // Shut down when all the profiles are activated
    String[] conditionOnActiveProfile() default {};

    // Shut down when all the properties exist
    String[] conditionOnProperties() default {};

    // Shut down when all the beans exist
    Class<?>[] conditionOnBean() default {};

    // Shut down when all the beans not exist
    Class<?>[] conditionOnMissingBean() default {};

    // Force shutdown ignoring other conditions
    boolean force() default false;
}

 

 

3. ShutDown filter

 

BeanFactoryPostProcessor 가 실행되는 과정에서 ShutDown 조건을 확인하고 이에 부합하면 Filter 를 생성하게 된다.

해당 Filter 의 Order와 Name 설정은 ShutDownGlobalConfig 으로 변경할 수 있다.

 

@Configuration
public class ShutDownConfig {

    @Bean
    public ShutDownGlobalConfig shutDown() {
        return new ShutDownGlobalConfigBuilder()
            .filterOrder(1)
            .filterPrefix("myShutDownFilter")
            .build();  
    }
}

 

추가 예정 사항

1. @ShutDown 핸들러

 

컨트롤러만이 아닌 핸들러를 기준으로도 어노테이션이 동작할 수 있도록 한다. 이때 핸들러의 설정이 더 우선 시 되고, 컨트롤러에 붙은 설정은 핸들러에 정의되지 않은 핸들러에 적용되는 기본 설정으로 한다. 컨트롤러에 붙은 @ShutDown 없이 핸들러에 붙은 조건만으로도 동작한다.

 

@ShutDown(
  message = "서버 상태 이상으로 현재 사용 불가능한 API 입니다.",
  status = HttpStatus.INTERNAL_SERVER_ERROR
)
@RestController
class ShutDownController {

    @ShutDown(
      message = "hi는 더이상 사용되지 않습니다.",
      status = HttpStatus.PERMANENT_REDIRECT
    )
    @GetMapping("/api/shutDownGet")
    public ResponseEntity<String> hi() {
        return ResponseEntity.ok("Hi");
    }

    @PostMapping("/api/shutDownPost")
    public ResponseEntity<String> hey() {
        return ResponseEntity.ok("Hey");
    }
}

 

2. 보다 구체적인 ShutDown 조건, Filter match 조건을 제공한다.

 

마무리 

내 경우처럼 특정 조건에서 FailOver 처리를 위해 사용하거나 아니면 아예 더 이상 지원하지 않은 API 의 응답을 코드 변경 없이 간단하게 통일하는데 사용될 수 있을 것 같다.

 

누군가에게 유용하다고, 재밌다고 여길 수 있는 라이브러리가 되었으면 좋겠다. 😄

 

Comments