1. ๊ฐœ์š”

Spring @Scheduled ๋Š” ๋ ˆํ”Œ๋ฆฌ์นด๊ฐ€ ๋‘˜์ด๋ฉด ๋‘˜ ๋‹ค ๋ˆ๋‹ค. ShedLock ์€ ๊ณต์œ  ์ €์žฅ์†Œ(Redis, DB)์— ๋ฝ์„ ๊ฑธ์–ด ํ•œ ๋ฒˆ๋งŒ ๋Œ๊ฒŒ ํ•œ๋‹ค. ์–ด๋…ธํ…Œ์ด์…˜ ํ•˜๋‚˜๋กœ ๋๋‚˜์„œ ์ข‹์€๋ฐ, โ€œ์กฐ์šฉํžˆโ€ ๋ผ๋Š” ๋‹จ์–ด๊ฐ€ ์ž๊พธ ๋”ฐ๋ผ๋ถ™๋Š”๋‹ค. ๋ฝ์ด ์žˆ์œผ๋ฉด ๋ณธ๋ฌธ์„ ์กฐ์šฉํžˆ ๊ฑด๋„ˆ๋›ด๋‹ค. ์ด๊ฒŒ ์„ค๊ณ„ ์˜๋„์ด๊ณ , ํ…Œ์ŠคํŠธ์™€ ๋งŒ๋‚˜๋ฉด ํ•จ์ •์ด ๋œ๋‹ค.


2. ํ•ต์‹ฌ ๋‚ด์šฉ

2-1. ๊ธฐ๋ณธ ๋™์ž‘

@Scheduled(cron = "0 * * * * *")
@SchedulerLock(name = "expireReservations", lockAtMostFor = "PT10M", lockAtLeastFor = "PT30S")
public void expireReservations() { ... }
  • ์‹คํ–‰ ์ „์— name ์œผ๋กœ ๋ฝ์„ ์‹œ๋„ํ•œ๋‹ค. ์ด๋ฏธ ์žˆ์œผ๋ฉด ๋ณธ๋ฌธ์„ ์‹คํ–‰ํ•˜์ง€ ์•Š๊ณ  ์ •์ƒ ์ข…๋ฃŒ ํ•œ๋‹ค. ์˜ˆ์™ธ ์—†์Œ. ๋กœ๊ทธ๋Š” DEBUG.
  • lockAtMostFor: ์žก์ด ์ฃฝ์–ด๋„ ์ด ์‹œ๊ฐ„ ๋’ค์—” ๋ฝ์ด ํ’€๋ฆฐ๋‹ค. ์‹คํ–‰ ์‹œ๊ฐ„์˜ ์ƒํ•œ๋ณด๋‹ค ๋„‰๋„‰ํ•˜๊ฒŒ.
  • lockAtLeastFor: ์‹คํ–‰์ด ๋นจ๋ฆฌ ๋๋‚˜๋„ ์ด ์‹œ๊ฐ„์€ ๋ฝ์„ ์œ ์ง€ํ•œ๋‹ค. ๋ ˆํ”Œ๋ฆฌ์นด ๊ฐ„ ์‹œ๊ณ„ ์ฐจ์ด๋กœ ๊ฐ™์€ cron ์ด ๋‘ ๋ฒˆ ๋„๋Š” ๊ฒƒ์„ ๋ง‰๋Š”๋‹ค.

2-2. ํ•จ์ • 1: ํ…Œ์ŠคํŠธ๊ฐ€ ํ”„๋ก์‹œ ๋นˆ์„ ์ง์ ‘ ํ˜ธ์ถœํ•œ๋‹ค

