Skip to content

Commit 12684cd

Browse files
committed
feat: CircuitBreaker 패턴 적용 QUZ-122
1 parent 722dd48 commit 12684cd

File tree

6 files changed

+215
-9
lines changed

6 files changed

+215
-9
lines changed

gateway-service/build.gradle.kts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ dependencies {
1919
implementation("org.springframework.boot:spring-boot-starter-security")
2020
testImplementation("org.springframework.security:spring-security-test")
2121

22-
//redis
22+
// Redis
2323
implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")
24+
25+
// Circuit Breaker
26+
implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j:3.1.0")
27+
2428
}
2529

2630
tasks.named<BootJar>("bootJar") {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.grepp.quizy.api
2+
3+
import com.grepp.quizy.common.api.ApiResponse
4+
import org.springframework.http.HttpStatus
5+
import org.springframework.http.ResponseEntity
6+
import org.springframework.web.bind.annotation.GetMapping
7+
import org.springframework.web.bind.annotation.RequestMapping
8+
import org.springframework.web.bind.annotation.RestController
9+
10+
@RestController
11+
@RequestMapping("/fallback")
12+
class GatewayFallbackController {
13+
@GetMapping("/user")
14+
fun userServiceFallback(): ResponseEntity<ApiResponse<Unit>> {
15+
return ResponseEntity
16+
.status(HttpStatus.SERVICE_UNAVAILABLE)
17+
.body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "User service is temporarily unavailable"))
18+
}
19+
20+
@GetMapping("/quiz")
21+
fun quizServiceFallback(): ResponseEntity<ApiResponse<Unit>> {
22+
return ResponseEntity
23+
.status(HttpStatus.SERVICE_UNAVAILABLE)
24+
.body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "Quiz service is temporarily unavailable"))
25+
}
26+
27+
@GetMapping("/game")
28+
fun gameServiceFallback(): ResponseEntity<ApiResponse<Unit>> {
29+
return ResponseEntity
30+
.status(HttpStatus.SERVICE_UNAVAILABLE)
31+
.body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "Game service is temporarily unavailable"))
32+
}
33+
34+
@GetMapping("/ws")
35+
fun webSocketServiceFallback(): ResponseEntity<ApiResponse<Unit>> {
36+
return ResponseEntity
37+
.status(HttpStatus.SERVICE_UNAVAILABLE)
38+
.body(
39+
ApiResponse.error(
40+
HttpStatus.SERVICE_UNAVAILABLE.name,
41+
"Game webSocket service is temporarily unavailable"
42+
)
43+
)
44+
}
45+
46+
@GetMapping("/matching")
47+
fun matchingServiceFallback(): ResponseEntity<ApiResponse<Unit>> {
48+
return ResponseEntity
49+
.status(HttpStatus.SERVICE_UNAVAILABLE)
50+
.body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "Matching service is temporarily unavailable"))
51+
}
52+
}

