Skip to main content

About Sharding and MSA

Β· 13 min read
Ryukato
BackEnd Software Developer

🧭 μ„œλ¬Έβ€‹

λ§Žμ€ νŒ€κ³Ό 쑰직이 λ°μ΄ν„°λ² μ΄μŠ€μ˜ μ„±λŠ₯ 병λͺ©μ΄λ‚˜ λŒ€μš©λŸ‰ 처리 μ΄μŠˆμ— μ§λ©΄ν–ˆμ„ λ•Œ, κ°€μž₯ λ¨Όμ € λ– μ˜¬λ¦¬λŠ” ν•΄κ²°μ±… 쀑 ν•˜λ‚˜κ°€ λ°”λ‘œ **샀딩(Sharding)**μž…λ‹ˆλ‹€.
샀딩은 λΆ„λͺ…νžˆ κ°•λ ₯ν•œ μˆ˜ν‰ ν™•μž₯ μ „λž΅μ΄μ§€λ§Œ, 그만큼 λ„μž…κ³Ό μš΄μ˜μ— λ”°λ₯΄λŠ” λ³΅μž‘λ„μ™€ μœ„ν—˜μ„±λ„ ν½λ‹ˆλ‹€.

특히, 샀딩은 λ‹¨μˆœν•œ 기술적 κΈ°λŠ₯이 μ•„λ‹ˆλΌ 전체 μ‹œμŠ€ν…œ μ•„ν‚€ν…μ²˜μ— 영ν–₯을 μ£ΌλŠ” ꡬ쑰적 결정이기 λ•Œλ¬Έμ—,
λ„μž…μ„ μ„œλ‘λ₯΄κΈ°λ³΄λ‹€λŠ” 샀딩 없이 ν•΄κ²°ν•  수 μžˆλŠ” λ°©μ•ˆλ“€μ„ λ¨Όμ € κ³ λ €ν•˜κ³ ,
샀딩이 ν•„μš”ν•œ μ‹œμ κ³Ό λ²”μœ„λ₯Ό λͺ…ν™•νžˆ νŒλ‹¨ν•œ 후에 μ μš©ν•˜λŠ” 것이 λ°”λžŒμ§ν•©λ‹ˆλ‹€.

이 κΈ€μ—μ„œλŠ” 샀딩 λ„μž… μ „ κ³ λ €ν•  수 μžˆλŠ” 단계,
μƒ€λ”©μ˜ 적용 원칙,
MSA 및 vertical slicing과의 관계,
그리고 ꢁ극적으둜 λ‚˜λ…Έ μ„œλΉ„μŠ€ μ•„ν‚€ν…μ²˜μ™€ BFF ꡬ쑰둜의 ν™•μž₯κΉŒμ§€ ν­λ„“κ²Œ λ‹€λ€„λ΄…λ‹ˆλ‹€.


βœ… 샀딩 λ„μž… μ „ κ³ λ € 사항​

1. MMM ꡬ성​

κ°€μž₯ λ¨Όμ € κ³ λ €ν•  수 μžˆλŠ” ν™•μž₯ 방식은 Master-Replica(MMM) κ΅¬μ„±μž…λ‹ˆλ‹€.
ν•˜λ‚˜μ˜ Master DB에 μ—¬λŸ¬ 개의 Read Replicaλ₯Ό λΆ™μ—¬μ„œ 읽기 λΆ€ν•˜λ₯Ό λΆ„μ‚°ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

  • μ“°κΈ°λŠ” 였직 Masterμ—μ„œλ§Œ λ°œμƒ
  • μ½κΈ°λŠ” Replicaμ—μ„œ 처리
  • Spring Bootμ—μ„œλŠ” AbstractRoutingDataSource 등을 톡해 read/write 뢄리λ₯Ό μ‰½κ²Œ κ΅¬ν˜„ κ°€λŠ₯
  • Replica Lag κ³ λ € ν•„μš”

이 κ΅¬μ‘°λ§ŒμœΌλ‘œλ„ λŒ€λΆ€λΆ„μ˜ μ„œλΉ„μŠ€λŠ” μ΄ˆλ‹Ή 수천 QPS μˆ˜μ€€μ˜ 처리 μ„±λŠ₯을 확보할 수 μžˆμŠ΅λ‹ˆλ‹€.