ํ†ตํ•ฉํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ„ํ—์ ์œผ๋กœ ์‹คํŒจํ–ˆ๋‹ค. expected: EXPIRED but was: WAITLISTED. ์žฌ์‹คํ–‰ํ•˜๋ฉด ํ†ต๊ณผ. ์„ธ ๊ฒน์ด์—ˆ๋‹ค.

  1. ํ…Œ์ŠคํŠธ ํ”„๋กœํŒŒ์ผ์—์„œ @EnableScheduling ์ด ๋‘ ๊ณณ์— ์žˆ์–ด ํ•œ์ชฝ๋งŒ ๊บผ๋„ ์ง„์งœ cron ์ด ์Šค์œ„ํŠธ ์ค‘์— ๋ˆ๋‹ค.
  2. ๊ทธ cron ์ด lockAtLeastFor = PT30S ์งœ๋ฆฌ ๋ฝ์„ ์žก๋Š”๋‹ค. ์‹คํ–‰์€ 3ms ์ง€๋งŒ ๋ฝ์€ 30์ดˆ.
  3. ํ…Œ์ŠคํŠธ๊ฐ€ @Autowired ๋กœ ๋ฐ›์€ ์Šค์ผ€์ค„๋Ÿฌ ๋นˆ์€ ShedLock ํ”„๋ก์‹œ ๋‹ค. ๊ทธ 30์ดˆ ์•ˆ์— ํ…Œ์ŠคํŠธ๊ฐ€ scheduler.expireReservations() ๋ฅผ ๋ถ€๋ฅด๋ฉด ํ”„๋ก์‹œ๊ฐ€ โ€œ๋ฝ ์žˆ์Œโ€ ์œผ๋กœ ๋ณธ๋ฌธ์„ ๊ฑด๋„ˆ๋›ด๋‹ค.

ํ…Œ์ŠคํŠธ๋Š” ๋ณธ๋ฌธ์ด ๋Œ์•˜๋‹ค๊ณ  ๋ฏฟ๊ณ  ๊ฒฐ๊ณผ๋ฅผ ๋‹จ์–ธํ•œ๋‹ค. ๋ณธ๋ฌธ์€ ์•ˆ ๋Œ์•˜๋‹ค. ์‹คํŒจ. ํ™•๋ฅ ์€ 30์ดˆ ์ฐฝ / ์Šค์œ„ํŠธ ๊ธธ์ด โ‰ˆ 10%. ๋”ฑ โ€œ๊ฐ€๋”โ€.

// ํ”„๋ก์‹œ๋ฅผ ๋ฒ—๊ฒจ ๋ณธ๋ฌธ์„ ์ง์ ‘ ํ˜ธ์ถœ
ReservationScheduler target = AopTestUtils.getTargetObject(schedulerBean);
target.expireReservations();

๋˜๋Š” ํ…Œ์ŠคํŠธ ํ”„๋กœํŒŒ์ผ์—์„œ @EnableScheduling ์„ ํ™•์‹คํžˆ ๋„๊ณ (๋‘ ๊ณณ ๋ชจ๋‘), ShedLock ์„ @Profile("!test") ๋กœ ๋บ€๋‹ค. ๊ฒฐ์ •์  ์žฌํ˜„์€ Redis ์— ๋ฝ ํ‚ค๋ฅผ ๋ฏธ๋ฆฌ ์‹ฌ๋Š” ๊ฒƒ.

๋ฐฐ์šด ๊ฒƒ: ์‹คํŒจํ•˜์ง€ ์•Š๊ณ  ์‹คํ–‰๋˜์ง€ ์•Š๋Š” ์ฝ”๋“œ ๋Š” catch ์—๋„ ๋กœ๊ทธ์—๋„ ์•ˆ ๋‚จ๋Š”๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ฒ˜์Œ ์„ธ์šด โ€œ์ฟผ๋ฆฌ ๊ฒฝํ•ฉโ€ ๊ฐ€์„ค์€ ํ‹€๋ ธ๋‹ค. ํ›„๋ณด ํ–‰์ด ํ•ญ์ƒ 1๊ฑด์ธ ๊ฑธ ํ™•์ธํ•˜๊ณ  ๋‚˜์„œ์•ผ ๊ฐ€์„ค์„ ๋ฒ„๋ ธ๋‹ค.

2-3. ํ•จ์ • 2: ๋™์  ์žก์—๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ด ์•ˆ ๊ฑธ๋ฆฐ๋‹ค

SchedulingConfigurer.addTriggerTask() ๋กœ ๋“ฑ๋กํ•œ ์žก(์ฃผ๊ธฐ๋ฅผ ๋Ÿฐํƒ€์ž„์— ๋ฐ”๊พธ๋Š” ์žก)์€ AOP ํ”„๋ก์‹œ๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๋Š”๋‹ค. @SchedulerLock ์„ ๋ถ™์—ฌ๋„ ์•„๋ฌด ์ผ์ด ์•ˆ ์ผ์–ด๋‚œ๋‹ค. ์กฐ์šฉํžˆ. ๋ ˆํ”Œ๋ฆฌ์นด ๋‘˜์ด ๋‘˜ ๋‹ค ๋ˆ๋‹ค.

// ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹ ๋ฝ
lockingTaskExecutor.executeWithLock(
    Runnable { syncCalendars() },
    LockConfiguration(Instant.now(), "iCalSync", Duration.ofMinutes(10), Duration.ofSeconds(30))
)

