Skip to content

Commit 96e90c2

Browse files
committed
Merge remote-tracking branch 'origin/feature/34-circuit-breaker-for-gRPC' into feature/34-circuit-breaker-for-gRPC
2 parents f4caf9b + 60e0115 commit 96e90c2

7 files changed

Lines changed: 218 additions & 31 deletions

File tree

buildSrc/src/main/kotlin/Dependencies.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,6 @@ object Dependencies {
105105
const val RESILIENCE4J_SPRING_BOOT = "io.github.resilience4j:resilience4j-spring-boot3:${DependencyVersions.RESILIENCE4J}"
106106
const val RESILIENCE4J_KOTLIN = "io.github.resilience4j:resilience4j-kotlin:${DependencyVersions.RESILIENCE4J}"
107107

108-
108+
// Netty
109+
const val NETTY = "io.netty:netty-resolver-dns-native-macos:${DependencyVersions.NETTY}"
109110
}

buildSrc/src/main/kotlin/DependencyVersions.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ object DependencyVersions {
4545
// Caffeine
4646
const val CAFFEINE = "3.1.8"
4747

48-
//Resilience4j
48+
// Resilience4j
4949
const val RESILIENCE4J = "2.0.2"
50+
51+
// Netty
52+
const val NETTY = "4.1.111.Final"
5053
}

casper-application-infrastructure/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ dependencies {
110110
implementation(Dependencies.RESILIENCE4J_RETRY)
111111
implementation(Dependencies.RESILIENCE4J_SPRING_BOOT)
112112
implementation(Dependencies.RESILIENCE4J_KOTLIN)
113+
114+
runtimeOnly(Dependencies.NETTY) {
115+
artifact {
116+
classifier = osdetector.classifier
117+
}
118+
}
113119
}
114120

