Spring ์ค์ผ์ค๋ฌ๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐฐ์น ์์ ๋ฐ ์์ฝ๋ ์์ ์ ์์ฝ๊ฒ ํ ์ ์์ง๋ง AWS ECS, EKS์ ๊ฐ์ ํด๋ผ์ฐ๋ ๊ธฐ๋ฐ ๋ถ์ฐ ์ปดํจํ ํ๊ฒฝ์์๋ ๋ฉํฐ ์ธ์คํด์ค๋ก ์๋น์ค๋ฅผ ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์๋ฐ ์ด๋ฌํ ๊ฒฝ์ฐ ๊ฐ ์ธ์คํด์ค์ ์ค์ผ์ค ํ์คํฌ๋ฅผ ๋๊ธฐํํ ์ ์์ด์ ๊ฐ์ ํ์คํฌ๊ฐ ์ค๋ณต ์คํ์ด ๋์ด ์๊ธฐ์น ์์ ๋ฌธ์ ๋ฅผ ๋ง๋ ์ ์๋ค.
ShedLock์ ์ด๋ฌํ ์ค๋ณต ์คํ ๋ฌธ์ ์ ๋ํด์ ๊ฐ ์ธ์คํด์ค ๊ฐ์ ์ ๊ธ ์ฒ๋ฆฌ๋ฅผ ์ ๊ณตํ์ฌ ํ๋์ ์ธ์คํด์ค์์๋ง ํ์คํฌ๋ฅผ ์คํํ ์ ์๋๋ก ํ๋ค. (ํ์คํฌ์ ์ ๊ธ ์ด๋ฆ์ ์ง์ ํ์ฌ ๋์ผํ ์ด๋ฆ์ ๋ํด์ ์ ๊ธ์ด ๋์ํ๋ฏ๋ก ๋ ์ข์ ์๋ฏธ๋ก๋ ํ์คํฌ ๊ฐ์ ์ ๊ธ ์ฒ๋ฆฌ๋ผ๊ณ ํ๋ ๊ฒ ๋ง์ ๊ฒ ๊ฐ๋ค.)
- ShedLock์ ํ์คํฌ ๊ฐ์ ๋ฝ ์ฒ๋ฆฌ๋ฅผ ์ํด์ MongoDB, JDBC, Redis, Hazelcast์ ๊ฐ์ ์ธ๋ถ ์ ์ฅ์๋ฅผ ์ง์ํ๋ค.
- ShedLock์ ๋ณ๋ ฌ๋ก ์คํํ ์ค๋น๊ฐ ๋์ง ์์์ง๋ง ์์ ํ๊ฒ ๋ฐ๋ณต ์คํํ ์ ์๋ ์์ฝ๋ ์์ ์ด ์๋ ์ํฉ์์ ์ฌ์ฉํ๋๋ก ์ค๊ณ๋์๋ค.
- ์ ๊ธ์ ์๊ฐ ๊ธฐ๋ฐ์ด๋ฉฐ ShedLock์ ๋ ธ๋์ ์๊ฐ์ด ์๋ก ๋๊ธฐํ ๋์ด ์๋ค๊ณ ๊ฐ์ ํ๋ค.
ShedLock ๊ตฌ์ฑ
ShedLock์ ์ธ ํํธ๋ก ๊ตฌ์ฑ๋์ด ์๋ค.
- Core – locking mechanism
- Integration – Spring AOP ๋๋ ์๋ ์ฝ๋๋ฅผ ์ฌ์ฉํ์ฌ ์ ํ๋ฆฌ์ผ์ด์ ๊ณผ ํตํฉ
- LockProvider – SQL DB, Mongo, Redis์ ๊ฐ์ ์ธ๋ถ ํ๋ก์ธ์ค๋ฅผ ์ฌ์ฉํ์ฌ ์ ๊ธ์ ์ ๊ณต
Usage
Dependency
ShedLock ์ ์ฌ์ฉํ๊ธฐ ์ํ ๋ํ๋์๋ ๋ค์๊ณผ ๊ฐ๋ค.
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>5.10.0</version>
</dependency>XMLAnnotation
Spring์์ ์ค์ผ์ค ์ ๊ธ์ ํ์ฑํํ๋ ค๋ฉด @EnableSchedulerLock ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋ค.
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
class MySpringConfiguration {
...
}Java์ค์ผ์ค ์์ (Task)์ @SchedulerLock ์ด๋ ธํ ์ด์ ์ ์ง์ ํ๋ค.
@Scheduled(...)
@SchedulerLock(name = "scheduledTaskName")
public void scheduledTask() {
...
...
}JavaSchedulerLock ์ด๋ ธํ ์ด์ ์๋ ๋ค์์ ์์ฑ์ด ์๋ค.
- name
- ๊ณ ์ ํ ์ด๋ฆ์ด ์ฌ์ฉ๋์ด์ผ ํ๋ค.
- ๋์ผํ ์ด๋ฆ์ ํ์คํฌ๋ ์ฌ๋ฌ ์ธ์คํด์ค์ ๋ํด์ ์ ๊ธ์ฒ๋ฆฌ๋ก ์ธํด ํ ๋ฒ๋ง ์ํ๋๋ค.
- lockAtLeastFor
- ์ ๊ธ์ด ์ ์ง๋์ด์ผ ํ๋ ์ต์ ์๊ฐ์ ์ง์ ํ๋ค.
- Task ์์ ์ด ์๋ฃ๋ ์๊ฐ๋ณด๋ค ์ง์ ๋ ์๊ฐ์ด ๊ธธ๋ฉด ์ง์ ๋ ์๊ฐ ๋์ lock ๋๋ค.
- Task ์์ ์์ ์๊ฐ์ด ์ง์ ๋ ์๊ฐ๋ณด๋ค ๊ธธ๋ฉด lockAtMostFor์ ์ง์ ๋ ์๊ฐ๊น์ง ์ ๊ธ์ด ์ ์ง๋์ง๋ง lockAtMostFor duration ์ด์ ์ ์์ ์ด ์๋ฃ๋๋ฉด ์ ๊ธ์ ํด์ ๋๋ค.
- lockAtMostFor
- ์คํ ๋ ธ๋๊ฐ ์ฃฝ์์ ๋ ์ ๊ธ์ ์ผ๋ง๋ ์ค๋ ์ ์งํด์ผ ํ๋์ง ์ง์ ํ๋ ์์ฑ์ด๋ค.
- ์ ์์ ์ธ ์ํฉ์์๋ Task ์์ ์ด ์๋ฃ๋๋ ์ฆ์ ์ ๊ธ์ด ํด์ ๋๋ค.
- lockAtLeastFor ์์ฑ์ด ์ง์ ๋์ง ์์ ๊ฒฝ์ฐ ์ ์์ ์ธ ์คํ ์๊ฐ๋ณด๋ค ํจ์ฌ ๊ธด ๊ฐ์ผ๋ก lockAtMost ์์ฑ์ ์ง์ ํด์ผ ํ๋ค.
lockAtLeastFor ๊ฐ์ด lockAtMostFor ์ค์ ๊ฐ๋ณด๋ค ํฐ ๊ฒฝ์ฐ ์์ธ๊ฐ ๋ฐ์ํ๋ ์ฃผ์๊ฐ ํ์ํ๋ค.
LockProvider Redis
Lock ๋ฐ์ดํฐ ์ ์ฅ์๋ฅผ Redis๋ฅผ ์ฌ์ฉํ ๊ฒ์ด๋ค. ๊ทธ ์ธ ์ ์ฅ์์ ๋ํด์๋ ํฌ์คํ
ํ๋จ์ ์๋ ์ฐธ๊ณ ๋งํฌ๋ฅผ ํ์ธํด ๋ณด๊ธฐ ๋ฐ๋๋ค.
LockProvider์ ๋ํ ๋ํ๋์๋ ๋ค์๊ณผ ๊ฐ๋ค.
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-redis-spring</artifactId>
<version>5.10.0</version>
</dependency>XMLLockProvider์ RedisConnectionFactory ๋ฅผ ์ ๋ฌํ๊ธฐ ์ํ ๊ตฌ์ฑ์ด ํ์ํ๋ค.
import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;
import org.springframework.data.redis.connection.RedisConnectionFactory;
...
@Bean
public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
return new RedisLockProvider(connectionFactory, ENV);
}Javaapplication.yml(. properties)์ Redis์ ๋ํ ์ฐ๊ฒฐ ์ค์ ์ด ๋์ด ์๋ค๋ฉด RedisConnectionFactory๋ ์๋ ๊ตฌ์ฑ์ ์ํด์ ๋น์ผ๋ก ์๋ ๋ฑ๋ก๋๋ค.
RedisLockProvider ํด๋์ค์ ์์ฑ์๋ ๋ค์๊ณผ ๊ฐ๋ค.
public RedisLockProvider(@NonNull RedisConnectionFactory redisConn) {
this(redisConn, "default");
}
public RedisLockProvider(@NonNull RedisConnectionFactory redisConn, @NonNull String environment) {
this(redisConn, environment, "job-lock");
}
public RedisLockProvider(@NonNull RedisConnectionFactory redisConn, @NonNull String environment, @NonNull String keyPrefix) {
this(new StringRedisTemplate(redisConn), environment, keyPrefix);
}
...JavaRedis์ Lock ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋ ๋ Key ํ์์ ๋ค์๊ณผ ๊ฐ๋ค.
{key-prefix}:{environment}:{SchedulerLock-name}
RedisLockProvider ํด๋์ค์์ keyPrefix, environment ๊ฐ ์ ๋ฌ๋์ง ์์ ๊ฒฝ์ฐ์๋ ๋ํดํธ๋ก ๋ค์ ๊ฐ์ด ์ฌ์ฉ๋๋ค.
- keyPrefix default : job-lock
- environment default : default
๊ธฐ๊ฐ ์ค์ ํ์
๊ด๋ จ ์ด๋ ธํ ์ด์ ์ ์ง์ ๋๋ duration ์์ฑ ํ์์ ๋ค์๊ณผ ๊ฐ์ ํ์์ด ์ง์๋๋ค.
- duration + unit – ex) 1s, 5ms, 5m, 1d (4.0.0๋ถํฐ)
- duration in ms – ex) 100 (Spring integration ๋ง)
- ISO-8601 – ex) PT15M
Examples:
"PT20.345S" -- parses as "20.345 seconds"
"PT15M" -- parses as "15 minutes" (where a minute is 60 seconds)
"PT10H" -- parses as "10 hours" (where an hour is 3600 seconds)
"P2D" -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
"P2DT3H4M" -- parses as "2 days, 3 hours and 4 minutes"
"P-6H3M" -- parses as "-6 hours and +3 minutes"
"-P6H3M" -- parses as "-6 hours and -3 minutes"
"-P-6H+3M" -- parses as "+6 hours and -3 minutes"JavaShedLock + Redis ์ํ ์ฝ๋
ShedLock์ ์ด์ฉํ์ฌ ๋ ๊ฐ์ Task๊ฐ ํ ํ์์ ํ ๋ฒ์ฉ ์คํํ๋๋ก ๊ฐ๋จํ ์ํ์ฝ๋๋ฅผ ๊ตฌํํด ๋ณด์๋ค.
- spring boot docker compose๋ฅผ ์ด์ฉํ์ฌ ์ ํ๋ฆฌ์ผ์ด์ ์คํ ์ redis๋ฅผ docker์ ์ฌ๋ฆฌ๋๋ก ๊ตฌ์ฑํ์๋ค. spring boot docker compose์ ๋ํ ๋ด์ฉ์ ์๋ ํฌ์คํ ์ ์ฐธ๊ณ ํ๊ธฐ ๋ฐ๋๋ค.
- spring boot docker compose๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ๋ก์ปฌ์ docker ์์ง์ด ๊ตฌ๋๋์ด ์์ด์ผ ํ๋ค.
- docker compose CLI๋ฅผ ์ฌ์ฉํ ์ ์์ด์ผ ํ๋ค.
dependency
ShedLock + Redis๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ dependency๋ ๋ค์๊ณผ ๊ฐ๋ค.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-redis-spring</artifactId>
<version>5.10.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>5.10.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
XMLspring boot docker compose๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์ spring-boot-docker-compose๋ฅผ ๋ํ๋์๋ก ์ถ๊ฐํ์๋ค.
start.spring.io์์ ๋ํ๋์๋ก spring-boot-docker-compose์ spring-boot-starter-data-redis ๋ํ๋์๋ฅผ ํฌํจํ๋ฉด ์๋์ผ๋ก compose.yaml ํ์ผ์ ์์ฑํด ์ค๋ค. ๋ค์์ ์๋ ์์ฑ๋ compose.yaml ๋ด์ฉ์ด๋ค.
services:
redis:
image: 'redis:latest'
ports:
- '6379'YAMLScheduler
๋งค๋ถ๋ง๋ค ์คํ๋๋ ๋ ๊ฐ์ Task๋ฅผ ์ ์ํ์๋ค.
@Service
@EnableScheduling
@Slf4j
public class MyScheduler {
@Scheduled(cron = "0 * * * * *")
@SchedulerLock(name = "perMinuteScheduler", lockAtLeastFor = "50s", lockAtMostFor = "55s")
public void task1() {
log.info("task1 run");
}
@Scheduled(cron = "0 * * * * *")
@SchedulerLock(name = "perMinuteScheduler", lockAtLeastFor = "50s", lockAtMostFor = "55s")
public void task2() {
log.info("task2 run");
}
}Java์ค์ ๋ก๋ ๋ ๊ฐ์ ์ปจํ ์ด๋ ์ธ์คํด์ค๋ก ํ ์คํธ๋ฅผ ํด๋ด์ผ๊ฒ ์ง๋ง ์ฌ๊ฑด์ ๋์ผํ ShedLock name (perMinuteScheduler)์ ๊ฐ์ง ๋ ๊ฐ์ Task๋ฅผ ๊ฐ์ง๊ณ ์ํํด ๋ณด์๋ค.
LockProvider Configuration
Lock ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ LockProvider ๊ตฌ์ฑ์ ์ ์ํด์ผ ํ๋ค.
@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "PT1H")
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
return new RedisLockProvider(connectionFactory);
}
}Java@EnableSchedulerLock(defaultLockAtMostFor = “PT1H”)์์ defaultLockAtMostFor ์์ฑ์ @SchedulerLock ์ด๋
ธํ
์ด์
์ lockAtMostFor ์ด๋
ธํ
์ด์
์ด ์ง์ ๋์ง ์์ ๊ฒฝ์ฐ ์ ์ฉ๋ ๋ํดํธ duration์ด๋ค.
Redis๋ฅผ ์ ๊ธ ๋ฐ์ดํฐ ์ ์ฅ์๋ก ์ฌ์ฉํ ๊ฒ์ด๋ฏ๋ก RedisLockProvider๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํด์ผ ํ๋ค. RedisConnectionFactory๋ Spring Boot Auto Configuration์ ์ํด์ ์๋ ์ฃผ์
๋๋ค.
ShedLock logging
ShedLock ๋์ ํ์ธ์ ์ํด์ net.javacrumbs.shedlock ํจํค์ง๋ฅผ debug ๋ ๋ฒจ๋ก ์ค์ ํ์๋ค.
application.yml
logging:
level:
net.javacrumbs.shedlock: debugYAML์ ํ๋ฆฌ์ผ์ด์ ์คํ
์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํ๋ฉด spring boot docker compose๋ฅผ ํตํด์ compose.yaml ์ด docker compose์ ์ํด์ ์คํ๋๋ค.
INFO 10030 --- [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container spring-shedlock-redis-1 Created
INFO 10030 --- [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container spring-shedlock-redis-1 Starting
INFO 10030 --- [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container spring-shedlock-redis-1 Started
INFO 10030 --- [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container spring-shedlock-redis-1 Waiting
INFO 10030 --- [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container spring-shedlock-redis-1 HealthyPlaintext๋ก๊ทธ๋ฅผ ์ดํด๋ด ๋ฉด task1๊ณผ task2๊ฐ ๋งค๋ถ๋ง๋ค ๋ ์ค์ ํ ๋ฒ๋ง ์คํ๋จ์ ์ ์ ์๋ค.
//task2๊ฐ lock์ ํ๋ํ์ฌ ์คํํ๋ฉฐ, ์ ๊ธ์ด ์ ์ง๋ ์ ์๋ ์ต๋ ์๊ฐ๋ ํ์๋๋ค. (lockAtMostFor 55์ด)
2023-11-20T22:56:00.005+09:00 DEBUG : Locked 'perMinuteScheduler', lock will be held at most until 2023-11-20T13:56:55.001Z
2023-11-20T22:56:00.006+09:00 INFO : task2 run
//์์
์ด ์๋ฃ๋์๊ณ lockAtLeastFor์ ์ํด์ 50์ด๊ฐ lock์ ์ ์ง๋จ์ ์ ์ ์๋ค.
2023-11-20T22:56:00.009+09:00 DEBUG : Task finished, lock 'perMinuteScheduler' will be released at 2023-11-20T13:56:50.001Z
//task1์ lock์ ํ๋ํ์ง ๋ชปํ์ฌ ์คํ๋์ง ์๋๋ค.
2023-11-20T22:56:00.012+09:00 DEBUG : Not executing 'perMinuteScheduler'. It's locked.
...
//task1์ด lock์ ํ๋ํ์ฌ ์คํํ๋ฉฐ, ์ ๊ธ์ด ์ ์ง๋ ์ ์๋ ์ต๋ ์๊ฐ๋ ํ์๋๋ค. (lockAtMostFor 55์ด)
2023-11-20T23:08:00.007+09:00 DEBUG : Locked 'perMinuteScheduler', lock will be held at most until 2023-11-20T14:08:55.002Z
2023-11-20T23:08:00.007+09:00 INFO : task1 run
//์์
์ด ์๋ฃ๋์๊ณ lockAtLeastFor์ ์ํด์ 50์ด๊ฐ lock์ ์ ์ง๋จ์ ์ ์ ์๋ค.
2023-11-20T23:08:00.010+09:00 DEBUG : Task finished, lock 'perMinuteScheduler' will be released at 2023-11-20T14:08:50.002Z
//task2๋ lock์ ํ๋ํ์ง ๋ชปํ์ฌ ์คํ๋์ง ์๋๋ค.
2023-11-20T23:08:00.016+09:00 DEBUG : Not executing 'perMinuteScheduler'. It's locked.
...
Plaintextredis-cli๋ฅผ ํตํด์ lock ๋ฐ์ดํฐ๊ฐ redis์ ์ ์ฅ๋ ๊ฒ์ ํ์ธํด ๋ณด์.

- ๋ก๊ทธ ๊ธฐ๋ก ์๊ฐ์ KST ์๊ฐ์ด์ง๋ง Redis์ ๊ธฐ๋ก๋ ์๊ฐ์ UTC์์ ์ฃผ์ํ์.
- ์ ๊ธ ๋ฐ์ดํฐ Key๋ {key-prefix}:{environment}:{SchedulerLock-name} ํฌ๋งท์ผ๋ก ์์ฑ๋๋ฏ๋ก job-lock:default:preMinuteScheduler ์ด๋ฆ์ผ๋ก ์ ์ฅ๋์์์ ์ ์ ์๋ค.
- ์ ๊ธ ๋ฐ์ดํฐ๋ ์ ๊ธ์ด ํด์ ๋๋ฉด ์ญ์ ๋๋ค.
lockAtLeastFor ์์ฑ์ ์ง์ ํ์ง ์์ผ๋ฉด ๋ํดํธ PT0S๊ฐ ์ ์ฉ๋๋ค.
-> lockAtLeastFor ์์ฑ์ ์ง์ ํ์ง ์์ผ๋ฉด ํ์คํฌ ์์ ์ด ์๋ฃ๋๋ฉด lock์ ํด์ ๋๋ค.
๋ง์น๋ฉฐ
ShedLock์ ๋์
ํจ์ผ๋ก์จ ๋ถ์ฐ ํ๊ฒฝ์์ ์ค์ผ์ค๋ฌ ์ค๋ณต ์คํ ๋ฌธ์ ๋ฅผ ๊ฐ๋จํ๊ฒ ํด๊ฒฐํ ์ ์๋ค. ๋ง์ฝ ๋ ๋ณต์กํ Job ๊ด๋ฆฌ๊ฐ ํ์ํ๋ค๋ฉด Spring Batch๋ Quartz๋ฅผ ๊ณ ๋ คํด ๋ณด๋ ๊ฒ๋ ์ข๊ฒ ๋ค.
์ ์ฒด ์ํ ์ฝ๋๋ gitlab ์์ ํ์ธํ ์ ์๋ค.
์ฐธ๊ณ ๋งํฌ
https://github.com/lukas-krecan/ShedLock