DefaultLockingTaskExecutor ๋ฅผ ๋นˆ์œผ๋กœ ๋‘๊ณ  ์ง์ ‘ ๊ฐ์‹ผ๋‹ค. ์–ด๋…ธํ…Œ์ด์…˜์ด ์•ˆ ๊ฑธ๋ฆฌ๋Š” ๊ณณ์€ ์ด๊ฒƒ๋ฟ์ด ์•„๋‹ˆ๋‹ค. ๊ฐ™์€ ํด๋ž˜์Šค ์•ˆ์—์„œ this.method() ๋กœ ๋ถ€๋ฅด๋Š” ๊ฒƒ๋„ ํ”„๋ก์‹œ๋ฅผ ์•ˆ ํƒ„๋‹ค. @Transactional ๊ณผ ๊ฐ™์€ ํ•จ์ •.

2-4. ํ•จ์ • 3: lockAtLeastFor ๋ฅผ ๊ธธ๊ฒŒ ์žก์œผ๋ฉด ์ˆ˜๋™ ์‹คํ–‰์ด ๋ง‰ํžŒ๋‹ค

๊ด€๋ฆฌ์ž ํ™”๋ฉด์— โ€œ์ง€๊ธˆ ๋™๊ธฐํ™”โ€ ๋ฒ„ํŠผ์ด ์žˆ๋‹ค. ์ด ๋ฒ„ํŠผ์ด ์Šค์ผ€์ค„๋Ÿฌ์™€ ๊ฐ™์€ ๋ฝ ์ด๋ฆ„์„ ์“ฐ๋ฉด, cron ์ด ๋ฐฉ๊ธˆ ๋Œ์•„ lockAtLeastFor ์ฐฝ ์•ˆ์ด๋ฉด ๋ฒ„ํŠผ์ด ์กฐ์šฉํžˆ ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ํ•œ๋‹ค. ์‚ฌ์šฉ์ž๋Š” โ€œ๋ฒ„ํŠผ์ด ๊ณ ์žฅ๋‚ฌ๋‹คโ€.

์„ ํƒ์ง€๋Š” ๋‘˜์ด๋‹ค. ์ˆ˜๋™ ์‹คํ–‰์€ ๋‹ค๋ฅธ ๋ฝ ์ด๋ฆ„์„ ์“ฐ๊ฑฐ๋‚˜(๊ทธ๋Ÿฌ๋ฉด cron ๊ณผ ๊ฒน์น  ์ˆ˜ ์žˆ๋‹ค), ๋ฝ ํš๋“ ์‹คํŒจ๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆฐ๋‹ค. executeWithLock ์˜ ๋ฐ˜ํ™˜๊ฐ’์ด๋‚˜ LockProvider.lock() ์˜ Optional ์ด ๋น„์—ˆ๋Š”์ง€ ๋ณด๊ณ  โ€œ๋ฐฉ๊ธˆ ๋™๊ธฐํ™”๋์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œโ€ ๋ฅผ ๋„์› ๋‹ค.

2-5. ํ•จ์ • 4: ๋ฝ ์ €์žฅ์†Œ๊ฐ€ ์ฃฝ์œผ๋ฉด

Redis ๋ฅผ ๋ฝ ์ €์žฅ์†Œ๋กœ ์“ฐ๋ฉด Redis ๊ฐ€ ์ฃฝ์—ˆ์„ ๋•Œ @SchedulerLock ๋ฉ”์„œ๋“œ๋Š” ์˜ˆ์™ธ๋กœ ์‹คํŒจ ํ•œ๋‹ค. ๋ฝ์„ ๋ชป ์žก์œผ๋ฉด ์‹คํ–‰ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ShedLock ์˜ ์•ˆ์ „ ๋ฐฉํ–ฅ์ด๋ผ ๋งž๋‹ค. ๊ทธ๋Ÿฐ๋ฐ Redis ํด๋ผ์ด์–ธํŠธ์˜ ํƒ€์ž„์•„์›ƒ์ด ๊ธฐ๋ณธ 60์ดˆ๋ฉด, cron ์Šค๋ ˆ๋“œ๊ฐ€ 60์ดˆ๋ฅผ ๊ธฐ๋‹ค๋ฆฐ๋‹ค. ์Šค์ผ€์ค„๋Ÿฌ ์Šค๋ ˆ๋“œ ํ’€์ด ์ž‘์œผ๋ฉด ๋‹ค๋ฅธ ์žก๋„ ๋ฐ€๋ฆฐ๋‹ค. Redis ํƒ€์ž„์•„์›ƒ์€ ์งง๊ฒŒ(3์ดˆ).