2. ν…Œμ΄λΈ” νŒŒν‹°μ…”λ‹β€‹

DBMS μ°¨μ›μ—μ„œ μ§€μ›ν•˜λŠ” μˆ˜ν‰ νŒŒν‹°μ…”λ‹λ„ 쒋은 μ „λž΅μž…λ‹ˆλ‹€.
예λ₯Ό λ“€μ–΄ PostgreSQL의 table partition, MySQL의 partition by range 등을 ν™œμš©ν•΄ λŒ€μš©λŸ‰ ν…Œμ΄λΈ”μ„ λ‚˜λˆŒ 수 μžˆμŠ΅λ‹ˆλ‹€.

  • λ‚ μ§œ κΈ°μ€€ νŒŒν‹°μ…˜ (예: 월별 μ£Όλ¬Έ ν…Œμ΄λΈ”)
  • μ§€μ—­/κ΅­κ°€ μ½”λ“œ κΈ°μ€€ νŒŒν‹°μ…˜
  • 인덱슀 및 I/O μ΅œμ ν™”μ— 효과적

단점은:

  • νŒŒν‹°μ…˜ κ°„ 쑰인이 어렀움
  • νŒŒν‹°μ…˜ 관리 μ •μ±… ν•„μš” (drop/recreate)

νŒŒν‹°μ…”λ‹λ§ŒμœΌλ‘œλ„ μ„±λŠ₯ 병λͺ©μ΄ μ™„ν™”λ˜λŠ” κ²½μš°κ°€ 많으며, 샀딩 이전에 λ°˜λ“œμ‹œ κ³ λ €ν•΄μ•Ό ν•  λ‹¨κ³„μž…λ‹ˆλ‹€.


βœ… 샀딩 ꡬ쑰의 핡심​

μƒ€λ”©μ˜ κΈ°λ³Έ κ°œλ…μ€ "ν•˜λ‚˜μ˜ ν…Œμ΄λΈ”μ„ μ—¬λŸ¬ DB μΈμŠ€ν„΄μŠ€λ‘œ λ‚˜λˆ„μ–΄ μ €μž₯"ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.

1. μƒ€λ“œ ν‚€ 선정​

μƒ€λ“œ ν‚€λŠ” 데이터λ₯Ό μ–΄λ–€ κΈ°μ€€μœΌλ‘œ λ‚˜λˆŒμ§€λ₯Ό κ²°μ •ν•˜λŠ” ν•΅μ‹¬μž…λ‹ˆλ‹€.
쒋은 μƒ€λ“œ ν‚€λŠ” λ‹€μŒ 쑰건을 λ§Œμ‘±ν•΄μ•Ό ν•©λ‹ˆλ‹€:

  • 거의 λͺ¨λ“  쿼리에 ν¬ν•¨λ˜μ–΄μ•Ό 함
  • κ· λ“±ν•˜κ²Œ λΆ„ν¬λ˜μ–΄μ•Ό 함
  • λ³€κ²½λ˜μ§€ μ•Šμ•„μ•Ό 함

λŒ€ν‘œμ μΈ ν‚€: user_id, tenant_id, contract_id, region_id


2. μƒ€λ“œ κ²°μ • 방식​

  • Mod 방식: shardId = user_id % N
  • Hash Slot 방식: slot = hash(user_id) % 1024 β†’ slotToShardMap
  • Range 방식: user_id κ°’μ˜ λ²”μœ„λ‘œ λΆ„κΈ°

μƒ€λ“œ 결정은 λ³€ν•˜μ§€ μ•Šλ„λ‘ κ³ μ •λœ μ•Œκ³ λ¦¬μ¦˜ λ˜λŠ” ν…Œμ΄λΈ” 기반으둜 관리해야 함


