Skip to content

Conversation

@qkrwndnjs1075
Copy link
Member

@qkrwndnjs1075 qkrwndnjs1075 commented Sep 1, 2025

Summary by CodeRabbit

  • 신기능
    • gRPC 호출에 재시도·서킷브레이커를 도입하여 장애 시에도 안전한 기본 응답을 제공하고 안정성을 향상했습니다.
    • Resilience 동작을 검증할 수 있는 테스트 엔드포인트를 추가하여 실패/지연 시나리오 제어와 메트릭 확인이 가능합니다.
    • 시험번호 부여 로직을 전형(일반/특별) 및 거리별 그룹 기준으로 개선해 배정 정확도를 높였습니다.
  • 작업
    • Resilience4j 및 Netty 관련 의존성을 추가해 안정성 기능을 지원했습니다.

@coderabbitai
Copy link

coderabbitai bot commented Sep 1, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Resilience4j 의존성과 Netty 버전을 추가하고, Spring Bean 구성(회로 차단기/재시도)과 gRPC 호출용 회복력 래퍼를 도입했습니다. gRPC 클라이언트 3종을 래퍼로 감싸고 폴백을 정의했으며, 시험번호 부여 유스케이스는 지원유형별·거리그룹별 부여 로직으로 확장됐습니다. 테스트용 더미 컨트롤러가 추가되었습니다.

Changes

Cohort / File(s) Summary
Dependency versions & coordinates
buildSrc/src/main/kotlin/DependencyVersions.kt, buildSrc/src/main/kotlin/Dependencies.kt
Resilience4j(2.0.2)와 Netty(4.1.111.Final) 버전 상수 추가. Resilience4j 4모듈 및 macOS용 Netty DNS native 좌표 상수 추가.
Module build wiring
casper-application-infrastructure/build.gradle.kts
Resilience4j 4모듈 implementation 추가. Netty native macOS runtimeOnly 추가(artifact classifier = osdetector.classifier).
Resilience configuration
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt
CircuitBreakerRegistry/RetryRegistry로부터 user/status/schedule용 CircuitBreaker·Retry 빈 6개 등록.
Resilience helper
.../global/extension/ResilienceGrpcExtensions.kt
executeGrpcCallWithResilience 추가: Retry→CircuitBreaker로 감싼 suspend 블록 실행, 실패 시 폴백 호출.
gRPC clients (resilience 적용)
.../grpc/client/user/UserGrpcClient.kt, .../grpc/client/status/StatusGrpcClient.kt, .../grpc/client/schedule/ScheduleGrpcClient.kt
각 클라이언트 생성자에 Retry/CircuitBreaker 주입(@qualifier). 모든 gRPC 호출을 래퍼로 감싸고 폴백 응답 제공. 기존 호출 흐름은 래퍼 내부로 이동.
Exam code use case
.../domain/examcode/usecase/GrantExamCodesUseCase.kt
생성자 파라미터 순서 변경. 지원유형(일반/특별)별 분리 부여 로직 추가. 거리 계산·그룹화 시 applicationType 포함. 저장 로직은 상태 계약을 통해 업데이트.
Dummy test controller
.../global/grpc/test/GrpcResilienceTestController.kt
더미 gRPC 시뮬레이션 REST 엔드포인트 추가(성공/지연/실패 제어, 메트릭/상태 노출). Resilience 래퍼 사용 및 폴백 응답 제공.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

귀가 쫑긋, 토끼는 뛰지요—retry 한 번, 두 번 또
회로는 톡, 닫혔다 열리고 흐름은 다시 고요
gRPC 숲을 건너며, 폴백 당근 챙겨요 🥕
거리 따라 번호 줄, 줄 맞춰 또 또또
오늘의 빌드는 바람처럼, 탄탄대로 고고!

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/34-circuit-breaker-for-gRPC

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@qkrwndnjs1075
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Sep 1, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@qkrwndnjs1075 qkrwndnjs1075 requested review from rudeh2926 and removed request for rudeh2926 September 1, 2025 06:54
Copy link

@coderabbitai coderabbitai bot left a 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() = Unit

Also 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() = Unit
casper-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.

📥 Commits

Reviewing files that changed from the base of the PR and between 9ba7c2b and 96e90c2.

📒 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: 리뷰 코멘트 무효화
updateExamCode RPC는 프로토 정의에서 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 구성에 필요한 최소 세트가 잘 들어갔습니다.

@qkrwndnjs1075 qkrwndnjs1075 merged commit 2a8d653 into main Sep 3, 2025
1 check passed
@qkrwndnjs1075 qkrwndnjs1075 deleted the feature/34-circuit-breaker-for-gRPC branch September 3, 2025 07:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants