[Spring] Shedlock 으둜 μŠ€μΌ€μ₯΄λ§ μ œμ–΄ν•˜κΈ°

2022. 8. 14. 16:26ㆍBackend/🌿 Spring

λͺ©μ°¨

  • μ„œλ‘ 
    • Shedlock μ΄λž€?
  • λ³Έλ‘ 
    • Shedlock μ‚¬μš©μ„ μœ„ν•œ ν”„λ‘œμ νŠΈ μ„€μ •
    • Shedlock μ„ μ‚¬μš©ν–ˆμ„ λ•Œ / μ‚¬μš©ν•˜μ§€ μ•Šμ„ λ•Œ λΉ„ꡐ
  • κ²°λ‘ 
    • μš”μ•½

 

 

 

μ„œλ‘ 

 

 

2λŒ€μ˜ λ™μΌν•œ μ„œλ²„ μΈμŠ€ν„΄μŠ€λ₯Ό λ„μš΄ (Scale-out) 상황을 κ°€μ •ν•΄λ³΄μž. μ„œλ²„μ—” 일정 주기둜 메일을 λ°œμ†‘ν•˜λŠ” μŠ€μΌ€μ₯΄λŸ¬κ°€ μžˆλ‹€. 2λŒ€μ˜ μΈμŠ€ν„΄μŠ€μ˜ μŠ€μΌ€μ₯΄λŸ¬κ°€ λ™μ‹œμ— μž‘λ™ν•œλ‹€λ©΄, λ˜‘κ°™μ€ 메일이 2λ²ˆμ”© λ°œμ†‘λ˜λŠ” 상황이 λ°œμƒν•œλ‹€.

λ˜‘κ°™μ€ 메일 2번 λ°›λŠ”κ²Œ λŒ€μˆ˜λƒκ³  ν•  수 μžˆμ§€λ§Œ, μ„œλ²„κ°€ 2λŒ€λ³΄λ‹€ λ§Žλ‹€λ©΄?  λ˜‘κ°™μ€ 메일을 μ—¬λŸ¬ 번 λ°›λŠ”κ²ƒμ€ μ°¨μΉ˜ν•˜λ”λΌλ„, μ„œλ²„ μž…μž₯μ—μ„œλŠ” μ€‘λ³΅λ˜λŠ” μž‘μ—…μ„ μˆ˜ν–‰ν•˜λŠ” 것이기 λ•Œλ¬Έμ— λ¦¬μ†ŒμŠ€ λ‚­λΉ„λ₯Ό μ€„μ—¬μ•Όν•œλ‹€.

 

 

Shedlock μ΄λž€?

shedlock 라이브러리λ₯Ό μ‚¬μš©ν•˜λ©΄, λ™μΌν•œ μŠ€μΌ€μ₯΄λŸ¬κ°€ 쀑볡 λ™μž‘ν•˜λŠ” 것을 방지할 수 μžˆλ‹€.

κ³΅μ‹λ¬Έμ„œλ₯Ό μ°Έμ‘°ν•˜λ©΄, shedlock 을 μ‚¬μš©ν•˜λ©΄ μŠ€μΌ€μ₯΄λŸ¬κ°€ λ™μ‹œμ— μ΅œλŒ€ ν•œλ²ˆλ§Œ μ‹€ν–‰λ˜λ„λ‘ 보μž₯(scheduled tasks are executed at most once at the same time) ν•œλ‹€.

ShedLock makes sure that your scheduled tasks are executed at most once at the same time. If a task is being executed on one node, it acquires a lock which prevents execution of the same task from another node (or thread). Please note, that if one task is already being executed on one node, execution on other nodes does not wait, it is simply skipped.

 

 

 

λ³Έλ‘ 

 

 

Shedlock μ‚¬μš©μ„ μœ„ν•œ ν”„λ‘œμ νŠΈ μ„€μ •

 

Requirements and dependencies : shedlock 을 μ‚¬μš©ν•˜λ €λ©΄ μ•„λž˜μ˜ 두 가지가 ν•„μš”ν•˜λ‹€. 

  • Java 8
  • slf4j-api

 

build.gradle

shedlock dependency λ₯Ό μΆ”κ°€ν•΄μ£Όμž.

// shed lock
implementation 'net.javacrumbs.shedlock:shedlock-spring:4.14.0'
implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:4.14.0'

 

shedlock Table 생성