3. λΌμš°νŒ… κ΅¬ν˜„ 방식​

  • μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 레벨 λΌμš°νŒ…: user_id 기반으둜 직접 컀λ„₯μ…˜ 선택
  • 미듀웨어 기반: ProxySQL, Vitess λ“±μ—μ„œ 쿼리λ₯Ό ν•΄μ„ν•˜κ³  μžλ™ λΆ„κΈ°
  • μ‘°ν•©ν˜•: λΌμš°νŒ… ν…Œμ΄λΈ”μ„ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ λ™μ μœΌλ‘œ λ‘œλ”© (slot β†’ shard λ§΅ν•‘)

βœ… 샀딩이 ν•„μš”ν•œ μ‹œμ β€‹

수치 기쀀​

ν•­λͺ©κΈ°μ€€
QPS5,000~10,000 이상
TPSμ΄ˆλ‹Ή μ“°κΈ° 1,000건 이상
ν…Œμ΄λΈ” μ‚¬μ΄μ¦ˆλ‹¨μΌ ν…Œμ΄λΈ” 100~200GB 이상
컀λ„₯μ…˜ 수500~1000 이상 μƒμ‹œ μœ μ§€
인덱슀 λ©”λͺ¨λ¦¬ 적재 μ‹€νŒ¨νŽ˜μ΄μ§€ μΊμ‹œ 히트율 μ €ν•˜

샀딩 ꡬ성 μ˜ˆμ‹œμ™€ Spring 기반 Kotlin μ½”λ“œβ€‹

πŸ“Œ 샀딩 ꡬ성 μ˜ˆμ‹œ (2개 μƒ€λ“œ)​

  • κΈ°μ€€: user_id % 2
  • μƒ€λ“œ:
    • shard1: user_id 짝수
    • shard2: user_id ν™€μˆ˜
        +--------------------+
| Application API |
+--------------------+
|
ShardRouter
|
+-------------+-------------+
| |
shard1(DBClient) shard2(DBClient)

βœ… Kotlin + Coroutine 기반 샀딩 λΌμš°νŒ… μ˜ˆμ‹œβ€‹

πŸ”Ή Slot 기반 λΌμš°ν„°β€‹

data class ShardInfo(val id: String, val client: DatabaseClient)

class ShardRouter(private val shards: List<ShardInfo>) {
fun route(userId: Long): DatabaseClient {
val shardIndex = (userId % shards.size).toInt()
return shards[shardIndex].client
}
}

πŸ”Ή μ„œλΉ„μŠ€μ—μ„œ λΌμš°νŒ… μ μš©β€‹

class UserService(private val router: ShardRouter) {
suspend fun getUser(userId: Long): User? {
val client = router.route(userId)
return client.sql("SELECT * FROM users WHERE user_id = :id")
.bind("id", userId)
.map { row -> User.from(row) }
.one()
.awaitFirstOrNull()
}
}

πŸ”Ή CoroutineContext 기반 전달 μ˜ˆμ‹œβ€‹

data class ShardContext(val userId: Long) : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<ShardContext>
override val key = Key
}

suspend fun <T> withShard(userId: Long, block: suspend () -> T): T {
return withContext(ShardContext(userId)) {
block()
}
}

β†’ 이후 ShardRouterμ—μ„œ coroutineContext[ShardContext]?.userId둜 μΆ”μΆœ κ°€λŠ₯


βœ… Spring + AbstractRoutingDataSource (JDBC 방식)​

object ShardContextHolder {
private val context = ThreadLocal<Long?>()
fun setUserId(id: Long) = context.set(id)
fun getUserId(): Long? = context.get()
fun clear() = context.remove()
}
class ShardRoutingDataSource(...) : AbstractRoutingDataSource() {
override fun determineCurrentLookupKey(): Any? {
val userId = ShardContextHolder.getUserId() ?: return "default"
return "shard${userId % 2}"
}
}

β†’ AOP둜 userId 값을 사전에 μ„ΈνŒ…ν•˜μ—¬ @Transactional μ‚¬μš© κ°€λŠ₯ν•˜κ²Œ 처리


