π μλ¬ μν©
νμ¬ μμμμ Slackμ ν΅κ³, μλ¬ μλ¦ΌμΌλ‘ μ¬μ©νκ³ μλ€.
κ·Όλ° κ°λ Slack λ©μμ§ μ μ‘μ΄ μ€ν¨νλ€κ³ μλ¦Όμ΄ μ¨λ€.


μ€μΌμ€λ¬ μλ¬μΈ κ²μ 보μ, ν΅κ³ μλ¦Όμ μ¬λ λ©μμ§ μ μ‘ λΆλΆμμ λ°μν κ²μ μ μ μλ€.
κ·Όλ° μμ 보μ¬μ€ 건 μ¬λ¬ κ° μ€ νλμ΄λ€.
λ€νν λͺ¨λ ν΅κ³ μλ¦Ό μλ¬μ΄λ€.
νμ§λ§ μ΄ μΌμ΄ μλ¬ μλ¦Όμμ λ°μν΄λ μ΄μνμ§ μλ€.
μΌνΈλ¦¬λ‘ μλ¬ μ¬νμ΄ κΈ°λ‘λκΈ΄ νμ§λ§ κ°λ°μκ° μ΄ μλ¬μ λ°μμ λΉ λ₯΄κ² νμ ν μ μλ κ²μ μ¬λ μλλΏμ΄λ€.
κ·Έλ¬λ―λ‘ μλμ΄ μ λ¬λ¨μ 보μ₯ν΄μ£Όλ λ‘μ§μ΄ νμνλ€.
π μ²λ¦¬μ κ΄ν κ³ λ―Ό
μ²μμ μν· λΈλ μ΄μ»€λ₯Ό μκ°νλ€.
μ¬λμ΄ μ€λ κΈ°κ° λμ μ₯μ κ° λ°μνλ€λ©΄ μμ€ν μ μμ μ±μ λμ΄λ λΆλΆμ μμ΄μ μ μ©ν κ²μ΄λ€.
νμ§λ§ μ¬λμ΄ μ€λ κΈ°κ° λμ μ₯μ κ° λ°μν κ²μ λ³΄μ§ λͺ»νλ€λ μ , μ§κΈκΉμ§ λ°μν μ¬λ 컀λ₯μ κ΄λ¦¬ λ¬Έμ λΌλ μ μ κ³ λ €νμ λ, μΌμμ μΈ μ€λ₯, νΉν νΉμ μμ²μμλ§ λ°μνλ μν©μμλ μν· λΈλ μ΄μ»€λ λΆνμνλ€κ³ μκ°νλ€.
κ·Έλ λ€λ©΄ retry λ‘μ§μ μ΄λ νκ°?
μΌμμ μΈ μ€λ₯κ° μμ£Ό λ°μνλ μμ μμ μ¬μλλ₯Ό ν΅ν΄ μ±κ³΅ κ°λ₯μ±μ λμΌ μ μλ€.
λ¬Όλ‘ μΌμμ μ€λ₯κ° μλ μ₯κΈ°κ° μ₯μ κ° μ΄μ΄μ§ κ²½μ° μμ²μ λν μ κ·Όμ μ ννλ λμ μ¬μ²λ¦¬λ§ λ°λ³΅νκΈ° λλ¬Έμ, μμ€ν μ 리μμ€κ° λλΉλλ μν©μ΄ μΌμ΄λ μ μλ€.
κ·Έλ¬λ―λ‘ μν· λΈλ μ΄μ»€ λ³΄λ€ μμ μ±μ λ¨μ΄μ§λ€.
νμ§λ§ μ λ¬Έμ μν©κ³Ό κ°μ μν©μ΄λΌλ©΄ retryκ° λ μ ν©νλ€ μκ°νλ€.
μλ λͺ¨λ μμ²μ λν μ²λ¦¬μ΄λ€.
μ°λ¦¬λ μ λ¬λμ§λͺ»ν μμ²μ λν μ²λ¦¬λ ν΄μ€μΌ νλ€.
μ€ν¨νλ©΄, μ μ₯ν΄ λκ³ λ°λ‘ μ²λ¦¬νλ©΄ λ κ²μ΄λΌ μκ°νλ€.
π κ³ λ―Όμ λν λμ λ΅
retryμ batch jobμ μ‘°ν©ν΄μ μ²λ¦¬νλ€.
retryλ μ€ν¨ μμ μ λ€μ μννλ μν μ΄κ³ , batch jobμ λ°λ³΅ μνν΄λ μ μ‘ μ€ν¨ν λ©μμ§λ₯Ό λͺ¨μμ λ€μ μ μ‘νλ κ²μ΄λ€.
π΅ Retry
retryλ 2κ°μ§ λ°©λ²μ΄ μμλ€.
- reactor
- spring retry
κ²°λ‘ λΆν° λ§νμλ©΄ λλ reactor retryλ₯Ό μ¬μ©νλ€.
spring retryλ μ΄λ Έν μ΄μ μΌλ‘ μ½κ² μ€μ νμ¬ μ¬μ©ν μ μλ€.
νμ§λ§ νμ¬ μΈλΆ μλΉμ€μ μ°κ²°μ΄ webclientλ‘ νμ λμ΄ μλ€λ μ κ³Ό retryλ₯Ό μ μ©νλ € νλ λΆλΆμ΄ webclientλ‘ νμ λμ΄ μλ€λ μ μ κ³ λ €νμ λ, μΆκ°μ μΈ λΌμ΄λΈλ¬λ¦¬μ μ€μΉμ μ€μ μ΄ νμν spring retry λ³΄λ¨ reactorκ° μ μ νλ€ νλ¨νλ€.
class SuspendableSlackClient(
private val webclient: WebClient,
private val slackWebhookConfig: SlackConfig.SlackWebhookConfig,
private val cacheService: CacheService,
) : SlackClient {
private val logger = KotlinLogging.logger { }
override suspend fun sendSummary(message: SlackMessageModel): String {
return sendMessage(message, slackWebhookConfig.summaryToken)
}
override suspend fun sendError(message: SlackMessageModel): String {
return sendMessage(message, slackWebhookConfig.errorToken)
}
override suspend fun sendMessage(message: SlackMessageModel, token: String, withRecover: Boolean): String {
return runCatching {
withMDCContext(Dispatchers.IO) {
webclient.post()
.uri("/${token}")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(message)
.retrieve()
.bodyToMono<String>()
.retryWhen(Retry.fixedDelay(2, Duration.ofMillis(500)))
.awaitSingleOrThrow()
}
}.onFailure {
if (withRecover) {
recoverSendMessage(message, token)
}
}.getOrThrow()
}
private suspend fun recoverSendMessage(message: SlackMessageModel, token: String) {
val model = FailedSentSlackMessageCache(
token = token,
message = message.text
)
cacheService.sSet(Cache.getFailedSentSlackMessageCache(LocalDateTime.now()), model)
}
}
μ μ‘ν΄ λ³΄κ³ μ€ν¨νλ©΄ κ°μ μ μ₯νλλ‘ νλ€.
μ¬μ€ μ μ₯νλ λ‘μ§λ doOnErrorλ‘ μ²λ¦¬ν μ μμλ€.
νμ§λ§ μ½λ£¨ν΄μΌλ‘ μ²λ¦¬νλ κ² κ°λ μ±μ λμΌ μ μκΈ° λλ¬Έμ μ΄λ μ΄μ©νμ§ μμλ€.
π΅ Batch Job
batchλ κ°λ¨νλ€
μ μ₯λ κ°μ κ°μ Έμμ λ³ν©ν ν, μ μ‘νλ κ² μ λΆμ΄λ€.
λν μ μ₯λ κ°μ μ¬μ μ‘ν λλ λ©μμ§μ μμ μ¬μ²λ¦¬λ λ©μμ§μμ νμνλ€.
μ΄ μμ μ 1λΆμ 1λ²μ©, 1λΆ μ κΉμ§ κΈ°λ‘λ κ°μ μ²λ¦¬ν΄ μ€λ€.
(νμ¬κ° 24λΆμ΄λ©΄ 23λΆ κ°μ μ²λ¦¬νλ€λ κ²)
μ΄λ, μλ¬ μν©μ΄ κΈΈμ΄μ Έμ 1λΆ λ€μλ μ μ‘νμ§ λͺ»νλ κ²½μ°λ λλΉν΄μΌ νλ€.
μ΄ κ²½μ°λ 1λΆ λ€μ μ‘°νλ κ°λ€μ μ μ‘λμ§ λͺ»ν κ°λ€μ μΆκ°νμ¬, λ©μμ§λ₯Ό 1λΆ λ€μ λ€μ μ μ‘νλλ‘ μ‘°μΉνλ€.
@Component
class ResendFailedSentSlackMessageJob(
private val cacheService: CacheService,
private val slackClient: SlackClient,
) {
private val logger = KotlinLogging.logger { }
companion object {
private const val RESEND_BEFORE_MINUTES = 1L
}
suspend fun resendFailedSentSlackMessage() {
logger.info { "start resend failed sent slack message" }
// 1λΆ μ μ μ€ν¨ν κ²μ΄ νκ² (νμ¬κ° 24λΆμ΄λ©΄ 23λΆμ λ§νλ κ²)
val targetTime = LocalDateTime.now().minusMinutes(RESEND_BEFORE_MINUTES)
// μ€ν¨ λ©μΈμ§ μ‘°ν λ° μμ
val failedMessages = withContext(Dispatchers.IO) {
cacheService.sGetMembers(Cache.getFailedSentSlackMessageCache(targetTime))
}
withContext(Dispatchers.IO) {
cacheService.sDelete(Cache.getFailedSentSlackMessageCache(targetTime))
}
// λ€μ λ©μΈμ§ token λ³λ‘ νλμ λ©μΈμ§λ‘ λ³ν©
val message = mergeFailedMessage(failedMessages)
// μ¬μ μ‘
runCatching {
coroutineScope {
val sendDeferreds = message.map { (token, message) ->
val slackMessageModel = SlackMessageModel(text = message)
async(Dispatchers.IO) {
slackClient.sendMessage(
message = slackMessageModel,
token = token,
withRecover = false
)
}
}.toTypedArray()
awaitAll(*sendDeferreds)
}
}.onFailure {
// μ¬μ μ‘ μ€ν¨μ 1λΆ λ€μ λ€μ λ³΄λΌ μ μκ², 1λΆ λ€μ 보λ΄λ λ©μΈμ§ λͺ©λ‘μ μΆκ°
logger.warn { "postpone resend slack message" }
postponeResendTimeOfFailedMessage(targetTime, message)
}
logger.info { "finish resend failed sent slack message" }
}
private suspend fun mergeFailedMessage(failedMessages: List<FailedSentSlackMessageCache>): Map<String, String> {
val message = mutableMapOf<String, String>()
failedMessages.forEach { model ->
val recoverMsg = if (model.isStacked) {
model.message
} else {
"[RECOVER - ${model.failedAt} slack failure] ${model.message}"
}
val stackedMessage = message[model.token]
message[model.token] = if (stackedMessage == null) {
recoverMsg
} else {
"$stackedMessage\n$recoverMsg"
}
}
return message
}
private suspend fun postponeResendTimeOfFailedMessage(targetTime: LocalDateTime, message: Map<String, String>) {
val nextTime = targetTime.plusMinutes(RESEND_BEFORE_MINUTES)
coroutineScope {
message.map { (token, message) ->
val model = FailedSentSlackMessageCache(
token = token,
message = message,
isStacked = true
)
async(Dispatchers.IO) {
cacheService.sSet(
cache = Cache.getFailedSentSlackMessageCache(nextTime),
value = model
)
}
}
}
}
}
π λ§λ¬΄λ¦¬
DLQλ₯Ό μλνκ³ λ§λ 건 μλλ°, λ§λ€κ³ 보λκΉ DLQλ λΉμ·ν κ² κ°λ€.
곧 μ¬λμμ λμ€μ½λλ‘ μ΄κ΄ν΄μΌ νλλ°, κ±°κΈ°λ μ΄λ° κ±° λ§λ€μ΄λ¬μΌκ² λ€.
λ§μ½μ μ¬λ μͺ½μ μ§μμ μΈ λ¬Έμ κ° λ°μνλ€λ©΄ μν· λΈλ μ΄μ»€ λμ μ κ³ λ €ν΄ λ΄μΌκ² λ€.
'Backend' μΉ΄ν κ³ λ¦¬μ λ€λ₯Έ κΈ
<Spring> Coroutine Actor μ΄μ©ν΄μ λ¨μΌ μλ² λ½ κ΅¬ννκΈ° 2 (0) | 2024.08.22 |
---|---|
<Spring> Coroutine Actor μ΄μ©ν΄μ λ¨μΌ μλ² λ½ κ΅¬ννκΈ° (0) | 2024.08.20 |
<Spring> Webflux + Coroutine vs MVC (0) | 2024.06.12 |
<Spring> SUSUμ Coroutine (0) | 2024.05.11 |
<Spring> Webflux + Coroutine + MDC (0) | 2024.05.10 |
π μλ¬ μν©
νμ¬ μμμμ Slackμ ν΅κ³, μλ¬ μλ¦ΌμΌλ‘ μ¬μ©νκ³ μλ€.
κ·Όλ° κ°λ Slack λ©μμ§ μ μ‘μ΄ μ€ν¨νλ€κ³ μλ¦Όμ΄ μ¨λ€.


