기존 배포 서버를 1개 사용하고 있었지만 최근 ECS로 배포 방법을 변경함에 따라 여러 컨테이너가 같은 DB 데이터에 접근할 수 있게 되었다. (사실 기존에도 개발용 서버를 배포하였기 때문에 상황은 동일하다...)
서버가 늘어날 수록 같은 데이터를 수정하는 스케줄러가 동시에 여러 개 시작되어 문제가 발생할 수 있기 때문에 이를 예방하기 위해 분산 락을 적용해야 할 필요성을 느꼈다.
특정 쿼리로 인해 발생할 수 있는 동시성 문제를 해결한다기보다 매일 정해진 스케줄러로 인한 문제를 예방하기 위한 것이므로 특정 서버 1개만 스케줄러를 실행해야 하는 상황이다. 즉 네임드 락을 적용하고자 한다.
Spring Data JPA를 사용하는 방법과 Redis를 사용하는 방법이 있는데, 기존에 Redis도 ECS 서비스로 운영하고 있기 때문에 Redis를 사용하는 방법을 선택했다.
1. 의존성 추가
dependencies {
// Redisson
implementation 'org.redisson:redisson-spring-boot-starter:3.27.0'
}
2. Redisson 빈 등록 (RedissonConfig)
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.password}")
private String password;
// 중략
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + host + ":" + port)
.setPassword(password);
return Redisson.create(config);
}
}
기존에 사용하던 RedisConfig에 RedissonClient를 추가한다.
3. 분산 락을 위한 RedisLockUtil
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisLockUtil {
private final RedissonClient redissonClient;
/**
* 분산 락을 활용하여 특정 작업 실행
*
* @param lockKey 락 키 (ex: "scheduler_lock")
* @param waitTime 락을 기다리는 시간 (초)
* @param leaseTime 락 유지 시간 (초)
* @param task 실행할 작업 (람다식으로 전달)
*/
public <T> void executeWithLock(String lockKey, long waitTime, long leaseTime, Supplier<T> task) {
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS)) {
try {
task.get();
} finally {
lock.unlock();
}
} else {
log.info("다른 서버에서 실행 중이므로 종료");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
코드 중복을 막고자 분산 락 관련 메소드가 있는 RedisLockUtil을 추가한다.
4. 스케줄러에 적용
@Slf4j(topic = "Operating Time Scheduler")
@Component
@RequiredArgsConstructor
public class OperatingScheduler {
private final OperatingService operatingService;
private final RedisLockUtil redisLockUtil;
@Scheduled(cron = "0 0 0 * * *") // 매일 자정마다
@EventListener(ApplicationReadyEvent.class)
public void updateOperatingTime() {
setState();
log.info("운영 시간 업데이트");
redisLockUtil.executeWithLock("lock", 1, 300, () -> {
operatingService.updateOperatingTime(dayOfWeek, isHoliday, isVacation, isEvenWeek);
return null;
});
}
}
기존에 사용하던 스케줄러 코드에 분산 락을 적용해준다.
5. 테스트 코드
- 락 획득 여부 확인
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class RedissonLockTest {
@Autowired
private RedissonClient redissonClient;
@Test
public void testLockAcquisition() throws InterruptedException {
String lockKey = "test_lock";
RLock lock = redissonClient.getLock(lockKey);
// 락을 획득할 수 있는지 확인
boolean isLocked = lock.tryLock(1, 5, TimeUnit.SECONDS);
assertThat(isLocked).isTrue(); // 정상적으로 락을 획득해야 함
// 락 해제
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
- 다중 스레드 테스트
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class RedissonMultiThreadTest {
@Autowired
private RedissonClient redissonClient;
private final String lockKey = "test_lock";
@Test
public void testConcurrentLocking() throws InterruptedException {
int threadCount = 5;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.execute(() -> {
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(1, 5, TimeUnit.SECONDS)) {
System.out.println("--- 락 획득: " + Thread.currentThread().getName());
Thread.sleep(1000); // 1초 동안 락 유지
} else {
System.out.println("--- 락 획득 실패: " + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println("--- 락 해제: " + Thread.currentThread().getName());
}
latch.countDown();
}
});
}
latch.await(); // 모든 스레드가 종료될 때까지 대기
executorService.shutdown();
}
}
'Spring & JPA' 카테고리의 다른 글
고급 매핑 - 1. 상속 관계 매핑 (0) | 2025.03.19 |
---|---|
연관관계 (0) | 2025.03.17 |
[고대로] static 변수와 영속성 컨텍스트 (0) | 2024.08.17 |
[고대로] 운영 시간 관리 (0) | 2024.08.17 |
알람 기능 구현 방법 (0) | 2024.05.05 |