경계의 경계

Spring Cloud Gateway 사용해보기 본문

Spring Cloud

Spring Cloud Gateway 사용해보기

gigyesik 2024. 5. 10. 02:47

들어가며

Spring Cloud Gateway는 API 게이트웨이 구축을 위한 Spring 프레임워크 하위 라이브러리이다.

API 게이트웨이란 라우팅, 프로토콜 교환 등의 역할을 어플리케이션과 마이크로서비스 사이에서 수행하는 서비스이다.

API 게이트웨이를 사용해 인증, 부하 제한, 캐싱 등의 기능도 수행할 수 있다.

Spring Cloud Gateway는 Spring, Spring Boot 뿐 아니라 Spring Cloud Netflix, Spring Security 등의 프레임워크와도 함께 사용할 수 있다.

마이크로서비스에 보낼 요청을 한 곳에서 관리함으로서 비즈니스 로직에만 집중할 수 있도록 해준다.

Spring Cloud Gateway 사용해보기

의존성 설정 - build.gradle

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j'
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'
}
  • reactive-gateway : Spring Cloud Gateway 구현
  • resilience4j : Circuit Breaker 패턴 구현
  • contract-stub-runner : 라우팅 테스트

라우터 추가 - route() Bean

    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder) {
        return builder.routes().build();
    }
  • Http 요청을 라우팅하는 라우터

라우터 설정

    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(p -> p
                        .path("/get")
                        .filters(f -> f.addRequestHeader("Hello", "World"))
                        .uri("<http://httpbin.org:80>"))
                .build();
    }
  • /get에 요청을 보낼 때
    • 해당 요청에 Key:Value가 Hello:World인 헤더를 추가한다.
    • 요청을 http://httpbin.org:80 으로 포워딩한다.
$ curl <http://localhost:8080/get>

// Res
{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Content-Length": "0", 
    "Forwarded": "proto=http;host=\\"localhost:8080\\";for=\\"[0:0:0:0:0:0:0:1]:53109\\"", 
    "Hello": "World", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/8.4.0", 
    "X-Amzn-Trace-Id": "Root=1-663b803e-6f14a00a28abf71835bcd75e", 
    "X-Forwarded-Host": "localhost:8080"
  }, 
  "origin": "0:0:0:0:0:0:0:1, 73.68.251.70", 
  "url": "<http://localhost:8080/get>"

Circuit Breaker 사용

    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(p -> p
                        .host("*.circuitbreaker.com")
                        .filters(f -> f.circuitBreaker(config -> config.setName("cmd")))
                        .uri("<http://httpbin.org:80>"))
                .build();
    }
  • 게이트웨이 동작에 예외를 처리하고 클라이언트를 차단할 수 있는 서킷 브레이커 패턴 사용
    • circuitbreaker.com 을 호스트 이름으로 갖는 클라이언트가 요청을 보내면 거절된다.
$ curl --dump-header - --header 'Host: www.circuitbreaker.com' <http://localhost:8080/delay/3>  

// Res
HTTP/1.1 504 Gateway Timeout

요청 Fallback

    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(p -> p
                        .host("*.circuitbreaker.com")
                        .filters(f -> f.circuitBreaker(config -> config.setName("cmd").setFallbackUri("forward:/fallback")))
                        .uri("<http://httpbin.org:80>"))
                .build();
    }
    
    @RequestMapping("/fallback")
    public Mono fallback() {
        return Mono.just("fallback");
    }
  • 504 Response Code 보다는 메시지를 받는 것이 더 명확하므로 fallback url을 설정한다.
$ curl --dump-header - --header 'Host: www.circuitbreaker.com' <http://localhost:8080/delay/3>

// Res
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 8

fallback                          

테스트하기

HTTP 요청에 의존하지 않고도 게이트웨이가 잘 동작하는지 Unit Test로 확인해보자.

먼저 HTTP 요청 경로를 하드코딩하기 위해 UriConfiguration 코드를 작성한다.

@ConfigurationProperties
public class UriConfiguration {
    private String httpbin = "<http://httpbin.org:80>";

    public String getHttpbin() {
        return httpbin;
    }

    public void setHttpbin(String httpbin) {
        this.httpbin = httpbin;
    }
}

@SpringBootApplication
@RestController
@EnableConfigurationProperties(UriConfiguration.class)
public class GetewaysampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(GetewaysampleApplication.class, args);
    }

    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder, UriConfiguration uriConfiguration) {
        String httpUri = uriConfiguration.getHttpbin();
        return builder.routes()
                .route(p -> p
                        .path("/get")
                        .filters(f -> f.addRequestHeader("Hello", "World"))
                        .uri(httpUri))
                .route(p -> p
                        .host("*.circuitbreaker.com")
                        .filters(f -> f.circuitBreaker(config -> config.setName("cmd").setFallbackUri("forward:/fallback")))
                        .uri(httpUri))
                .build();
    }
}
  • URI 경로를 httpbin 변수를 가져다 쓰도록 하드코딩한다.
  • route() Bean 에서 UriConfiguration.httpUri로 대체한다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {"httpbin=http://localhost:${wiremock.server.port}"})
@AutoConfigureWireMock(port = 0)
class GetewaysampleApplicationTests {
    @Autowired
    private WebTestClient webClient;

    @Test
    public void contextLoads() throws Exception {
        stubFor(get(urlEqualTo("/get"))
                .willReturn(aResponse()
                        .withBody("{\\"headers\\":{\\"Hello\\":\\"World\\"}}")
                        .withHeader("Content-Type", "application/json")));
        stubFor(get(urlEqualTo("/delay/3"))
                .willReturn(aResponse()
                        .withBody("no fallback")
                        .withFixedDelay(3000)));

        webClient
                .get().uri("/get")
                .exchange()
                .expectStatus().isOk()
                .expectBody()
                .jsonPath("$.headers.Hello").isEqualTo("World");

        webClient
                .get().uri("/delay/3")
                .header("Host", "www.circuitbreaker.com")
                .exchange()
                .expectStatus().isOk()
                .expectBody()
                .consumeWith(
                        response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
    }

}
  • @AutoConfigureWireMock 어노테이션으로 HTTP 요청을 mocking한다
  • 위에서 설정한 /get 에 대한 헤더 옵션, 서킷 브레이커 패턴을 확인할 수 있다.

Resources