2-6. ๋ช‡ ๊ฐœ๊นŒ์ง€ ๋‘๋‚˜

ํ•œ ํ”„๋กœ์ ํŠธ์— @SchedulerLock ์ด 15๊ณณ์ด๋‹ค. ์˜ˆ์•ฝ ๋งŒ๋ฃŒ, ๋…ธ์‡ผ ์ฒ˜๋ฆฌ, ์•Œ๋ฆผ ๋ฆฌ๋งˆ์ธ๋”, ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”, ํŒŒ์ผ ์ •๋ฆฌ, ํƒˆํ‡ด ๋ฐ์ดํ„ฐ ํผ์ง€, ๋ฐฑํ•„ ๋Ÿฌ๋„ˆ. ๋ฝ ์ด๋ฆ„์ด 15๊ฐœ๋‹ค. ์ด๋ฆ„์ด ๊ฒน์น˜๋ฉด ์„œ๋กœ ๋ง‰๋Š”๋‹ค. ์ด๋ฆ„ ๋ชฉ๋ก์„ ํ•œ enum ์ด๋‚˜ ์ƒ์ˆ˜ ํด๋ž˜์Šค์— ๋ชจ์•„ ์ค‘๋ณต์„ ์ปดํŒŒ์ผ ์‹œ์ ์— ์žก๋Š” ํŽธ์ด ๋‚ซ๋‹ค.

๋ฐฑํ•„์ฒ˜๋Ÿผ ํ•œ ๋ฒˆ ๋Œ๊ณ  ๋๋‚  ์žก์€ ํ‚ฌ ์Šค์œ„์น˜๋ฅผ ๋”ฐ๋กœ ๋‘”๋‹ค. Redis ์— ํ”Œ๋ž˜๊ทธ ํ‚ค๋ฅผ ๋‘๊ณ  ์žก์ด ์‹œ์ž‘ํ•  ๋•Œ ์ฝ๋Š”๋‹ค. ์žก์ด ์ž˜๋ชป ๋Œ๊ธฐ ์‹œ์ž‘ํ–ˆ์„ ๋•Œ ์žฌ๋ฐฐํฌ ์—†์ด ๋ฉˆ์ถœ ์ˆ˜ ์žˆ๋‹ค. dry-run ๋ชจ๋“œ๋ฅผ ๋จผ์ € ํ•œ ๋ฐ”ํ€ด ๋Œ๋ฆฌ๊ณ  ์ผœ๋Š” 2๋‹จ๊ณ„.


3. ๋งˆ๋ฌด๋ฆฌ

์š”์•ฝ

  • ShedLock ์€ ๋ฝ์ด ์žˆ์œผ๋ฉด ๋ณธ๋ฌธ์„ ์กฐ์šฉํžˆ ๊ฑด๋„ˆ๋›ด๋‹ค. ์˜ˆ์™ธ๋„ ๋กœ๊ทธ๋„ ์—†๋‹ค.
  • ํ…Œ์ŠคํŠธ๊ฐ€ ํ”„๋ก์‹œ ๋นˆ์„ ๋ถ€๋ฅด๋ฉด ์ง„์งœ cron ์˜ ๋ฝ ์ฐฝ์—์„œ ์Šคํ‚ต๋œ๋‹ค. AopTestUtils.getTargetObject ๋กœ ๋ฒ—๊ธด๋‹ค.
  • ๋™์  ์žกยทthis. ํ˜ธ์ถœ์€ ์–ด๋…ธํ…Œ์ด์…˜์ด ์•ˆ ๊ฑธ๋ฆฐ๋‹ค. LockingTaskExecutor ๋กœ ์ง์ ‘.
  • ์ˆ˜๋™ ์‹คํ–‰ ๋ฒ„ํŠผ์€ ๋ฝ ํš๋“ ์‹คํŒจ๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋งํ•ด์•ผ ํ•œ๋‹ค.
  • ๋ฝ ์ €์žฅ์†Œ ํƒ€์ž„์•„์›ƒ์€ ์งง๊ฒŒ. ๋ฝ ์ด๋ฆ„์€ ํ•œ ๊ณณ์— ๋ชจ์•„ ์ค‘๋ณต์„ ์žก๋Š”๋‹ค.