μ•„λž˜μ˜ sql 문을 μ‚¬μš©ν•΄ shedlock ν…Œμ΄λΈ”μ„ μƒμ„±ν•˜μž. ν•΄λ‹Ή ν…Œμ΄λΈ”μ˜ 이름은 κΌ­ shedlock 으둜 μ§€μ •ν•΄μ•Όν•œλ‹€. 그렇지 μ•ŠμœΌλ©΄ μ—λŸ¬ λ‚œλ‹€. (μ œκ°€ κ²ͺμ–΄λ΄μ†Œ . . . )

CREATE TABLE shedlock
(
    name       VARCHAR(64)  NOT NULL COMMENT '이름',
    locked_at  TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '잠금 μΌμ‹œ',
    lock_until TIMESTAMP(3) NOT NULL COMMENT '잠금 κΈ°κ°„',
    locked_by  VARCHAR(255) NOT NULL COMMENT '잠금 μ‹€ν–‰μž',
    PRIMARY KEY (name)
) COMMENT 'μŠ€μΌ€μ₯΄ 락킹';

 

 

Scheduler Configuration

LockProvider 클래슀λ₯Ό μƒμ„±ν•œλ‹€.  LockProviderλŠ” 데이터 μ €μž₯μ†Œ(μœ„μ—μ„œ μƒμ„±ν•œ shedlock ν…Œμ΄λΈ”)λ₯Ό μ‚¬μš©ν•˜μ—¬ Lock정보λ₯Ό μ‚½μž…/κ°±μ‹ ν•œλ‹€. μ£Όμ˜ν•  점은 shedlock 에 기둝된 데이터λ₯Ό μˆ˜λ™μœΌλ‘œ λ³€κ²½/μ‚­μ œν•˜λ©΄ μ•ˆλœλ‹€.

@Configuration
public class SchedulerConfiguration{

    @Bean
    public LockProvider lockProvider(DataSource dataSource){
        return new JdbcTemplateLockProvider(
                JdbcTemplateLockProvider.Configuration.builder()
                        .withJdbcTemplate(new JdbcTemplate((dataSource)))
                        .usingDbTime()
                        .build()
        );
    }
}

 

μ—¬κΈ°κΉŒμ§€ 잘 따라왔닀면, scheduler 에 μ •μƒμ μœΌλ‘œ lock 을 κ±Έ 수 μžˆλ‹€.

자체적으둜 κ°„λ‹¨ν•œ scheduler 클래슀λ₯Ό λ§Œλ“€μ–΄ shedlock 이 μ μš©λμ„ λ•Œμ™€, 그렇지 μ•Šμ„ λ•Œμ˜ 차이λ₯Ό λΉ„κ΅ν•΄λ³΄μž. 

 

 

 

Shedlock 을 μ‚¬μš©ν–ˆμ„ λ•Œ / μ‚¬μš©ν•˜μ§€ μ•Šμ„ λ•Œ 비ꡐ

 

μš°μ„  κ°„λ‹¨ν•œ μŠ€μΌ€μ₯΄λŸ¬λ₯Ό λ§Œλ“€μ–΄λ³΄μž. 

@EnableScheduling μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•΄ ν•΄λ‹Ή ν΄λž˜μŠ€κ°€ μŠ€μΌ€μ₯΄λŸ¬ μž„μ„ λͺ…μ‹œν•˜κ³ , @Scheduled(cron = "0 0/1 * * * ?") μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ ν•΄λ‹Ή μŠ€μΌ€μ₯΄λŸ¬κ°€ μ–΄λŠ 주기둜 λ™μž‘ν•˜λŠ”μ§€ λͺ…μ‹œν•œλ‹€.

@Component
@RequiredArgsConstructor
@Slf4j
@EnableScheduling
public class AlarmScheduler {

    @Scheduled(cron = "0 0/1 * * * ?")
    public void sendAlarm(){
    
        log.info("------------- : HOO ");
        
    }

}

 

ν•΄λ‹Ή μŠ€μΌ€μ₯΄λŸ¬λŠ” 맀 1λΆ„λ§ˆλ‹€ 둜그λ₯Ό μ°λŠ”λ‹€. κ°€λ Ή ν˜„μž¬ μ‹œκ°„μ΄ μ˜€ν›„ 14μ‹œ 52λΆ„ 30초라면, ν•΄λ‹Ή μŠ€μΌ€μ₯΄λŸ¬λŠ” 14μ‹œ 53λΆ„ 00초 λΆ€ν„° 맀 1λΆ„λ§ˆλ‹€ 둜그λ₯Ό μ°λŠ”λ‹€. λ§Œμ•½ λ™μΌν•œ μ„œλ²„κ°€ nλŒ€ 싀행쀑이라면, 맀 λΆ„λ§ˆλ‹€ n개의 같은 λ‘œκ·Έκ°€ 좜λ ₯λœλ‹€.

