Skip to content

Latest commit

ย 

History

History
714 lines (574 loc) ยท 26.4 KB

2021-04-15-spring-gateway.md

File metadata and controls

714 lines (574 loc) ยท 26.4 KB

์šฉ์–ด

๋ช…์นญ ์„ค๋ช…
๋ผ์šฐํŠธ(Route) ๋ผ์šฐํŠธ๋Š” ๋ชฉ์ ์ง€ URI, ์กฐ๊ฑด์ž ๋ชฉ๋ก๊ณผ ํ•„ํ„ฐ์˜ ๋ชฉ๋ก์„ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•œ ๊ณ ์œ  ID๋กœ ๊ตฌ์„ฑ๋œ๋‹ค. ๋ผ์šฐํŠธ๋Š” ๋ชจ๋“  ์กฐ๊ฑด์ž๊ฐ€ ์ถฉ์กฑ๋์„ ๋•Œ๋งŒ ๋งค์นญ๋œ๋‹ค
์กฐ๊ฑด์ž(Predicates) ๊ฐ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์ „์— ์‹คํ–‰๋˜๋Š” ๋กœ์ง, ํ—ค๋”์™€ ์ž…๋ ฅ๋œ ๊ฐ’ ๋“ฑ ๋‹ค์–‘ํ•œ HTTP ์š”์ฒญ์ด ์ •์˜๋œ ๊ธฐ์ค€์— ๋งž๋Š”์ง€๋ฅผ ์ฐพ๋Š”๋‹ค.
ํ•„ํ„ฐ(Filters) HTTP ์š”์ฒญ ๋˜๋Š” ๋‚˜๊ฐ€๋Š” HTTP ์‘๋‹ต์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒํ•œ๋‹ค. ๋‹ค์šด์ŠคํŠธ๋ฆผ ์š”์ฒญ์„ ๋ณด๋‚ด๊ธฐ์ „์ด๋‚˜ ํ›„์— ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ผ์šฐํŠธ ํ•„ํ„ฐ๋Š” ํŠน์ • ๋ผ์šฐํŠธ์— ํ•œ์ •๋œ๋‹ค.

Getting Started

implementation("org.springframework.cloud:spring-cloud-starter-gateway")
implementation("org.springframework.boot:spring-boot-starter-actuator")
@SpringBootApplication
class GatewayServerApplication

fun main(args: Array<String>) {
    runApplication<GatewayServerApplication>(*args)
}

ํ•„์š”ํ•œ ์˜์กด์„ฑ๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋น ๋ฅด๊ฒŒ Spring Cloud Gateway๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Gateway Route ๋…ธ์ถœ

management:
    endpoints:
        web:
            exposure:
                include:
                    - "gateway"
    endpoint:
        gateway:
            enabled: true

์œ„์—์„œ ์ถ”๊ฐ€ํ•œ actuator์˜์กด์„ฑ์œผ๋กœ gateway๋ฅผ ๋…ธ์ถœํ•˜๋ฉด ์•„๋ž˜์ฒ˜๋Ÿผ url mapping ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ์•„๋ฌด๊ฒƒ๋„ ์„ค์ •ํ•˜์ง€ ์•Š์€ ์ƒํƒœ์ด๊ธฐ ๋•Œ๋ฌธ์— /actuator/gateway/routes๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

GET http://127.0.0.1:5555/actuator/gateway/routes

HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/json

[]

Response code: 200 (OK); Time: 321ms; Content length: 2 bytes

Route ์„ค์ •

