From 365f32d9e4db315f5e1c850f9cff9f8a747889f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Fri, 29 Aug 2025 19:40:12 +0900 Subject: [PATCH 01/21] =?UTF-8?q?feat=20(=20#34=20)=20:=20RESILIENCE4J=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- buildSrc/src/main/kotlin/Dependencies.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 6ee49ddd..88f44d32 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -99,4 +99,11 @@ object Dependencies { // Cache (스프링 캐시) const val SPRING_CACHE = "org.springframework.boot:spring-boot-starter-cache" + //Resilience4j + const val RESILIENCE4J_CIRCUITBREAKER = "io.github.resilience4j:resilience4j-circuitbreaker:${DependencyVersions.RESILIENCE4J}" + const val RESILIENCE4J_RETRY = "io.github.resilience4j:resilience4j-retry:${DependencyVersions.RESILIENCE4J}" + const val RESILIENCE4J_SPRING_BOOT = "io.github.resilience4j:resilience4j-spring-boot3:${DependencyVersions.RESILIENCE4J}" + const val RESILIENCE4J_KOTLIN = "io.github.resilience4j:resilience4j-kotlin:${DependencyVersions.RESILIENCE4J}" + + } From 2e1470d12b316460c4e893086dd03e6984bf5f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Sun, 31 Aug 2025 13:56:40 +0900 Subject: [PATCH 02/21] =?UTF-8?q?feat=20(=20#34=20)=20:=20RESILIENCE4J=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- buildSrc/src/main/kotlin/DependencyVersions.kt | 3 +++ casper-application-infrastructure/build.gradle.kts | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/buildSrc/src/main/kotlin/DependencyVersions.kt b/buildSrc/src/main/kotlin/DependencyVersions.kt index 0218932e..d6d48f01 100644 --- a/buildSrc/src/main/kotlin/DependencyVersions.kt +++ b/buildSrc/src/main/kotlin/DependencyVersions.kt @@ -44,4 +44,7 @@ object DependencyVersions { // Caffeine const val CAFFEINE = "3.1.8" + + //Resilience4j + const val RESILIENCE4J = "2.0.2" } diff --git a/casper-application-infrastructure/build.gradle.kts b/casper-application-infrastructure/build.gradle.kts index cb3351d0..1365feb9 100644 --- a/casper-application-infrastructure/build.gradle.kts +++ b/casper-application-infrastructure/build.gradle.kts @@ -104,6 +104,12 @@ dependencies { // MySQL runtimeOnly(Dependencies.MYSQL_CONNECTOR) + + // Resilience4j + implementation(Dependencies.RESILIENCE4J_CIRCUITBREAKER) + implementation(Dependencies.RESILIENCE4J_RETRY) + implementation(Dependencies.RESILIENCE4J_SPRING_BOOT) + implementation(Dependencies.RESILIENCE4J_KOTLIN) } sourceSets { From 10ce71b44a095aee6e228a40f29b6790ce412afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Sun, 31 Aug 2025 13:56:58 +0900 Subject: [PATCH 03/21] =?UTF-8?q?feat=20(=20#34=20)=20:=20RESILIENCE4J=20+?= =?UTF-8?q?=20Retry=20config=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/ResilienceConfig.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt new file mode 100644 index 00000000..166a46e6 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt @@ -0,0 +1,35 @@ +package hs.kr.entrydsm.application.global.config + +import io.github.resilience4j.circuitbreaker.CircuitBreaker +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry +import io.github.resilience4j.retry.Retry +import io.github.resilience4j.retry.RetryRegistry +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class ResilienceConfig( + private val circuitBreakerRegistry: CircuitBreakerRegistry, + private val retryRegistry: RetryRegistry +) { + + @Bean + fun userGrpcCircuitBreaker(): CircuitBreaker { + return circuitBreakerRegistry.circuitBreaker("user-grpc") + } + + @Bean + fun statusGrpcCircuitBreaker(): CircuitBreaker { + return circuitBreakerRegistry.circuitBreaker("status-grpc") + } + + @Bean + fun userGrpcRetry(): Retry { + return retryRegistry.retry("user-grpc") + } + + @Bean + fun statusGrpcRetry(): Retry { + return retryRegistry.retry("status-grpc") + } +} From 1cf6b8b22107a6b3f8d2e7cae2d26ca8b1131b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Sun, 31 Aug 2025 13:57:21 +0900 Subject: [PATCH 04/21] =?UTF-8?q?feat=20(=20#34=20)=20:=20RESILIENCE4J=20e?= =?UTF-8?q?xtensions=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/extension/ResilienceExtensions.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceExtensions.kt diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceExtensions.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceExtensions.kt new file mode 100644 index 00000000..6452f49e --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceExtensions.kt @@ -0,0 +1,22 @@ +package hs.kr.entrydsm.application.global.extension + +import io.github.resilience4j.circuitbreaker.CircuitBreaker +import io.github.resilience4j.kotlin.circuitbreaker.executeSuspendFunction +import io.github.resilience4j.kotlin.retry.executeSuspendFunction +import io.github.resilience4j.retry.Retry +import kotlinx.coroutines.coroutineScope + +suspend fun executeGrpcCallWithResilience( + retry: Retry, + circuitBreaker: CircuitBreaker, + fallback: suspend () -> T, + block: suspend () -> T +): T = coroutineScope { + try { + circuitBreaker.executeSuspendFunction { + retry.executeSuspendFunction(block) + } + }catch (e: Exception) { + fallback() + } +} \ No newline at end of file From 57ebb8e0a1e8625a3e9e75b888ad13511d18edc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Sun, 31 Aug 2025 13:57:40 +0900 Subject: [PATCH 05/21] =?UTF-8?q?feat=20(=20#34=20)=20:=20RESILIENCE4J=20e?= =?UTF-8?q?xtenstions=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grpc/client/status/StatusGrpcClient.kt | 111 ++++++++++++------ .../global/grpc/client/user/UserGrpcClient.kt | 79 ++++++++----- 2 files changed, 123 insertions(+), 67 deletions(-) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/status/StatusGrpcClient.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/status/StatusGrpcClient.kt index 473f3ebd..d90370c1 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/status/StatusGrpcClient.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/status/StatusGrpcClient.kt @@ -1,15 +1,19 @@ package hs.kr.entrydsm.application.global.grpc.client.status import com.google.protobuf.Empty +import hs.kr.entrydsm.application.global.extension.executeGrpcCallWithResilience import hs.kr.entrydsm.application.global.grpc.dto.status.ApplicationStatus import hs.kr.entrydsm.application.global.grpc.dto.status.InternalStatusListResponse import hs.kr.entrydsm.application.global.grpc.dto.status.InternalStatusResponse import hs.kr.entrydsm.casper.status.proto.StatusServiceGrpc import hs.kr.entrydsm.casper.status.proto.StatusServiceProto +import io.github.resilience4j.circuitbreaker.CircuitBreaker +import io.github.resilience4j.retry.Retry import io.grpc.Channel import io.grpc.stub.StreamObserver import kotlinx.coroutines.suspendCancellableCoroutine import net.devh.boot.grpc.client.inject.GrpcClient +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Component import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -20,7 +24,10 @@ import kotlin.coroutines.resumeWithException * @property channel gRPC 통신을 위한 채널 (status-service로 자동 주입됨) */ @Component -class StatusGrpcClient { +class StatusGrpcClient( + @Qualifier("statusGrpcRetry") private val retry: Retry, + @Qualifier("statusGrpcCircuitBreaker") private val circuitBreaker: CircuitBreaker +) { @GrpcClient("status-service") lateinit var channel: Channel @@ -33,6 +40,15 @@ class StatusGrpcClient { * @throws java.util.concurrent.CancellationException 코루틴이 취소된 경우 */ suspend fun getStatusList(): InternalStatusListResponse { + return executeGrpcCallWithResilience( + retry = retry, + circuitBreaker = circuitBreaker, + fallback = { + InternalStatusListResponse(statusList = emptyList()) + } + ) { + + val statusStub = StatusServiceGrpc.newStub(channel) val request = Empty.getDefaultInstance() @@ -55,7 +71,7 @@ class StatusGrpcClient { ) } - return InternalStatusListResponse( + InternalStatusListResponse( statusList = response.statusListList.map { statusElement -> InternalStatusResponse( @@ -69,6 +85,7 @@ class StatusGrpcClient { }, ) } +} /** * 접수번호로 특정 상태를 비동기적으로 조회합니다. @@ -80,15 +97,27 @@ class StatusGrpcClient { * @throws java.util.concurrent.CancellationException 코루틴이 취소된 경우 */ suspend fun getStatusByReceiptCode(receiptCode: Long): InternalStatusResponse { - val statusStub = StatusServiceGrpc.newStub(channel) - - val request = - StatusServiceProto.GetStatusByReceiptCodeRequest.newBuilder() + return executeGrpcCallWithResilience( + retry = retry, + circuitBreaker = circuitBreaker, + fallback = { + // Fallback: 기본 상태 반환 + InternalStatusResponse( + id = 0L, + applicationStatus = ApplicationStatus.NOT_APPLIED, + examCode = null, + isFirstRoundPass = false, + isSecondRoundPass = false, + receiptCode = receiptCode + ) + } + ) { + val statusStub = StatusServiceGrpc.newStub(channel) + val request = StatusServiceProto.GetStatusByReceiptCodeRequest.newBuilder() .setReceiptCode(receiptCode) .build() - val response = - suspendCancellableCoroutine { continuation -> + val response = suspendCancellableCoroutine { continuation -> statusStub.getStatusByReceiptCode( request, object : StreamObserver { @@ -101,17 +130,19 @@ class StatusGrpcClient { } override fun onCompleted() {} - }, + } ) } - return InternalStatusResponse( - id = response.status.id, - applicationStatus = mapProtoApplicationStatus(response.status.applicationStatus), - examCode = response.status.examCode.takeIf { it.isNotBlank() }, - isFirstRoundPass = response.status.isFirstRoundPass, - isSecondRoundPass = response.status.isSecondRoundPass, - receiptCode = response.status.receiptCode, - ) + + InternalStatusResponse( + id = response.status.id, + applicationStatus = mapProtoApplicationStatus(response.status.applicationStatus), + examCode = response.status.examCode.takeIf { it.isNotBlank() }, + isFirstRoundPass = response.status.isFirstRoundPass, + isSecondRoundPass = response.status.isSecondRoundPass, + receiptCode = response.status.receiptCode + ) + } } /** @@ -123,33 +154,37 @@ class StatusGrpcClient { * @throws io.grpc.StatusRuntimeException gRPC 서버에서 오류가 발생한 경우 * @throws java.util.concurrent.CancellationException 코루틴이 취소된 경우 */ - suspend fun updateExamCode( - receiptCode: Long, - examCode: String, - ) { - val statusStub = StatusServiceGrpc.newStub(channel) - - val request = - StatusServiceProto.GetExamCodeRequest.newBuilder() + suspend fun updateExamCode(receiptCode: Long, examCode: String) { + return executeGrpcCallWithResilience( + retry = retry, + circuitBreaker = circuitBreaker, + fallback = { + // Fallback: 로깅만 하고 조용히 실패 + println("Failed to update exam code for receiptCode: $receiptCode") + } + ) { + val statusStub = StatusServiceGrpc.newStub(channel) + val request = StatusServiceProto.GetExamCodeRequest.newBuilder() .setReceiptCode(receiptCode) .setExamCode(examCode) .build() - suspendCancellableCoroutine { continuation -> - statusStub.updateExamCode( - request, - object : StreamObserver { - override fun onNext(value: Empty) { - continuation.resume(Unit) - } + suspendCancellableCoroutine { continuation -> + statusStub.updateExamCode( + request, + object : StreamObserver { + override fun onNext(value: Empty) { + continuation.resume(Unit) + } - override fun onError(t: Throwable) { - continuation.resumeWithException(t) - } + override fun onError(t: Throwable) { + continuation.resumeWithException(t) + } - override fun onCompleted() {} - }, - ) + override fun onCompleted() {} + } + ) + } } } diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/user/UserGrpcClient.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/user/UserGrpcClient.kt index 339476e1..221574ab 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/user/UserGrpcClient.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/user/UserGrpcClient.kt @@ -1,15 +1,19 @@ package hs.kr.entrydsm.application.global.grpc.client.user +import hs.kr.entrydsm.application.global.extension.executeGrpcCallWithResilience import hs.kr.entrydsm.application.global.grpc.dto.user.InternalUserResponse import hs.kr.entrydsm.application.global.grpc.dto.user.UserRole import hs.kr.entrydsm.casper.user.proto.UserServiceGrpc import hs.kr.entrydsm.casper.user.proto.UserServiceProto +import io.github.resilience4j.circuitbreaker.CircuitBreaker +import io.github.resilience4j.retry.Retry import io.grpc.Channel import io.grpc.stub.StreamObserver import kotlinx.coroutines.suspendCancellableCoroutine import net.devh.boot.grpc.client.inject.GrpcClient +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Component -import java.util.UUID +import java.util.* import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -19,7 +23,10 @@ import kotlin.coroutines.resumeWithException * @property channel gRPC 통신을 위한 채널 (user-service로 자동 주입됨) */ @Component -class UserGrpcClient { +class UserGrpcClient( + @Qualifier("userGrpcRetry") private val retry: Retry, + @Qualifier("userGrpcCircuitBreaker") private val circuitBreaker: CircuitBreaker +) { @GrpcClient("user-service") lateinit var channel: Channel @@ -33,38 +40,52 @@ class UserGrpcClient { * @throws java.util.concurrent.CancellationException 코루틴이 취소된 경우 */ suspend fun getUserInfoByUserId(userId: UUID): InternalUserResponse { - val userStub = UserServiceGrpc.newStub(channel) + return executeGrpcCallWithResilience( + retry = retry, + circuitBreaker = circuitBreaker, + fallback = { + InternalUserResponse( + id = userId, + phoneNumber = "N/A", + name = "Unknown User", + isParent = false, + role = UserRole.USER + ) + } + ) { + val userStub = UserServiceGrpc.newStub(channel) - val request = - UserServiceProto.GetUserInfoRequest.newBuilder() - .setUserId(userId.toString()) - .build() + val request = + UserServiceProto.GetUserInfoRequest.newBuilder() + .setUserId(userId.toString()) + .build() - val response = - suspendCancellableCoroutine { continuation -> - userStub.getUserInfoByUserId( - request, - object : StreamObserver { - override fun onNext(value: UserServiceProto.GetUserInfoResponse) { - continuation.resume(value) - } + val response = + suspendCancellableCoroutine { continuation -> + userStub.getUserInfoByUserId( + request, + object : StreamObserver { + override fun onNext(value: UserServiceProto.GetUserInfoResponse) { + continuation.resume(value) + } - override fun onError(t: Throwable) { - continuation.resumeWithException(t) - } + override fun onError(t: Throwable) { + continuation.resumeWithException(t) + } - override fun onCompleted() {} - }, - ) - } + override fun onCompleted() {} + }, + ) + } - return InternalUserResponse( - id = UUID.fromString(response.id), - phoneNumber = response.phoneNumber, - name = response.name, - isParent = response.isParent, - role = mapProtoUserRole(response.role), - ) + InternalUserResponse( + id = UUID.fromString(response.id), + phoneNumber = response.phoneNumber, + name = response.name, + isParent = response.isParent, + role = mapProtoUserRole(response.role), + ) + } } /** From 2c2efe154760a2c8fd26d996f29e911221d13408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Sun, 31 Aug 2025 13:57:53 +0900 Subject: [PATCH 06/21] =?UTF-8?q?feat=20(=20#34=20)=20:=20=EC=84=9C?= =?UTF-8?q?=ED=82=B7=20=EB=B8=8C=EB=A0=88=EC=9D=B4=EC=BB=A4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grpc/test/GrpcResilienceTestController.kt | 296 ++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt new file mode 100644 index 00000000..a4be8f44 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt @@ -0,0 +1,296 @@ +package hs.kr.entrydsm.application.global.grpc.test + +import hs.kr.entrydsm.application.global.extension.executeGrpcCallWithResilience +import hs.kr.entrydsm.application.global.grpc.dto.status.ApplicationStatus +import hs.kr.entrydsm.application.global.grpc.dto.status.InternalStatusListResponse +import hs.kr.entrydsm.application.global.grpc.dto.status.InternalStatusResponse +import hs.kr.entrydsm.application.global.grpc.dto.user.InternalUserResponse +import hs.kr.entrydsm.application.global.grpc.dto.user.UserRole +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry +import io.github.resilience4j.retry.RetryRegistry +import kotlinx.coroutines.delay +import org.springframework.web.bind.annotation.* +import java.util.* +import kotlin.random.Random + +@RestController +@RequestMapping("/test/dummy-grpc") +class DummyGrpcResilienceTestController( + private val circuitBreakerRegistry: CircuitBreakerRegistry, + private val retryRegistry: RetryRegistry +) { + + // 실패 시뮬레이션을 위한 플래그 + private var shouldFailUser = false + private var shouldFailStatus = false + private var shouldBeSlowUser = false + private var shouldBeSlowStatus = false + + @GetMapping("/user/{userId}") + suspend fun testDummyUserGrpc(@PathVariable userId: String): Map { + val startTime = System.currentTimeMillis() + val retry = retryRegistry.retry("user-grpc") + val circuitBreaker = circuitBreakerRegistry.circuitBreaker("user-grpc") + + return try { + val response = executeGrpcCallWithResilience( + retry = retry, + circuitBreaker = circuitBreaker, + fallback = { + // Fallback 응답 + InternalUserResponse( + id = UUID.fromString(userId), + phoneNumber = "N/A", + name = "Unknown User (Fallback)", + isParent = false, + role = UserRole.USER + ) + } + ) { + // 더미 gRPC 호출 시뮬레이션 + simulateUserGrpcCall(userId) + } + + mapOf( + "success" to true, + "data" to response, + "executionTime" to "${System.currentTimeMillis() - startTime}ms", + "circuitBreakerState" to getCircuitBreakerState("user-grpc"), + "retryMetrics" to getRetryMetrics("user-grpc"), + "source" to if (response.name.contains("Fallback")) "fallback" else "grpc" + ) + } catch (e: Exception) { + mapOf( + "success" to false, + "error" to (e.message ?: "Unknown error"), + "executionTime" to "${System.currentTimeMillis() - startTime}ms", + "circuitBreakerState" to getCircuitBreakerState("user-grpc"), + "retryMetrics" to getRetryMetrics("user-grpc") + ) + } + } + + @GetMapping("/status/list") + suspend fun testDummyStatusListGrpc(): Map { + val startTime = System.currentTimeMillis() + val retry = retryRegistry.retry("status-grpc") + val circuitBreaker = circuitBreakerRegistry.circuitBreaker("status-grpc") + + return try { + val response = executeGrpcCallWithResilience( + retry = retry, + circuitBreaker = circuitBreaker, + fallback = { + // Fallback 응답 + InternalStatusListResponse(statusList = emptyList()) + } + ) { + // 더미 gRPC 호출 시뮬레이션 + simulateStatusListGrpcCall() + } + + mapOf( + "success" to true, + "data" to response, + "executionTime" to "${System.currentTimeMillis() - startTime}ms", + "circuitBreakerState" to getCircuitBreakerState("status-grpc"), + "retryMetrics" to getRetryMetrics("status-grpc"), + "source" to if (response.statusList.isEmpty()) "fallback" else "grpc" + ) + } catch (e: Exception) { + mapOf( + "success" to false, + "error" to (e.message ?: "Unknown error"), + "executionTime" to "${System.currentTimeMillis() - startTime}ms", + "circuitBreakerState" to getCircuitBreakerState("status-grpc"), + "retryMetrics" to getRetryMetrics("status-grpc") + ) + } + } + + // 실패/성공 시뮬레이션 제어 API + @PostMapping("/control/user/fail/{shouldFail}") + fun setUserFailure(@PathVariable shouldFail: Boolean): Map { + shouldFailUser = shouldFail + return mapOf( + "message" to "User service failure simulation set to: $shouldFail", + "currentState" to mapOf( + "shouldFailUser" to shouldFailUser, + "shouldBeSlowUser" to shouldBeSlowUser + ) + ) + } + + @PostMapping("/control/status/fail/{shouldFail}") + fun setStatusFailure(@PathVariable shouldFail: Boolean): Map { + shouldFailStatus = shouldFail + return mapOf( + "message" to "Status service failure simulation set to: $shouldFail", + "currentState" to mapOf( + "shouldFailStatus" to shouldFailStatus, + "shouldBeSlowStatus" to shouldBeSlowStatus + ) + ) + } + + @PostMapping("/control/user/slow/{shouldBeSlow}") + fun setUserSlow(@PathVariable shouldBeSlow: Boolean): Map { + shouldBeSlowUser = shouldBeSlow + return mapOf( + "message" to "User service slow call simulation set to: $shouldBeSlow", + "currentState" to mapOf( + "shouldFailUser" to shouldFailUser, + "shouldBeSlowUser" to shouldBeSlowUser + ) + ) + } + + @PostMapping("/control/status/slow/{shouldBeSlow}") + fun setStatusSlow(@PathVariable shouldBeSlow: Boolean): Map { + shouldBeSlowStatus = shouldBeSlow + return mapOf( + "message" to "Status service slow call simulation set to: $shouldBeSlow", + "currentState" to mapOf( + "shouldFailStatus" to shouldFailStatus, + "shouldBeSlowStatus" to shouldBeSlowStatus + ) + ) + } + + @GetMapping("/control/status") + fun getControlStatus(): Map { + return mapOf( + "userService" to mapOf( + "shouldFail" to shouldFailUser, + "shouldBeSlow" to shouldBeSlowUser + ), + "statusService" to mapOf( + "shouldFail" to shouldFailStatus, + "shouldBeSlow" to shouldBeSlowStatus + ) + ) + } + + @PostMapping("/reset") + fun resetAll(): Map { + // 플래그 리셋 + shouldFailUser = false + shouldFailStatus = false + shouldBeSlowUser = false + shouldBeSlowStatus = false + + // 서킷 브레이커 리셋 + circuitBreakerRegistry.circuitBreaker("user-grpc").reset() + circuitBreakerRegistry.circuitBreaker("status-grpc").reset() + + return mapOf("message" to "All controls and metrics reset successfully") + } + + @GetMapping("/metrics") + fun getMetrics(): Map { + return mapOf( + "userGrpc" to mapOf( + "circuitBreaker" to getCircuitBreakerState("user-grpc"), + "retry" to getRetryMetrics("user-grpc") + ), + "statusGrpc" to mapOf( + "circuitBreaker" to getCircuitBreakerState("status-grpc"), + "retry" to getRetryMetrics("status-grpc") + ), + "controlFlags" to mapOf( + "shouldFailUser" to shouldFailUser, + "shouldFailStatus" to shouldFailStatus, + "shouldBeSlowUser" to shouldBeSlowUser, + "shouldBeSlowStatus" to shouldBeSlowStatus + ) + ) + } + + // 더미 gRPC 호출 시뮬레이션 함수들 + private suspend fun simulateUserGrpcCall(userId: String): InternalUserResponse { + // 느린 호출 시뮬레이션 (3초 지연) + if (shouldBeSlowUser) { + delay(3000) + } + + // 일반 지연 (실제 네트워크 지연 시뮬레이션) + delay(Random.nextLong(100, 500)) + + // 실패 시뮬레이션 + if (shouldFailUser) { + throw RuntimeException("Simulated gRPC User Service failure") + } + + // 성공 응답 + return InternalUserResponse( + id = UUID.fromString(userId), + phoneNumber = "010-1234-5678", + name = "김철수", + isParent = false, + role = UserRole.USER + ) + } + + private suspend fun simulateStatusListGrpcCall(): InternalStatusListResponse { + // 느린 호출 시뮬레이션 (3초 지연) + if (shouldBeSlowStatus) { + delay(3000) + } + + // 일반 지연 (실제 네트워크 지연 시뮬레이션) + delay(Random.nextLong(100, 500)) + + // 실패 시뮬레이션 + if (shouldFailStatus) { + throw RuntimeException("Simulated gRPC Status Service failure") + } + + // 성공 응답 + return InternalStatusListResponse( + statusList = listOf( + InternalStatusResponse( + id = 1L, + applicationStatus = ApplicationStatus.SUBMITTED, + examCode = "A123", + isFirstRoundPass = true, + isSecondRoundPass = false, + receiptCode = 12345L + ), + InternalStatusResponse( + id = 2L, + applicationStatus = ApplicationStatus.WRITING, + examCode = null, + isFirstRoundPass = false, + isSecondRoundPass = false, + receiptCode = 12346L + ) + ) + ) + } + + private fun getCircuitBreakerState(name: String): Map { + val circuitBreaker = circuitBreakerRegistry.circuitBreaker(name) + val metrics = circuitBreaker.metrics + + return mapOf( + "state" to circuitBreaker.state.name, + "failureRate" to String.format("%.2f%%", metrics.failureRate), + "slowCallRate" to String.format("%.2f%%", metrics.slowCallRate), + "numberOfCalls" to metrics.numberOfBufferedCalls, + "numberOfFailedCalls" to metrics.numberOfFailedCalls, + "numberOfSlowCalls" to metrics.numberOfSlowCalls, + "numberOfSuccessfulCalls" to metrics.numberOfSuccessfulCalls + ) + } + + private fun getRetryMetrics(name: String): Map { + val retry = retryRegistry.retry(name) + val eventPublisher = retry.eventPublisher + + return mapOf( + "numberOfSuccessfulCalls" to 0L, + "numberOfFailedCalls" to 0L, + "numberOfRetryAttempts" to 0L + ) + } +} \ No newline at end of file From f387985e4c2e31da8f81a65f6ed66efea42781eb Mon Sep 17 00:00:00 2001 From: coehgns Date: Sun, 31 Aug 2025 14:28:40 +0900 Subject: [PATCH 07/21] =?UTF-8?q?chore=20(=20#34=20)=20:=20DependencyVersi?= =?UTF-8?q?ons=20Netty=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- buildSrc/src/main/kotlin/DependencyVersions.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/DependencyVersions.kt b/buildSrc/src/main/kotlin/DependencyVersions.kt index d6d48f01..854d2785 100644 --- a/buildSrc/src/main/kotlin/DependencyVersions.kt +++ b/buildSrc/src/main/kotlin/DependencyVersions.kt @@ -45,6 +45,9 @@ object DependencyVersions { // Caffeine const val CAFFEINE = "3.1.8" - //Resilience4j + // Resilience4j const val RESILIENCE4J = "2.0.2" + + // Netty + const val NETTY = "4.1.111.Final" } From 9f10a4934726519f84c10bc8bd3b68204e57ec75 Mon Sep 17 00:00:00 2001 From: coehgns Date: Sun, 31 Aug 2025 14:28:49 +0900 Subject: [PATCH 08/21] =?UTF-8?q?chore=20(=20#34=20)=20:=20Dependencies=20?= =?UTF-8?q?Netty=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- buildSrc/src/main/kotlin/Dependencies.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 88f44d32..931036be 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -105,5 +105,6 @@ object Dependencies { const val RESILIENCE4J_SPRING_BOOT = "io.github.resilience4j:resilience4j-spring-boot3:${DependencyVersions.RESILIENCE4J}" const val RESILIENCE4J_KOTLIN = "io.github.resilience4j:resilience4j-kotlin:${DependencyVersions.RESILIENCE4J}" - + // Netty + const val NETTY = "io.netty:netty-resolver-dns-native-macos:${DependencyVersions.NETTY}" } From f15de21dafaa423185a8d1774662aa3ad6be9b14 Mon Sep 17 00:00:00 2001 From: coehgns Date: Sun, 31 Aug 2025 14:29:24 +0900 Subject: [PATCH 09/21] =?UTF-8?q?fix=20(=20#34=20)=20:=20build.gradle.kts?= =?UTF-8?q?=EC=97=90=20netty=20=EC=84=A4=EC=A0=95=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=98=EC=97=AC=20netty=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- casper-application-infrastructure/build.gradle.kts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/casper-application-infrastructure/build.gradle.kts b/casper-application-infrastructure/build.gradle.kts index 1365feb9..6853312c 100644 --- a/casper-application-infrastructure/build.gradle.kts +++ b/casper-application-infrastructure/build.gradle.kts @@ -110,6 +110,12 @@ dependencies { implementation(Dependencies.RESILIENCE4J_RETRY) implementation(Dependencies.RESILIENCE4J_SPRING_BOOT) implementation(Dependencies.RESILIENCE4J_KOTLIN) + + runtimeOnly(Dependencies.NETTY) { + artifact { + classifier = osdetector.classifier + } + } } sourceSets { From 38ef85d2e70c1cbf380f499b8672b4941863a6c3 Mon Sep 17 00:00:00 2001 From: coehgns Date: Sun, 31 Aug 2025 14:30:05 +0900 Subject: [PATCH 10/21] =?UTF-8?q?feat=20(=20#34=20)=20:=20ResilienceConfig?= =?UTF-8?q?=EC=97=90=20scheduleGrpcCircuitBreaker=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entrydsm/application/global/config/ResilienceConfig.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt index 166a46e6..b6d14736 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt @@ -23,6 +23,11 @@ class ResilienceConfig( return circuitBreakerRegistry.circuitBreaker("status-grpc") } + @Bean + fun scheduleGrpcCircuitBreaker(): CircuitBreaker { + return circuitBreakerRegistry.circuitBreaker("schedule-grpc") + } + @Bean fun userGrpcRetry(): Retry { return retryRegistry.retry("user-grpc") From 85c724250671708d4387b30bc21057841d692fa0 Mon Sep 17 00:00:00 2001 From: coehgns Date: Sun, 31 Aug 2025 14:30:15 +0900 Subject: [PATCH 11/21] =?UTF-8?q?feat=20(=20#34=20)=20:=20ResilienceConfig?= =?UTF-8?q?=EC=97=90=20scheduleGrpcRetry=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entrydsm/application/global/config/ResilienceConfig.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt index b6d14736..2278394f 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt @@ -37,4 +37,9 @@ class ResilienceConfig( fun statusGrpcRetry(): Retry { return retryRegistry.retry("status-grpc") } + + @Bean + fun scheduleGrpcRetry(): Retry { + return retryRegistry.retry("schedule-grpc") + } } From 7d6616f2dd52efcd3b4e1ba553f24b1a573168f0 Mon Sep 17 00:00:00 2001 From: coehgns Date: Sun, 31 Aug 2025 14:30:41 +0900 Subject: [PATCH 12/21] =?UTF-8?q?feat=20(=20#34=20)=20:=20ScheduleGrpcClie?= =?UTF-8?q?nt=EC=97=90=20executeGrpcCallWithResilience=20extension=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/schedule/ScheduleGrpcClient.kt | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt index 027c147f..28ebc7bb 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt @@ -1,13 +1,17 @@ package hs.kr.entrydsm.application.global.grpc.client.schedule +import hs.kr.entrydsm.application.global.extension.executeGrpcCallWithResilience import hs.kr.entrydsm.application.global.grpc.dto.schedule.InternalScheduleResponse import hs.kr.entrydsm.application.global.grpc.dto.schedule.ScheduleType import hs.kr.entrydsm.casper.schedule.proto.ScheduleServiceGrpc import hs.kr.entrydsm.casper.schedule.proto.ScheduleServiceProto +import io.github.resilience4j.circuitbreaker.CircuitBreaker +import io.github.resilience4j.retry.Retry import io.grpc.Channel import io.grpc.stub.StreamObserver import kotlinx.coroutines.suspendCancellableCoroutine import net.devh.boot.grpc.client.inject.GrpcClient +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Component import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -18,7 +22,10 @@ import kotlin.coroutines.resumeWithException * Schedule Service와 gRPC 통신을 하는 클라이언트입니다. */ @Component -class ScheduleGrpcClient { +class ScheduleGrpcClient( + @Qualifier("scheduleGrpcRetry") private val retry: Retry, + @Qualifier("scheduleGrpcCircuitBreaker") private val circuitBreaker: CircuitBreaker +) { @GrpcClient("schedule-service") lateinit var channel: Channel @@ -28,37 +35,48 @@ class ScheduleGrpcClient { * @return 일정 정보 */ suspend fun getScheduleByType(type: String): InternalScheduleResponse { - val scheduleStub = ScheduleServiceGrpc.newStub(channel) + return executeGrpcCallWithResilience( + retry = retry, + circuitBreaker = circuitBreaker, + fallback = { + InternalScheduleResponse( + type = toInternal(ScheduleServiceProto.Type.valueOf(type.uppercase())), + date = LocalDateTime.now() + ) + } + ) { + val scheduleStub = ScheduleServiceGrpc.newStub(channel) - val request = - ScheduleServiceProto.TypeRequest.newBuilder() - .setType(ScheduleServiceProto.Type.valueOf(type.uppercase())) - .build() + val request = + ScheduleServiceProto.TypeRequest.newBuilder() + .setType(ScheduleServiceProto.Type.valueOf(type.uppercase())) + .build() - val response = - suspendCancellableCoroutine { continuation -> - scheduleStub.getScheduleByType( - request, - object : StreamObserver { - override fun onNext(value: ScheduleServiceProto.GetScheduleResponse) { - continuation.resume(value) - } + val response = + suspendCancellableCoroutine { continuation -> + scheduleStub.getScheduleByType( + request, + object : StreamObserver { + override fun onNext(value: ScheduleServiceProto.GetScheduleResponse) { + continuation.resume(value) + } - override fun onError(t: Throwable) { - continuation.resumeWithException(t) - } + override fun onError(t: Throwable) { + continuation.resumeWithException(t) + } - override fun onCompleted() {} - }, - ) - } + override fun onCompleted() {} + }, + ) + } - val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME + val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME - val scheduleType = toInternal(response.type) - val date = LocalDateTime.parse(response.date, formatter) + val scheduleType = toInternal(response.type) + val date = LocalDateTime.parse(response.date, formatter) - return InternalScheduleResponse(scheduleType, date) + InternalScheduleResponse(scheduleType, date) + } } /** From 9115ff29fec6e13e28e821d0973dedc058a04370 Mon Sep 17 00:00:00 2001 From: coehgns Date: Sun, 31 Aug 2025 14:49:26 +0900 Subject: [PATCH 13/21] =?UTF-8?q?chore=20(=20#34=20)=20:=20GrantExamCodesU?= =?UTF-8?q?seCase=EC=97=90=20=EB=88=84=EB=9D=BD=EB=90=9C=20kdoc=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../examcode/usecase/GrantExamCodesUseCase.kt | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt index 50b35033..a9ce54df 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt @@ -15,14 +15,23 @@ import hs.kr.entrydsm.application.domain.examcode.util.DistanceUtil import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +/** + * 1차 전형에 합격한 학생들에게 수험번호를 부여하는 유스케이스입니다. + * + * @property applicationContract ApplicationAggregate를 가져옵니다. + * @property statusContract StatusAggregate를 업데이트합니다. + * @property kakaoGeocodeContract 카카오 맵 API와 상호작용합니다. + * @property distanceUtil 두 지점 사이의 거리를 구합니다. + * @property baseLocationContract 기준이 되는 장소의 위경도를 가져옵니다. + */ @UseCase class GrantExamCodesUseCase( private val applicationContract: ApplicationContract, - private val statusContract: StatusContract, private val kakaoGeocodeContract: KakaoGeocodeContract, - private val distanceUtil: DistanceUtil, private val baseLocationContract: BaseLocationContract, -) : GrantExamCodesContract { + private val statusContract: StatusContract, + private val distanceUtil: DistanceUtil, + ) : GrantExamCodesContract { companion object { /** 일반전형 수험번호 접두사 */ @@ -31,6 +40,10 @@ class GrantExamCodesUseCase( private const val SPECIAL_EXAM_CODE_PREFIX = "02" } + /** + * 1차 전형에 합격한 학생들의 주소와 학교까지의 거리를 계산하고, + * 전형별로 그룹화하여 수험번호를 부여합니다. + */ override suspend fun execute() { val allFirstRoundPassedApplication = applicationContract.queryAllFirstRoundPassedApplication() val examCodeInfos = collectDistanceInfo(allFirstRoundPassedApplication) @@ -46,16 +59,26 @@ class GrantExamCodesUseCase( saveExamCodes(examCodeInfos) } + /** + * 학생들의 주소를 위경도로 변환하고, 학교와의 거리를 계산합니다. + * + * @param applications 1차 전형에 합격한 학생 리스트 + * @return 학생들의 접수 코드, 전형 유형, 학교까지의 거리를 담은 리스트 + * @throws ExamCodeException.failedGeocodeConversion 주소 변환에 실패했을 경우 + */ private suspend fun collectDistanceInfo(applications: List): List = coroutineScope { applications.map { application -> async { val address = application.streetAddress as String val coordinate = kakaoGeocodeContract.geocode(address) ?: throw ExamCodeException.failedGeocodeConversion(address) + val baseLat = baseLocationContract.baseLat val baseLon = baseLocationContract.baseLon + val userLat = coordinate.first val userLon = coordinate.second + val distance = distanceUtil.haversine(baseLat, baseLon, userLat, userLon) ExamCodeInfo( receiptCode = application.receiptCode, @@ -66,6 +89,12 @@ class GrantExamCodesUseCase( }.map { it.await() } } + /** + * 학생들을 학교까지의 거리를 기준으로 그룹화하고, 그룹 내에서 수험번호를 부여합니다. + * + * @param examCodeInfos 학생들의 정보 리스트 + * @param applicationType 전형 유형 (일반, 특별) + */ private fun assignExamCodes(examCodeInfos: List, applicationType: String) { val sortedByDistance = examCodeInfos.sortedByDescending { it.distance } @@ -76,6 +105,13 @@ class GrantExamCodesUseCase( } } + /** + * 학생들을 학교까지의 거리가 같은 그룹으로 묶습니다. + * + * @param sortedInfos 거리를 기준으로 내림차순 정렬된 학생 정보 리스트 + * @param applicationType 전형 유형 + * @return 거리가 같은 학생들끼리 묶인 그룹 리스트 + */ private fun createDistanceGroups(sortedInfos: List, applicationType: String): List { val groups = mutableListOf() val uniqueDistances = sortedInfos.map { it.distance }.distinct() @@ -88,6 +124,11 @@ class GrantExamCodesUseCase( } + /** + * 같은 거리 그룹 내의 학생들에게 수험번호를 부여합니다. + * + * @param distanceGroup 거리가 같은 학생 그룹 + */ private fun assignNumbersInGroup(distanceGroup: DistanceGroup) { distanceGroup.examCodeInfoList.forEach { examCodeInfo -> val receiptCode = String.format("%03d", examCodeInfo.receiptCode) @@ -96,7 +137,11 @@ class GrantExamCodesUseCase( } } - + /** + * 부여된 수험번호를 저장합니다. + * + * @param examCodeInfos 수험번호가 부여된 학생 정보 리스트 + */ private suspend fun saveExamCodes(examCodeInfos: List) { examCodeInfos.forEach { info -> info.examCode?.let { examCode -> From 38307ffc27bb4ad729c384e62a4248a276e64fb4 Mon Sep 17 00:00:00 2001 From: coehgns Date: Sun, 31 Aug 2025 14:51:10 +0900 Subject: [PATCH 14/21] =?UTF-8?q?refactor=20(=20#34=20)=20:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/examcode/usecase/GrantExamCodesUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt index a9ce54df..0b8cadc0 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt @@ -31,7 +31,7 @@ class GrantExamCodesUseCase( private val baseLocationContract: BaseLocationContract, private val statusContract: StatusContract, private val distanceUtil: DistanceUtil, - ) : GrantExamCodesContract { +) : GrantExamCodesContract { companion object { /** 일반전형 수험번호 접두사 */ From 60e0115fd6f725c128ca3837811400cd5bbf0e9f Mon Sep 17 00:00:00 2001 From: coehgns Date: Sun, 31 Aug 2025 15:11:28 +0900 Subject: [PATCH 15/21] =?UTF-8?q?feat=20(=20#34=20)=20:=20GrpcResilienceTe?= =?UTF-8?q?stController=EC=97=90=20Schedule=20Test=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grpc/test/GrpcResilienceTestController.kt | 106 +++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt index a4be8f44..0d979549 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt @@ -1,6 +1,8 @@ package hs.kr.entrydsm.application.global.grpc.test import hs.kr.entrydsm.application.global.extension.executeGrpcCallWithResilience +import hs.kr.entrydsm.application.global.grpc.dto.schedule.InternalScheduleResponse +import hs.kr.entrydsm.application.global.grpc.dto.schedule.ScheduleType import hs.kr.entrydsm.application.global.grpc.dto.status.ApplicationStatus import hs.kr.entrydsm.application.global.grpc.dto.status.InternalStatusListResponse import hs.kr.entrydsm.application.global.grpc.dto.status.InternalStatusResponse @@ -10,6 +12,7 @@ import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry import io.github.resilience4j.retry.RetryRegistry import kotlinx.coroutines.delay import org.springframework.web.bind.annotation.* +import java.time.LocalDateTime import java.util.* import kotlin.random.Random @@ -25,6 +28,49 @@ class DummyGrpcResilienceTestController( private var shouldFailStatus = false private var shouldBeSlowUser = false private var shouldBeSlowStatus = false + private var shouldFailSchedule = false + private var shouldBeSlowSchedule = false + + + @GetMapping("/schedule") + suspend fun testDummyScheduleGrpc(type: String): Map { + val startTime = System.currentTimeMillis() + val retry = retryRegistry.retry("schedule-grpc") + val circuitBreaker = circuitBreakerRegistry.circuitBreaker("schedule-grpc") + + return try { + val response = executeGrpcCallWithResilience( + retry = retry, + circuitBreaker = circuitBreaker, + fallback = { + // Fallback 응답 + InternalScheduleResponse( + type = ScheduleType.FIRST_ANNOUNCEMENT, + date = LocalDateTime.now() + ) + } + ) { + simulateScheduleGrpcCall(type) + } + + mapOf( + "success" to true, + "data" to response, + "executionTime" to "${System.currentTimeMillis() - startTime}ms", + "circuitBreakerState" to getCircuitBreakerState("schedule-grpc"), + "retryMetrics" to getRetryMetrics("schedule-grpc"), + "source" to if (response.type.name.contains("FIRST_ANNOUNCEMENT")) "fallback" else "grpc" + ) + } catch (e: Exception) { + mapOf( + "success" to false, + "error" to (e.message ?: "Unknown error"), + "executionTime" to "${System.currentTimeMillis() - startTime}ms", + "circuitBreakerState" to getCircuitBreakerState("schedule-grpc"), + "retryMetrics" to getRetryMetrics("schedule-grpc") + ) + } + } @GetMapping("/user/{userId}") suspend fun testDummyUserGrpc(@PathVariable userId: String): Map { @@ -133,6 +179,18 @@ class DummyGrpcResilienceTestController( ) } + @PostMapping("/control/schedule/fail/{shouldFail}") + fun setScheduleFailure(@PathVariable shouldFail: Boolean): Map { + shouldFailSchedule = shouldFail + return mapOf( + "message" to "Schedule service failure simulation set to: $shouldFail", + "currentState" to mapOf( + "shouldFailSchedule" to shouldFailSchedule, + "shouldBeSlowSchedule" to shouldBeSlowSchedule + ) + ) + } + @PostMapping("/control/user/slow/{shouldBeSlow}") fun setUserSlow(@PathVariable shouldBeSlow: Boolean): Map { shouldBeSlowUser = shouldBeSlow @@ -157,6 +215,18 @@ class DummyGrpcResilienceTestController( ) } + @PostMapping("/control/schedule/slow/{shouldBeSlow}") + fun setScheduleSlow(@PathVariable shouldBeSlow: Boolean): Map { + shouldBeSlowSchedule = shouldBeSlow + return mapOf( + "message" to "Schedule service slow call simulation set to: $shouldBeSlow", + "currentState" to mapOf( + "shouldFailSchedule" to shouldFailSchedule, + "shouldBeSlowSchedule" to shouldBeSlowSchedule + ) + ) + } + @GetMapping("/control/status") fun getControlStatus(): Map { return mapOf( @@ -167,6 +237,10 @@ class DummyGrpcResilienceTestController( "statusService" to mapOf( "shouldFail" to shouldFailStatus, "shouldBeSlow" to shouldBeSlowStatus + ), + "scheduleService" to mapOf( + "shouldFail" to shouldFailSchedule, + "shouldBeSlow" to shouldBeSlowSchedule ) ) } @@ -176,12 +250,15 @@ class DummyGrpcResilienceTestController( // 플래그 리셋 shouldFailUser = false shouldFailStatus = false + shouldFailSchedule = false shouldBeSlowUser = false shouldBeSlowStatus = false + shouldBeSlowSchedule = false // 서킷 브레이커 리셋 circuitBreakerRegistry.circuitBreaker("user-grpc").reset() circuitBreakerRegistry.circuitBreaker("status-grpc").reset() + circuitBreakerRegistry.circuitBreaker("schedule-grpc").reset() return mapOf("message" to "All controls and metrics reset successfully") } @@ -197,11 +274,17 @@ class DummyGrpcResilienceTestController( "circuitBreaker" to getCircuitBreakerState("status-grpc"), "retry" to getRetryMetrics("status-grpc") ), + "scheduleGrpc" to mapOf( + "circuitBreaker" to getCircuitBreakerState("schedule-grpc"), + "retry" to getRetryMetrics("schedule-grpc") + ), "controlFlags" to mapOf( "shouldFailUser" to shouldFailUser, "shouldFailStatus" to shouldFailStatus, + "shouldFailSchedule" to shouldFailSchedule, "shouldBeSlowUser" to shouldBeSlowUser, - "shouldBeSlowStatus" to shouldBeSlowStatus + "shouldBeSlowStatus" to shouldBeSlowStatus, + "shouldBeSlowSchedule" to shouldBeSlowSchedule, ) ) } @@ -268,6 +351,27 @@ class DummyGrpcResilienceTestController( ) } + private suspend fun simulateScheduleGrpcCall(type: String): InternalScheduleResponse { + // 느린 호출 시뮬레이션 (3초 지연) + if (shouldBeSlowSchedule) { + delay(3000) + } + + // 일반 지연 (실제 네트워크 지연 시뮬레이션) + delay(Random.nextLong(100, 500)) + + // 실패 시뮬레이션 + if (shouldFailSchedule) { + throw RuntimeException("Simulated gRPC Schedule Service failure") + } + + // 성공 응답 + return InternalScheduleResponse( + type = ScheduleType.FIRST_ANNOUNCEMENT, + date = LocalDateTime.now() + ) + } + private fun getCircuitBreakerState(name: String): Map { val circuitBreaker = circuitBreakerRegistry.circuitBreaker(name) val metrics = circuitBreaker.metrics From f4caf9b4546972737e7a7fc07bf263706b0cd900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Mon, 1 Sep 2025 15:50:22 +0900 Subject: [PATCH 16/21] =?UTF-8?q?chore=20(=20#34=20)=20:=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EB=84=A4=EC=9E=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ResilienceExtensions.kt => ResilienceGrpcExtensions.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/{ResilienceExtensions.kt => ResilienceGrpcExtensions.kt} (100%) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceExtensions.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceGrpcExtensions.kt similarity index 100% rename from casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceExtensions.kt rename to casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceGrpcExtensions.kt From 0a93e10d463af1da081ce1c9c06f80c6d61c2af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Mon, 1 Sep 2025 16:06:56 +0900 Subject: [PATCH 17/21] =?UTF-8?q?chore=20(=20#34=20)=20:=20test=20Controll?= =?UTF-8?q?er=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grpc/test/GrpcResilienceTestController.kt | 400 ------------------ 1 file changed, 400 deletions(-) delete mode 100644 casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt deleted file mode 100644 index 0d979549..00000000 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt +++ /dev/null @@ -1,400 +0,0 @@ -package hs.kr.entrydsm.application.global.grpc.test - -import hs.kr.entrydsm.application.global.extension.executeGrpcCallWithResilience -import hs.kr.entrydsm.application.global.grpc.dto.schedule.InternalScheduleResponse -import hs.kr.entrydsm.application.global.grpc.dto.schedule.ScheduleType -import hs.kr.entrydsm.application.global.grpc.dto.status.ApplicationStatus -import hs.kr.entrydsm.application.global.grpc.dto.status.InternalStatusListResponse -import hs.kr.entrydsm.application.global.grpc.dto.status.InternalStatusResponse -import hs.kr.entrydsm.application.global.grpc.dto.user.InternalUserResponse -import hs.kr.entrydsm.application.global.grpc.dto.user.UserRole -import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry -import io.github.resilience4j.retry.RetryRegistry -import kotlinx.coroutines.delay -import org.springframework.web.bind.annotation.* -import java.time.LocalDateTime -import java.util.* -import kotlin.random.Random - -@RestController -@RequestMapping("/test/dummy-grpc") -class DummyGrpcResilienceTestController( - private val circuitBreakerRegistry: CircuitBreakerRegistry, - private val retryRegistry: RetryRegistry -) { - - // 실패 시뮬레이션을 위한 플래그 - private var shouldFailUser = false - private var shouldFailStatus = false - private var shouldBeSlowUser = false - private var shouldBeSlowStatus = false - private var shouldFailSchedule = false - private var shouldBeSlowSchedule = false - - - @GetMapping("/schedule") - suspend fun testDummyScheduleGrpc(type: String): Map { - val startTime = System.currentTimeMillis() - val retry = retryRegistry.retry("schedule-grpc") - val circuitBreaker = circuitBreakerRegistry.circuitBreaker("schedule-grpc") - - return try { - val response = executeGrpcCallWithResilience( - retry = retry, - circuitBreaker = circuitBreaker, - fallback = { - // Fallback 응답 - InternalScheduleResponse( - type = ScheduleType.FIRST_ANNOUNCEMENT, - date = LocalDateTime.now() - ) - } - ) { - simulateScheduleGrpcCall(type) - } - - mapOf( - "success" to true, - "data" to response, - "executionTime" to "${System.currentTimeMillis() - startTime}ms", - "circuitBreakerState" to getCircuitBreakerState("schedule-grpc"), - "retryMetrics" to getRetryMetrics("schedule-grpc"), - "source" to if (response.type.name.contains("FIRST_ANNOUNCEMENT")) "fallback" else "grpc" - ) - } catch (e: Exception) { - mapOf( - "success" to false, - "error" to (e.message ?: "Unknown error"), - "executionTime" to "${System.currentTimeMillis() - startTime}ms", - "circuitBreakerState" to getCircuitBreakerState("schedule-grpc"), - "retryMetrics" to getRetryMetrics("schedule-grpc") - ) - } - } - - @GetMapping("/user/{userId}") - suspend fun testDummyUserGrpc(@PathVariable userId: String): Map { - val startTime = System.currentTimeMillis() - val retry = retryRegistry.retry("user-grpc") - val circuitBreaker = circuitBreakerRegistry.circuitBreaker("user-grpc") - - return try { - val response = executeGrpcCallWithResilience( - retry = retry, - circuitBreaker = circuitBreaker, - fallback = { - // Fallback 응답 - InternalUserResponse( - id = UUID.fromString(userId), - phoneNumber = "N/A", - name = "Unknown User (Fallback)", - isParent = false, - role = UserRole.USER - ) - } - ) { - // 더미 gRPC 호출 시뮬레이션 - simulateUserGrpcCall(userId) - } - - mapOf( - "success" to true, - "data" to response, - "executionTime" to "${System.currentTimeMillis() - startTime}ms", - "circuitBreakerState" to getCircuitBreakerState("user-grpc"), - "retryMetrics" to getRetryMetrics("user-grpc"), - "source" to if (response.name.contains("Fallback")) "fallback" else "grpc" - ) - } catch (e: Exception) { - mapOf( - "success" to false, - "error" to (e.message ?: "Unknown error"), - "executionTime" to "${System.currentTimeMillis() - startTime}ms", - "circuitBreakerState" to getCircuitBreakerState("user-grpc"), - "retryMetrics" to getRetryMetrics("user-grpc") - ) - } - } - - @GetMapping("/status/list") - suspend fun testDummyStatusListGrpc(): Map { - val startTime = System.currentTimeMillis() - val retry = retryRegistry.retry("status-grpc") - val circuitBreaker = circuitBreakerRegistry.circuitBreaker("status-grpc") - - return try { - val response = executeGrpcCallWithResilience( - retry = retry, - circuitBreaker = circuitBreaker, - fallback = { - // Fallback 응답 - InternalStatusListResponse(statusList = emptyList()) - } - ) { - // 더미 gRPC 호출 시뮬레이션 - simulateStatusListGrpcCall() - } - - mapOf( - "success" to true, - "data" to response, - "executionTime" to "${System.currentTimeMillis() - startTime}ms", - "circuitBreakerState" to getCircuitBreakerState("status-grpc"), - "retryMetrics" to getRetryMetrics("status-grpc"), - "source" to if (response.statusList.isEmpty()) "fallback" else "grpc" - ) - } catch (e: Exception) { - mapOf( - "success" to false, - "error" to (e.message ?: "Unknown error"), - "executionTime" to "${System.currentTimeMillis() - startTime}ms", - "circuitBreakerState" to getCircuitBreakerState("status-grpc"), - "retryMetrics" to getRetryMetrics("status-grpc") - ) - } - } - - // 실패/성공 시뮬레이션 제어 API - @PostMapping("/control/user/fail/{shouldFail}") - fun setUserFailure(@PathVariable shouldFail: Boolean): Map { - shouldFailUser = shouldFail - return mapOf( - "message" to "User service failure simulation set to: $shouldFail", - "currentState" to mapOf( - "shouldFailUser" to shouldFailUser, - "shouldBeSlowUser" to shouldBeSlowUser - ) - ) - } - - @PostMapping("/control/status/fail/{shouldFail}") - fun setStatusFailure(@PathVariable shouldFail: Boolean): Map { - shouldFailStatus = shouldFail - return mapOf( - "message" to "Status service failure simulation set to: $shouldFail", - "currentState" to mapOf( - "shouldFailStatus" to shouldFailStatus, - "shouldBeSlowStatus" to shouldBeSlowStatus - ) - ) - } - - @PostMapping("/control/schedule/fail/{shouldFail}") - fun setScheduleFailure(@PathVariable shouldFail: Boolean): Map { - shouldFailSchedule = shouldFail - return mapOf( - "message" to "Schedule service failure simulation set to: $shouldFail", - "currentState" to mapOf( - "shouldFailSchedule" to shouldFailSchedule, - "shouldBeSlowSchedule" to shouldBeSlowSchedule - ) - ) - } - - @PostMapping("/control/user/slow/{shouldBeSlow}") - fun setUserSlow(@PathVariable shouldBeSlow: Boolean): Map { - shouldBeSlowUser = shouldBeSlow - return mapOf( - "message" to "User service slow call simulation set to: $shouldBeSlow", - "currentState" to mapOf( - "shouldFailUser" to shouldFailUser, - "shouldBeSlowUser" to shouldBeSlowUser - ) - ) - } - - @PostMapping("/control/status/slow/{shouldBeSlow}") - fun setStatusSlow(@PathVariable shouldBeSlow: Boolean): Map { - shouldBeSlowStatus = shouldBeSlow - return mapOf( - "message" to "Status service slow call simulation set to: $shouldBeSlow", - "currentState" to mapOf( - "shouldFailStatus" to shouldFailStatus, - "shouldBeSlowStatus" to shouldBeSlowStatus - ) - ) - } - - @PostMapping("/control/schedule/slow/{shouldBeSlow}") - fun setScheduleSlow(@PathVariable shouldBeSlow: Boolean): Map { - shouldBeSlowSchedule = shouldBeSlow - return mapOf( - "message" to "Schedule service slow call simulation set to: $shouldBeSlow", - "currentState" to mapOf( - "shouldFailSchedule" to shouldFailSchedule, - "shouldBeSlowSchedule" to shouldBeSlowSchedule - ) - ) - } - - @GetMapping("/control/status") - fun getControlStatus(): Map { - return mapOf( - "userService" to mapOf( - "shouldFail" to shouldFailUser, - "shouldBeSlow" to shouldBeSlowUser - ), - "statusService" to mapOf( - "shouldFail" to shouldFailStatus, - "shouldBeSlow" to shouldBeSlowStatus - ), - "scheduleService" to mapOf( - "shouldFail" to shouldFailSchedule, - "shouldBeSlow" to shouldBeSlowSchedule - ) - ) - } - - @PostMapping("/reset") - fun resetAll(): Map { - // 플래그 리셋 - shouldFailUser = false - shouldFailStatus = false - shouldFailSchedule = false - shouldBeSlowUser = false - shouldBeSlowStatus = false - shouldBeSlowSchedule = false - - // 서킷 브레이커 리셋 - circuitBreakerRegistry.circuitBreaker("user-grpc").reset() - circuitBreakerRegistry.circuitBreaker("status-grpc").reset() - circuitBreakerRegistry.circuitBreaker("schedule-grpc").reset() - - return mapOf("message" to "All controls and metrics reset successfully") - } - - @GetMapping("/metrics") - fun getMetrics(): Map { - return mapOf( - "userGrpc" to mapOf( - "circuitBreaker" to getCircuitBreakerState("user-grpc"), - "retry" to getRetryMetrics("user-grpc") - ), - "statusGrpc" to mapOf( - "circuitBreaker" to getCircuitBreakerState("status-grpc"), - "retry" to getRetryMetrics("status-grpc") - ), - "scheduleGrpc" to mapOf( - "circuitBreaker" to getCircuitBreakerState("schedule-grpc"), - "retry" to getRetryMetrics("schedule-grpc") - ), - "controlFlags" to mapOf( - "shouldFailUser" to shouldFailUser, - "shouldFailStatus" to shouldFailStatus, - "shouldFailSchedule" to shouldFailSchedule, - "shouldBeSlowUser" to shouldBeSlowUser, - "shouldBeSlowStatus" to shouldBeSlowStatus, - "shouldBeSlowSchedule" to shouldBeSlowSchedule, - ) - ) - } - - // 더미 gRPC 호출 시뮬레이션 함수들 - private suspend fun simulateUserGrpcCall(userId: String): InternalUserResponse { - // 느린 호출 시뮬레이션 (3초 지연) - if (shouldBeSlowUser) { - delay(3000) - } - - // 일반 지연 (실제 네트워크 지연 시뮬레이션) - delay(Random.nextLong(100, 500)) - - // 실패 시뮬레이션 - if (shouldFailUser) { - throw RuntimeException("Simulated gRPC User Service failure") - } - - // 성공 응답 - return InternalUserResponse( - id = UUID.fromString(userId), - phoneNumber = "010-1234-5678", - name = "김철수", - isParent = false, - role = UserRole.USER - ) - } - - private suspend fun simulateStatusListGrpcCall(): InternalStatusListResponse { - // 느린 호출 시뮬레이션 (3초 지연) - if (shouldBeSlowStatus) { - delay(3000) - } - - // 일반 지연 (실제 네트워크 지연 시뮬레이션) - delay(Random.nextLong(100, 500)) - - // 실패 시뮬레이션 - if (shouldFailStatus) { - throw RuntimeException("Simulated gRPC Status Service failure") - } - - // 성공 응답 - return InternalStatusListResponse( - statusList = listOf( - InternalStatusResponse( - id = 1L, - applicationStatus = ApplicationStatus.SUBMITTED, - examCode = "A123", - isFirstRoundPass = true, - isSecondRoundPass = false, - receiptCode = 12345L - ), - InternalStatusResponse( - id = 2L, - applicationStatus = ApplicationStatus.WRITING, - examCode = null, - isFirstRoundPass = false, - isSecondRoundPass = false, - receiptCode = 12346L - ) - ) - ) - } - - private suspend fun simulateScheduleGrpcCall(type: String): InternalScheduleResponse { - // 느린 호출 시뮬레이션 (3초 지연) - if (shouldBeSlowSchedule) { - delay(3000) - } - - // 일반 지연 (실제 네트워크 지연 시뮬레이션) - delay(Random.nextLong(100, 500)) - - // 실패 시뮬레이션 - if (shouldFailSchedule) { - throw RuntimeException("Simulated gRPC Schedule Service failure") - } - - // 성공 응답 - return InternalScheduleResponse( - type = ScheduleType.FIRST_ANNOUNCEMENT, - date = LocalDateTime.now() - ) - } - - private fun getCircuitBreakerState(name: String): Map { - val circuitBreaker = circuitBreakerRegistry.circuitBreaker(name) - val metrics = circuitBreaker.metrics - - return mapOf( - "state" to circuitBreaker.state.name, - "failureRate" to String.format("%.2f%%", metrics.failureRate), - "slowCallRate" to String.format("%.2f%%", metrics.slowCallRate), - "numberOfCalls" to metrics.numberOfBufferedCalls, - "numberOfFailedCalls" to metrics.numberOfFailedCalls, - "numberOfSlowCalls" to metrics.numberOfSlowCalls, - "numberOfSuccessfulCalls" to metrics.numberOfSuccessfulCalls - ) - } - - private fun getRetryMetrics(name: String): Map { - val retry = retryRegistry.retry(name) - val eventPublisher = retry.eventPublisher - - return mapOf( - "numberOfSuccessfulCalls" to 0L, - "numberOfFailedCalls" to 0L, - "numberOfRetryAttempts" to 0L - ) - } -} \ No newline at end of file From e5c32e1bbf6d2810fe7f917d09f0cf73c0942541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Mon, 1 Sep 2025 16:08:50 +0900 Subject: [PATCH 18/21] =?UTF-8?q?fix=20(=20#34=20)=20:=20import=20*=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=ED=9B=84=20=ED=95=98=EB=82=98=EC=94=A9=20?= =?UTF-8?q?import=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/global/grpc/client/user/UserGrpcClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/user/UserGrpcClient.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/user/UserGrpcClient.kt index 221574ab..72b75b5b 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/user/UserGrpcClient.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/user/UserGrpcClient.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine import net.devh.boot.grpc.client.inject.GrpcClient import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Component -import java.util.* +import java.util.UUID import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException From 76d94c658c81244818dde85a3bb5ef5e5c130c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Mon, 1 Sep 2025 16:18:14 +0900 Subject: [PATCH 19/21] =?UTF-8?q?refactor=20(=20#34=20)=20:=20Retry=20?= =?UTF-8?q?=EB=B0=94=EA=B9=A5=20=E2=86=92=20CircuitBreaker=20=EC=95=88?= =?UTF-8?q?=EC=AA=BD=20=EC=88=9C=EC=84=9C=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extension/ResilienceGrpcExtensions.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceGrpcExtensions.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceGrpcExtensions.kt index 6452f49e..c4cf817b 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceGrpcExtensions.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceGrpcExtensions.kt @@ -4,19 +4,24 @@ import io.github.resilience4j.circuitbreaker.CircuitBreaker import io.github.resilience4j.kotlin.circuitbreaker.executeSuspendFunction import io.github.resilience4j.kotlin.retry.executeSuspendFunction import io.github.resilience4j.retry.Retry -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.CancellationException +import org.slf4j.LoggerFactory + +private val log = LoggerFactory.getLogger("ResilienceGrpcExtensions") suspend fun executeGrpcCallWithResilience( retry: Retry, circuitBreaker: CircuitBreaker, fallback: suspend () -> T, block: suspend () -> T -): T = coroutineScope { +): T = try { - circuitBreaker.executeSuspendFunction { - retry.executeSuspendFunction(block) + retry.executeSuspendFunction { + circuitBreaker.executeSuspendFunction(block) } - }catch (e: Exception) { + } catch (ce: CancellationException) { + throw ce + } catch (e: Exception) { + log.warn("gRPC 호출 실패, fallback 실행: {}", e.toString()) fallback() - } -} \ No newline at end of file + } \ No newline at end of file From 889b17388ff38bac9ca4ccb6ebcbfdae4a21cf03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Mon, 1 Sep 2025 16:29:22 +0900 Subject: [PATCH 20/21] =?UTF-8?q?fix=20(=20#34=20)=20:=20Netty=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- casper-application-infrastructure/build.gradle.kts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/casper-application-infrastructure/build.gradle.kts b/casper-application-infrastructure/build.gradle.kts index 6853312c..1365feb9 100644 --- a/casper-application-infrastructure/build.gradle.kts +++ b/casper-application-infrastructure/build.gradle.kts @@ -110,12 +110,6 @@ dependencies { implementation(Dependencies.RESILIENCE4J_RETRY) implementation(Dependencies.RESILIENCE4J_SPRING_BOOT) implementation(Dependencies.RESILIENCE4J_KOTLIN) - - runtimeOnly(Dependencies.NETTY) { - artifact { - classifier = osdetector.classifier - } - } } sourceSets { From d8c2bd77db8e0a1fe1faf7212cbb0ca809178d09 Mon Sep 17 00:00:00 2001 From: coehgns Date: Tue, 2 Sep 2025 19:07:23 +0900 Subject: [PATCH 21/21] =?UTF-8?q?refactor=20(=20#34=20)=20:=20ScheduleGrpc?= =?UTF-8?q?Client=20full=20back=20=EC=8B=A4=ED=8C=A8=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EB=8D=98=EC=A7=80=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/grpc/client/schedule/ScheduleGrpcClient.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt index 28ebc7bb..02db8b6a 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt @@ -40,7 +40,10 @@ class ScheduleGrpcClient( circuitBreaker = circuitBreaker, fallback = { InternalScheduleResponse( - type = toInternal(ScheduleServiceProto.Type.valueOf(type.uppercase())), + type = toInternal( + runCatching { ScheduleServiceProto.Type.valueOf(type.uppercase()) } + .getOrDefault(ScheduleServiceProto.Type.START_DATE) + ), date = LocalDateTime.now() ) }