From 722dd48f5953ac52272af3993c79d03a4510345d Mon Sep 17 00:00:00 2001 From: HMWG Date: Fri, 6 Dec 2024 11:13:06 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20rate=20limiting=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20QUZ-122?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway-service/build.gradle.kts | 2 +- .../grepp/quizy/config/RateLimiterConfig.kt | 55 +++++++++ .../src/main/resources/application.yml | 110 +++++++++++------- 3 files changed, 126 insertions(+), 41 deletions(-) create mode 100644 gateway-service/src/main/kotlin/com/grepp/quizy/config/RateLimiterConfig.kt diff --git a/gateway-service/build.gradle.kts b/gateway-service/build.gradle.kts index 83c63dce..805e58d5 100644 --- a/gateway-service/build.gradle.kts +++ b/gateway-service/build.gradle.kts @@ -20,7 +20,7 @@ dependencies { testImplementation("org.springframework.security:spring-security-test") //redis - implementation("org.springframework.boot:spring-boot-starter-data-redis") + implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive") } tasks.named("bootJar") { diff --git a/gateway-service/src/main/kotlin/com/grepp/quizy/config/RateLimiterConfig.kt b/gateway-service/src/main/kotlin/com/grepp/quizy/config/RateLimiterConfig.kt new file mode 100644 index 00000000..55bb6572 --- /dev/null +++ b/gateway-service/src/main/kotlin/com/grepp/quizy/config/RateLimiterConfig.kt @@ -0,0 +1,55 @@ +package com.grepp.quizy.config + +import com.grepp.quizy.jwt.JwtProvider +import com.grepp.quizy.user.api.global.util.CookieUtils +import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpHeaders +import org.springframework.http.server.reactive.ServerHttpRequest +import reactor.core.publisher.Mono + +@Configuration +class RateLimiterConfig( + private val jwtProvider: JwtProvider, +) { + + @Bean + fun userKeyResolver(): KeyResolver { + return KeyResolver { exchange -> + // JWT 토큰에서 사용자 ID를 추출하여 rate limit key로 사용 + val token = extractToken(exchange.request) + + if (token != null) { + try { + val userId = jwtProvider.getUserIdFromToken(token) + Mono.just(userId.value.toString()) + } catch (e: Exception) { + // 토큰이 유효하지 않은 경우 IP 주소를 key로 사용 + Mono.just(exchange.request.remoteAddress?.address?.hostAddress ?: "anonymous") + } + + } else { + // 토큰이 없는 경우 IP 주소를 key로 사용 + Mono.just(exchange.request.remoteAddress?.address?.hostAddress ?: "anonymous") + } + } + } + + private fun extractToken(request: ServerHttpRequest): String? { + return if (request.headers.containsKey(HttpHeaders.AUTHORIZATION)) { + resolveToken(request) + } else { + CookieUtils.getCookieValue(request, "refreshToken") ?: "" + } + } + + private fun resolveToken(request: ServerHttpRequest): String? { + val authHeader = request.headers[HttpHeaders.AUTHORIZATION]?.get(0) ?: "" + return if (authHeader.startsWith("Bearer ")) { + authHeader.substring(7) + } else { + null + } + } +} \ No newline at end of file diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index a26826bf..72a6a5c5 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -57,7 +57,15 @@ spring: - id: game uri: http://localhost:8081 predicates: - - Path=/api/game/** + - Path=/api/game/**, /ws/** + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 + key-resolver: "#{@userKeyResolver}" + - id: matching uri: http://localhost:8082 predicates: @@ -65,30 +73,37 @@ spring: metadata: response-timeout: 300000 connect-timeout: 300000 + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 + key-resolver: "#{@userKeyResolver}" + - id: quiz uri: http://localhost:8083 predicates: - Path=/api/quiz/** + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 + key-resolver: "#{@userKeyResolver}" + - id: user uri: http://localhost:8085 predicates: - - Path=/api/user/** - - id: oauth2 - uri: http://localhost:8085 - predicates: - - Path=/oauth2/** - - id: auth - uri: http://localhost:8085 - predicates: - - Path=/api/auth/** - - id: login - uri: http://localhost:8085 - predicates: - - Path=/login/** - - id: sockjs - uri: http://localhost:8081 - predicates: - - Path=/ws/** + - Path=/api/user/**, /oauth2/**, /api/auth/**, /login/** + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 + key-resolver: "#{@userKeyResolver}" data: redis: @@ -133,18 +148,18 @@ spring: cloud: gateway: routes: - - id: quiz - uri: http://dev-quiz-service:8080 - predicates: - - Path=/api/quiz/** - id: game uri: http://dev-game-service:8080 predicates: - - Path=/api/game/** - - id: user - uri: http://dev-user-service:8080 - predicates: - - Path=/api/user/** + - Path=/api/game/**, /ws/** + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 + key-resolver: "#{@userKeyResolver}" + - id: matching uri: http://dev-matching-service:8080 predicates: @@ -152,22 +167,37 @@ spring: metadata: response-timeout: 300000 connect-timeout: 300000 - - id: oauth2 - uri: http://dev-user-service:8080 - predicates: - - Path=/oauth2/** - - id: auth - uri: http://dev-user-service:8080 + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 + key-resolver: "#{@userKeyResolver}" + + - id: quiz + uri: http://dev-quiz-service:8080 predicates: - - Path=/api/auth/** - - id: login + - Path=/api/quiz/** + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 + key-resolver: "#{@userKeyResolver}" + + - id: user uri: http://dev-user-service:8080 predicates: - - Path=/login/** - - id: sockjs - uri: http://dev-game-service:8080 - predicates: - - Path=/ws/** + - Path=/api/user/**, /oauth2/**, /api/auth/**, /login/** + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 + key-resolver: "#{@userKeyResolver}" kubernetes: discovery: From 12684cd44e7b242bd303a311b5ff053016c94c27 Mon Sep 17 00:00:00 2001 From: HMWG Date: Fri, 6 Dec 2024 21:33:16 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20CircuitBreaker=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9=20QUZ-122?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway-service/build.gradle.kts | 6 +- .../quizy/api/GatewayFallbackController.kt | 52 ++++++++ .../grepp/quizy/global/AuthGlobalFilter.kt | 2 +- .../com/grepp/quizy/webclient/UserClient.kt | 7 ++ .../grepp/quizy/webclient/UserClientImpl.kt | 39 ++++++ .../src/main/resources/application.yml | 118 ++++++++++++++++-- 6 files changed, 215 insertions(+), 9 deletions(-) create mode 100644 gateway-service/src/main/kotlin/com/grepp/quizy/api/GatewayFallbackController.kt create mode 100644 gateway-service/src/main/kotlin/com/grepp/quizy/webclient/UserClient.kt create mode 100644 gateway-service/src/main/kotlin/com/grepp/quizy/webclient/UserClientImpl.kt diff --git a/gateway-service/build.gradle.kts b/gateway-service/build.gradle.kts index 805e58d5..f6ba0237 100644 --- a/gateway-service/build.gradle.kts +++ b/gateway-service/build.gradle.kts @@ -19,8 +19,12 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-security") testImplementation("org.springframework.security:spring-security-test") - //redis + // Redis implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive") + + // Circuit Breaker + implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j:3.1.0") + } tasks.named("bootJar") { diff --git a/gateway-service/src/main/kotlin/com/grepp/quizy/api/GatewayFallbackController.kt b/gateway-service/src/main/kotlin/com/grepp/quizy/api/GatewayFallbackController.kt new file mode 100644 index 00000000..0f48815a --- /dev/null +++ b/gateway-service/src/main/kotlin/com/grepp/quizy/api/GatewayFallbackController.kt @@ -0,0 +1,52 @@ +package com.grepp.quizy.api + +import com.grepp.quizy.common.api.ApiResponse +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/fallback") +class GatewayFallbackController { + @GetMapping("/user") + fun userServiceFallback(): ResponseEntity> { + return ResponseEntity + .status(HttpStatus.SERVICE_UNAVAILABLE) + .body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "User service is temporarily unavailable")) + } + + @GetMapping("/quiz") + fun quizServiceFallback(): ResponseEntity> { + return ResponseEntity + .status(HttpStatus.SERVICE_UNAVAILABLE) + .body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "Quiz service is temporarily unavailable")) + } + + @GetMapping("/game") + fun gameServiceFallback(): ResponseEntity> { + return ResponseEntity + .status(HttpStatus.SERVICE_UNAVAILABLE) + .body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "Game service is temporarily unavailable")) + } + + @GetMapping("/ws") + fun webSocketServiceFallback(): ResponseEntity> { + return ResponseEntity + .status(HttpStatus.SERVICE_UNAVAILABLE) + .body( + ApiResponse.error( + HttpStatus.SERVICE_UNAVAILABLE.name, + "Game webSocket service is temporarily unavailable" + ) + ) + } + + @GetMapping("/matching") + fun matchingServiceFallback(): ResponseEntity> { + return ResponseEntity + .status(HttpStatus.SERVICE_UNAVAILABLE) + .body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "Matching service is temporarily unavailable")) + } +} \ No newline at end of file diff --git a/gateway-service/src/main/kotlin/com/grepp/quizy/global/AuthGlobalFilter.kt b/gateway-service/src/main/kotlin/com/grepp/quizy/global/AuthGlobalFilter.kt index 6513ec26..2e8411d2 100644 --- a/gateway-service/src/main/kotlin/com/grepp/quizy/global/AuthGlobalFilter.kt +++ b/gateway-service/src/main/kotlin/com/grepp/quizy/global/AuthGlobalFilter.kt @@ -6,7 +6,7 @@ import com.grepp.quizy.jwt.JwtValidator import com.grepp.quizy.user.RedisTokenRepository import com.grepp.quizy.user.UserId import com.grepp.quizy.user.api.global.util.CookieUtils -import com.grepp.quizy.web.UserClient +import com.grepp.quizy.webclient.UserClient import org.slf4j.LoggerFactory import org.springframework.cloud.gateway.filter.GatewayFilterChain import org.springframework.cloud.gateway.filter.GlobalFilter diff --git a/gateway-service/src/main/kotlin/com/grepp/quizy/webclient/UserClient.kt b/gateway-service/src/main/kotlin/com/grepp/quizy/webclient/UserClient.kt new file mode 100644 index 00000000..9993b5ff --- /dev/null +++ b/gateway-service/src/main/kotlin/com/grepp/quizy/webclient/UserClient.kt @@ -0,0 +1,7 @@ +package com.grepp.quizy.webclient + +import reactor.core.publisher.Mono + +interface UserClient { + fun validateUser(userId: Long): Mono +} \ No newline at end of file diff --git a/gateway-service/src/main/kotlin/com/grepp/quizy/webclient/UserClientImpl.kt b/gateway-service/src/main/kotlin/com/grepp/quizy/webclient/UserClientImpl.kt new file mode 100644 index 00000000..27a624cd --- /dev/null +++ b/gateway-service/src/main/kotlin/com/grepp/quizy/webclient/UserClientImpl.kt @@ -0,0 +1,39 @@ +package com.grepp.quizy.webclient + +import com.grepp.quizy.exception.CustomJwtException +import com.grepp.quizy.user.RedisTokenRepository +import com.grepp.quizy.user.UserId +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono + +@Component +class UserClientImpl( + private val webClient: WebClient, + private val redisTokenRepository: RedisTokenRepository +) : UserClient { + + @Value("\${service.user.url}") + private lateinit var BASE_URL: String + + + override fun validateUser(userId: Long): Mono { + if (redisTokenRepository.isExistUser(UserId(userId))) { + return Mono.just(Unit) + } + + return webClient.get() + .uri("$BASE_URL/api/internal/user/validate/$userId") + .retrieve() + .toEntity(Unit::class.java) + .handle { response, sink -> + when (response.statusCode) { + HttpStatus.OK -> sink.next(Unit) + HttpStatus.UNAUTHORIZED -> sink.error(CustomJwtException.JwtUnknownException) + else -> sink.error(CustomJwtException.NotExistUserException) + } + } + } +} \ No newline at end of file diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 72a6a5c5..b38c2868 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -4,7 +4,7 @@ management: endpoints: web: exposure: - include: health, info + include: health, info, circuitbreakers springdoc: swagger-ui: use-root-path: true @@ -46,6 +46,46 @@ spring: - "*" allowCredentials: true +resilience4j: + circuitbreaker: + configs: + default: + slidingWindowSize: 10 + failureRateThreshold: 50 + waitDurationInOpenState: 60000 + permittedNumberOfCallsInHalfOpenState: 3 + record-exceptions: + - java.io.IOException + - java.util.concurrent.TimeoutException + instances: + userServiceCircuitBreaker: + baseConfig: default + gameServiceCircuitBreaker: + baseConfig: default + matchingServiceCircuitBreaker: + baseConfig: default + quizServiceCircuitBreaker: + baseConfig: default + wsCircuitBreaker: + baseConfig: default + timelimiter: + configs: + default: + timeoutDuration: 4s + SSE: + timeoutDuration: 86400s # 24시간 또는 적절한 값으로 설정 + instances: + userServiceCircuitBreaker: + baseConfig: default + gameServiceCircuitBreaker: + baseConfig: default + matchingServiceCircuitBreaker: + baseConfig: SSE + quizServiceCircuitBreaker: + baseConfig: default + wsCircuitBreaker: + baseConfig: SSE + --- spring: config: @@ -57,7 +97,23 @@ spring: - id: game uri: http://localhost:8081 predicates: - - Path=/api/game/**, /ws/** + - Path=/api/game/** + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 + key-resolver: "#{@userKeyResolver}" + - name: CircuitBreaker + args: + name: gameServiceCircuitBreaker + fallbackUri: forward:/fallback/game + + - id: webSocket + uri: http://localhost:8081 + predicates: + - Path=/ws/** filters: - name: RequestRateLimiter args: @@ -65,14 +121,18 @@ spring: redis-rate-limiter.burstCapacity: 20 redis-rate-limiter.requestedTokens: 1 key-resolver: "#{@userKeyResolver}" + - name: CircuitBreaker + args: + name: wsCircuitBreaker + fallbackUri: forward:/fallback/ws - id: matching uri: http://localhost:8082 predicates: - Path=/api/matching/** metadata: - response-timeout: 300000 - connect-timeout: 300000 + response-timeout: 330000 + connect-timeout: 330000 filters: - name: RequestRateLimiter args: @@ -80,6 +140,10 @@ spring: redis-rate-limiter.burstCapacity: 20 redis-rate-limiter.requestedTokens: 1 key-resolver: "#{@userKeyResolver}" + - name: CircuitBreaker + args: + name: matchingServiceCircuitBreaker + fallbackUri: forward:/fallback/matching - id: quiz uri: http://localhost:8083 @@ -92,6 +156,10 @@ spring: redis-rate-limiter.burstCapacity: 20 redis-rate-limiter.requestedTokens: 1 key-resolver: "#{@userKeyResolver}" + - name: CircuitBreaker + args: + name: quizServiceCircuitBreaker + fallbackUri: forward:/fallback/quiz - id: user uri: http://localhost:8085 @@ -104,6 +172,10 @@ spring: redis-rate-limiter.burstCapacity: 20 redis-rate-limiter.requestedTokens: 1 key-resolver: "#{@userKeyResolver}" + - name: CircuitBreaker + args: + name: userServiceCircuitBreaker + fallbackUri: forward:/fallback/user data: redis: @@ -151,7 +223,7 @@ spring: - id: game uri: http://dev-game-service:8080 predicates: - - Path=/api/game/**, /ws/** + - Path=/api/game/** filters: - name: RequestRateLimiter args: @@ -159,14 +231,34 @@ spring: redis-rate-limiter.burstCapacity: 20 redis-rate-limiter.requestedTokens: 1 key-resolver: "#{@userKeyResolver}" + - name: CircuitBreaker + args: + name: gameServiceCircuitBreaker + fallbackUri: forward:/fallback/game + + - id: webSocket + uri: http://dev-game-service:8080 + predicates: + - Path=/ws/** + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 + key-resolver: "#{@userKeyResolver}" + - name: CircuitBreaker + args: + name: wsCircuitBreaker + fallbackUri: forward:/fallback/ws - id: matching uri: http://dev-matching-service:8080 predicates: - Path=/api/matching/** metadata: - response-timeout: 300000 - connect-timeout: 300000 + response-timeout: 330000 + connect-timeout: 330000 filters: - name: RequestRateLimiter args: @@ -174,6 +266,10 @@ spring: redis-rate-limiter.burstCapacity: 20 redis-rate-limiter.requestedTokens: 1 key-resolver: "#{@userKeyResolver}" + - name: CircuitBreaker + args: + name: matchingServiceCircuitBreaker + fallbackUri: forward:/fallback/matching - id: quiz uri: http://dev-quiz-service:8080 @@ -186,6 +282,10 @@ spring: redis-rate-limiter.burstCapacity: 20 redis-rate-limiter.requestedTokens: 1 key-resolver: "#{@userKeyResolver}" + - name: CircuitBreaker + args: + name: quizServiceCircuitBreaker + fallbackUri: forward:/fallback/quiz - id: user uri: http://dev-user-service:8080 @@ -198,6 +298,10 @@ spring: redis-rate-limiter.burstCapacity: 20 redis-rate-limiter.requestedTokens: 1 key-resolver: "#{@userKeyResolver}" + - name: CircuitBreaker + args: + name: userServiceCircuitBreaker + fallbackUri: forward:/fallback/user kubernetes: discovery: From eb68c43adf83794821d419c171e3f2309f6d13a2 Mon Sep 17 00:00:00 2001 From: HMWG Date: Fri, 6 Dec 2024 21:46:54 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20CircuitBreaker=20fallback=20api?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=98=88=EC=99=B8=EB=A5=BC=20=EB=8D=98?= =?UTF-8?q?=EC=A7=80=EA=B2=8C=20=EC=88=98=EC=A0=95=20QUZ-122?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../quizy/api/GatewayFallbackController.kt | 39 +++++------------ .../exception/CircuitBreakerErrorCode.kt | 22 ++++++++++ .../CustomCircuitBreakerException.kt | 42 +++++++++++++++++++ 3 files changed, 75 insertions(+), 28 deletions(-) create mode 100644 gateway-service/src/main/kotlin/com/grepp/quizy/exception/CircuitBreakerErrorCode.kt create mode 100644 gateway-service/src/main/kotlin/com/grepp/quizy/exception/CustomCircuitBreakerException.kt diff --git a/gateway-service/src/main/kotlin/com/grepp/quizy/api/GatewayFallbackController.kt b/gateway-service/src/main/kotlin/com/grepp/quizy/api/GatewayFallbackController.kt index 0f48815a..243928f8 100644 --- a/gateway-service/src/main/kotlin/com/grepp/quizy/api/GatewayFallbackController.kt +++ b/gateway-service/src/main/kotlin/com/grepp/quizy/api/GatewayFallbackController.kt @@ -1,8 +1,6 @@ package com.grepp.quizy.api -import com.grepp.quizy.common.api.ApiResponse -import org.springframework.http.HttpStatus -import org.springframework.http.ResponseEntity +import com.grepp.quizy.exception.CustomCircuitBreakerException import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -11,42 +9,27 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("/fallback") class GatewayFallbackController { @GetMapping("/user") - fun userServiceFallback(): ResponseEntity> { - return ResponseEntity - .status(HttpStatus.SERVICE_UNAVAILABLE) - .body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "User service is temporarily unavailable")) + fun userServiceFallback() { + throw CustomCircuitBreakerException.UserServiceUnavailableException } @GetMapping("/quiz") - fun quizServiceFallback(): ResponseEntity> { - return ResponseEntity - .status(HttpStatus.SERVICE_UNAVAILABLE) - .body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "Quiz service is temporarily unavailable")) + fun quizServiceFallback() { + throw CustomCircuitBreakerException.QuizServiceUnavailableException } @GetMapping("/game") - fun gameServiceFallback(): ResponseEntity> { - return ResponseEntity - .status(HttpStatus.SERVICE_UNAVAILABLE) - .body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "Game service is temporarily unavailable")) + fun gameServiceFallback() { + throw CustomCircuitBreakerException.GameServiceUnavailableException } @GetMapping("/ws") - fun webSocketServiceFallback(): ResponseEntity> { - return ResponseEntity - .status(HttpStatus.SERVICE_UNAVAILABLE) - .body( - ApiResponse.error( - HttpStatus.SERVICE_UNAVAILABLE.name, - "Game webSocket service is temporarily unavailable" - ) - ) + fun webSocketServiceFallback() { + throw CustomCircuitBreakerException.WsUnavailableException } @GetMapping("/matching") - fun matchingServiceFallback(): ResponseEntity> { - return ResponseEntity - .status(HttpStatus.SERVICE_UNAVAILABLE) - .body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "Matching service is temporarily unavailable")) + fun matchingServiceFallback() { + throw CustomCircuitBreakerException.MatchingServiceUnavailableException } } \ No newline at end of file diff --git a/gateway-service/src/main/kotlin/com/grepp/quizy/exception/CircuitBreakerErrorCode.kt b/gateway-service/src/main/kotlin/com/grepp/quizy/exception/CircuitBreakerErrorCode.kt new file mode 100644 index 00000000..c6855ea9 --- /dev/null +++ b/gateway-service/src/main/kotlin/com/grepp/quizy/exception/CircuitBreakerErrorCode.kt @@ -0,0 +1,22 @@ +package com.grepp.quizy.exception + +import com.grepp.quizy.common.exception.BaseErrorCode +import com.grepp.quizy.common.exception.ErrorReason +import org.springframework.http.HttpStatus + +enum class CircuitBreakerErrorCode( + private val status: Int, + private val errorCode: String, + private val message: String, +) : BaseErrorCode { + + GAME_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE.value(), "CIRCUIT_BREAKER_503", "게임 서비스를 이용할 수 없습니다."), + MATCHING_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE.value(), "CIRCUIT_BREAKER_503", "매칭 서비스를 이용할 수 없습니다."), + QUIZ_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE.value(), "CIRCUIT_BREAKER_503", "퀴즈 서비스를 이용할 수 없습니다."), + USER_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE.value(), "CIRCUIT_BREAKER_503", "사용자 서비스를 이용할 수 없습니다."), + WS_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE.value(), "CIRCUIT_BREAKER_503", "웹소켓 서비스를 이용할 수 없습니다."), + ; + + override val errorReason: ErrorReason + get() = ErrorReason(status, errorCode, message) +} \ No newline at end of file diff --git a/gateway-service/src/main/kotlin/com/grepp/quizy/exception/CustomCircuitBreakerException.kt b/gateway-service/src/main/kotlin/com/grepp/quizy/exception/CustomCircuitBreakerException.kt new file mode 100644 index 00000000..4c6a326d --- /dev/null +++ b/gateway-service/src/main/kotlin/com/grepp/quizy/exception/CustomCircuitBreakerException.kt @@ -0,0 +1,42 @@ +package com.grepp.quizy.exception + +import com.grepp.quizy.common.exception.WebException + +sealed class CustomCircuitBreakerException(errorCode: CircuitBreakerErrorCode) : WebException(errorCode) { + data object GameServiceUnavailableException : + CustomCircuitBreakerException(CircuitBreakerErrorCode.GAME_UNAVAILABLE) { + private fun readResolve(): Any = GameServiceUnavailableException + + val EXCEPTION: CustomCircuitBreakerException = GameServiceUnavailableException + } + + data object UserServiceUnavailableException : + CustomCircuitBreakerException(CircuitBreakerErrorCode.USER_UNAVAILABLE) { + private fun readResolve(): Any = UserServiceUnavailableException + + val EXCEPTION: CustomCircuitBreakerException = UserServiceUnavailableException + } + + data object QuizServiceUnavailableException : + CustomCircuitBreakerException(CircuitBreakerErrorCode.QUIZ_UNAVAILABLE) { + private fun readResolve(): Any = QuizServiceUnavailableException + + val EXCEPTION: CustomCircuitBreakerException = QuizServiceUnavailableException + } + + data object MatchingServiceUnavailableException : + CustomCircuitBreakerException(CircuitBreakerErrorCode.MATCHING_UNAVAILABLE) { + private fun readResolve(): Any = MatchingServiceUnavailableException + + val EXCEPTION: CustomCircuitBreakerException = MatchingServiceUnavailableException + } + + data object WsUnavailableException : + CustomCircuitBreakerException(CircuitBreakerErrorCode.WS_UNAVAILABLE) { + private fun readResolve(): Any = WsUnavailableException + + val EXCEPTION: CustomCircuitBreakerException = WsUnavailableException + } +} + +