๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
ํ”„๋กœ์ ํŠธ ํšŒ๊ณ /ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

์ž์ • ๋ฐฐ์น˜ ์Šค์ผ€์ค„๋Ÿฌ ์‹คํ–‰ ์ค‘ ๋ฐœ์ƒํ•˜๋Š” ์บ์‹œ ์žฌ์˜ค์—ผ(Stale Data) ๋ฐฉ์ง€ ๋ฐ ๋ถ„์‚ฐ ๋ฝ ์ œ์–ด

by ์ ผ์ ผ39 2026. 6. 30.
  • ๋ถ„์•ผ: 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์„ ๋ฐœ๊ฒฌํ•จ.

[์‹œ๋‚˜๋ฆฌ์˜ค ํƒ€์ž„๋ผ์ธ]

  1. [00:00:00] ์ž์ • ๋ฐฐ์น˜ ์‹œ์ž‘ -> @CacheEvict๊ฐ€ ๋ฐœ๋™๋˜์–ด products_cache ์บ์‹œ๊ฐ€ ์ผ๊ด„ ์ดˆ๊ธฐํ™”๋จ.
  2. [00:00:02] ๋ฐฐ์น˜๊ฐ€ DB๋ฅผ 10% ์ •๋„ ์—…๋ฐ์ดํŠธํ•˜๊ณ  ์žˆ๋Š” ๋„์ค‘, ์ผ๋ฐ˜ ์œ ์ €๊ฐ€ ๋ฉ”์ธ ํ™”๋ฉด ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ๋ฅผ ์š”์ฒญํ•จ.
  3. [00:00:02] ์บ์‹œ๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฏ€๋กœ(Cache Miss), ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ DB์—์„œ ์ง์ ‘ ์ƒํ’ˆ์„ ์กฐํšŒํ•จ.
    (๋ฌธ์ œ ๋ฐœ์ƒ) ์ด๋•Œ ๋ฐฐ์น˜๊ฐ€ ์•„์ง ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์€ '๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ(Stale Data)'๋ฅผ ์กฐํšŒํ•˜์—ฌ ๋‹ค์‹œ ์บ์‹œ์— ์ €์žฅํ•ด ๋ฒ„๋ฆผ.
  4. [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)์€ ํฌ์ง€ ์•Š๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๊ณ , ๋ฏธ๋ž˜์˜ ํ™•์žฅ์„ ๋Œ€๋น„ํ•ด ์Šค์ผ€์ค„๋Ÿฌ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ์‚ฌ์ „์— ์ฐจ๋‹จํ•  ์ˆ˜ ์žˆ์—ˆ๊ธฐ์— ๋งž๋Š” ์„ ํƒ์ด์—ˆ๋‹ค๊ณ  ํŒ๋‹จํ•จ.