μ€μΌμ€λ¬ μλ¬μΈ κ²μ 보μ, ν΅κ³ μλ¦Όμ μ¬λ λ©μμ§ μ μ‘ λΆλΆμμ λ°μν κ²μ μ μ μλ€.
κ·Όλ° μμ 보μ¬μ€ 건 μ¬λ¬ κ° μ€ νλμ΄λ€.
λ€νν λͺ¨λ ν΅κ³ μλ¦Ό μλ¬μ΄λ€.
νμ§λ§ μ΄ μΌμ΄ μλ¬ μλ¦Όμμ λ°μν΄λ μ΄μνμ§ μλ€.
μΌνΈλ¦¬λ‘ μλ¬ μ¬νμ΄ κΈ°λ‘λκΈ΄ νμ§λ§ κ°λ°μκ° μ΄ μλ¬μ λ°μμ λΉ λ₯΄κ² νμ ν μ μλ κ²μ μ¬λ μλλΏμ΄λ€.
κ·Έλ¬λ―λ‘ μλμ΄ μ λ¬λ¨μ 보μ₯ν΄μ£Όλ λ‘μ§μ΄ νμνλ€.
π μ²λ¦¬μ κ΄ν κ³ λ―Ό
μ²μμ μν· λΈλ μ΄μ»€λ₯Ό μκ°νλ€.
μ¬λμ΄ μ€λ κΈ°κ° λμ μ₯μ κ° λ°μνλ€λ©΄ μμ€ν μ μμ μ±μ λμ΄λ λΆλΆμ μμ΄μ μ μ©ν κ²μ΄λ€.
νμ§λ§ μ¬λμ΄ μ€λ κΈ°κ° λμ μ₯μ κ° λ°μν κ²μ λ³΄μ§ λͺ»νλ€λ μ , μ§κΈκΉμ§ λ°μν μ¬λ 컀λ₯μ κ΄λ¦¬ λ¬Έμ λΌλ μ μ κ³ λ €νμ λ, μΌμμ μΈ μ€λ₯, νΉν νΉμ μμ²μμλ§ λ°μνλ μν©μμλ μν· λΈλ μ΄μ»€λ λΆνμνλ€κ³ μκ°νλ€.
κ·Έλ λ€λ©΄ retry λ‘μ§μ μ΄λ νκ°?
μΌμμ μΈ μ€λ₯κ° μμ£Ό λ°μνλ μμ μμ μ¬μλλ₯Ό ν΅ν΄ μ±κ³΅ κ°λ₯μ±μ λμΌ μ μλ€.
λ¬Όλ‘ μΌμμ μ€λ₯κ° μλ μ₯κΈ°κ° μ₯μ κ° μ΄μ΄μ§ κ²½μ° μμ²μ λν μ κ·Όμ μ ννλ λμ μ¬μ²λ¦¬λ§ λ°λ³΅νκΈ° λλ¬Έμ, μμ€ν μ 리μμ€κ° λλΉλλ μν©μ΄ μΌμ΄λ μ μλ€.
κ·Έλ¬λ―λ‘ μν· λΈλ μ΄μ»€ λ³΄λ€ μμ μ±μ λ¨μ΄μ§λ€.
νμ§λ§ μ λ¬Έμ μν©κ³Ό κ°μ μν©μ΄λΌλ©΄ retryκ° λ μ ν©νλ€ μκ°νλ€.
μλ λͺ¨λ μμ²μ λν μ²λ¦¬μ΄λ€.
μ°λ¦¬λ μ λ¬λμ§λͺ»ν μμ²μ λν μ²λ¦¬λ ν΄μ€μΌ νλ€.
μ€ν¨νλ©΄, μ μ₯ν΄ λκ³ λ°λ‘ μ²λ¦¬νλ©΄ λ κ²μ΄λΌ μκ°νλ€.
π κ³ λ―Όμ λν λμ λ΅
retryμ batch jobμ μ‘°ν©ν΄μ μ²λ¦¬νλ€.
retryλ μ€ν¨ μμ μ λ€μ μννλ μν μ΄κ³ , batch jobμ λ°λ³΅ μνν΄λ μ μ‘ μ€ν¨ν λ©μμ§λ₯Ό λͺ¨μμ λ€μ μ μ‘νλ κ²μ΄λ€.
π΅ Retry
retryλ 2κ°μ§ λ°©λ²μ΄ μμλ€.
- reactor
- spring retry
κ²°λ‘ λΆν° λ§νμλ©΄ λλ reactor retryλ₯Ό μ¬μ©νλ€.
spring retryλ μ΄λ Έν μ΄μ μΌλ‘ μ½κ² μ€μ νμ¬ μ¬μ©ν μ μλ€.
νμ§λ§ νμ¬ μΈλΆ μλΉμ€μ μ°κ²°μ΄ webclientλ‘ νμ λμ΄ μλ€λ μ κ³Ό retryλ₯Ό μ μ©νλ € νλ λΆλΆμ΄ webclientλ‘ νμ λμ΄ μλ€λ μ μ κ³ λ €νμ λ, μΆκ°μ μΈ λΌμ΄λΈλ¬λ¦¬μ μ€μΉμ μ€μ μ΄ νμν spring retry λ³΄λ¨ reactorκ° μ μ νλ€ νλ¨νλ€.
class SuspendableSlackClient(
private val webclient: WebClient,
private val slackWebhookConfig: SlackConfig.SlackWebhookConfig,
private val cacheService: CacheService,
) : SlackClient {
private val logger = KotlinLogging.logger { }
override suspend fun sendSummary(message: SlackMessageModel): String {
return sendMessage(message, slackWebhookConfig.summaryToken)
}
override suspend fun sendError(message: SlackMessageModel): String {
return sendMessage(message, slackWebhookConfig.errorToken)
}
override suspend fun sendMessage(message: SlackMessageModel, token: String, withRecover: Boolean): String {
return runCatching {
withMDCContext(Dispatchers.IO) {
webclient.post()
.uri("/${token}")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(message)
.retrieve()
.bodyToMono<String>()
.retryWhen(Retry.fixedDelay(2, Duration.ofMillis(500)))
.awaitSingleOrThrow()
}
}.onFailure {
if (withRecover) {
recoverSendMessage(message, token)
}
}.getOrThrow()
}
private suspend fun recoverSendMessage(message: SlackMessageModel, token: String) {
val model = FailedSentSlackMessageCache(
token = token,
message = message.text
)
cacheService.sSet(Cache.getFailedSentSlackMessageCache(LocalDateTime.now()), model)
}
}
μ μ‘ν΄ λ³΄κ³ μ€ν¨νλ©΄ κ°μ μ μ₯νλλ‘ νλ€.
μ¬μ€ μ μ₯νλ λ‘μ§λ doOnErrorλ‘ μ²λ¦¬ν μ μμλ€.
νμ§λ§ μ½λ£¨ν΄μΌλ‘ μ²λ¦¬νλ κ² κ°λ μ±μ λμΌ μ μκΈ° λλ¬Έμ μ΄λ μ΄μ©νμ§ μμλ€.
π΅ Batch Job
batchλ κ°λ¨νλ€
μ μ₯λ κ°μ κ°μ Έμμ λ³ν©ν ν, μ μ‘νλ κ² μ λΆμ΄λ€.
λν μ μ₯λ κ°μ μ¬μ μ‘ν λλ λ©μμ§μ μμ μ¬μ²λ¦¬λ λ©μμ§μμ νμνλ€.
μ΄ μμ μ 1λΆμ 1λ²μ©, 1λΆ μ κΉμ§ κΈ°λ‘λ κ°μ μ²λ¦¬ν΄ μ€λ€.
(νμ¬κ° 24λΆμ΄λ©΄ 23λΆ κ°μ μ²λ¦¬νλ€λ κ²)
μ΄λ, μλ¬ μν©μ΄ κΈΈμ΄μ Έμ 1λΆ λ€μλ μ μ‘νμ§ λͺ»νλ κ²½μ°λ λλΉν΄μΌ νλ€.
μ΄ κ²½μ°λ 1λΆ λ€μ μ‘°νλ κ°λ€μ μ μ‘λμ§ λͺ»ν κ°λ€μ μΆκ°νμ¬, λ©μμ§λ₯Ό 1λΆ λ€μ λ€μ μ μ‘νλλ‘ μ‘°μΉνλ€.
@Component
class ResendFailedSentSlackMessageJob(
private val cacheService: CacheService,
private val slackClient: SlackClient,
) {
private val logger = KotlinLogging.logger { }
companion object {
private const val RESEND_BEFORE_MINUTES = 1L
}
suspend fun resendFailedSentSlackMessage() {
logger.info { "start resend failed sent slack message" }
// 1λΆ μ μ μ€ν¨ν κ²μ΄ νκ² (νμ¬κ° 24λΆμ΄λ©΄ 23λΆμ λ§νλ κ²)
val targetTime = LocalDateTime.now().minusMinutes(RESEND_BEFORE_MINUTES)
// μ€ν¨ λ©μΈμ§ μ‘°ν λ° μμ
val failedMessages = withContext(Dispatchers.IO) {
cacheService.sGetMembers(Cache.getFailedSentSlackMessageCache(targetTime))
}
withContext(Dispatchers.IO) {
cacheService.sDelete(Cache.getFailedSentSlackMessageCache(targetTime))
}
// λ€μ λ©μΈμ§ token λ³λ‘ νλμ λ©μΈμ§λ‘ λ³ν©
val message = mergeFailedMessage(failedMessages)
// μ¬μ μ‘
runCatching {
coroutineScope {
val sendDeferreds = message.map { (token, message) ->
val slackMessageModel = SlackMessageModel(text = message)
async(Dispatchers.IO) {
slackClient.sendMessage(
message = slackMessageModel,
token = token,
withRecover = false
)
}
}.toTypedArray()
awaitAll(*sendDeferreds)
}
}.onFailure {
// μ¬μ μ‘ μ€ν¨μ 1λΆ λ€μ λ€μ λ³΄λΌ μ μκ², 1λΆ λ€μ 보λ΄λ λ©μΈμ§ λͺ©λ‘μ μΆκ°
logger.warn { "postpone resend slack message" }
postponeResendTimeOfFailedMessage(targetTime, message)
}
logger.info { "finish resend failed sent slack message" }
}
private suspend fun mergeFailedMessage(failedMessages: List<FailedSentSlackMessageCache>): Map<String, String> {
val message = mutableMapOf<String, String>()
failedMessages.forEach { model ->
val recoverMsg = if (model.isStacked) {
model.message
} else {
"[RECOVER - ${model.failedAt} slack failure] ${model.message}"
}
val stackedMessage = message[model.token]
message[model.token] = if (stackedMessage == null) {
recoverMsg
} else {
"$stackedMessage\n$recoverMsg"
}
}
return message
}
private suspend fun postponeResendTimeOfFailedMessage(targetTime: LocalDateTime, message: Map<String, String>) {
val nextTime = targetTime.plusMinutes(RESEND_BEFORE_MINUTES)
coroutineScope {
message.map { (token, message) ->
val model = FailedSentSlackMessageCache(
token = token,
message = message,
isStacked = true
)
async(Dispatchers.IO) {
cacheService.sSet(
cache = Cache.getFailedSentSlackMessageCache(nextTime),
value = model
)
}
}
}
}
}
π λ§λ¬΄λ¦¬
DLQλ₯Ό μλνκ³ λ§λ 건 μλλ°, λ§λ€κ³ 보λκΉ DLQλ λΉμ·ν κ² κ°λ€.
곧 μ¬λμμ λμ€μ½λλ‘ μ΄κ΄ν΄μΌ νλλ°, κ±°κΈ°λ μ΄λ° κ±° λ§λ€μ΄λ¬μΌκ² λ€.
λ§μ½μ μ¬λ μͺ½μ μ§μμ μΈ λ¬Έμ κ° λ°μνλ€λ©΄ μν· λΈλ μ΄μ»€ λμ μ κ³ λ €ν΄ λ΄μΌκ² λ€.
'Backend' μΉ΄ν κ³ λ¦¬μ λ€λ₯Έ κΈ
<Spring> Coroutine Actor μ΄μ©ν΄μ λ¨μΌ μλ² λ½ κ΅¬ννκΈ° 2 (0) | 2024.08.22 |
---|---|
<Spring> Coroutine Actor μ΄μ©ν΄μ λ¨μΌ μλ² λ½ κ΅¬ννκΈ° (0) | 2024.08.20 |
<Spring> Webflux + Coroutine vs MVC (0) | 2024.06.12 |
<Spring> SUSUμ Coroutine (0) | 2024.05.11 |
<Spring> Webflux + Coroutine + MDC (0) | 2024.05.10 |