๐ Actor
๐ต Actor๋?
Actor ๋ชจ๋ธ์ ๋ณ๋ ฌ ํ๋ก๊ทธ๋๋ฐ์์ ์ํ๋ฅผ ๊ฐ์ง ๊ฐ์ฒด๊ฐ ๋ฉ์์ง๋ฅผ ์ฃผ๊ณ ๋ฐ์ผ๋ฉฐ ๋น๋๊ธฐ์ ์ผ๋ก ๋์ํ๋ ๊ฐ๋ ์ด๋ค.
์ฆ, ์์ฒด์ ์ธ ์ํ๋ฅผ ๊ฐ์ง๊ณ ๋ฉ์์ง๋ฅผ ์์ ํ๋ฉฐ ์ด๋ฅผ ์ฒ๋ฆฌํ๋ ๋ ผ๋ฆฌ๋ฅผ ๊ฐ์ง๊ณ ์๋ ์คํ ๋จ์์ด๋ค.
์ฝ๋ฃจํด์์๋ ์ค๋ ๋ ๊ฐ ๋๊ธฐํ๋ฅผ ์ง์ํ๊ธฐ ์ํด ์ด๋ฅผ ์ง์ํด ์ค๋ค.
actor
Launches new coroutine that is receiving messages from its mailbox channel and returns a reference to its mailbox channel as a SendChannel. The resulting object can be used to send messages to this coroutine. The scope of the coroutine contains ActorScope
kotlinlang.org
์ํ์ ์ก์ธ์ค๋ ๋จ์ผ ์ค๋ ๋๋ก ํ์ ๋๋ฉฐ, actor์์ ํต์ ์ SendChannel์ ํตํด ์ด๋ค์ง๋ค.
Channel์ ํตํด ์ ๋ฌ๋ ๋ฉ์์ง๋ ํ๋์ฉ ์์๋๋ก ์ฒ๋ฆฌ๋๊ฒ ๋๋ค.
๐ต ์ด๊ฑธ๋ก ์ด๋ป๊ฒ ๋ฝ์?
actor์ ๋จ์ผ ์ค๋ ๋๋ก ์๋ํ๋ค๋ ํน์ง์ ์ด์ฉํ๋ฉด ๋ ๊ฒ ๊ฐ์๋ค.
๋จ์ผ ์ค๋ ๋๋ผ์ ์์ ์ ํ๋์ฉ ์ฒ๋ฆฌํ๊ฒ ๋๊ณ , ์ด ๋๋ฌธ์ ๋ฝ ์์ ์ ์์ด์ ๋์์ฑ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค.
๋ํ ์ฝ๋ฃจํด์ด๋ผ์ ๋์์ฑ ์ฒ๋ฆฌ + suspend๊ฐ ๊ฐ๋ฅํ์ง ์์๊น ์๊ฐํ๋ค.
๐ ๊ทธ๋์ ์ด๋ป๊ฒ ๋ง๋ค์๋?
๐ต Pub / Sub
๋ฝ์ ๋ง๋ค๋ ค ํ ๋, ํฌ๊ฒ 2๊ฐ์ง ์ ํ์ง๊ฐ ์๋ค
- ์คํ๋ฝ
- pub / sub
์ฝ๋ฃจํด์ ์ฌ์ฉํ๊ณ ์๋ ์ํ์์ ์คํ๋ฝ์ ์ฌ์ฉํ๋ค๋ฉด, ์ฝ๋ฃจํด์ suspend function์ ์ฅ์ ์ ์ฌ์ฉํ ์ ์๋ค๊ณ ํ๋จํ๋ค.
์ด์ pub / sub์ผ๋ก ๊ตฌํํด์ผ ํ๋ค.
๊ทธ๋ ๋ค๋ฉด ๋ฝ ํ๋์ ์์ฒญํ๊ณ , ๋ฝ์ ํ๋ํ์ ๋ ๋ฝ ํ๋์ ์ด๋ป๊ฒ ์๋ฆด ์ ์์๊น?๋ผ๋ ๊ณ ๋ฏผ์ ํด๊ฒฐํด์ผ ํ๋ค.
Actor๋ SendChannel์ ํตํด ๋ฉ์์ง๋ฅผ ์ ๋ฌํ ๋ฟ, Actor์ชฝ์์ Message Sender์ชฝ์ผ๋ก ํน์ ์๋ต์ ์ค ์ ์๋ค.
๊ทธ๋ ๋ค๋ฉด ํน์ ์์ฒญ์ ํด๋นํ๋ Channel์ Actor๋ก ๋๊ฒจ์ฃผ๊ณ , Actor์์ ๋ฝ ๊ด๋ จ ์์ฒญ์ ๊ดํ ์๋ต์ ์ ๋ฌํ ์ฑ๋์ ํตํด ๋ณด๋ด์ค๋ค๋ฉด, ๋ฝ ํ๋์ ๋ํ ๋ฉ์ธ์ง๋ฅผ ๋ฐ์ ์ ์์ ๊ฒ์ด๋ผ ์๊ฐํ๋ค.
๋ํ ์ฝ๋ฃจํด์ ์ด์ฉํ๊ธฐ ๋๋ฌธ์ non-blocking ํ๊ฒ ๋ฝ์ ๋ํ ์์ฒญ์ ํ ์ ์์ ๊ฒ์ด๋ผ ์๊ฐํ๋ค.
๐ต LockActor
private enum class LockReturn {
/**
* ๋ฝ ์คํ
*/
PROCESS_LOCK,
/**
* ๋ฝ ํด์ ๋จ
*/
UNLOCK,
/**
* ๋ฑ๋ก๋ ์ฑ๋ ์ญ์
*/
DELETE_CHANNEL,
/**
* ๋ฝ ํ๊ฐ ์๋น์์
*/
NOT_EMPTY_QUEUE,
/**
* ๋ฝ ํ๊ฐ ๋น์์
*/
EMPTY_QUEUE,
;
}
private sealed class LockMsg {
/** ๋ฝ ํ๋ ์๋ */
class TryLock(val channel: SendChannel<LockReturn>) : LockMsg()
/** ๋ฝ ํด์ */
class UnLock(val channel: SendChannel<LockReturn>) : LockMsg()
/** ๋ฑ๋ก๋ ์ฑ๋ ์ง์ฐ๊ธฐ */
class DeleteChannel(val channel: SendChannel<LockReturn>) : LockMsg()
/** ํ ๋น์๋์ง ํ์ธ */
class CheckQueueEmpty(val channel: SendChannel<LockReturn>) : LockMsg()
}
@OptIn(ObsoleteCoroutinesApi::class)
private fun lockActor() = CoroutineScope(Dispatchers.IO).actor<LockMsg>(capacity = 1000) {
// queue ๋งจ ์ == ๋ฝ ์ค์
val lockQueue = LinkedList<SendChannel<LockReturn>>()
for (msg in channel) {
when (msg) {
is LockMsg.TryLock -> {
// ํ์ ์ฑ๋ ๋ฑ๋กํ๊ธฐ
lockQueue.offer(msg.channel)
// ๋ง์ฝ ๋ฐฉ๊ธ ๋ฑ๋กํ ์ฑ๋์ด ํ์ ๋งจ ์์ด๋ผ๋ฉด ๋ฐ๋ก ์คํ
if (lockQueue.peek() == msg.channel) {
msg.channel.send(LockReturn.PROCESS_LOCK)
}
}
is LockMsg.UnLock -> {
// ํ์ฌ ๋ฝ์ ํ๋ํ ์ฑ๋์ ํ์์ ์ญ์
lockQueue.poll()
// ๋ค์ ๋ฝ ํ๋ ๋์ notifyํ๊ธฐ
if (lockQueue.peek() != null) {
lockQueue.peek().send(LockReturn.PROCESS_LOCK)
}
// ๋ฝ ํด์ ๋ฐ ํ ์ญ์ ์๋ฃ ์๋ฆฌ๊ธฐ
msg.channel.send(LockReturn.UNLOCK)
}
is LockMsg.DeleteChannel -> {
if (lockQueue.peek() == msg.channel) {
// ์ญ์ ํ๋ ค๋ ์ฑ๋์ด ํ์ ๋งจ ์์ผ ๋, ํ์์ ์ญ์ ํ๊ณ ๋ค์๊บผ ์คํ
lockQueue.poll()
if (lockQueue.peek() != null) {
lockQueue.peek().send(LockReturn.PROCESS_LOCK)
}
} else {
// ์ญ์ ํ๋ ค๋ ์ฑ๋์ด ํ์ ๋งจ ์์ด ์๋ ๋, ํ์์๋ง ์ญ์
lockQueue.remove(msg.channel)
}
// ์ญ์ ์๋ฃ ์ฒ๋ฆฌ ์๋ฆฌ๊ธฐ
msg.channel.send(LockReturn.DELETE_CHANNEL)
}
is LockMsg.CheckQueueEmpty -> {
if (lockQueue.peek() == null) {
msg.channel.send(LockReturn.EMPTY_QUEUE)
} else {
msg.channel.send(LockReturn.NOT_EMPTY_QUEUE)
}
}
}
}
}
actor์ ๊ธฐ๋ฅ์ ๋ค์๊ณผ ๊ฐ๋ค
- ๋ฝ ์ค์
- ๋ฝ ํด์
- ๋ฑ๋ก๋ ์ฑ๋ ์ญ์
- ๋ฑ๋ก๋ ์ฑ๋์ด ์๋์ง ํ์ธ
actor๋ ๋ฝ ํ๋ ์์ฒญ์ ๋ํ ๊ด๋ฆฌ์ ํ์ฌ ๋ฝ์ ํ๋ํ ์์ฒญ์๊ฒ ๋ฝ ํ๋ ๋ฉ์์ง๋ฅผ ๋ณด๋ด์ฃผ๋ ์ญํ ์ ํ๋ค.
subscriber์ ์ฑ๋์ ํ์ ๋ฃ์ด๋๊ณ , ๋ฝ ํ๋์ ์ ๋ณด๋ฅผ ์ฑ๋์ ํตํด ๋ฉ์ธ์ง๋ฅผ ์ ๋ฌํ๋ ์ผ์ ํ๋ค.
Lock ํ๋์ ์์ฒญํ๋ฉด ์ฑ๋์ queue์ ๋ฃ์ด๋๊ณ , Lock ํด์ ๋ฅผ ์์ฒญํ๋ฉด queue์์ ๋ณธ์ธ ์ฑ๋์ ์ ๊ฑฐํ ํ ๋ค๋ฅธ ์ฑ๋์ ๋ฝ ํ๋ ๋ฉ์ธ์ง๋ฅผ ์ ํ๋ค.
๋ํ ์๋ฌ ์ฒ๋ฆฌ์ ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ๋ฅผ ํ์ ์์นํ๋ ์ฑ๋ ์ ๊ฑฐ ๋ฐ ํ์ ๋ฑ๋ก๋ ์ฑ๋์ด ์๋์ง ํ์ธํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค.
'๋ฝ์ ํ๋ํ๋ค'์ ๊ธฐ์ค์ queue์ ๋งจ ์์ ๋ฌด์์ด ์๋๊ฐ์ด๋ค.
queue ๋งจ ์์ ์์นํ ์ฑ๋์ด ๋ฝ์ ์์ ํ๋ค๊ณ ์ฌ๊ธด๋ค.
lock์ ํ๋ํ๋ ค ํ ๋ queue์ ์ฑ๋์ ์ถ๊ฐํ ํ ๋๊ธฐํ๋ค.
๋ํ lock์ ํด์ ํ ๋ ๋งจ ์์ ์์นํ ์ฑ๋, ์ฆ, ํ์ฌ ๋ฝ์ ์์ ํ ์ฑ๋์ ์ ๊ฑฐํ๊ณ ๋ค์ ๋ฝ ํ๋ ๋์์ ๋ฉ์์ง๋ฅผ ๋ณด๋ด๊ฒ ๋๋ค.
์ด๋ฌํ ๋ฐฉ์์ผ๋ก ๋ฝ์ ํ๋๊ณผ ํด์ ๊ฐ ์ด๋ค์ง๋ค.
์ฐ์ actor์ ๋ฝ ์๋น์ค ๊ฐ ํต์ ์ ์ฑ๋์ ๊ธฐ๋ฐ์ผ๋ก ์งํ๋๋ค.
lockActor์ ๊ธฐ๋ฅ์ ํ๋ฆฌ๋ฏธํฐ์ channel์ ํฌํจํ๋ค.
์ด channel์ด actor์์ ๋ฝ ์๋น์ค๋ก ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ ์ญํ ์ ํ๋ค.
๊ทธ๋์ actor์์ ์์ฒญ ์ฒ๋ฆฌ๋ ๋ชจ๋ ์๋์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์งํ๋๋ค.
actor.send(LockMsg.TryLock(channel))
channel.receive()
๐ต ๋ฝ ์ฒ๋ฆฌ
@Component
class SuspendableLockManager : LockManager {
companion object {
private const val WAIT_TIME = 3000L
private const val LEASE_TIME = 3000L
}
private val actorMap = ConcurrentHashMap<String, SendChannel<LockMsg>>()
override suspend fun <T> lock(key: String, block: suspend () -> T): T {
// lock ๊ด๋ จ ๋ฆฌํด ๋ฐ์ ์ฑ๋
Channel<LockReturn>().run {
val channel = this
// ๋ฝ ์ค์
val actor = tryLock(key, channel)
try {
// ๋ก์ง ์คํ
return withTimeout(LEASE_TIME) {
block()
}
} catch (e: TimeoutCancellationException) {
// ๋ฝ ๋ณด์ ์๊ฐ ์๋ฌ ์ฒ๋ฆฌ
throw FailToExecuteException(ErrorCode.LOCK_TIMEOUT_ERROR)
} catch (e: Exception) {
// ๋๋จธ์ง ์๋ฌ ์ฒ๋ฆฌ
throw e
} finally {
// ๋ฝ ํด์
releaseLock(actor, channel)
// ํ๊ฐ ๋น ์กํฐ ์ญ์
deleteEmptyQueueActor(channel, key)
logger.info { actorMap }
}
}
}
private suspend fun tryLock(key: String, channel: Channel<LockReturn>): SendChannel<LockMsg> {
val actor = actorMap.compute(key) { _, value ->
val actor = value ?: lockActor()
runBlocking(Dispatchers.Unconfined) {
actor.send(LockMsg.TryLock(channel))
}
actor
} ?: throw FailToExecuteException(ErrorCode.FAIL_TO_GET_LOCK)
try {
withTimeout(WAIT_TIME) {
channel.receive()
}
} catch (e: TimeoutCancellationException) {
// ๋ฝ ํ๋ ์๊ฐ ์๋ฌ ์ฒ๋ฆฌ
throw FailToExecuteException(ErrorCode.ACQUIRE_LOCK_TIMEOUT)
} catch (e: Exception) {
// ์์ ์ฑ๋ ์ง์ฐ๊ธฐ
actor.send(LockMsg.DeleteChannel(channel))
channel.receive()
throw FailToExecuteException(ErrorCode.FAIL_TO_EXECUTE_LOCK)
}
return actor
}
private suspend fun releaseLock(actor: SendChannel<LockMsg>, channel: Channel<LockReturn>) {
actor.send(LockMsg.UnLock(channel))
channel.receive()
}
private suspend fun deleteEmptyQueueActor(channel: Channel<LockReturn>, key: String) {
actorMap.computeIfPresent(key) { _, value ->
val rtn = runBlocking(Dispatchers.Unconfined) {
value.send(LockMsg.CheckQueueEmpty(channel))
channel.receive()
}
if (rtn == LockReturn.EMPTY_QUEUE) {
null
} else {
value
}
}
}
}
์ฝ๋๋ ํฌ๊ฒ 4๊ฐ ๋ถ๋ถ์ผ๋ก ๋๋๋ค.
- ๋ฝ ํ๋
- ๋ฝ ํด์
- actor ์ญ์
์์ธํ ์ค๋ช ์ ํ์ ํ๊ฒ ๋ค.
๐ต ๋ฝ ํ๋
๋ฝ์ ๋ฒ์๋ฅผ ์ต์ํํ๊ณ ์ถ์๋ค.
๊ทธ๋์ ํค๋ฅผ ๊ธฐ์ค์ผ๋ก actor๋ฅผ ์์ฑํด์ ์ฌ์ฉํ๋ค.
์ด๋, actorMap์ ์กฐํ ๋ฐ ์ ๋ฐ์ดํธ์ ์์ด์ ๋์์ฑ์ ๋ณด์ฅํ ํ์๊ฐ ์์๋ค.
๊ทธ๋์ ConcurrentHashMap์ compute๋ฅผ ์ด์ฉํ๋ค.
ํค๊ฐ ๋ค๋ฅด๋ค๋ฉด ๋ค๋ฅธ actor๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ์๋ก ๋ค๋ฅธ ํค๋ฅผ ์ด์ฉํ ๋ฝ์ด ์๋ก์๊ฒ ์ํฅ์ ์ค ์ ์๊ฒ ๋ผ์, ๋ฝ์ ๋ฒ์๋ฅผ ํด๋น ํค๋ฅผ ์ด์ฉํ๋ ๋ก์ง์ผ๋ก ํ์ ํ ์ ์์๋ค.
๋ํ compute ์ฐ์ฐ์์ actor์ lock ์์ฒญ ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋๋ก ํ๋ค.
๋ฌผ๋ก ConcurrentHashMap์์ actor๋ฅผ ๊ฐ์ ธ์ฌ ๋, compute๋ฅผ ํตํ ์ฐ์ฐ์ ํ๊ธฐ ๋๋ฌธ์ ์๋ก ๊ฐ ์ํฅ์ด ์์ง๋ง, compute ๋ด๋ถ ๋ก์ง์์ ๋ฝ ํ๋์ ๋ํด ๋๊ธฐํ์ง ์๊ธฐ ๋๋ฌธ์, ์ฐ์ฐ ์๋์ ์์ด์ ํฐ ์ํฅ์ ์์ ๊ฑฐ๋ผ ์๊ฐํ๋ค.
์ด ์์ฒญ์ ๋ฐ์ actor๋ ๋ฝ ํ๋ ๊ด๋ จ ์ฑ๋์ queue์ ๋ฃ๋๋ค.
์ด ๊ณผ์ ์ด ๋ค ์ํ๋๋๋ผ๋ channel.receive()๊ฐ ์ฒ๋ฆฌ๋์ง ์๊ธฐ ๋๋ฌธ์, ๋ฝ์ ์์ฒญํ ์ฝ๋ฃจํด์ suspend ๋ ์ํ์ด๋ค.
๋ง์ฝ ๋ค๋ฅธ ๋ฝ์ด unlock ํ๋ฉด์ ์ด ์ฑ๋์ ๋ฝ ํ๋ํ๋ค๋ฉด, actor์์ ์ฑ๋์ ํตํด ๋ฉ์์ง๋ฅผ ๋ณด๋ด๊ฒ ๋๊ณ , ์ด๋ ํด๋น ์ฝ๋ฃจํด์ด ๋ค์ ์คํ๋จ์ ์๋ฏธํ๋ค.
๊ทธ๋ฌ๋ฏ๋ก ๋ฝ์ ์ป์ ์ํ์์ ์ฃผ์ด์ง ๋ก์ง์ ์งํํ ์ ์๊ฒ ๋๋ค.
๋ฝ ํ๋ ๊ณผ์ ์์ ์๋ฌ๊ฐ ๋ฐ์ํ๊ฑฐ๋, ๋ฝ ํ๋์ ์ค๋ ์๊ฐ์ด ์์๋ ๊ฒฝ์ฐ, ์๋ฌ์ฒ๋ฆฌ๊ฐ ํ์ํ๋ค.
๋จผ์ ๋ฝ ํ๋์ ์ค๋ ์๊ฐ์ด ์์๋๋ ๊ฒฝ์ฐ๋ withTimeout()์ผ๋ก ์ฒ๋ฆฌํ๋ค.
๋ํ ํ๋ ๊ณผ์ ์์์ ์๋ฌ๋ actor์ queue์์ ์ฑ๋์ ์ ๊ฑฐํ๋ ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌํ๋ค.
์ด๋ฅผ ํตํด actor๊ฐ ์๋ฌ๊ฐ ๋ฐ์ํ ์์ฒญ์ ๊ดํด ์ฒ๋ฆฌํ์ง ์๋๋ก ํ๋ค.
๐ต ๋ฝ ํด์
๋ฝ ํด์ ๋ ๋น๊ต์ ๊ฐ๋จํ๋ค.
๋ฝ์ ํ์ฌ ๋ณด์ ํ ๋์ ์ฆ, queue์ ๋งจ ์ ์ฑ๋์ ์ ๊ฑฐํ ํ, queue์ ๋งจ ์์ ์์นํ ์ฑ๋์ ๋ฝ ํ๋ ๋ฉ์์ง๋ฅผ ๋ณด๋ธ๋ค.
๐ต Actor ์ญ์
์ด ๋ถ๋ถ์ด ์์ด๋ ์ ์๋ํ ๊ฒ์ด๋ค.
ํ์ง๋ง ์ฐ๋ฆฌ๋ ์๋ฒ ๋ฉ๋ชจ๋ฆฌ ๋ํ ๊ณ ๋ คํด์ผ ํ๋ค.
์ฒ๋ฆฌํด์ผ ํ๋ ์์ฒญ์ด ์๋๋ผ๋ ํด๋น ์์ฒญ์ ๋ํ ํค์ ์กํฐ๊ฐ ์์ฑ๋์ด ์๋ค๋ฉด, ์ด๋ ์ญ์ ๋์ง ์๊ณ ๋จ์์ ๋ฉ๋ชจ๋ฆฌ๋ง ์ฐจ์งํ๊ฒ ๋๋ค.
๊ทธ๋์ OOM์ ๋ง๊ธฐ ์ํด ์ฌ์ฉํ์ง ์๋ ์กํฐ๋ฅผ ์ง์์ผ ํ๋ค.
์ด๋ actor๋ฅผ ์กฐํํ๋ ๋ถ๋ถ๊ณผ์ ๋ฝ๋ ๊ณ ๋ คํด์ผ ํ๋ค.
actor๋ฅผ ์ญ์ ํ๋ ๋ก์ง๊ณผ actor๋ฅผ ์กฐํ, ํ์ ์ฑ๋์ ๋ฑ๋กํ๋ ๋ก์ง์ด ๋์์ ์๋ํ๋ค๋ฉด, ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒ์ด ๋ถ๋ช ํ๋ค.
๊ทธ๋์ ์ด ๋ ์์ ์ ๋ํ ๋์์ฑ์ ๊ณ ๋ คํ๊ธฐ ์ํด ConcurrentHashmap์ compute๋ฅผ ์ด์ฉํ๋ค.
์ด๋ฅผ ํตํด ์ฌ์ฉํ์ง ์๋ actor๋ฅผ ์ง์์ค์ผ๋ก์จ ๋ถํ์ํ ๋ฉ๋ชจ๋ฆฌ ๋ญ๋น๋ฅผ ๋ฐฉ์งํ๋ค.
๐ต ์ฌ์ฉ
suspend fun vote(user: AuthUser, id: Long, request: CreateVoteHistoryRequest) {
lockManager.lock(LockKey.getVoteKey(id)) {
when (request.isCancel) {
true -> cancelVote(user.uid, id, request.optionId)
false -> castVote(user.uid, id, request.optionId)
}
}
}
lock ํจ์ ํ๋ผ๋ฏธํฐ๋ก ์๋น์ค ๋ก์ง์ ๋๊ฒจ์ฃผ๋ฉด, ํด๋น ๋ก์ง์ ๊ดํด ๋ฝ์ ์ ์ฉํ์ฌ ๋์ํ๊ฒ ๋๋ค.
ํค ์์ฑ์๋ LockKey๋ผ๋ ํด๋์ค๋ฅผ ๋ฐ๋ก ๋ง๋ค์ด์ ์ฒ๋ฆฌํ๋ค.
๐ ๋ง๋ฌด๋ฆฌ
์ผ๋จ ์ฝ๋ ๋จธ์ง๋ ๋์์ผ๋, ๊ฐ์ ํด์ผ ํ ๋ถ๋ถ์ ๋จ์๋ค ์๊ฐํ๋ค.
์์ง์ ์ด๋์ง ๋ชจ๋ฅด๊ฒ ์ง๋ง, ๋ฐ๋๋ฝ์ด ๋ฐ์ํ ๊ฒ ๊ฐ์ ๋ถ์ํจ(?)์ด ์๋ค.
์ผ๋จ ๋ค ์ ์ฉํ์ง ์์๊ณ , ์ฌ์ฉํ๋ฉด์ ์์ ์ฑ์ด ๊ฒ์ฆ๋๋ค๋ฉด, ๋ฝ์ด ํ์ํ ๊ณณ์ ์ ์ฐจ ์ ์ฉํด ๋๊ฐ ์๊ฐ์ด๋ค.
'Backend' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
<Spring> Coroutine Actor ์ด์ฉํด์ ๋จ์ผ ์๋ฒ ๋ฝ ๊ตฌํํ๊ธฐ 2 (0) | 2024.08.22 |
---|---|
<Spring> Slack ๋ฉ์ธ์ง ์ ์ก ์๋ฌ ์ฒ๋ฆฌ (0) | 2024.08.04 |
<Spring> Webflux + Coroutine vs MVC (0) | 2024.06.12 |
<Spring> SUSU์ Coroutine (0) | 2024.05.11 |
<Spring> Webflux + Coroutine + MDC (0) | 2024.05.10 |