diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 2113de1..9869413 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -58,4 +58,10 @@ object Dependencies { // Spring Cloud Config const val SPRING_CLOUD_CONFIG = "org.springframework.cloud:spring-cloud-starter-config" + + //Resilience4j + const val RESILIENCE4J_CIRCUITBREAKER = "io.github.resilience4j:resilience4j-circuitbreaker:${DependencyVersion.RESILIENCE4J}" + const val RESILIENCE4J_RETRY = "io.github.resilience4j:resilience4j-retry:${DependencyVersion.RESILIENCE4J}" + const val RESILIENCE4J_SPRING_BOOT = "io.github.resilience4j:resilience4j-spring-boot3:${DependencyVersion.RESILIENCE4J}" + const val RESILIENCE4J_KOTLIN = "io.github.resilience4j:resilience4j-kotlin:${DependencyVersion.RESILIENCE4J}" } diff --git a/buildSrc/src/main/kotlin/DependencyVersion.kt b/buildSrc/src/main/kotlin/DependencyVersion.kt index 9fc4c15..9e8a242 100644 --- a/buildSrc/src/main/kotlin/DependencyVersion.kt +++ b/buildSrc/src/main/kotlin/DependencyVersion.kt @@ -24,4 +24,7 @@ object DependencyVersion { const val OPEN_FEIGN_VERSION = "3.1.4" const val SPRING_CLOUD_CONFIG_VERSION = "2023.0.3" + + //Resilience4j + const val RESILIENCE4J = "2.0.2" } diff --git a/casper-feed/build.gradle.kts b/casper-feed/build.gradle.kts index 36a3563..42e2128 100644 --- a/casper-feed/build.gradle.kts +++ b/casper-feed/build.gradle.kts @@ -97,6 +97,12 @@ dependencies { // Cloud Config implementation(Dependencies.SPRING_CLOUD_CONFIG) + + // Resilience4j + implementation(Dependencies.RESILIENCE4J_CIRCUITBREAKER) + implementation(Dependencies.RESILIENCE4J_RETRY) + implementation(Dependencies.RESILIENCE4J_SPRING_BOOT) + implementation(Dependencies.RESILIENCE4J_KOTLIN) } protobuf { diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/ResilienceConfig.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/ResilienceConfig.kt new file mode 100644 index 0000000..0845e1a --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/ResilienceConfig.kt @@ -0,0 +1,41 @@ +package hs.kr.entrydsm.feed.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 + +/** + * Resilience4j 관련 설정을 담당하는 클래스입니다. + * + * @property circuitBreakerRegistry CircuitBreaker 인스턴스를 생성하고 관리합니다. + * @property retryRegistry Retry 인스턴스를 생성하고 관리합니다. + */ +@Configuration +class ResilienceConfig( + private val circuitBreakerRegistry: CircuitBreakerRegistry, + private val retryRegistry: RetryRegistry +) { + + /** + * user-grpc 서킷 브레이커를 생성합니다. + * + * @return user-grpc 이름의 CircuitBreaker 인스턴스 + */ + @Bean + fun userGrpcCircuitBreaker(): CircuitBreaker { + return circuitBreakerRegistry.circuitBreaker("user-grpc") + } + + /** + * user-grpc 리트라이를 생성합니다. + * + * @return user-grpc 이름의 Retry 인스턴스 + */ + @Bean + fun userGrpcRetry(): Retry { + return retryRegistry.retry("user-grpc") + } +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/extension/ResilienceGrpcExtensions.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/extension/ResilienceGrpcExtensions.kt new file mode 100644 index 0000000..ebcf56d --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/extension/ResilienceGrpcExtensions.kt @@ -0,0 +1,37 @@ +package hs.kr.entrydsm.feed.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.CancellationException +import org.slf4j.LoggerFactory + +private val log = LoggerFactory.getLogger("ResilienceGrpcExtensions") + +/** + * gRPC 호출에 대해 Retry와 CircuitBreaker를 적용합니다. + * + * @param T gRPC 함수의 반환 타입 + * @param retry 적용할 Retry 인스턴스 + * @param circuitBreaker 적용할 CircuitBreaker 인스턴스 + * @param fallback gRPC 호출 실패 시 실행할 함수 + * @param block 실행할 gRPC 함수 + * @return gRPC 함수의 실행 결과를 반환하거나, 실패 시 fallback 함수의 결과를 반환 + */ +suspend fun executeGrpcCallWithResilience( + retry: Retry, + circuitBreaker: CircuitBreaker, + fallback: suspend () -> T, + block: suspend () -> T +): T = + try { + retry.executeSuspendFunction { + circuitBreaker.executeSuspendFunction(block) + } + } catch (ce: CancellationException) { + throw ce + } catch (e: Exception) { + log.warn("gRPC 호출 실패, fallback 실행: {}", e.toString()) + fallback() + } diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/utils/admin/AdminUtils.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/utils/admin/AdminUtils.kt index 98215f1..8ba70b0 100644 --- a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/utils/admin/AdminUtils.kt +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/utils/admin/AdminUtils.kt @@ -1,8 +1,8 @@ package hs.kr.entrydsm.feed.global.utils.admin import hs.kr.entrydsm.feed.global.exception.InvalidTokenException -import hs.kr.entrydsm.feed.infrastructure.grpc.client.AdminGrpcClient -import hs.kr.entrydsm.feed.infrastructure.grpc.client.dto.response.InternalAdminResponse +import hs.kr.entrydsm.feed.infrastructure.grpc.user.client.UserGrpcClient +import hs.kr.entrydsm.feed.infrastructure.grpc.user.client.dto.response.InternalAdminResponse import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Component import java.util.* @@ -12,11 +12,11 @@ import java.util.* * Spring Security의 SecurityContext를 사용하여 현재 인증된 관리자 정보를 조회하고, * gRPC를 통해 관리자 서비스에서 추가 정보를 조회합니다. * - * @property adminGrpcClient 관리자 정보 조회를 위한 gRPC 클라이언트 + * @property userGrpcClient 관리자 정보 조회를 위한 gRPC 클라이언트 */ @Component class AdminUtils( - private val adminGrpcClient: AdminGrpcClient + private val userGrpcClient: UserGrpcClient ) { /** @@ -28,7 +28,7 @@ class AdminUtils( * @throws hs.kr.entrydsm.feed.global.exception.InvalidTokenException 인증 토큰이 유효하지 않은 경우 * @throws io.grpc.StatusRuntimeException gRPC 통신 중 오류가 발생한 경우 */ - suspend fun getCurrentAdmin(): InternalAdminResponse = adminGrpcClient.getAdminInfoByAdminId(getCurrentAdminId()) + suspend fun getCurrentAdmin(): InternalAdminResponse = userGrpcClient.getAdminInfoByAdminId(getCurrentAdminId()) /** * 현재 인증된 관리자의 고유 식별자(UUID)를 조회합니다. diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/grpc/client/AdminGrpcClient.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/grpc/client/AdminGrpcClient.kt deleted file mode 100644 index eb60c30..0000000 --- a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/grpc/client/AdminGrpcClient.kt +++ /dev/null @@ -1,56 +0,0 @@ -package hs.kr.entrydsm.feed.infrastructure.grpc.client - -import hs.kr.entrydsm.casper.admin.proto.AdminServiceGrpc -import hs.kr.entrydsm.casper.admin.proto.AdminServiceProto -import hs.kr.entrydsm.feed.infrastructure.grpc.client.dto.response.InternalAdminResponse -import io.grpc.Channel -import io.grpc.stub.StreamObserver -import kotlinx.coroutines.suspendCancellableCoroutine -import net.devh.boot.grpc.client.inject.GrpcClient -import org.springframework.stereotype.Component -import java.util.UUID -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -/** - * 관리자 서비스와의 gRPC 통신을 담당하는 클라이언트 클래스입니다. - * - * @property channel gRPC 통신을 위한 채널 (user-service로 자동 주입됨) - */ -@Component -class AdminGrpcClient { - - @GrpcClient("user-service") - lateinit var channel: Channel - - /** - * 관리자 ID를 기반으로 관리자 정보를 비동기적으로 조회합니다. - * gRPC 비동기 스트리밍을 사용하여 관리자 서비스로부터 정보를 가져옵니다. - * - * @param adminId 조회할 관리자의 고유 식별자(UUID) - * @return 조회된 관리자 정보를 담은 [InternalAdminResponse] 객체 - * @throws io.grpc.StatusRuntimeException gRPC 서버에서 오류가 발생한 경우 - * @throws java.util.concurrent.CancellationException 코루틴이 취소된 경우 - */ - suspend fun getAdminInfoByAdminId(adminId: UUID): InternalAdminResponse { - val adminStub = AdminServiceGrpc.newStub(channel) - - val request = AdminServiceProto.GetAdminIdRequest.newBuilder() - .setAdminId(adminId.toString()) - .build() - - val response = suspendCancellableCoroutine { continuation -> - adminStub.getAdminByUUID(request, object : StreamObserver { - override fun onNext(value: AdminServiceProto.GetAdminIdResponse) { - continuation.resume(value) - } - override fun onError(t: Throwable) { - continuation.resumeWithException(t) - } - override fun onCompleted() {} - }) - } - - return InternalAdminResponse(id = UUID.fromString(response.adminId)) - } -} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/grpc/user/client/UserGrpcClient.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/grpc/user/client/UserGrpcClient.kt new file mode 100644 index 0000000..70a28fa --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/grpc/user/client/UserGrpcClient.kt @@ -0,0 +1,70 @@ +package hs.kr.entrydsm.feed.infrastructure.grpc.user.client + +import hs.kr.entrydsm.casper.admin.proto.AdminServiceGrpc +import hs.kr.entrydsm.casper.admin.proto.AdminServiceProto +import hs.kr.entrydsm.feed.global.extension.executeGrpcCallWithResilience +import hs.kr.entrydsm.feed.infrastructure.grpc.user.client.dto.response.InternalAdminResponse +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 kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +/** + * 관리자 서비스와의 gRPC 통신을 담당하는 클라이언트 클래스입니다. + * + * @property channel gRPC 통신을 위한 채널 (user-service로 자동 주입됨) + */ +@Component +class UserGrpcClient( + @Qualifier("userGrpcRetry") private val retry: Retry, + @Qualifier("userGrpcCircuitBreaker") private val circuitBreaker: CircuitBreaker +) { + + @GrpcClient("user-service") + lateinit var channel: Channel + + /** + * 관리자 ID를 기반으로 관리자 정보를 비동기적으로 조회합니다. + * gRPC 비동기 스트리밍을 사용하여 관리자 서비스로부터 정보를 가져옵니다. + * + * @param adminId 조회할 관리자의 고유 식별자(UUID) + * @return 조회된 관리자 정보를 담은 [InternalAdminResponse] 객체 + * @throws io.grpc.StatusRuntimeException gRPC 서버에서 오류가 발생한 경우 + * @throws java.util.concurrent.CancellationException 코루틴이 취소된 경우 + */ + suspend fun getAdminInfoByAdminId(adminId: UUID): InternalAdminResponse { + + return executeGrpcCallWithResilience( + retry = retry, + circuitBreaker = circuitBreaker, + fallback = { InternalAdminResponse(adminId) } + ) { + val adminStub = AdminServiceGrpc.newStub(channel) + + val request = AdminServiceProto.GetAdminIdRequest.newBuilder() + .setAdminId(adminId.toString()) + .build() + + val response = suspendCancellableCoroutine { continuation -> + adminStub.getAdminByUUID(request, object : StreamObserver { + override fun onNext(value: AdminServiceProto.GetAdminIdResponse) { + continuation.resume(value) + } + override fun onError(t: Throwable) { + continuation.resumeWithException(t) + } + override fun onCompleted() {} + }) + } + + InternalAdminResponse(id = UUID.fromString(response.adminId)) + } + } +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/grpc/client/dto/response/InternalAdminResponse.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/grpc/user/client/dto/response/InternalAdminResponse.kt similarity index 73% rename from casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/grpc/client/dto/response/InternalAdminResponse.kt rename to casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/grpc/user/client/dto/response/InternalAdminResponse.kt index e4e33cc..5f6575b 100644 --- a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/grpc/client/dto/response/InternalAdminResponse.kt +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/grpc/user/client/dto/response/InternalAdminResponse.kt @@ -1,4 +1,4 @@ -package hs.kr.entrydsm.feed.infrastructure.grpc.client.dto.response +package hs.kr.entrydsm.feed.infrastructure.grpc.user.client.dto.response import java.util.UUID