๋ช ์นญ | ์ค๋ช |
---|---|
๋ผ์ฐํธ(Route) | ๋ผ์ฐํธ๋ ๋ชฉ์ ์ง URI, ์กฐ๊ฑด์ ๋ชฉ๋ก๊ณผ ํํฐ์ ๋ชฉ๋ก์ ์๋ณํ๊ธฐ ์ํ ๊ณ ์ ID๋ก ๊ตฌ์ฑ๋๋ค. ๋ผ์ฐํธ๋ ๋ชจ๋ ์กฐ๊ฑด์๊ฐ ์ถฉ์กฑ๋์ ๋๋ง ๋งค์นญ๋๋ค |
์กฐ๊ฑด์(Predicates) | ๊ฐ ์์ฒญ์ ์ฒ๋ฆฌํ๊ธฐ ์ ์ ์คํ๋๋ ๋ก์ง, ํค๋์ ์ ๋ ฅ๋ ๊ฐ ๋ฑ ๋ค์ํ HTTP ์์ฒญ์ด ์ ์๋ ๊ธฐ์ค์ ๋ง๋์ง๋ฅผ ์ฐพ๋๋ค. |
ํํฐ(Filters) | HTTP ์์ฒญ ๋๋ ๋๊ฐ๋ HTTP ์๋ต์ ์์ ํ ์ ์๊ฒํ๋ค. ๋ค์ด์คํธ๋ฆผ ์์ฒญ์ ๋ณด๋ด๊ธฐ์ ์ด๋ ํ์ ์์ ํ ์ ์๋ค. ๋ผ์ฐํธ ํํฐ๋ ํน์ ๋ผ์ฐํธ์ ํ์ ๋๋ค. |
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๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.
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
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๋ฅผ ๋ค์ ์์ฑํฉ๋๋ค.
cart-service
, order-service
2 ๊ฐ์ API ์๋ฒ๋ฅผ ๊ตฌ์ฑํฉ๋๋ค. ๊ฐ ํฌํธ์ ์ค์ ์ cloud.gateway.routes
์ ๋ฑ๋ก๋ ํฌํธ๋ฅผ ์ค์ ํฉ๋๋ค.
@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()
}
@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()
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๋ ์กฐ๊ฑด์ผ๋ก์ ํด๋น ๋ผ์ฐํฐ์ ๋ผ์ฐํ
๋ ์กฐ๊ฑด์ ํ์ํฉ๋๋ค. ์ ์์ ์์๋ Path=/order/**
, Path=/cart/**
์ผ๋ก ํด๋น path๋ก ๋ค์ด์ค๋ ๊ฒฝ์ฐ ํด๋น ๋ผ์ฐํฐ๋ก ๋ผ์ฐํ
๋ฉ๋๋ค. ๊ทธ ๋ฐ์๋ ์ฌ๋ฌ ๊ฐ์ง๋ฅผ ์ง์ํฉ๋๋ค. ๋ํ์ ์ธ ๋ช ๊ฐ๋ฅผ ์ ๋ฆฌํด๋ณด์์ต๋๋ค. ๋ ์ง ๊ด๋ จ ๋งค๊ฐ๋ณ์๋ ZonedDateTime
๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
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
์ ์๋ต ๋ฐ๊ฒ๋ฉ๋๋ค.
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๊ฐ ๋๋ ๊ฒฝ์ฐ ์ ์ฉํฉ๋๋ค.
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 ๋ฑ์ ์ฌ์ฉํ๋ฉด ์ ์ฉํฉ๋๋ค.
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
์ผ๋ก ๋ผ์ฐํ
์ ๋ถ๋ฐฐํฉ๋๋ค.
HTTP Request, Reponse์ ๋ํ ์์ ์ ํ ์ ์์ต๋๋ค. ํน์ ๋ผ์ฐํฐ์์์ ์์์ ๋์ํ๊ฒ ๋ฉ๋๋ค.
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
๋ฅผ ํธ์ถํ๊ฒ ๋ฉ๋๋ค.
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
์์๋ ๋์ผํ ์์ฒญ์ด๋ฉด ๋์ผํ ์ด์ ๋ก ์คํจํ๊ธฐ ๋๋ฌธ์ ์ฌ์๋๋ฅผ ์ ํ๋ ๊ฒ ๋ ํจ์จ์ ์
๋๋ค. ๋จ์ ์กฐํ ์ฉ์ด ์๋๋ฉด ์ ์คํ๊ฒ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
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 ์ค์ ๊ณผ๋ ์ฐจ์ด๊ฐ ์์ต๋๋ค.
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)๋ ๋ง์ดํฌ๋ก ์๋น์ค ํ๊ฒฝ์์ ์๋ก ๋ค๋ฅธ ์์คํ ์ ์์ฒญ์ ์ฐ๊ฒฐํ์ฌ ๋ก๊น ์ ํด์ค ์ ์๊ฒ ํด์ฃผ๋ ๋๊ตฌ์ ๋๋ค. ์ด๋ฐ ๊ฒฝ์ฐ ์ฌ๋ฃจ์ค๋ฅผ ์ด์ฉํด์ ์ฝ๊ฒ ์์ฒญ์ ๋ํ ๋ก๊น ์ ์ฐ๊ฒฐํด์ ๋ณผ ์ ์์ต๋๋ค. ๋ 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
๊ฐ ํ๋๋ก ์ฐ๊ฒฐ๋ ํ๋์ ์์ฒญ์ ์ถ์ ํ ์ ์์ต๋๋ค.
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
ํด๋ผ์ด์ธํธ๋ Spring Cloud Gateway๋ฅผ ํตํด ์์ฒญ์ ํ๊ณ ๊ฒ์ดํธ์จ์ด๋ ๋งคํ์์ ์์ฒญ์ด ๊ฒฝ๋ก์ ์ผ์นํ๋ค๊ณ ํ๋จํ๋ฉด ๊ฒ์ดํธ์จ์ด ์น ์ฒ๋ฆฌ๊ธฐ๋ก ์์ฒญ์ ์ ์กํ๊ฒ ๋ฉ๋๋ค.
@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 ๋ก๊ทธ๋ ์๋์ ๊ฐ์ต๋๋ค.