μš°λ¦¬λŠ” μ—¬λŸ¬λŒ€μ˜ μ„œλ²„κ°€ μ‹€ν–‰ 쀑일 λ•Œ shedlock 을 μ‚¬μš©ν•˜μ—¬, ν•œ λŒ€μ˜ μ„œλ²„μ—μ„œλ§Œ μŠ€μΌ€μ₯΄λŸ¬κ°€ λ™μž‘ν•˜λ„λ‘ ν•΄λ³΄μž. 

 

 

μš°μ„  ν•„μžλŠ” λ™μΌν•œ μ„œλ²„λ₯Ό 2개 μ‹€ν–‰μ‹œμΌœ μ‹€ν—˜ν•΄λ³΄κ² λ‹€. μΈν…”λ¦¬μ œμ΄μ—μ„œ μ—¬λŸ¬κ°œμ˜ μŠ€ν”„λ§λΆ€νŠΈ μ„œλ²„λ₯Ό μ‹€ν–‰μ‹œν‚€λŠ” 방법은 μ•„λž˜λ₯Ό ν™•μΈν•΄λ³΄μž.

 

* μ°Έκ³  ) μΈν…”λ¦¬μ œμ΄μ—μ„œ μŠ€ν”„λ§λΆ€νŠΈ μ„œλ²„ μ—¬λŸ¬κ°œ μ‹€ν–‰μ‹œν‚€κΈ°

 

1. 락(shedlock)을 걸지 μ•Šμ„ λ•Œ

@Component
@RequiredArgsConstructor
@Slf4j
@EnableScheduling
//@EnableSchedulerLock(defaultLockAtMostFor = "10s") // shedlock 주석 처리 -> 락 걸지 μ•ŠλŠ”λ‹€.
public class AlarmScheduler {

    @Scheduled(cron = "0 0/1 * * * ?")
    //@SchedulerLock(name = "AlarmScheduler_scheduledTask", lockAtLeastFor = "9s", lockAtMostFor = "9s")
    public void sendAlarm(){

        log.info("------------- : HOO ");
        
	...
	...

    }

}

 

@EnableSchedulerLock μ–΄λ…Έν…Œμ΄μ…˜μ„ 주석 μ²˜λ¦¬ν•˜μ—¬, shedlock 을 λΉ„ν™œμ„±ν™” ν–ˆλ‹€. 

이 경우 2개의 μŠ€μΌ€μ₯΄λŸ¬κ°€ λ™μ‹œμ— μ‹€ν–‰λ˜λ©°, 각각의 ν„°λ―Έλ„μ—μ„œ λ‘œκ·Έκ°€ 찍힌 것을 확인할 수 μžˆλ‹€.

 

각각의 μ„œλ²„μ—μ„œ 거의 λ™μΌν•œ μ‹œκ°„μ— λ‘œκ·Έκ°€ μ°ν˜”λ‹€. λ§Œμ•½ μŠ€μΌ€μ₯΄λŸ¬μ˜ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 메일을 λ°œμ†‘ν•˜λŠ” κ²ƒμ΄μ—ˆλ‹€λ©΄, λ™μΌν•œ 메일 2κ°œκ°€ 거의 λ™μ‹œμ— λ°œμ†‘λœ 것이닀.

 

2. 락을 κ±Έ λ•Œ

@Component
@RequiredArgsConstructor
@Slf4j
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10s") // 주석 ν•΄μ œ -> 락 건닀.
public class AlarmScheduler {

    @Scheduled(cron = "0 0/1 * * * ?")
    @SchedulerLock(name = "AlarmScheduler_scheduledTask", lockAtLeastFor = "9s", lockAtMostFor = "9s")
    public void sendAlarm(){

        log.info("------------- : HOO ");
        
	...
	...

    }

}

@EnableSchedulerLock, @SchedulerLock μ–΄λ…Έν…Œμ΄μ…˜μ„ μΆ”κ°€ν•΄μ£Όμž. 

 

@SchedulerLock μ–΄λ…Έν…Œμ΄μ…˜은 name, lockAtLeastFor, LockAtMostFor μ΄λž€ 속성이 μžˆλ‹€.

 