β˜‘οΈ 결둠​

  • 샀딩 λΌμš°νŒ…μ€ Kotlin Coroutine ν™˜κ²½μ—μ„  CoroutineContextλ₯Ό, Spring JDBC ν™˜κ²½μ—μ„  AbstractRoutingDataSourceλ₯Ό 기반으둜 섀계 κ°€λŠ₯
  • 샀딩 λΌμš°ν„°λŠ” κ°€λŠ₯ν•œ λ²”μš© ꡬ쑰둜 μΆ”μƒν™”ν•˜μ—¬ μ„œλΉ„μŠ€μ— μœ μ—°ν•˜κ²Œ μ£Όμž…

βœ… 샀딩 ν™˜κ²½μ—μ„œ νŠΈλžœμž­μ…˜β€‹

πŸ”Ή 샀딩 ν™˜κ²½μ—μ„œ νŠΈλžœμž­μ…˜ 처리 μ˜ˆμ œβ€‹

샀딩 ν™˜κ²½μ—μ„œλŠ” ν•˜λ‚˜μ˜ νŠΈλžœμž­μ…˜μ΄ 두 개 μ΄μƒμ˜ μƒ€λ“œμ— 걸쳐 λ°œμƒν•˜μ§€ μ•Šλ„λ‘ μ„€κ³„ν•˜λŠ” 것이 μ΄μƒμ μž…λ‹ˆλ‹€.
ν•˜μ§€λ§Œ ν•„μš”ν•œ 경우, μƒ€λ“œ λ‹¨μœ„ νŠΈλžœμž­μ…˜μ„ κ°œλ³„ μˆ˜ν–‰ν•˜κ±°λ‚˜, 보상 νŠΈλžœμž­μ…˜ ꡬ쑰둜 λŒ€μ²΄ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

βœ… Kotlin Coroutine 기반 μƒ€λ“œ νŠΈλžœμž­μ…˜ μ˜ˆμ‹œβ€‹

suspend fun createOrder(userId: Long, order: Order): Boolean {
val client = router.route(userId)

return client.inTransaction { tx ->
tx.sql("INSERT INTO orders (user_id, order_id, amount) VALUES (:uid, :oid, :amt)")
.bind("uid", userId)
.bind("oid", order.id)
.bind("amt", order.amount)
.then()
.awaitFirstOrNull()

tx.sql("UPDATE users SET order_count = order_count + 1 WHERE user_id = :uid")
.bind("uid", userId)
.then()
.awaitFirstOrNull()

true
}
}

μœ„ μ½”λ“œλŠ” ν•˜λ‚˜μ˜ μƒ€λ“œ λ‚΄μ—μ„œλ§Œ μ‹€ν–‰λ˜λŠ” 둜컬 νŠΈλžœμž­μ…˜ κ΅¬μ‘°μž…λ‹ˆλ‹€.


βœ… 두 μƒ€λ“œμ— 걸친 처리: 보상 νŠΈλžœμž­μ…˜ νŒ¨ν„΄ μ˜ˆμ‹œβ€‹

suspend fun transferBalance(senderId: Long, receiverId: Long, amount: Long): Boolean {
val senderClient = router.route(senderId)
val receiverClient = router.route(receiverId)

try {
senderClient.inTransaction { tx ->
tx.sql("UPDATE accounts SET balance = balance - :amt WHERE user_id = :sid")
.bind("amt", amount)
.bind("sid", senderId)
.then()
.awaitFirstOrNull()
}

receiverClient.inTransaction { tx ->
tx.sql("UPDATE accounts SET balance = balance + :amt WHERE user_id = :rid")
.bind("amt", amount)
.bind("rid", receiverId)
.then()
.awaitFirstOrNull()
}

return true
} catch (e: Exception) {
// 보상 둜직 μˆ˜ν–‰ (예: senderμ—κ²Œ κΈˆμ•‘ 볡원 μ‹œλ„)
senderClient.sql("UPDATE accounts SET balance = balance + :amt WHERE user_id = :sid")
.bind("amt", amount)
.bind("sid", senderId)
.then()
.awaitFirstOrNull()
return false
}
}