gateway-service/src/main/kotlin/com/grepp/quizy/global/AuthGlobalFilter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import com.grepp.quizy.jwt.JwtValidator
66
import com.grepp.quizy.user.RedisTokenRepository
77
import com.grepp.quizy.user.UserId
88
import com.grepp.quizy.user.api.global.util.CookieUtils
9-
import com.grepp.quizy.web.UserClient
9+
import com.grepp.quizy.webclient.UserClient
1010
import org.slf4j.LoggerFactory
1111
import org.springframework.cloud.gateway.filter.GatewayFilterChain
1212
import org.springframework.cloud.gateway.filter.GlobalFilter
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.grepp.quizy.webclient
2+
3+
import reactor.core.publisher.Mono
4+
5+
interface UserClient {
6+
fun validateUser(userId: Long): Mono<Unit>
7+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.grepp.quizy.webclient
2+
3+
import com.grepp.quizy.exception.CustomJwtException
4+
import com.grepp.quizy.user.RedisTokenRepository
5+
import com.grepp.quizy.user.UserId
6+
import org.springframework.beans.factory.annotation.Value
7+
import org.springframework.http.HttpStatus
8+
import org.springframework.stereotype.Component
9+
import org.springframework.web.reactive.function.client.WebClient
10+
import reactor.core.publisher.Mono
11+
12+
@Component
13+
class UserClientImpl(
14+
private val webClient: WebClient,
15+
private val redisTokenRepository: RedisTokenRepository
16+
) : UserClient {
17+
18+
@Value("\${service.user.url}")
19+
private lateinit var BASE_URL: String
20+
21+
22+
override fun validateUser(userId: Long): Mono<Unit> {
23+
if (redisTokenRepository.isExistUser(UserId(userId))) {
24+
return Mono.just(Unit)
25+
}
26+
27+
return webClient.get()
28+
.uri("$BASE_URL/api/internal/user/validate/$userId")
29+
.retrieve()
30+
.toEntity(Unit::class.java)
31+
.handle<Unit> { response, sink ->
32+
when (response.statusCode) {
33+
HttpStatus.OK -> sink.next(Unit)
34+
HttpStatus.UNAUTHORIZED -> sink.error(CustomJwtException.JwtUnknownException)
35+
else -> sink.error(CustomJwtException.NotExistUserException)
36+
}
37+
}
38+
}
39+
}

gateway-service/src/main/resources/application.yml

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ management:
44
endpoints:
55
web:
66
exposure:
7-
include: health, info
7+
include: health, info, circuitbreakers
88
springdoc:
99
swagger-ui:
1010
use-root-path: true
@@ -46,6 +46,46 @@ spring:
4646
- "*"
4747
allowCredentials: true
4848

49+
resilience4j:
50+
circuitbreaker:
51+
configs:
52+
default:
53+
slidingWindowSize: 10
54+
failureRateThreshold: 50
55+
waitDurationInOpenState: 60000
56+
permittedNumberOfCallsInHalfOpenState: 3
57+
record-exceptions:
58+
- java.io.IOException
59+
- java.util.concurrent.TimeoutException
60+
instances:
61+
userServiceCircuitBreaker:
62+
baseConfig: default
63+
gameServiceCircuitBreaker:
64+
baseConfig: default
65+
matchingServiceCircuitBreaker:
66+
baseConfig: default
67+
quizServiceCircuitBreaker:
68+
baseConfig: default
69+
wsCircuitBreaker:
70+
baseConfig: default
71+
timelimiter:
72+
configs:
73+
default:
74+
timeoutDuration: 4s
75+
SSE:
76+
timeoutDuration: 86400s # 24시간 또는 적절한 값으로 설정
77+
instances:
78+
userServiceCircuitBreaker:
79+
baseConfig: default
80+
gameServiceCircuitBreaker:
81+
baseConfig: default
82+
matchingServiceCircuitBreaker:
83+
baseConfig: SSE
84+
quizServiceCircuitBreaker:
85+
baseConfig: default
86+
wsCircuitBreaker:
87+
baseConfig: SSE
88+
4989
---
5090
spring:
5191
config:
@@ -57,29 +97,53 @@ spring:
5797
- id: game
5898
uri: http://localhost:8081
5999
predicates:
60-
- Path=/api/game/**, /ws/**
100+
- Path=/api/game/**
101+
filters:
102+
- name: RequestRateLimiter
103+
args:
104+
redis-rate-limiter.replenishRate: 10
105+
redis-rate-limiter.burstCapacity: 20
106+
redis-rate-limiter.requestedTokens: 1
107+
key-resolver: "#{@userKeyResolver}"
108+
- name: CircuitBreaker
109+
args:
110+
name: gameServiceCircuitBreaker
111+
fallbackUri: forward:/fallback/game
112+
113+
- id: webSocket
114+
uri: http://localhost:8081
115+
predicates:
116+
- Path=/ws/**
61117
filters:
62118
- name: RequestRateLimiter
63119
args:
64120
redis-rate-limiter.replenishRate: 10
65121
redis-rate-limiter.burstCapacity: 20
66122
redis-rate-limiter.requestedTokens: 1
67123
key-resolver: "#{@userKeyResolver}"
124+
- name: CircuitBreaker
125+
args:
126+
name: wsCircuitBreaker
127+
fallbackUri: forward:/fallback/ws
68128

69129
- id: matching
70130
uri: http://localhost:8082
71131
predicates:
72132
- Path=/api/matching/**
73133
metadata:
74-
response-timeout: 300000
75-
connect-timeout: 300000
134+
response-timeout: 330000
135+
connect-timeout: 330000
76136
filters:
77137
- name: RequestRateLimiter
78138
args:
79139
redis-rate-limiter.replenishRate: 10
80140
redis-rate-limiter.burstCapacity: 20
81141
redis-rate-limiter.requestedTokens: 1
82142
key-resolver: "#{@userKeyResolver}"
143+
- name: CircuitBreaker
144+
args:
145+
name: matchingServiceCircuitBreaker
146+
fallbackUri: forward:/fallback/matching
83147

84148
- id: quiz
85149
uri: http://localhost:8083
@@ -92,6 +156,10 @@ spring:
92156
redis-rate-limiter.burstCapacity: 20
93157
redis-rate-limiter.requestedTokens: 1
94158
key-resolver: "#{@userKeyResolver}"
159+
- name: CircuitBreaker
160+
args:
161+
name: quizServiceCircuitBreaker
162+
fallbackUri: forward:/fallback/quiz
95163

96164
- id: user
97165
uri: http://localhost:8085
@@ -104,6 +172,10 @@ spring:
104172
redis-rate-limiter.burstCapacity: 20
105173
redis-rate-limiter.requestedTokens: 1
106174
key-resolver: "#{@userKeyResolver}"
175+
- name: CircuitBreaker
176+
args:
177+
name: userServiceCircuitBreaker
178+
fallbackUri: forward:/fallback/user
107179

108180
data:
109181
redis:
@@ -151,29 +223,53 @@ spring:
151223
- id: game
152224
uri: http://dev-game-service:8080
153225
predicates:
154-
- Path=/api/game/**, /ws/**
226+
- Path=/api/game/**
155227
filters:
156228
- name: RequestRateLimiter
157229
args:
158230
redis-rate-limiter.replenishRate: 10
159231
redis-rate-limiter.burstCapacity: 20
160232
redis-rate-limiter.requestedTokens: 1
161233
key-resolver: "#{@userKeyResolver}"
234+
- name: CircuitBreaker
235+
args:
236+
name: gameServiceCircuitBreaker
237+
fallbackUri: forward:/fallback/game
238+
239+
- id: webSocket
240+
uri: http://dev-game-service:8080
241+
predicates:
242+
- Path=/ws/**
243+
filters:
244+
- name: RequestRateLimiter
245+
args:
246+
redis-rate-limiter.replenishRate: 10
247+
redis-rate-limiter.burstCapacity: 20
248+
redis-rate-limiter.requestedTokens: 1
249+
key-resolver: "#{@userKeyResolver}"
250+
- name: CircuitBreaker
251+
args:
252+
name: wsCircuitBreaker
253+
fallbackUri: forward:/fallback/ws
162254

163255
- id: matching
164256
uri: http://dev-matching-service:8080
165257
predicates:
166258
- Path=/api/matching/**
167259
metadata:
168-
response-timeout: 300000
169-
connect-timeout: 300000
260+
response-timeout: 330000
261+
connect-timeout: 330000
170262
filters:
171263
- name: RequestRateLimiter
172264
args:
173265
redis-rate-limiter.replenishRate: 10
174266
redis-rate-limiter.burstCapacity: 20
175267
redis-rate-limiter.requestedTokens: 1
176268
key-resolver: "#{@userKeyResolver}"
269+
- name: CircuitBreaker
270+
args:
271+
name: matchingServiceCircuitBreaker
272+
fallbackUri: forward:/fallback/matching
177273

178274
- id: quiz
179275
uri: http://dev-quiz-service:8080
@@ -186,6 +282,10 @@ spring:
186282
redis-rate-limiter.burstCapacity: 20
187283
redis-rate-limiter.requestedTokens: 1
188284
key-resolver: "#{@userKeyResolver}"
285+
- name: CircuitBreaker
286+
args:
287+
name: quizServiceCircuitBreaker
288+
fallbackUri: forward:/fallback/quiz
189289

190290
- id: user
191291
uri: http://dev-user-service:8080
@@ -198,6 +298,10 @@ spring:
198298
redis-rate-limiter.burstCapacity: 20
199299
redis-rate-limiter.requestedTokens: 1
200300
key-resolver: "#{@userKeyResolver}"
301+
- name: CircuitBreaker
302+
args:
303+
name: userServiceCircuitBreaker
304+
fallbackUri: forward:/fallback/user
201305

202306
kubernetes:
203307
discovery:

0 commit comments

Comments
 (0)