name 은 unique ν•œ 값이기 λ•Œλ¬Έμ— λ‹€λ₯Έ μŠ€μΌ€μ₯΄λŸ¬μ™€ κ²ΉμΉ˜μ§€ μ•Šκ²Œ μ„€μ •ν•˜λŠ” 것이 μ€‘μš”ν•˜λ‹€. 

lockAtLeastFor : μ‹€μ§ˆμ μœΌλ‘œ lock 이 κ±Έλ¦¬λŠ” μ΅œμ†Œ 보μž₯된 lock μ‹œκ°„

lockAtMostFor: ν•΄λ‹Ή μŠ€μΌ€μ€„λŸ¬ λ…Έλ“œκ°€ λ‹€μš΄λμ„ λ•Œ, lock 을 μ–Έμ œκΉŒμ§€ 보μž₯ν•΄μ£ΌλŠ”μ§€ μ΅œλŒ€ μ‹œκ°„μ„ μ„€μ •ν•œλ‹€.

 

 

μœ„ μ΄λ―Έμ§€μ˜ 둜그λ₯Ό 확인해보면, 2λŒ€μ˜ μ„œλ²„μ—μ„œ μ„œλ‘œ λ²ˆκ°ˆμ•„κ°€λ©° lock 이 κ±Έλ¦¬λŠ” 것을 확인할 수 μžˆλ‹€. 즉 A μ„œλ²„μ—μ„œ μŠ€μΌ€μ₯΄λŸ¬κ°€ λˆλ‹€λ©΄, B μ„œλ²„μ˜ μŠ€μΌ€μ₯΄λŸ¬λŠ” 락이 걸리고, λ‹€μŒ μ£ΌκΈ°μ—μ„œλŠ” B μ„œλ²„μ—μ„œ μŠ€μΌ€μ₯΄λŸ¬λ₯Ό λ™μž‘μ‹œν‚€κ³ , A μŠ€μΌ€μ₯΄λŸ¬κ°€ 락이 κ±Έλ¦¬λŠ” 것이닀. λΌμš΄λ“œ 둜빈 λ°©μ‹μœΌλ‘œ 락이 κ±Έλ¦¬λŠ” 것 같은데, 이건 κ³΅μ‹λ¬Έμ„œμ—μ„œ 더 찾아봐야겠닀.

 

결둠적으둜 shedlock 라이브러리λ₯Ό μ μš©ν•˜λ©΄ μ—¬λŸ¬개의 μŠ€μΌ€μ₯΄λŸ¬κ°€ μ‘΄μž¬ν•˜λ”λΌλ„, 단 ν•˜λ‚˜λ§Œμ˜ μŠ€μΌ€μ₯΄λŸ¬λ§Œ μ‹€ν–‰ν•˜κ³  λ‚˜λ¨Έμ§€ μŠ€μΌ€μ₯΄λŸ¬μ˜ λ™μž‘μ€ μ œμ–΄ν•  수 있게 λœλ‹€.

 

 

κ²°λ‘ 

νšŒμ‚¬ 업무 쀑 μŠ€μΌ€μ₯΄λŸ¬μ— 락을 κ±Έμ–΄μ•Ό ν•΄μ„œ μ‘°μ‚¬ν•˜κ²Œ 됐닀.

shedlock 을 λ„μž…ν•œ 이유λ₯Ό 업무와 연관지어 생각해보면, 

-> μš΄μ˜ν™˜κ²½μ—μ„œ μ„œλ²„ μΈμŠ€ν„΄μŠ€ λ‹€μˆ˜ 쑴재 -> μΈμŠ€ν„΄μŠ€ 개수 만큼 μŠ€μΌ€μ₯΄λŸ¬κ°€ λ™μž‘ν•œλ‹€λ©΄, 쀑볡 μž‘μ—…μ΄ 진행될 수 μžˆλ‹€ -> λΉ„μ¦ˆλ‹ˆμŠ€ 였λ₯˜

-> μŠ€μΌ€μ₯΄λŸ¬μ˜ 쀑볡 μš΄μ˜μ„ λ°©μ§€ν•˜κ³ μž shedlock μ΄λΌλŠ” 라이브러리 λ„μž…. 

-> μŠ€μΌ€μ₯΄λŸ¬κ°€ ν˜„μž¬ 적용된 μž‘μ—…(μΉ΄μΉ΄μ˜€ν†‘ μ•Œλ¦Ό λ°œμ†‘, 였래된 κ°œμΈμ •λ³΄ μ‚­μ œ)의 쀑볡 진행을 λ§‰λŠ”λ‹€. 

μ •λ„λ‘œ μš”μ•½ν•  수 μžˆλ‹€.