API๋ฅผ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค๊ณ  ๊ฒŒ์ดํŠธ์›จ์ด์™€ ์—ฐ๊ฒฐํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

    cloud:
        gateway:
            routes:
                -   id: order-service
                    uri: http://localhost:8181
                    predicates:
                        - Path=/order/**
                    filters:
                        - RewritePath=/order/(?<path>.*),/$\{path}

                -   id: cart-service
                    uri: http://localhost:8181
                    predicates:
                        - Path=/cart/**
                    filters:
                        - RewritePath=/cart/(?<path>.*),/$\{path}
  • id: ํ•ด๋‹น ๋ผ์šฐํŠธ์˜ ๊ณ ์œ  ์‹๋ณ„์ž๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
  • uri: ํ•ด๋‹น ๋ผ์šฐํ„ฐ์˜ ์ฃผ์†Œ๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
  • predicates: ํ•ด๋‹น ๋ผ์šฐํ„ฐ์˜ ์กฐ๊ฑด์„ ์ž‘์„ฑ, /order/**์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ์š”์ฒญ์˜ ๊ฒฝ์šฐ ํ•ด๋‹น ๋ผ์šฐํ„ฐ๋กœ ์š”์ฒญ์„ ๋ณด๋ƒ„
  • filters: ํ•ด๋‹น ๋ผ์šฐํ„ฐ์˜ ํ•„ํ„ฐ๋กœ, RewritePath๋Š” ๊ฐ•์ œ๋กœ Path๋ฅผ ๋‹ค์‹œ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

์—ฐ๊ฒฐํ•  API Server

cart-service, order-service 2 ๊ฐœ์˜ API ์„œ๋ฒ„๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ํฌํŠธ์˜ ์„ค์ •์€ cloud.gateway.routes์— ๋“ฑ๋ก๋œ ํฌํŠธ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

order-service

@RestController
@RequestMapping("/orders")
class OrderApi(
        private val orderRepository: OrderRepository
) {

    @GetMapping
    fun getOrders(pageable: Pageable) = orderRepository.findAll(pageable)
}

@Entity
@Table(name = "orders")
class Order(
        @Column(name = "product_id", nullable = false)
        val productId: Long
) : EntityAuditing() {
    @Column(name = "order_number", nullable = false)
    val orderNumber: String = UUID.randomUUID().toString()
}

cart-service

@RestController
@RequestMapping("/carts")
class CartApi(
        private val cartRepository: CartRepository
) {
    @GetMapping
    fun getCarts(pageable: Pageable) = cartRepository.findAll(pageable)
}

@Entity
@Table(name = "cart")
class Cart(
        @Column(name = "product_id", nullable = false)
        var productId: Long
) : EntityAuditing()

Router ํ™•์ธ

actuator/gateway/routes ํ™•์ธ์„ ํ•ด๋ณด๋ฉด ์œ„์—์„œ ์„ค์ •ํ•œ ๋ผ์šฐํ„ฐ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

GET http://127.0.0.1:5555/actuator/gateway/routes

HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/json

[
  {
    "predicate": "Paths: [/order/**], match trailing slash: true",
    "route_id": "order-service",
    "filters": [
      "[[RewritePath /order/(?<path>.*) = '/${path}'], order = 1]"
    ],
    "uri": "http://localhost:8181",
    "order": 0
  },
  {
    "predicate": "Paths: [/cart/**], match trailing slash: true",
    "route_id": "cart-service",
    "filters": [
      "[[RewritePath /cart/(?<path>.*) = '/${path}'], order = 1]"
    ],
    "uri": "http://localhost:8181",
    "order": 0
  }
]

Response code: 200 (OK); Time: 207ms; Content length: 404 bytes

์—ฐ๊ฒฐ๋œ ์„œ๋น„์Šค ํ™•์ธ

GET http://localhost:5555/order/orders?page=0&size=5

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 1075
Date: Sat, 22 Aug 2020 09:29:57 GMT
CUSTOM-RESPONSE-HEADER: It worked

{
  "content": [
    {
      "productId": 1,
      "id": 1,
      "createdAt": "2020-08-22T17:19:08.038",
      "updatedAt": "2020-08-22T17:19:08.038",
      "orderNumber": "7d684c44-1ea3-4dc4-9247-12c351606df3"
    },
    ...
  ],
  "pageable": {
    ...
  },
  "last": false,
  ...
}

Response code: 200 (OK); Time: 168ms; Content length: 1075 bytes

๊ฒŒ์ดํŠธ์›จ์ด /order/orders?page=0&size=5๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด filters.RewritePath์— ์˜ํ•ด์„œ orders?page=0&size=5๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰ ๋ผ์šฐํ„ฐ์— ๋“ฑ๋ก๋œ order-service๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

Predicates

Predicates๋Š” ์กฐ๊ฑด์œผ๋กœ์„œ ํ•ด๋‹น ๋ผ์šฐํ„ฐ์— ๋ผ์šฐํŒ… ๋  ์กฐ๊ฑด์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์œ„ ์˜ˆ์ œ์—์„œ๋Š” Path=/order/**, Path=/cart/**์œผ๋กœ ํ•ด๋‹น path๋กœ ๋“ค์–ด์˜ค๋Š” ๊ฒฝ์šฐ ํ•ด๋‹น ๋ผ์šฐํ„ฐ๋กœ ๋ผ์šฐํŒ… ๋ฉ๋‹ˆ๋‹ค. ๊ทธ ๋ฐ–์—๋„ ์—ฌ๋Ÿฌ ๊ฐ€์ง€๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ๋Œ€ํ‘œ์ ์ธ ๋ช‡ ๊ฐœ๋ฅผ ์ •๋ฆฌํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ๋‚ ์งœ ๊ด€๋ จ ๋งค๊ฐœ๋ณ€์ˆ˜๋Š” ZonedDateTime๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

After

routes:
    -   id: order-service
        uri: http://localhost:8181
        predicates:
            - Path=/order/**
            - After=2020-08-23T19:25:19.126+09:00[Asia/Seoul]

After๋Š” ํŠน์ • ๋‚ ์งœ ์ดํ›„์— ํ˜ธ์ถœ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๋‚ ์งœ๊ฐ€ After์—์„œ ์ง€์ •ํ•œ ๋‚ ์งœ ๋ณด๋‹ค ์ดํ›„ ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์„œ๋น„์Šค์— ๋Œ€ํ•œ ์ด๋ฒคํŠธ API ๋“ฑ ํŠน์ • ์‹œ์ ์— Open ์‹œํ‚ฌ API๊ฐ€ ์žˆ๋‹ค๋ฉด ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

GET http://localhost:5555/order/orders?page=0&size=5

HTTP/1.1 404 Not Found
Content-Type: application/json
Content-Length: 141

{
  "timestamp": "2020-08-22T10:37:11.955+00:00",
  "path": "/order/orders",
  "status": 404,
  "error": "Not Found",
  "message": null,
  "requestId": "9b635742-2"
}

Response code: 404 (Not Found); Time: 28ms; Content length: 141 bytes

ํ˜„์žฌ ์‹œ๊ฐ 2020-08-22T19:25:19.126+09:00[Asia/Seoul] ์ด๋ผ๋ฉด HTTP/1.1 404 Not Found์„ ์‘๋‹ต ๋ฐ›๊ฒŒ๋ฉ๋‹ˆ๋‹ค.

Before

routes:
    -   id: order-service
        uri: http://localhost:8181
        predicates:
            - Path=/order/**
            - Before=2020-08-20T19:25:19.126+09:00[Asia/Seoul]

Before๋Š” ํŠน์ • ๋‚ ์งœ ์ด์ „ ํ˜ธ์ถœ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๋‚ ์งœ๊ฐ€ Before์—์„œ ์ง€์ •ํ•œ ๋‚ ์งœ ๋ณด๋‹ค ์ด์ „ ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํŠน์ • API๊ฐ€ deprecate๊ฐ€ ๋˜๋Š” ๊ฒฝ์šฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

Between

routes:
    -   id: order-service
        uri: http://localhost:8181
        predicates:
            - Path=/order/**
            - Between=2020-08-17T19:25:19.126+09:00[Asia/Seoul], 2020-08-20T19:25:19.126+09:00[Asia/Seoul]

Between๋Š” ํŠน์ • ๋‚ ์งœ ์‚ฌ์ด์—๋งŒ ํ˜ธ์ถœ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ํŠน์ • ๊ธฐ๊ฐ„์—๋งŒ ์‚ฌ์šฉํ•˜๋Š” ์ด๋ฒคํŠธ API ๋“ฑ์— ์‚ฌ์šฉํ•˜๋ฉด ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

Weight

routes:
    -   id: order-service-high
        uri: http://localhost:8181
        predicates:
            - Path=/order/**
            - Weight=group-order, 7
        filters:
            - RewritePath=/order/(?<path>.*),/$\{path}

    -   id: order-service-low
        uri: http://localhost:8787
        predicates:
            - Path=/order/**
            - Weight=group-order, 3
        filters:
            - RewritePath=/order/(?<path>.*),/$\{path}

group, weight๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ทธ๋ฃน๋ณ„๋กœ ๊ฐ€์ค‘์น˜๋ฅผ ๊ณ„์‚ฐํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์œ„ ์„ค์ •์€ 70% order-service-high, 30% order-service-low์œผ๋กœ ๋ผ์šฐํŒ…์„ ๋ถ„๋ฐฐํ•ฉ๋‹ˆ๋‹ค.

Filters

HTTP Request, Reponse์— ๋Œ€ํ•œ ์ˆ˜์ •์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠน์ • ๋ผ์šฐํ„ฐ์—์—์„œ ์•ˆ์—์„œ ๋™์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

RewritePath

RewritePath๋Š” HTTP Request๋ฅผ ์ˆ˜์ •ํ•˜์—ฌ ํŠน์ • Server์— ์ „๋‹ฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ •๊ทœํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•ด์„œ ์œ ์—ฐํ•˜๊ฒŒ HTTP Request Path๋ฅผ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.

 routes:
     -   id: order-service
         uri: http://localhost:8181
         filters:
             - RewritePath=/order/(?<path>.*),/$\{path}

RewritePath๋ฅผ ํ†ตํ•ด์„œ /order/orders -> /order/orders์œผ๋กœ ์žฌ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, /order/orders?page=0&size=5 ์š”์ฒญ์ด ์˜ค๋ฉด /order/๋ฅผ์ œ๊ฑฐํ•˜๊ณ  orders?page=0&size=5๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ order-service๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

Retry

name ์„ค๋ช… ๊ธฐ๋ณธ๊ฐ’
retries ์žฌ์‹œ๋„ ํšŸ์ˆ˜ 3๋ฒˆ
statuses ์žฌ์‹œ๋„ํ•ด์•ผํ•˜๋Š” HTTP ์ƒํƒœ ์ฝ”๋“œ(org.springframework.http.HttpStatus) -
series ์žฌ์‹œ๋„ํ•ด์•ผํ•˜๋Š” HTTP ์ƒํƒœ ์ฝ”๋“œ์‹œ๋ฆฌ์ฆˆ(org.springframework.http.HttpStatus.Series) 5XX
methods ์žฌ์‹œ๋„ํ•ด์•ผํ•˜๋Š” HTTP ๋ฉ”์†Œ๋“œ(org.springframework.http.HttpMethod) GET
exceptions ์žฌ์‹œ๋„ํ•ด์•ผํ•˜๋Š” Exception IOException, TimeoutException
backoff ์žฌ์‹œ๋„ํ•˜๋Š” ์‹œ๊ฐ„ํ…€ ์ง€์ • firstBackoff * (factor ^ n) n๋ฒˆ ๋ฐ˜๋ณต ๋น„ํ™œ์„ฑํ™”
spring:
    cloud:
        gateway:
            discovery:
                locator:
                    enabled: true
            routes:
                -   id: order-service
                    uri: lb://order-service
                    predicates:
                        - Path=/order/**
                    filters:
                        - RewritePath=/order/(?<path>.*),/$\{path}
                        -   name: Retry
                            args:
                                retries: 3
                                statuses: INTERNAL_SERVER_ERROR
                                methods: GET
                                backoff:
                                    firstBackoff: 1000ms
                                    maxBackoff: 6000ms
                                    factor: 2
                                    basedOnPreviousValue: false

์ œ์‹œ๋„ ํšŸ์ˆ˜๋Š” retries: 3, ์žฌ์‹œ๋„ HTTP Status๋Š” statuses: INTERNAL_SERVER_ERROR (500), ์žฌ์‹œ๋„ HTTP method๋Š” GET backoff ์„ค์ •์€ 1000ms(firstBackoff) * (2(factor) ^ n(retries))์œผ๋กœ retries ๋งŒํผ ๋ฐ˜๋ณต๋ฉ๋‹ˆ๋‹ค.

@RestController
@RequestMapping("/orders")
class OrderApi(
        private val orderRepository: OrderRepository
) {

    @GetMapping
    fun getOrders(pageable: Pageable): Page<Order> {
        println("getOrders ํ˜ธ์ถœ")
        if(true){
            throw RuntimeException("Error")
        }
        return orderRepository.findAll(pageable)
    }

    @GetMapping("/carts/{id}")
    fun getCarts(@PathVariable id: Long) = cartClient.getCart(id)
}

ํ•ด๋‹น API๋Š” RuntimeException("Error")๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ  ์žˆ์–ด Status 500์„ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค.

GET http://localhost:5555/order/orders?page=0&size=5

HTTP/1.1 500 Internal Server Error
transfer-encoding: chunked
Content-Type: application/json
Date: Sat, 22 Aug 2020 14:57:15 GMT
CUSTOM-RESPONSE-HEADER: It did not work

{
  "timestamp": "2020-08-22T14:57:15.122+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "",
  "path": "/orders"
}

Response code: 500 (Internal Server Error); Time: 7062ms; Content length: 120 bytes

๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๋ฉด 3๋ฒˆ์˜ Retry๊ฐ€ ์žˆ์—ˆ๊ณ , ๊ฒฐ๊ตญ 500์„ ๋ฆฌํ„ดํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

getOrders ํ˜ธ์ถœ
2020-08-22 23:57:08.080 ERROR [order-service,,,] 17139 --- [nio-8181-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: Error] with root cause

getOrders ํ˜ธ์ถœ
2020-08-22 23:57:09.091 ERROR [order-service,,,] 17139 --- [nio-8181-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: Error] with root cause

getOrders ํ˜ธ์ถœ
2020-08-22 23:57:11.107 ERROR [order-service,,,] 17139 --- [nio-8181-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: Error] with root cause

order-service ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•ด๋ณด๋ฉด 3๋ฒˆ์˜ ํ˜ธ์ถœ์ด ์žˆ์—ˆ๋Š”์ง€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@RestController
@RequestMapping("/orders")
class OrderApi(
        private val orderRepository: OrderRepository
) {

    var errorCount = 0

    @GetMapping
    fun getOrders(pageable: Pageable): Page<Order> {
        println("getOrders ํ˜ธ์ถœ")
        if (errorCount < 2) {
            println("์˜ˆ์™ธ๋ฐœ์ƒ $errorCount 1์ฆ๊ฐ€")
            errorCount++
            throw RuntimeException("Error")
        }
        errorCount = 0 // ์ดˆ๊ธฐํ™”
        return orderRepository.findAll(pageable)
    }
}

ํ•ด๋‹น ์ฝ”๋“œ๋Š” 2๋ฒˆ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€๋งŒ 3๋ฒˆ์งธ์—์„œ ์‘๋‹ต์„ ๋ฆฌํ„ดํ•ด์ฃผ๋Š” ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. ์žฌ์‹œ๋„๋ฅผ 3๋ฒˆ ์‹คํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— 3๋ฒˆ์งธ์—๋Š” ์ •์ƒ์ ์ธ ์‘๋‹ต์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

GET http://localhost:5555/order/orders?page=0&size=5

HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/json
Date: Sat, 22 Aug 2020 15:13:16 GMT
CUSTOM-RESPONSE-HEADER: It worked

{
  "content": [
    {
      "productId": 1,
      "id": 1,
      "createdAt": "2020-08-23T00:11:24.747",
      "updatedAt": "2020-08-23T00:11:24.747",
      "orderNumber": "519011ff-7eaf-4e85-b48f-a9aa6ab879ac"
    }
    ...
  ],
  "pageable": {
   ...
  "totalPages": 2,
   ...
}

Response code: 200 (OK); Time: 3034ms; Content length: 1075 bytes

3๋ฒˆ์˜ ์‘๋‹ต์‹œ๊ฐ„์„ ๊ธฐ๋‹ค๋ ค์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— 3034ms ์ •๋„ ๊ฑธ๋ฆฌ๋Š” ๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์žฌ์‹œ๋„๋Š” ๋‹จ์ˆœ ์กฐํšŒ๋งŒ ํ•˜๋Š” GET ์š”์ฒญ์— ์™ธ์—๋Š” ์‹ ์ค‘ํ•˜๊ฒŒ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ฒŒ์ดํŠธ์›จ์ด์—์„œ ์žฌ์‹œ๋„๋ฅผ ์ง„ํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ ์„œ๋น„์Šค ๊ฐ„์˜ ํ†ต์‹ ์—์„œ ์ƒ์„ฑ, ์‚ญ์ œ, ์ˆ˜์ • ๋“ฑ ์กฐํšŒ ์กฐ๊ฑด ์™ธ์— ๋™์ž‘์ด ์žˆ๋‹ค๋ฉด ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค. ๋˜ HTTP Status 5XX ์‘๋‹ต์€ ์žฌ์‹œ๋„๋ฅผ ํ•˜๋Š” ๊ฒƒ์€ ๋ฐ”๋žŒ์งํ•˜์ง€๋งŒ, HTTP Status 4XXX์—์„œ๋Š” ๋™์ผํ•œ ์š”์ฒญ์ด๋ฉด ๋™์ผํ•œ ์ด์œ ๋กœ ์‹คํŒจํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์žฌ์‹œ๋„๋ฅผ ์•ˆ ํ•˜๋Š” ๊ฒŒ ๋” ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค. ๋‹จ์ˆœ ์กฐํšŒ ์šฉ์ด ์•„๋‹ˆ๋ฉด ์‹ ์ค‘ํ•˜๊ฒŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

HTTP Timeout ์„ค์ •

๊ธ€๋กœ๋ฒŒ ์„ค์ •

spring:
    cloud:
        gateway:
            httpclient:
                connect-timeout: 10000
                response-timeout: 10s

connect-timeout ๋ฐ€๋ฆฌ ์ดˆ ๋‹จ์œ„๋กœ ์ง€์ •, response-timeout Duration์œผ๋กœ ์ง€์ • ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

@RestController
@RequestMapping("/orders")
class OrderApi(
        private val orderRepository: OrderRepository
) {

    @GetMapping
    fun getOrders(pageable: Pageable): Page<Order> {
        Thread.sleep(1100) // timeout ๋ฐœ์ƒ
        return orderRepository.findAll(pageable)
    }

ํด๋ผ์ด์–ธํŠธ ์‘๋‹ต์‹œ๊ฐ„์ด 1์ดˆ๋กœ ์„ค์ •ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— 1์ดˆ๋ฅผ ๋„˜์–ด๊ฐ€๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด HTTP/1.1 504 Gateway Timeout์‘๋‹ต์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

GET http://localhost:5555/order/orders?page=0&size=5

HTTP/1.1 504 Gateway Timeout
Content-Type: application/json
Content-Length: 145

{
  "timestamp": "2020-08-22T14:05:09.267+00:00",
  "path": "/order/orders",
  "status": 504,
  "error": "Gateway Timeout",
  "message": "",
  "requestId": "0d492aaf-1"
}

Response code: 504 (Gateway Timeout); Time: 4798ms; Content length: 145 bytes

๋ผ์šฐํ„ฐ๋ณ„ ์„ค์ •

spring:
    cloud:
        gateway:
            routes:
                -   id: order-service
                    uri: lb://order-service
                    predicates:
                        - Path=/order/**
                    filters:
                        - RewritePath=/order/(?<path>.*),/$\{path}
                    metadata:
                        connect-timeout: 1000
                        response-timeout: 1000

metadata์„ค์ •์„ ํ†ตํ•ด์„œ ๋ผ์šฐํ„ฐ๋ณ„ ์„ค์ •์„ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ์ ์€ metadata ์„ค์ • ์‹œ connect-timeout, response-timeout ๋ชจ๋‘ ๋ฐ€๋ฆฌ ์ดˆ ๋‹จ์œ„๋กœ ์ง€์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Global ์„ค์ •๊ณผ๋Š” ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

Logging Sleuth

Gateway Logging

class GatewayServerApplication

fun main(args: Array<String>) {
    System.setProperty("reactor.netty.http.server.accessLogEnabled", "true")
    runApplication<GatewayServerApplication>(*args)
}

Reactor Netty ์•ก์„ธ์Šค ๋กœ๊ทธ๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด System.setProperty("reactor.netty.http.server.accessLogEnabled", "true")์„ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ณต์‹ ๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด Spring Boot ์„ค์ •์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— yml์œผ๋กœ ์„ค์ •ํ•˜์ง€ ์•Š๊ณ  ์œ„์ฒ˜๋Ÿผ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์ •์ƒ์ ์œผ๋กœ ๋กœํ‚น์ด ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Sleuth

์Šคํ”„๋ง ํด๋ผ์šฐ๋“œ ์Šฌ๋ฃจ์Šค(Sleuth)๋Š” ๋งˆ์ดํฌ๋กœ ์„œ๋น„์Šค ํ™˜๊ฒฝ์—์„œ ์„œ๋กœ ๋‹ค๋ฅธ ์‹œ์Šคํ…œ์˜ ์š”์ฒญ์„ ์—ฐ๊ฒฐํ•˜์—ฌ ๋กœ๊น…์„ ํ•ด์ค„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ ์Šฌ๋ฃจ์Šค๋ฅผ ์ด์šฉํ•ด์„œ ์‰ฝ๊ฒŒ ์š”์ฒญ์— ๋Œ€ํ•œ ๋กœ๊น…์„ ์—ฐ๊ฒฐํ•ด์„œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ RestTemplate, ํŽ˜์ธ ํด๋ผ์ด์–ธํŠธ, ๋ฉ”์‹œ์ง€ ์ฑ„๋„ ๋“ฑ๋“ฑ ๋‹ค์–‘ํ•œ ํ”Œ๋žซํผ๊ณผ ์—ฐ๊ฒฐํ•˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜ ์˜ˆ์ œ์—์„œ๋Š” ํ์ธ ํด๋ผ์ด์–ธํŠธ์™€ ์—ฐ๊ฒฐํ•ด์„œ ๋กœ๊น…ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์˜์กด์„ฑ ์ถ”๊ฐ€

implementation("org.springframework.cloud:spring-cloud-starter-sleuth")

gateway-server, order-service. cart-service ๋ชจ๋“ˆ์— ํ•ด๋‹น ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

cloud:
    gateway:
        discovery:
            locator:
                enabled: true
        routes:
            -   id: order-service
                uri: lb://order-service
                predicates:
                    - Path=/order/**
                filters:
                    - RewritePath=/order/(?<path>.*),/$\{path}

            -   id: cart-service
                uri: lb://cart-service
                predicates:
                    - Path=/cart/**
                filters:
                    - RewritePath=/cart/(?<path>.*),/$\{path}

openfeign์„ ์ด์šฉํ•ด์„œ ํด๋ผ์ด์–ธํŠธ๋ฅผ ํ˜ธ์ถœํ•  ๊ฒƒ์ด๋ฏ€๋กœ discovery(Eureka) ์„ค์ •์„ ํ•ฉ๋‹ˆ๋‹ค.(์•„๋ž˜ ํฌ์ŠคํŒ…์—์„œ ์œ ๋ ˆ์นด, ํŽ˜์ธ ๊ด€๋ จ ์„ค์ •์„ ์ง„ํ–‰ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.) ํ˜ธ์ถœ ์ˆœ์„œ๊ฐ€ Gateway -> order-service -> cart-service ํ˜ธ์ถœ์— ๋Œ€ํ•œ ๋กœ๊น…์ž…๋‹ˆ๋‹ค.

# spring-gateway
2020-08-22 22:11:20.671 DEBUG [gateway-server,d1905ab24f0b5d1a,d1905ab24f0b5d1a,true] 12133 --- [ctor-http-nio-4] o.s.c.g.h.RoutePredicateHandlerMapping   : Route matched: order-service
2020-08-22 22:11:20.671 DEBUG [gateway-server,d1905ab24f0b5d1a,d1905ab24f0b5d1a,true] 12133 --- [ctor-http-nio-4] o.s.c.g.h.RoutePredicateHandlerMapping   : Mapping [Exchange: GET http://localhost:5555/order/orders/carts/2] to Route{id='order-service', uri=lb://order-service, order=0, predicate=Paths: [/order/**], match trailing slash: true, gatewayFilters=[[[RewritePath /order/(?<path>.*) = '/${path}'], order = 1], [[Retry retries = 3, series = list[SERVER_ERROR], statuses = list[502 BAD_GATEWAY], methods = list[GET, POST], exceptions = list[IOException, TimeoutException]], order = 2]], metadata={}}

# order-service
2020-08-22 22:11:20.685  INFO [order-service,d1905ab24f0b5d1a,ba6672843cb90f99,true] 11949 --- [nio-8181-exec-6] com.service.order.HttpLoggingFilter      : 
    โŠ™ GET /orders/carts/2
    โ”œโ”€ Headers: accept: application/json, user-agent: Apache-HttpClient/4.5.12 (Java/11.0.7), accept-encoding: gzip,deflate, custom-request-header: userName, forwarded: proto=http;host="localhost:5555";for="127.0.0.1:57509", x-forwarded-for: 127.0.0.1, x-forwarded-proto: http, x-forwarded-prefix: /order, x-forwarded-port: 5555, x-forwarded-host: localhost:5555, host: 192.168.0.5:8181, x-b3-traceid: d1905ab24f0b5d1a, x-b3-spanid: ba6672843cb90f99, x-b3-parentspanid: d1905ab24f0b5d1a, x-b3-sampled: 1, content-length: 0

# cart-service
2020-08-22 22:11:20.683  INFO [cart-service,d1905ab24f0b5d1a,716b9cd4e4e52bdf,true] 11935 --- [nio-8282-exec-5] com.service.cart.HttpLoggingFilter       : 
    โŠ™ GET /carts/2
    โ”œโ”€ Headers: x-b3-traceid: d1905ab24f0b5d1a, x-b3-spanid: 716b9cd4e4e52bdf, x-b3-parentspanid: ba6672843cb90f99, x-b3-sampled: 1, accept: */*, user-agent: Java/1.8.0_212, host: 192.168.0.5:8282, connection: keep-alive

