-
Notifications
You must be signed in to change notification settings - Fork 1
feature/34-circuit-breaker-for-grpc #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…RPC' into feature/34-circuit-breaker-for-gRPC
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughResilience4j 의존성과 Netty 버전을 추가하고, Spring Bean 구성(회로 차단기/재시도)과 gRPC 호출용 회복력 래퍼를 도입했습니다. gRPC 클라이언트 3종을 래퍼로 감싸고 폴백을 정의했으며, 시험번호 부여 유스케이스는 지원유형별·거리그룹별 부여 로직으로 확장됐습니다. 테스트용 더미 컨트롤러가 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Caller
participant Retry
participant CircuitBreaker
participant Fallback
Caller->>CircuitBreaker: executeSuspendFunction { ... }
activate CircuitBreaker
CircuitBreaker->>Retry: executeSuspendFunction(block)
activate Retry
Retry-->>CircuitBreaker: 결과 또는 예외
deactivate Retry
alt 성공
CircuitBreaker-->>Caller: 결과 반환
else 예외
CircuitBreaker-->>Caller: 예외 전파
Caller->>Fallback: fallback()
Fallback-->>Caller: 폴백 결과
end
deactivate CircuitBreaker
sequenceDiagram
autonumber
participant Controller/Service
participant ScheduleGrpcClient
participant Retry
participant CircuitBreaker
participant gRPC Server
Controller/Service->>ScheduleGrpcClient: getScheduleByType(type)
ScheduleGrpcClient->>CircuitBreaker: execute { ... }
activate CircuitBreaker
CircuitBreaker->>Retry: execute { gRPC 호출 }
activate Retry
Retry->>gRPC Server: GetSchedule(request)
alt 성공
gRPC Server-->>Retry: response
Retry-->>CircuitBreaker: response
CircuitBreaker-->>ScheduleGrpcClient: response
ScheduleGrpcClient-->>Controller/Service: InternalScheduleResponse
else 실패/타임아웃
gRPC Server--x Retry: error
Retry--x CircuitBreaker: exception
CircuitBreaker--x ScheduleGrpcClient: exception
ScheduleGrpcClient->>ScheduleGrpcClient: 폴백 생성(LocalDateTime.now 등)
ScheduleGrpcClient-->>Controller/Service: 폴백 응답
end
deactivate Retry
deactivate CircuitBreaker
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
✨ Finishing Touches🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt (1)
69-90: Null 강제 캐스트/언랩으로 인한 NPE/CCE 가능성 및 불필요한 기준 좌표 반복 조회
application.streetAddress as String는 null/타입 불일치 시 예외 납니다.application.applicationType!!도 NPE 리스크가 큽니다.baseLat/baseLon을 매 반복마다 가져옵니다.map { it.await() }대신awaitAll()이 간결합니다.아래처럼 null‑세이프 처리, 기준 좌표 1회 조회,
awaitAll()사용으로 보완을 제안합니다.- private suspend fun collectDistanceInfo(applications: List<Application>): List<ExamCodeInfo> = 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, - applicationType = application.applicationType!!, // 전형 유형 - distance = distance - ) - } - }.map { it.await() } - } + private suspend fun collectDistanceInfo(applications: List<Application>): List<ExamCodeInfo> = coroutineScope { + val baseLat = baseLocationContract.baseLat + val baseLon = baseLocationContract.baseLon + applications.map { application -> + async { + val address = application.streetAddress?.takeIf { it.isNotBlank() } + ?: throw ExamCodeException.failedGeocodeConversion("empty or null address for receiptCode=${application.receiptCode}") + val (userLat, userLon) = kakaoGeocodeContract.geocode(address) + ?: throw ExamCodeException.failedGeocodeConversion(address) + val distance = distanceUtil.haversine(baseLat, baseLon, userLat, userLon) + ExamCodeInfo( + receiptCode = application.receiptCode, + applicationType = requireNotNull(application.applicationType) { "applicationType is null for receiptCode=${application.receiptCode}" }, + distance = distance + ) + } + }.awaitAll() + }추가로, 지오코딩 외부 호출 동시성에 상한(예: 16~32) 두기를 권합니다.
Semaphore또는Dispatchers.IO.limitedParallelism(n)사용을 검토해주세요(요청 시 샘플 코드 제공 가능).추가 import:
import kotlinx.coroutines.awaitAll
🧹 Nitpick comments (18)
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt (5)
18-26: KDoc에 수험번호 포맷/거리 단위 명시 권장수험번호 구조(예: [전형 접두사 2자리][거리그룹 3자리][접수코드 3자리])와 거리 단위(km/m) 명시가 있으면 유지보수성이 올라갑니다.
92-106: 파라미터 이름이 의미와 다릅니다 — 접두사 의도라면 명확히 변경 권장
applicationType: String은 실제로 “수험번호 접두사(01/02)”를 의미합니다. 혼동 방지를 위해 이름을examCodePrefix로 바꾸세요.- * @param applicationType 전형 유형 (일반, 특별) + * @param examCodePrefix 수험번호 접두사(일반=01, 특별=02) - private fun assignExamCodes(examCodeInfos: List<ExamCodeInfo>, applicationType: String) { + private fun assignExamCodes(examCodeInfos: List<ExamCodeInfo>, examCodePrefix: String) { val sortedByDistance = examCodeInfos.sortedByDescending { it.distance } - - val distanceGroups = createDistanceGroups(sortedByDistance, applicationType) + val distanceGroups = createDistanceGroups(sortedByDistance, examCodePrefix)필요 시
createDistanceGroups두 번째 파라미터도 동일하게 치환되어야 합니다.
108-124: 그룹 생성 로직 O(n^2) → O(n)으로 단순화; 실수 비교 동등성도 재검토현재
distinct()후filter()반복으로 최악 O(n^2)입니다. 정렬 순회 1패스로 동일 거리 변화 시에만 그룹을 닫도록 바꾸면 성능/메모리 모두 개선됩니다.- private fun createDistanceGroups(sortedInfos: List<ExamCodeInfo>, applicationType: String): List<DistanceGroup> { - val groups = mutableListOf<DistanceGroup>() - val uniqueDistances = sortedInfos.map { it.distance }.distinct() - uniqueDistances.forEachIndexed { index, distance -> - val distanceCode = String.format("%03d", index + 1) - val applicationsInGroup = sortedInfos.filter { it.distance == distance }.toMutableList() - groups.add(DistanceGroup(applicationType, distanceCode, applicationsInGroup)) - } - return groups - } + private fun createDistanceGroups(sortedInfos: List<ExamCodeInfo>, examCodePrefix: String): List<DistanceGroup> { + if (sortedInfos.isEmpty()) return emptyList() + val groups = mutableListOf<DistanceGroup>() + var currentDistance = sortedInfos.first().distance + var bucket = mutableListOf<ExamCodeInfo>() + var idx = 1 + for (info in sortedInfos) { + if (info.distance != currentDistance) { + groups.add(DistanceGroup(examCodePrefix, "%03d".format(idx++), bucket)) + bucket = mutableListOf() + currentDistance = info.distance + } + bucket.add(info) + } + groups.add(DistanceGroup(examCodePrefix, "%03d".format(idx), bucket)) + return groups + }또한 부동소수 동등 비교(
==)는 실사용에서 거의 단독 그룹을 유발합니다. “동일 거리” 정의가 오차 허용(예: ±50m)인지 확인 부탁드립니다. 필요 시 반올림/버킷팅(예: 0.1km 단위)을 적용하세요.
132-137: 포맷팅 사소 개선 및 범위 확인
- 정수 포맷은 로케일 영향이 거의 없지만
"%03d".format(...)로 간단히 쓸 수 있습니다.receiptCode가 999를 넘어설 가능성도 확인 부탁드립니다(넘을 경우 그대로 확장되며 문제는 없지만 사양 명시가 필요).- val receiptCode = String.format("%03d", examCodeInfo.receiptCode) + val receiptCode = "%03d".format(examCodeInfo.receiptCode)
145-151: 저장 단계의 부분 실패/멱등성/재시도 전략 검토
- 다수 업데이트 중 일부 실패 시 롤백/재시도/보상 절차가 없습니다.
- 중복 실행에 대비한 멱등성 보장이 필요합니다(이미 동일 수험번호면 no-op).
- 대량 업데이트 시 배치/트랜잭션 지원 여부 확인 바랍니다.
간단한 예: 실패 수집 후 한 번 재시도, 그래도 실패하면 에러 리포팅/알람.
buildSrc/src/main/kotlin/DependencyVersions.kt (1)
48-52: 버전 상수 추가는 OK, NETTY 상수명은 더 구체적으로.
NETTY는 실제로netty-resolver-dns-native-macos용 버전이므로NETTY_DNS_NATIVE_MACOS등으로 명확히 해두면 혼동(코어 Netty와의 버전 정합성 착각)을 줄일 수 있습니다.- // Netty - const val NETTY = "4.1.111.Final" + // Netty (native resolver for macOS only) + const val NETTY_DNS_NATIVE_MACOS = "4.1.111.Final"casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/user/UserGrpcClient.kt (2)
43-55: Fallback 반환은 로깅 추가 및 조건부 적용 고려.모든 예외에 대해
"Unknown User"를 돌려주면 도메인 상의 실제 오류(권한, 잘못된 파라미터)까지 정상 데이터로 오인될 수 있습니다. 최소 WARN 로그 남기고, 필요 시 일시적 오류(Status.UNAVAILABLE/DEADLINE_EXCEEDED 등)에 한해 fallback 하도록 정책을 분리하는 것을 권합니다.
56-87: Java async stub + suspendCancellableCoroutine 대신 Kotlin Coroutine Stub 사용 제안.코루틴 스텁을 쓰면 취소 전파, 백프레셔, 예외 매핑이 자동 처리되어 코드가 단순·안전해집니다.
- val userStub = UserServiceGrpc.newStub(channel) + val userStub = hs.kr.entrydsm.casper.user.proto.UserServiceGrpcKt.UserServiceCoroutineStub(channel) - val request = - UserServiceProto.GetUserInfoRequest.newBuilder() - .setUserId(userId.toString()) - .build() - - val response = - suspendCancellableCoroutine { continuation -> - userStub.getUserInfoByUserId( - request, - object : StreamObserver<UserServiceProto.GetUserInfoResponse> { - override fun onNext(value: UserServiceProto.GetUserInfoResponse) { - continuation.resume(value) - } - override fun onError(t: Throwable) { - continuation.resumeWithException(t) - } - override fun onCompleted() {} - }, - ) - } + val request = UserServiceProto.GetUserInfoRequest.newBuilder() + .setUserId(userId.toString()) + .build() + val response = userStub.getUserInfoByUserId(request) - 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), + )casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/status/StatusGrpcClient.kt (4)
65-70: detekt EmptyFunctionBlock 경고 해소 권장 (onCompleted 빈 블록)빈 블록 대신 표현식 본문으로 명시해 경고를 없애세요.
-override fun onCompleted() {} +override fun onCompleted() = UnitAlso applies to: 128-133, 180-186
101-114: 폴백이 도메인 의미를 바꿀 수 있음: 기본 NOT_APPLIED 반환 재검토서버 오류·차단 시에도 유효한 “미지원” 상태로 보일 수 있어, 장애 감지가 늦어질 수 있습니다. 호출부가 폴백 여부를 구분할 수 있는 신호(예: isFallback 플래그, Telemetry 기록, null 허용 설계) 도입을 검토해 주세요.
161-165: println 대신 로거 사용 및 컨텍스트 포함 로그로 교체운영 환경에서는 표준 로깅 사용이 필요합니다. 예외 원인(가능 시)을 포함해 경고 레벨로 남기는 것을 권장합니다.
- // Fallback: 로깅만 하고 조용히 실패 - println("Failed to update exam code for receiptCode: $receiptCode") + // Fallback: 로깅만 하고 조용히 실패 + log.warn("Failed to update exam code (receiptCode={}, examCode={})", receiptCode, examCode)추가: 클래스 상단에 로거 필드를 선언하세요.
// 클래스 내부 상단에 추가 private val log = org.slf4j.LoggerFactory.getLogger(StatusGrpcClient::class.java)
157-189: 업데이트 폴백의 “조용한 실패” 정책 재검토쓰기 작업에 재시도/차단 폴백으로 무시(로그만)하면 데이터 불일치가 숨겨질 수 있습니다. 호출자에 실패 신호 반환(Boolean/Result), 이벤트 적재(아웃박스) 또는 지연 재시도 큐 사용을 검토하세요.
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt (2)
51-53: 입력 유효성 사전 검증 고려메인 경로에서도 valueOf가 예외를 던질 수 있으므로 사전 검증 또는 안전 변환(위와 동일 로직 재사용)을 고려하세요.
68-69: detekt EmptyFunctionBlock 경고 제거 (onCompleted 빈 블록)표현식 본문으로 치환하세요.
- override fun onCompleted() {} + override fun onCompleted() = Unitcasper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt (4)
26-33: 동시성 안전성: 플래그는 AtomicBoolean 사용 권장싱글톤 컨트롤러의 가변 플래그는 동시 요청 간 경쟁 상태가 생깁니다. AtomicBoolean으로 전환하세요.
선언부 변경:
- private var shouldFailUser = false - private var shouldFailStatus = false - private var shouldBeSlowUser = false - private var shouldBeSlowStatus = false - private var shouldFailSchedule = false - private var shouldBeSlowSchedule = false + private val shouldFailUser = java.util.concurrent.atomic.AtomicBoolean(false) + private val shouldFailStatus = java.util.concurrent.atomic.AtomicBoolean(false) + private val shouldBeSlowUser = java.util.concurrent.atomic.AtomicBoolean(false) + private val shouldBeSlowStatus = java.util.concurrent.atomic.AtomicBoolean(false) + private val shouldFailSchedule = java.util.concurrent.atomic.AtomicBoolean(false) + private val shouldBeSlowSchedule = java.util.concurrent.atomic.AtomicBoolean(false)외부 사용 예(다른 위치 적용 필요):
// set shouldFailUser.set(shouldFail) // get if (shouldBeSlowUser.get()) { ... }
125-146: fallback 감지 방식 통일 및 하드코딩 제거status 엔드포인트도 usedFallback 플래그로 통일하세요(빈 리스트 여부로 판단하지 않음).
- val response = executeGrpcCallWithResilience( + var usedFallback = false + val response = executeGrpcCallWithResilience( retry = retry, circuitBreaker = circuitBreaker, fallback = { + usedFallback = true // Fallback 응답 InternalStatusListResponse(statusList = emptyList()) } ) { // 더미 gRPC 호출 시뮬레이션 simulateStatusListGrpcCall() } @@ - "source" to if (response.statusList.isEmpty()) "fallback" else "grpc" + "source" to if (usedFallback) "fallback" else "grpc"
375-388: Locale 경고 제거 및 수치형 지표 반환 권장String.format의 암묵적 Locale 경고(detekt)가 발생합니다. 포맷 문자열 대신 원시 수치(Double/Long)를 반환해 소비측에서 표현하도록 하세요.
return mapOf( "state" to circuitBreaker.state.name, - "failureRate" to String.format("%.2f%%", metrics.failureRate), - "slowCallRate" to String.format("%.2f%%", metrics.slowCallRate), + "failureRate" to metrics.failureRate, + "slowCallRate" to metrics.slowCallRate, "numberOfCalls" to metrics.numberOfBufferedCalls, "numberOfFailedCalls" to metrics.numberOfFailedCalls, "numberOfSlowCalls" to metrics.numberOfSlowCalls, "numberOfSuccessfulCalls" to metrics.numberOfSuccessfulCalls )대안: 꼭 문자열이 필요하면 Locale.ROOT를 명시하세요.
295-297: 느린 호출 시뮬레이션을 서킷 구성과 정합시키기slowCallRate 관측을 정확히 하려면 각 이름별 CircuitBreaker의 slowCallDurationThreshold를 초과하도록 지연값을 결정하세요.
예:
val thresholdMs = circuitBreakerRegistry.circuitBreaker("user-grpc") .circuitBreakerConfig.slowCallDurationThreshold.toMillis() if (shouldBeSlowUser.get()) delay(thresholdMs + 100)status/schedule에도 동일 패턴 적용.
Also applies to: 319-321, 356-358
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (10)
buildSrc/src/main/kotlin/Dependencies.kt(1 hunks)buildSrc/src/main/kotlin/DependencyVersions.kt(1 hunks)casper-application-infrastructure/build.gradle.kts(1 hunks)casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt(7 hunks)casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt(1 hunks)casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceGrpcExtensions.kt(1 hunks)casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt(3 hunks)casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/status/StatusGrpcClient.kt(8 hunks)casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/user/UserGrpcClient.kt(3 hunks)casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt(1 hunks)
🧰 Additional context used
🪛 detekt (1.23.8)
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/extension/ResilienceGrpcExtensions.kt
[warning] 19-19: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/test/GrpcResilienceTestController.kt
[warning] 381-381: String.format("%.2f%%", metrics.failureRate) uses implicitly default locale for string formatting.
(detekt.potential-bugs.ImplicitDefaultLocale)
[warning] 382-382: String.format("%.2f%%", metrics.slowCallRate) uses implicitly default locale for string formatting.
(detekt.potential-bugs.ImplicitDefaultLocale)
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt
[warning] 68-68: This empty block of code can be removed.
(detekt.empty-blocks.EmptyFunctionBlock)
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/status/StatusGrpcClient.kt
[warning] 184-184: This empty block of code can be removed.
(detekt.empty-blocks.EmptyFunctionBlock)
🔇 Additional comments (11)
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt (2)
43-60: 전형별 그룹화/접두사 부여는 적절합니다. 정렬 방향(내림차순) 요구사항 확인 필요현재 가장 먼 거리일수록 그룹 번호가 작아집니다(001이 최장거리). 의도된 규칙인지 확인 부탁드립니다.
98-106: 검증: DistanceGroup.applicationType과 geocode 함수 시그니처 확인
- DistanceGroup.applicationType은 실제 전형유형(입학 전형)을 나타내며, prefix로 재정의된 값이 아닙니다.
- KakaoGeocodeContract.geocode의 suspend 여부를 확인해 적절한 Dispatcher와 동시성 제한을 적용하세요.
- Application 엔티티에는 streetAddress·applicationType 프로퍼티가 존재하지 않으므로 해당 nullability 검증은 생략 가능합니다.
casper-application-infrastructure/build.gradle.kts (1)
108-113: Resilience4j 의존성 추가 LGTM.코틀린 확장(
executeSuspendFunction)과 스프링 부트 오토컨피그를 쓰기 위한 구성으로 적절합니다.casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt (2)
16-45: 빈 노출 구성 LGTM.레지스트리 기반 네임드 인스턴스 바인딩이 명확합니다(qualifier로 그대로 사용 가능).
16-45: application 설정 내 Resilience4j 인스턴스 정의 확인 필요
application.yml 또는 .properties 파일에
- resilience4j.circuitbreaker.instances.user-grpc, status-grpc, schedule-grpc
- resilience4j.retry.instances.user-grpc, status-grpc, schedule-grpc
설정이 정의되어 있는지, 슬라이딩 윈도우, 실패율 임계치, 대기시간 등이 올바르게 구성되었는지 검증하세요.casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/status/StatusGrpcClient.kt (4)
27-30: DI로 Retry/CircuitBreaker를 생성자 주입한 설계 좋습니다.의존성 주입 경로가 명확하고 테스트 용이성이 올라갑니다.
43-49: getStatusList 폴백 기본값 적절빈 리스트 반환은 호출부에서의 후속 처리에 안전합니다.
74-86: DTO 매핑 로직 일관성 양호takeIf, enum 매핑 등 변환이 명확합니다.
167-171: 리뷰 코멘트 무효화
updateExamCodeRPC는 프로토 정의에서GetExamCodeRequest를 인자로 사용하도록 되어 있어 클라이언트 코드가 올바릅니다.Likely an incorrect or invalid review comment.
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt (1)
25-28: Retry/CircuitBreaker 주입 패턴 일관성 좋습니다상태/유저 클라이언트와 동일 패턴 유지로 유지보수성↑.
buildSrc/src/main/kotlin/Dependencies.kt (1)
102-107: Resilience4j 의존성 추가 적절CircuitBreaker/Retry/Kotlin/SB3 구성에 필요한 최소 세트가 잘 들어갔습니다.
Summary by CodeRabbit