λΆ„μ‚° νŠΈλžœμž­μ…˜μ΄ ν•„μš”ν•œ 경우, XAλ³΄λ‹€λŠ” 이런 Try-Fail-Reverse λ°©μ‹μ˜ 보상 μ²˜λ¦¬κ°€ μ„ ν˜Έλ©λ‹ˆλ‹€.

μƒ€λ”©λ˜λ©΄ 둜컬 νŠΈλžœμž­μ…˜μ΄ μƒ€λ“œ λ‹¨μœ„λ‘œ λΆ„λ¦¬λ©λ‹ˆλ‹€.
즉, ν•˜λ‚˜μ˜ μ„œλΉ„μŠ€ λ‚΄μ—μ„œ 두 μƒ€λ“œμ— λ™μ‹œμ— μ“°κΈ°λ₯Ό μˆ˜ν–‰ν•  경우, 단일 νŠΈλžœμž­μ…˜μœΌλ‘œ 묢을 수 μ—†μŠ΅λ‹ˆλ‹€.

ν•΄κ²° λ°©μ•ˆβ€‹

  • JTA / 2PC: XA νŠΈλžœμž­μ…˜ 기반, 무겁고 느림
  • Saga νŒ¨ν„΄: 보상 νŠΈλžœμž­μ…˜. μ‹€νŒ¨ μ‹œ μ·¨μ†Œ 둜직 μˆ˜ν–‰
  • TCC νŒ¨ν„΄: Try β†’ Confirm/Cancel λ‹¨κ³„λ‘œ 처리

보톡은 μƒ€λ“œ κ°„ νŠΈλžœμž­μ…˜μ΄ ν•„μš” 없도둝 μ„€κ³„ν•˜κ±°λ‚˜, Saga둜 μ •ν•©μ„± 보μž₯을 ν•©λ‹ˆλ‹€.


βœ… MSA와 μƒ€λ”©μ˜ 관계​

MSA의 핡심은 **μ„œλΉ„μŠ€ λ‹¨μœ„μ˜ μ±…μž„ 뢄리(Bounded Context)**μž…λ‹ˆλ‹€.
λ”°λΌμ„œ, ν•˜λ‚˜μ˜ 큰 DBλ₯Ό μͺΌκ°œμ„œ μƒ€λ”©ν•˜λŠ” 것이 μ•„λ‹ˆλΌ,
μ„œλΉ„μŠ€λ§ˆλ‹€ 독립적인 DBλ₯Ό κ°€μ§€κ²Œ ν•˜κ³ , 각 μ„œλΉ„μŠ€μ˜ νŠΉμ„±μ— 따라 샀딩 적용 μ—¬λΆ€λ₯Ό λ…λ¦½μ μœΌλ‘œ νŒλ‹¨ν•˜λŠ” 것이 μ›μΉ™μž…λ‹ˆλ‹€.

μ˜ˆμ‹œβ€‹

μ„œλΉ„μŠ€λ°μ΄ν„° νŠΉμ„±μƒ€λ”© μ—¬λΆ€
μ£Όλ¬Έ μ„œλΉ„μŠ€μ“°κΈ° λΆ€ν•˜ λ†’μŒβœ… 샀딩
μœ μ € μ„œλΉ„μŠ€μ½κΈ° μœ„μ£ΌβŒ 단일 λ…Έλ“œλ‘œ μΆ©λΆ„
결제 μ„œλΉ„μŠ€κ°•ν•œ μ •ν•©μ„± μš”κ΅¬βš  νŒŒν‹°μ…”λ‹ λ˜λŠ” NoSQL κ³ λ €

βœ… Vertical Slicing + DB 연동 μ „λž΅β€‹

μ„œλΉ„μŠ€λ₯Ό 도메인 λ‹¨μœ„λ‘œ μ„ΈλΆ„ν™”(vertical slice)ν•˜λ©΄,
각 sliceλ§ˆλ‹€ μƒ€λ”©μš© DB ν΄λΌμ΄μ–ΈνŠΈ, 단일 DB ν΄λΌμ΄μ–ΈνŠΈλ₯Ό μœ μ—°ν•˜κ²Œ 선택할 수 μžˆμŠ΅λ‹ˆλ‹€.

  • base-db-client β†’ 단일 DB용
  • sharded-db-client β†’ μƒ€λ”©μš© λΌμš°νŒ… 포함
  • 곡톡 μΈν„°νŽ˜μ΄μŠ€ UserRepositoryλ₯Ό μ‚¬μš©ν•˜μ—¬ DI둜 λΆ„κΈ°

