- ๋ถ์ผ: MSA, Batch Processing, Cache Consistency (Stale Data), Distributed Lock (ShedLock), Race Condition
- ๊ด๋ จ ํ์ผ: CatalogVisibilityScheduler.java, ShedLockConfig.java
1. ๋ฐ๋จ: ์กฐํ ์ฑ๋ฅ ์ต์ ํ์ ์์ ๋ฐฐ์น ์ค์ผ์ค๋ฌ ๋์
์นดํ๋ก๊ทธ ์๋น์ค๋ ์ปค๋จธ์ค ๋ฉ์ธ ํ๋ฉด์ ๋๊ท๋ชจ ์กฐํ ํธ๋ํฝ์ ๊ฐ๋นํ๊ธฐ ์ํด ์ํ ๋ชฉ๋ก์ Redis์ ์บ์ฑ(products_cache)ํ์ฌ ์ ๊ณตํ๊ณ ์์์.
๋ํ ๋งค์ผ ์์ (00:00:00), ์ํ์ ๋
ธ์ถ ์ํ(isVisible)๋ฅผ ์ผ๊ด ๋๊ธฐํํ๋ ๋ฐฐ์น ์ค์ผ์ค๋ฌ(CatalogVisibilityScheduler)๊ฐ ๊ตฌ๋๋จ.
์ด๊ธฐ ์ค๊ณ์์๋ DB ์ ๋ฐ์ดํธ ์ ์บ์๋ฅผ ์ด๊ธฐํํ๊ธฐ ์ํด @CacheEvict(beforeInvocation = true)๋ฅผ ์ ์ฉํ์.
2. ๋ฌธ์ ์ธ์: ์๊ฐ์ฐจ ์ฃ์ง ์ผ์ด์ค๋ก ์ธํ ์บ์ ์ค์ผ (Stale Data)
๋ฐฐ์น ์ค์ผ์ค๋ฌ๊ฐ 10๋ง ๊ฑด ์ด์์ ๋ฐ์ดํฐ๋ฅผ ์ฒญํฌ ๋จ์๋ก ์ ๋ฐ์ดํธํ๋ ๋ฐ ์ฝ 10์ด๊ฐ ์์๋๋ค๊ณ ๊ฐ์ ํ์ ๋, "๋ฐฐ์น๊ฐ ๋๋ ๋์ค ์ ์ ๊ฐ ์ ์ํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น?"๋ผ๋ ์๋ฌธ์์ Race Condition์ ๋ฐ๊ฒฌํจ.
[์๋๋ฆฌ์ค ํ์๋ผ์ธ]
- [00:00:00] ์์ ๋ฐฐ์น ์์ -> @CacheEvict๊ฐ ๋ฐ๋๋์ด products_cache ์บ์๊ฐ ์ผ๊ด ์ด๊ธฐํ๋จ.
- [00:00:02] ๋ฐฐ์น๊ฐ DB๋ฅผ 10% ์ ๋ ์ ๋ฐ์ดํธํ๊ณ ์๋ ๋์ค, ์ผ๋ฐ ์ ์ ๊ฐ ๋ฉ์ธ ํ๋ฉด ์ํ ๋ชฉ๋ก ์กฐํ๋ฅผ ์์ฒญํจ.
- [00:00:02] ์บ์๊ฐ ๋น์ด์์ผ๋ฏ๋ก(Cache Miss), ์ ํ๋ฆฌ์ผ์ด์
์ DB์์ ์ง์ ์ํ์ ์กฐํํจ.
(๋ฌธ์ ๋ฐ์) ์ด๋ ๋ฐฐ์น๊ฐ ์์ง ๊ฑด๋๋ฆฌ์ง ์์ '๊ณผ๊ฑฐ ๋ฐ์ดํฐ(Stale Data)'๋ฅผ ์กฐํํ์ฌ ๋ค์ ์บ์์ ์ ์ฅํด ๋ฒ๋ฆผ. - [00:00:10] ๋ฐฐ์น๊ฐ ์ข ๋ฃ๋๊ณ DB ์ ๋ฐ์ดํธ๊ฐ 100% ์๋ฃ๋จ.
๊ฒฐ๊ณผ: DB์๋ ์ต์ ์ํ๊ฐ ๋ฐ์๋์์ผ๋, ์บ์์๋ ๋ฐฐ์น ๋์ค ์ ์ ์กฐํ๋ก ์ธํด ์ฑ์์ง ๊ณผ๊ฑฐ ๋ฐ์ดํฐ๊ฐ ๋จ์์๊ฒ ๋จ. ๊ฒฐ๊ณผ์ ์ผ๋ก ๋ค์ ๋ ๋ฐฐ์น๊ฐ ๋๊ธฐ ์ ๊น์ง DB์ ์บ์ ๊ฐ์ ์ฌ๊ฐํ ๋ฐ์ดํฐ ๋ถ์ผ์น(์ ํฉ์ฑ ํ๊ดด)๊ฐ ์ง์๋ ๊ฒ์ด ์์๋๋ ์ํฉ์ด์์.
3. 1์ฐจ ํด๊ฒฐ: ๋ถ์ฐ ๋ฝ(ShedLock) ๋์ ๊ณผ AOP ์ฐ์ ์์ ์ ์ด
์บ์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ ์, MSA ํ๊ฒฝ(Scale-out)์์ ๋ฐ์ํ ์ ์๋ ๋ ๋ค๋ฅธ ๋ฌธ์ ๋ฅผ ์ ์ ์ ์ผ๋ก ๋ฐฉ์ดํ์. ํ์ฌ๋ ์ธ์คํด์ค๊ฐ 1๋์ด์ง๋ง, ์๋ฒ ์ธ์คํด์ค๊ฐ 3๋๋ก ๋์ด๋ ๊ฒฝ์ฐ, ์์ ์ 3๋์ ์๋ฒ๊ฐ ๋์์ ๋ฐฐ์น๋ฅผ ์คํํ์ฌ DB ํ ์ค์บ๊ณผ ์ ๋ฐ์ดํธ๋ฅผ ์ค๋ณต์ผ๋ก ์น๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํจ.
์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ShedLock(๋ถ์ฐ ๋ฝ)์ ๋์ ํ์ฌ ๋ฑ 1๋์ ์๋ฒ๋ง ๋ฐฐ์น๋ฅผ ์คํํ๋๋ก ์ ์ดํจ. ์ด๋, ์คํ๋ง AOP์ ์คํ ์์๋ฅผ ์ ํํ ์ ์ดํ์ง ์์ผ๋ฉด "๋ฝ ํ๋ ์ ์ 3๋๊ฐ ๋์์ ์บ์๋ถํฐ ์ง์๋ฒ๋ฆฌ๋" ํ์์ด ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก, ShedLockConfig์ ์ฐ์ ์์(order = 0)๋ฅผ ์บ์ ์ญ์ ๋ณด๋ค ๋๊ฒ ์ค์ ํจ.
// [ShedLockConfig.java]
@Configuration
// order = 0 ์ผ๋ก ์ค์ ํ์ฌ @CacheEvict ๋ณด๋ค ๋จผ์ ๋ถ์ฐ ๋ฝ์ ํ๋ํ๋๋ก ๊ฐ์ !
@EnableSchedulerLock(defaultLockAtMostFor = "PT30M", order = 0)
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
return new RedisLockProvider(connectionFactory, "catalog-service-lock");
}
}
4. 2์ฐจ ํด๊ฒฐ: Double Eviction (try-finally) ํจํด ์ ์ฉ
์บ์ ์ค์ผ(Stale Data)์ ๊ทผ๋ณธ์ ์ผ๋ก ํด๊ฒฐํ๊ธฐ ์ํด, ๋ฐฐ์น๊ฐ ๋๋ ๋์ค์๋ ์ต์ข ์ผ๊ด์ฑ๋ฅผ ํ์ฉํ๋, ๋ฐฐ์น๊ฐ ๋๋๋ ์์ ์ ์บ์๋ฅผ ๋ช ์์ ์ผ๋ก ํ ๋ฒ ๋ ์ด๊ธฐํ(Double Eviction)ํ๋ ํจํด์ ์ ์ฉํ์.
๋ฐฐ์น ๋์ค OptimisticLockingFailureException ๋ฑ์ ์์ธ๊ฐ ๋ฐ์ํ์ฌ ๋ก์ง์ด ์ค๋จ๋๋๋ผ๋ ๋ฐ๋์ ์บ์ ์ธํ์ด ๋ณด์ฅ๋๋๋ก try-finally ๋ธ๋ก์ ํ์ฉํ์.
// [CatalogVisibilityScheduler.java]
@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
@SchedulerLock(name = "syncVisibilityAtMidnight", lockAtLeastFor = "PT5M", lockAtMostFor = "PT25M")
@CacheEvict(value = "products_cache", allEntries = true, cacheManager = "catalogCacheManager", beforeInvocation = true)
public void syncVisibilityAtMidnight() {
log.info("[Catalog Batch] ์์ isVisible ์ผ๊ด ๋๊ธฐํ ์ค์ผ์ค๋ฌ ์์");
int totalUpdated = 0;
try {
// ... (์ฒญํฌ ๋จ์ DB ์
๋ฐ์ดํธ ๋ก์ง / ๋๊ด์ ๋ฝ Fallback ์ฒ๋ฆฌ) ...
log.info("[Catalog Batch] ์ด {}๊ฐ์ ์ํ ๋
ธ์ถ ์ํ(isVisible) ๋๊ธฐํ ์๋ฃ", totalUpdated);
} finally {
// ๋ฐฐ์น ๋์ค ๋ฐ์ํ ์ ์ ์์ฒญ์ผ๋ก ์ธํด ์บ์๊ฐ ๊ณผ๊ฑฐ ๋ฐ์ดํฐ๋ก ์ฌ์ค์ผ๋์์ ๊ฐ๋ฅ์ฑ์ ์ฐจ๋จ.
// ๋ฉ์๋ ์ข
๋ฃ ์ง์ , ๋ช
์์ ์ผ๋ก ์บ์๋ฅผ ํ ๋ฒ ๋ ๋ ๋ ค์ฃผ์ด ์ต์ข
์ ํฉ์ฑ ๋ณด์ฅ.
Cache cache = catalogCacheManager.getCache("products_cache");
if (cache != null) {
cache.clear();
log.info("[Catalog Batch] ๋ฐฐ์น ์ข
๋ฃ ํ products_cache ๋ช
์์ ์ด๊ธฐํ ์๋ฃ (Stale ๋ฐ์ดํฐ ๋ฐฉ์ง)");
}
}
}

5. ๊ฒ์ฆ ๋ฐ ๊ธฐ์ ์ ์์ฌ๊ฒฐ์
๐ก ๊ฒ์ฆ (์ค์ ์ด์ ๋ก๊ทธ)
ํ
์คํธ ๊ฒฐ๊ณผ, ๋ถ์ฐ ๋ฝ์ ์ํด ๋จ 1๋์ ์ธ์คํด์ค๋ง ๋ฐฐ์น๋ฅผ ์ํํ์ผ๋ฉฐ, ๋ฐฐ์น ๋์ค ์ ๋๋ ์ ์ ์ ์กฐํ ํธ๋ํฝ์ผ๋ก ์ธํ ์บ์ ์ค์ผ๋ finally ๋ธ๋ก์ ๋ช
์์ ์ด๊ธฐํ๋ฅผ ํตํด ๊น๋ํ๊ฒ ์ง์ ์.
[Instance-1] INFO: [Catalog Batch] ์์ isVisible ์ผ๊ด ๋๊ธฐํ ์ค์ผ์ค๋ฌ ์์
[Instance-2] (ShedLock์ ์ํด ์คํ ์ฐจ๋จ๋จ - ์กฐ์ฉํจ)
[Instance-1] INFO: [Catalog Batch] ์ด 10500๊ฐ์ ์ํ ๋
ธ์ถ ์ํ(isVisible) ๋๊ธฐํ ์๋ฃ
[Instance-1] INFO: [Catalog Batch] ๋ฐฐ์น ์ข
๋ฃ ํ products_cache ๋ช
์์ ์ด๊ธฐํ ์๋ฃ (Stale ๋ฐ์ดํฐ ๋ฐฉ์ง)


๐ก ๊ธฐ์ ์ ์์ฌ๊ฒฐ์ Q&A
Q. try-finally๋ฅผ ์ด์ฉํ Double Eviction์ด ๊ฐ์ฅ ์ฌ๋ฐ๋ฅธ ํด๊ฒฐ์ฑ ์ธ๊ฐ?
A. ๋์์ผ๋ก '๋ฐฐ์น ์ํ ์ค ์บ์๋ DB ํ ์ด๋ธ์ ๊ธ๋ก๋ฒ Lock์ ๊ฑธ์ด ์ ์ ์กฐํ๋ฅผ ๋ง๋ ๋ฐฉ๋ฒ'์ด ์์ผ๋, ์ด๋ ์๋น์ค Downtime์ ์ ๋ฐํ๋ฏ๋ก ๋๊ท๋ชจ ํธ๋ํฝ ํ๊ฒฝ์์๋ ๋ง์ง ์๋ค๊ณ ์๊ฐํ์. ๋ฐฐ์น ์ํ ์๊ฐ(N์ด) ๋์์ ์ฝ๊ฐ์ ์ง์ฐ(Eventual Consistency)์ ํ์ฉํ๋, ๋ฐฐ์น ์ข ๋ฃ ์งํ ์บ์๋ฅผ ํ์ ์ ์ผ๋ก ๋น์์ฃผ์ด ์ต์ข ์ ํฉ์ฑ์ ๋ณด์ฅํ๋ ๋ฐฉ์์ด ๊ฐ์ฅ ๋ณดํธ์ ์ด๊ณ ์์ ํ ์ ์์ด๋ผ๊ณ ํ๋จํ์.
Q. ShedLock ๋์ ์ด ์ค๋ฒ์คํ์ ์๋๊ฐ? ํ์ฌ ๋จ์ผ ์ธ์คํด์ค์ธ๋ฐ?
A. ๋ณธ ํ๋ก์ ํธ๋ Eureka, Kafka ๋ฑ์ ํ์ฉํ MSA ํ๊ฒฝ์ผ๋ก ์ค๊ณ๋์์. ์ด๋ ๊ณง "ํธ๋ํฝ์ ๋ฐ๋ผ ์ธ์ ๋ Catalog ์๋ฒ๊ฐ Scale-out ๋ ์ ์์"์ ์๋ฏธํจ. ๋จ์ผ ์ธ์คํด์ค์ผ ๋๋ ๋ถ์ฐ ๋ฝ ๋น์ฉ(Redis I/O ์ ms)์ ํฌ์ง ์๋ค๊ณ ์๊ฐํ๊ณ , ๋ฏธ๋์ ํ์ฅ์ ๋๋นํด ์ค์ผ์ค๋ฌ ๋์์ฑ ๋ฌธ์ ๋ฅผ ์ฌ์ ์ ์ฐจ๋จํ ์ ์์๊ธฐ์ ๋ง๋ ์ ํ์ด์๋ค๊ณ ํ๋จํจ.