๋กœ๊ทธ๋ฅผ ๋ณด๋ฉด gateway-server์—์„œ traceId: d1905ab24f0b5d1a๋ฐœ๊ธ‰ํ•˜๊ณ  order-service์—๊ฒŒ ์ „๋‹ฌํ•  ๋•Œ header ์ •๋ณด์— x-b3-traceid: d1905ab24f0b5d1a๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , cart-service๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ traceId๋ฅผ ์ „๋‹ฌ๋ฐ›๊ณ  ์ž์‹ ์˜ ๊ณ ์œ ํ•œ ID x-b3-parentspanid: ba6672843cb90f99(order-service์—์„œ ์ „๋‹ฌ๋ฐ›์€) ๋ฐœ๊ธ‰ํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ตญ d1905ab24f0b5d1a ๊ฐ’ ํ•˜๋‚˜๋กœ ์—ฐ๊ฒฐ๋œ ํ•˜๋‚˜์˜ ์š”์ฒญ์„ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Eureka & Feign & Ribbon

Spring Cloud Gateway๋Š” ์œ ๋ ˆ์นด ์—ฐ๋™๋„ ์†์‰ฝ๊ฒŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋ณธ ํฌ์ŠคํŒ…์€ Spring Cloud Gateway์— ๋Œ€ํ•œ ํฌ์ŠคํŒ…์ด๋ฏ€๋กœ ์œ ๋ ˆ์นด์— ๋Œ€ํ•œ ์„ค์ •์€ ๋‹ค๋ฃจ์ง€ ์•Š๊ฒ ์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ๋‚ด์šฉ์€ ์‹ค์ œ ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.

order-service, cart-service ์„œ๋น„์Šค๋ฅผ ์œ ๋ ˆ์นด์— ๋“ฑ๋ก ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค. ์ด์ œ ๋ผ์šฐํ„ฐ์— uri๋ฅผ ์—ฐ๊ฒฐํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ์†์‰ฝ๊ฒŒ ์—ฐ๊ฒฐ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

gateway:
    discovery:
        locator:
            enabled: true
    routes:
        -   id: order-service
            #                    uri: http://localhost:8181 # ๊ธฐ์กด ๋ฐฉ์‹œ
            uri: lb://order-service # ์œ ๋ ˆ์นด๋ฅผ ํ†ตํ•œ ๋ฐฉ์‹
            predicates:
                - Path=/order/**
            filters:
                - RewritePath=/order/(?<path>.*),/$\{path}

        -   id: cart-service
            #                    uri: http://localhost:8181 # ๊ธฐ์กด ๋ฐฉ์‹œ
            uri: lb://cart-service # ์œ ๋ ˆ์นด๋ฅผ ํ†ตํ•œ ๋ฐฉ์‹
            predicates:
                - Path=/cart/**
            filters:
                - RewritePath=/cart/(?<path>.*),/$\{path}

์„ค์ •์€ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. uri: lb://{service-name}ํ˜•์‹์œผ๋กœ ์œ ๋ ˆ์นด์— ๋“ฑ๋ก๋œ ์„œ๋น„์Šค ๋„ค์ž„์„ ์ž‘์„ฑํ•˜๊ฒŒ ๋˜๋ฉด ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค. ์œ ๋ ˆ์นด์— ๋“ฑ๋กํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— Feign, Ribbon ์ด์šฉํ•œ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

@FeignClient("cart-service")
@RibbonClient("cart-service")
interface CartClient {

    @GetMapping("/carts/{id}")
    fun getCart(@PathVariable id: Long): CartResponse

    data class CartResponse(
            val productId: Long
    )
}

@RestController
@RequestMapping("/orders")
class OrderApi(
        private val cartClient: CartClient
) {

    @GetMapping("/carts/{id}")
    fun getCarts(@PathVariable id: Long) = cartClient.getCart(id)
}

๊ฒŒ์ดํŠธ์›จ์ด๋ฅผ ํ˜ธ์ถœํ•ด์„œ order-service๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ํŽ˜์ธ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ด์šฉํ•ด์„œ cart-service๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

GET http://localhost:5555/order/orders/carts/2

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 15
Date: Sat, 22 Aug 2020 13:37:48 GMT
CUSTOM-RESPONSE-HEADER: It worked

{
  "productId": 2
}

Response code: 200 (OK); Time: 109ms; Content length: 15 bytes

Filter ์„ค๋ช…

ํด๋ผ์ด์–ธํŠธ๋Š” Spring Cloud Gateway๋ฅผ ํ†ตํ•ด ์š”์ฒญ์„ ํ•˜๊ณ  ๊ฒŒ์ดํŠธ์›จ์ด๋Š” ๋งคํ•‘์—์„œ ์š”์ฒญ์ด ๊ฒฝ๋กœ์™€ ์ผ์น˜ํ•œ๋‹ค๊ณ  ํŒ๋‹จํ•˜๋ฉด ๊ฒŒ์ดํŠธ์›จ์ด ์›น ์ฒ˜๋ฆฌ๊ธฐ๋กœ ์š”์ฒญ์„ ์ „์†กํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

Spring Cloud Gateway Document

@Component
class CustomFilter : AbstractGatewayFilterFactory<CustomFilter.Config>(Config::class.java) {
    val log by logger()

    override fun apply(config: Config): GatewayFilter {
        return GatewayFilter { exchange, chain ->
            val request = exchange.request
            val response = exchange.response
            log.info("CustomFilter request id: ${request.id}")
            chain.filter(exchange).then(Mono.fromRunnable { log.info("CustomFilter response status code: ${response.statusCode}") })
        }
    }

    class Config
}

@Component
class GlobalFilter : AbstractGatewayFilterFactory<GlobalFilter.Config>(Config::class.java) {
    val log by logger()

    override fun apply(config: Config): GatewayFilter {
        return GatewayFilter { exchange, chain ->
            val request = exchange.request
            val response = exchange.response

            log.info("Global request id: ${request.id}")
            chain.filter(exchange).then(Mono.fromRunnable {
                log.info("Global response status code: ${response.statusCode}")
            })
        }
    }

    class Config
}

ํ•„ํ„ฐ๋Š” ๋ชจ๋‘ AbstractGatewayFilterFactory๋ฅผ ์ƒ์†๋ฐ›์•„ ๊ตฌํ˜„์„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ Gateay ๋กœ๊ทธ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์ถœ์ฒ˜