이 방식은 샀딩이 ν•„μš”ν•œ μ„œλΉ„μŠ€λ§Œ λ³΅μž‘ν•œ ꡬ쑰λ₯Ό 갖도둝 λ§Œλ“€ 수 있으며,
샀딩이 ν•„μš” μ—†λŠ” μ„œλΉ„μŠ€λŠ” λ‹¨μˆœν•œ ꡬ쑰λ₯Ό μœ μ§€ν•  수 μžˆμŠ΅λ‹ˆλ‹€.


βœ… MSAλ₯Ό λ„˜μ–΄μ„œ: Nano Service + BFF ꡬ쑰​

MSAκ°€ 일정 규λͺ¨ 이상 되면,

  • μ„œλΉ„μŠ€ 수 증가 β†’ 운영 λ³΅μž‘λ„ 폭발
  • 데이터 μ‘°ν•© 둜직 λΆ„μ‚° β†’ UI APIκ°€ 느렀짐

이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•œ λ°©μ•ˆ 쀑 ν•˜λ‚˜λŠ” Nano Service + BFF(Backend for Frontend)* κ΅¬μ‘°μž…λ‹ˆλ‹€.

ꡬ쑰​

[Nano Service 1]    [Nano Service 2]    ...
\ | /
[BFF Layer]
|
[Web/App UI]
  • 각 Nano ServiceλŠ” μ›μžμ μΈ μ±…μž„λ§Œ λ‹΄λ‹Ή (예: user-profile, user-preference)
  • BFFκ°€ λͺ¨λ“  μ„œλΉ„μŠ€ ν˜ΈμΆœμ„ μ‘°ν•©ν•˜μ—¬ μ΅œμ ν™”λœ 응닡 제곡
  • μ„œλΉ„μŠ€λ³„ 샀딩 μ—¬λΆ€λŠ” 숨겨짐 β†’ μ‘°ν•© μ±…μž„λ§Œ 뢄리

βœ… μ΅œμ’… 정리​

  • 샀딩은 무쑰건 λ„μž…ν•΄μ•Ό ν•˜λŠ” κΈ°λŠ₯이 μ•„λ‹ˆλΌ, λ§ˆμ§€λ§‰ 선택지
  • MMM ꡬ성 + νŒŒν‹°μ…”λ‹ + νŠœλ‹ λ“± κ°€λŠ₯ν•œ λͺ¨λ“  ν™•μž₯을 λ¨Όμ € μ‹œλ„
  • 샀딩 λ„μž… μ‹œμ—λŠ” μƒ€λ“œ ν‚€, λΌμš°νŒ… μ „λž΅, νŠΈλžœμž­μ…˜ κ³ λ €, 운영 μžλ™ν™” 등을 μ² μ €νžˆ 섀계
  • MSAμ—μ„œλŠ” μ„œλΉ„μŠ€ λ‹¨μœ„λ‘œ 샀딩 μ—¬λΆ€λ₯Ό νŒλ‹¨
  • Vertical Slice ꡬ쑰λ₯Ό ν™œμš©ν•˜λ©΄ λ³΅μž‘λ„μ™€ ν™•μž₯성을 뢄리 적용 κ°€λŠ₯
  • λ³΅μž‘ν•œ 데이터 쑰합은 BFF λ˜λŠ” ν΄λΌμ΄μ–ΈνŠΈλ‘œ μ΄κ΄€ν•˜μ—¬ λ‹¨μˆœν™”

πŸ”— μ°Έκ³  μžλ£Œβ€‹

πŸ“˜ 샀딩 μΌλ°˜β€‹

πŸ“— Spring 관련​

πŸ“™ Kotlin Coroutine​

πŸ“• 샀딩 ν”Œλž«νΌ 및 μ˜€ν”ˆμ†ŒμŠ€β€‹