115121
sourceSets {

casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,22 @@ import hs.kr.entrydsm.application.domain.examcode.util.DistanceUtil
1515
import kotlinx.coroutines.async
1616
import kotlinx.coroutines.coroutineScope
1717

18+
/**
19+
* 1차 전형에 합격한 학생들에게 수험번호를 부여하는 유스케이스입니다.
20+
*
21+
* @property applicationContract ApplicationAggregate를 가져옵니다.
22+
* @property statusContract StatusAggregate를 업데이트합니다.
23+
* @property kakaoGeocodeContract 카카오 맵 API와 상호작용합니다.
24+
* @property distanceUtil 두 지점 사이의 거리를 구합니다.
25+
* @property baseLocationContract 기준이 되는 장소의 위경도를 가져옵니다.
26+
*/
1827
@UseCase
1928
class GrantExamCodesUseCase(
2029
private val applicationContract: ApplicationContract,
21-
private val statusContract: StatusContract,
2230
private val kakaoGeocodeContract: KakaoGeocodeContract,
23-
private val distanceUtil: DistanceUtil,
2431
private val baseLocationContract: BaseLocationContract,
32+
private val statusContract: StatusContract,
33+
private val distanceUtil: DistanceUtil,
2534
) : GrantExamCodesContract {
2635

2736
companion object {
@@ -31,6 +40,10 @@ class GrantExamCodesUseCase(
3140
private const val SPECIAL_EXAM_CODE_PREFIX = "02"
3241
}
3342

43+
/**
44+
* 1차 전형에 합격한 학생들의 주소와 학교까지의 거리를 계산하고,
45+
* 전형별로 그룹화하여 수험번호를 부여합니다.
46+
*/
3447
override suspend fun execute() {
3548
val allFirstRoundPassedApplication = applicationContract.queryAllFirstRoundPassedApplication()
3649
val examCodeInfos = collectDistanceInfo(allFirstRoundPassedApplication)
@@ -46,16 +59,26 @@ class GrantExamCodesUseCase(
4659
saveExamCodes(examCodeInfos)
4760
}
4861

62+
/**
63+
* 학생들의 주소를 위경도로 변환하고, 학교와의 거리를 계산합니다.
64+
*
65+
* @param applications 1차 전형에 합격한 학생 리스트
66+
* @return 학생들의 접수 코드, 전형 유형, 학교까지의 거리를 담은 리스트
67+
* @throws ExamCodeException.failedGeocodeConversion 주소 변환에 실패했을 경우
68+
*/
4969
private suspend fun collectDistanceInfo(applications: List<Application>): List<ExamCodeInfo> = coroutineScope {
5070
applications.map { application ->
5171
async {
5272
val address = application.streetAddress as String
5373
val coordinate = kakaoGeocodeContract.geocode(address)
5474
?: throw ExamCodeException.failedGeocodeConversion(address)
75+
5576
val baseLat = baseLocationContract.baseLat
5677
val baseLon = baseLocationContract.baseLon
78+
5779
val userLat = coordinate.first
5880
val userLon = coordinate.second
81+
5982
val distance = distanceUtil.haversine(baseLat, baseLon, userLat, userLon)
6083
ExamCodeInfo(
6184
receiptCode = application.receiptCode,
@@ -66,6 +89,12 @@ class GrantExamCodesUseCase(
6689
}.map { it.await() }
6790
}
6891

92+
/**
93+
* 학생들을 학교까지의 거리를 기준으로 그룹화하고, 그룹 내에서 수험번호를 부여합니다.
94+
*
95+
* @param examCodeInfos 학생들의 정보 리스트
96+
* @param applicationType 전형 유형 (일반, 특별)
97+
*/
6998
private fun assignExamCodes(examCodeInfos: List<ExamCodeInfo>, applicationType: String) {
7099
val sortedByDistance = examCodeInfos.sortedByDescending { it.distance }
71100

@@ -76,6 +105,13 @@ class GrantExamCodesUseCase(
76105
}
77106
}
78107

108+
/**
109+
* 학생들을 학교까지의 거리가 같은 그룹으로 묶습니다.
110+
*
111+
* @param sortedInfos 거리를 기준으로 내림차순 정렬된 학생 정보 리스트
112+
* @param applicationType 전형 유형
113+
* @return 거리가 같은 학생들끼리 묶인 그룹 리스트
114+
*/
79115
private fun createDistanceGroups(sortedInfos: List<ExamCodeInfo>, applicationType: String): List<DistanceGroup> {
80116
val groups = mutableListOf<DistanceGroup>()
81117
val uniqueDistances = sortedInfos.map { it.distance }.distinct()
@@ -88,6 +124,11 @@ class GrantExamCodesUseCase(
88124
}
89125

90126

127+
/**
128+
* 같은 거리 그룹 내의 학생들에게 수험번호를 부여합니다.
129+
*
130+
* @param distanceGroup 거리가 같은 학생 그룹
131+
*/
91132
private fun assignNumbersInGroup(distanceGroup: DistanceGroup) {
92133
distanceGroup.examCodeInfoList.forEach { examCodeInfo ->
93134
val receiptCode = String.format("%03d", examCodeInfo.receiptCode)
@@ -96,7 +137,11 @@ class GrantExamCodesUseCase(
96137
}
97138
}
98139

99-
140+
/**
141+
* 부여된 수험번호를 저장합니다.
142+
*
143+
* @param examCodeInfos 수험번호가 부여된 학생 정보 리스트
144+
*/
100145
private suspend fun saveExamCodes(examCodeInfos: List<ExamCodeInfo>) {
101146
examCodeInfos.forEach { info ->
102147
info.examCode?.let { examCode ->

casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/ResilienceConfig.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ class ResilienceConfig(
2323
return circuitBreakerRegistry.circuitBreaker("status-grpc")
2424
}
2525

26+
@Bean
27+
fun scheduleGrpcCircuitBreaker(): CircuitBreaker {
28+
return circuitBreakerRegistry.circuitBreaker("schedule-grpc")
29+
}
30+
2631
@Bean
2732
fun userGrpcRetry(): Retry {
2833
return retryRegistry.retry("user-grpc")
@@ -32,4 +37,9 @@ class ResilienceConfig(
3237
fun statusGrpcRetry(): Retry {
3338
return retryRegistry.retry("status-grpc")
3439
}
40+
41+
@Bean
42+
fun scheduleGrpcRetry(): Retry {
43+
return retryRegistry.retry("schedule-grpc")
44+
}
3545
}

casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package hs.kr.entrydsm.application.global.grpc.client.schedule
22

3+
import hs.kr.entrydsm.application.global.extension.executeGrpcCallWithResilience
34
import hs.kr.entrydsm.application.global.grpc.dto.schedule.InternalScheduleResponse
45
import hs.kr.entrydsm.application.global.grpc.dto.schedule.ScheduleType
56
import hs.kr.entrydsm.casper.schedule.proto.ScheduleServiceGrpc
67
import hs.kr.entrydsm.casper.schedule.proto.ScheduleServiceProto
8+
import io.github.resilience4j.circuitbreaker.CircuitBreaker
9+
import io.github.resilience4j.retry.Retry
710
import io.grpc.Channel
811
import io.grpc.stub.StreamObserver
912
import kotlinx.coroutines.suspendCancellableCoroutine
1013
import net.devh.boot.grpc.client.inject.GrpcClient
14+
import org.springframework.beans.factory.annotation.Qualifier
1115
import org.springframework.stereotype.Component
1216
import java.time.LocalDateTime
1317
import java.time.format.DateTimeFormatter
@@ -18,7 +22,10 @@ import kotlin.coroutines.resumeWithException
1822
* Schedule Service와 gRPC 통신을 하는 클라이언트입니다.
1923
*/
2024
@Component
21-
class ScheduleGrpcClient {
25+
class ScheduleGrpcClient(
26+
@Qualifier("scheduleGrpcRetry") private val retry: Retry,
27+
@Qualifier("scheduleGrpcCircuitBreaker") private val circuitBreaker: CircuitBreaker
28+
) {
2229
@GrpcClient("schedule-service")
2330
lateinit var channel: Channel
2431

@@ -28,37 +35,48 @@ class ScheduleGrpcClient {
2835
* @return 일정 정보
2936
*/
3037
suspend fun getScheduleByType(type: String): InternalScheduleResponse {
31-
val scheduleStub = ScheduleServiceGrpc.newStub(channel)
38+
return executeGrpcCallWithResilience(
39+
retry = retry,
40+
circuitBreaker = circuitBreaker,
41+
fallback = {
42+
InternalScheduleResponse(
43+
type = toInternal(ScheduleServiceProto.Type.valueOf(type.uppercase())),
44+
date = LocalDateTime.now()
45+
)
46+
}
47+
) {
48+
val scheduleStub = ScheduleServiceGrpc.newStub(channel)
3249

33-
val request =
34-
ScheduleServiceProto.TypeRequest.newBuilder()
35-
.setType(ScheduleServiceProto.Type.valueOf(type.uppercase()))
36-
.build()
50+
val request =
51+
ScheduleServiceProto.TypeRequest.newBuilder()
52+
.setType(ScheduleServiceProto.Type.valueOf(type.uppercase()))
53+
.build()
3754

38-
val response =
39-
suspendCancellableCoroutine { continuation ->
40-
scheduleStub.getScheduleByType(
41-
request,
42-
object : StreamObserver<ScheduleServiceProto.GetScheduleResponse> {
43-
override fun onNext(value: ScheduleServiceProto.GetScheduleResponse) {
44-
continuation.resume(value)
45-
}
55+
val response =
56+
suspendCancellableCoroutine { continuation ->
57+
scheduleStub.getScheduleByType(
58+
request,
59+
object : StreamObserver<ScheduleServiceProto.GetScheduleResponse> {
60+
override fun onNext(value: ScheduleServiceProto.GetScheduleResponse) {
61+
continuation.resume(value)
62+
}
4663

47-
override fun onError(t: Throwable) {
48-
continuation.resumeWithException(t)
49-
}
64+
override fun onError(t: Throwable) {
65+
continuation.resumeWithException(t)
66+
}
5067

51-
override fun onCompleted() {}
52-
},
53-
)
54-
}
68+
override fun onCompleted() {}
69+
},
70+
)
71+
}
5572

56-
val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
73+
val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
5774

58-
val scheduleType = toInternal(response.type)
59-
val date = LocalDateTime.parse(response.date, formatter)
75+
val scheduleType = toInternal(response.type)
76+
val date = LocalDateTime.parse(response.date, formatter)
6077

61-
return InternalScheduleResponse(scheduleType, date)
78+
InternalScheduleResponse(scheduleType, date)
79+
}
6280
}
6381

6482
/**

0 commit comments

Comments
 (0)