들어가며
MSA(Microservices Architecture) 환경에서 개발을 하다 보면 한 가지 자주 마주하게 되는 문제가 있습니다.
바로 여러 서버 인스턴스가 동시에 동일한 스케줄러를 실행하는 상황입니다.
제가 개발 중인 음악 크라우드펀딩 플랫폼에서도 같은 문제가 발생했습니다.
“프로젝트 시작 스케줄러”, “프로젝트 종료 스케줄러”, “Outbox 이벤트 재시도 스케줄러”가 서버 2대 이상에서 동시에 실행되면서, 동일한 프로젝트가 중복 처리되는 상황이 생긴 것입니다. 이런 경우 데이터 정합성이 깨지고, 불필요한 중복 작업으로 인해 시스템 부하가 증가할 수 있습니다.
이 문제를 해결하기 위해 다양한 방식을 검토한 끝에, Redis 기반의 Redisson 분산 락을 도입하게 되었습니다. 아래에서는 이를 AOP 기반으로 어떻게 적용했는지 소개드리려고 합니다.

Redisson을 선택한 이유
분산 락을 구현하는 방법은 여러 가지가 있습니다.
예를 들어 비교적 간단하게 적용할 수 있는 ShedLock도 있고, 스케줄러를 별도의 배치 서버로 분리하는 방식도 있습니다.
하지만 저희 프로젝트는 Redis를 이미 도입하려 하고 있었기 때문에, 이를 적극 활용하는 편이 장기적으로 확장성 측면에서 더 적합하다고 판단했습니다.
또한 스케줄러뿐만 아니라 일반 API에도 락을 걸어야 할 가능성이 있어, 다양한 상황에서 재사용할 수 있는 Redisson이 좋은 선택이었습니다.
설정이 다소 번거롭다는 단점은 있지만, 한 번 구조를 잡아두면 재사용성과 확장성이 뛰어나기 때문에 충분히 투자할 가치가 있다고 생각했습니다.
AOP 기반으로 분산 락 적용하기
분산 락을 사용할 때는 보통 다음과 같은 반복적인 작업이 필요합니다.
- 락 키 생성
- 락 획득 시도
- 처리 후 락 해제
이 작업을 매번 수동으로 작성하게 되면 코드 중복도 많아지고, 실수로 unlock을 누락하는 등의 문제가 생길 수 있습니다.
그래서 저는 이런 공통 로직을 AOP로 분리하여, 애플리케이션 전반에서 간단하게 락을 적용할 수 있는 구조를 만들었습니다.
AOP 기반으로 구현하면, 비즈니스 로직에는 락 관련 코드가 전혀 등장하지 않고,
메서드에 @DistributedLock만 붙이면 자연스럽게 락이 적용되기 때문에 유지보수성도 크게 향상됩니다.
Redisson 설정 및 주요 파라미터
Redisson을 적용하려면 먼저 의존성 추가와 설정이 필요합니다. spring-boot 프로젝트에서는 Redisson 클라이언트를 빈으로 등록해 두면 편리하게 재사용할 수 있습니다. 설정에서 특히 고민한 값들은 connectionPoolSize, timeout, retryAttempts, retryInterval 같은 항목들이었습니다.
저는 동시 처리 작업 수를 고려해 커넥션 풀을 5로 잡았고, 네트워크 문제가 생겼을 때 빠르게 실패 처리하도록 타임아웃을 3초로 설정했습니다. 일시적인 네트워크 장애를 대비해 재시도는 3회, 재시도 간격은 1.5초 정도로 두어 지수 백오프 효과를 기대했습니다.
이런 설정은 처음부터 완벽할 필요는 없고, 운영 환경에서 RTT(왕복 지연)나 Redis의 안정성에 따라 조정하면 됩니다. 중요한 것은 기본값을 너무 관대하게 잡아 데드락이나 긴 대기로 서비스 응답성이 떨어지지 않게 하는 것입니다.
의존성 추가

RedissonConfig 구성

AOP 기반 분산 락 설계 포인트
분산 락을 AOP로 구현할 때 설계상으로 중점을 둔 부분들입니다.
먼저 락 키는 정적 문자열이 아니라 SpEL을 통해 동적으로 생성할 수 있게 했습니다. 이를 통해 API 요청이나 스케줄러별로 식별자를 유연하게 만들 수 있습니다. 그리고 락을 얻기 위해 기다리는 최대 시간(waitTime)과 락을 자동 해제하는 시간(leaseTime)을 옵션으로 열어두었습니다. waitTime이 너무 길면 요청이 블로킹되므로 짧게 설정하고, leaseTime은 작업이 정상적으로 완료될 것으로 예상되는 시간보다 약간 여유 있게 잡는 것이 안전합니다. 마지막으로 API에서 트랜잭션 처리를 별도로 하고 싶을 때를 대비해 useTransaction 플래그를 두어, 트랜잭션 커밋 시점과 락 해제 시점을 조절할 수 있게 했습니다.
SpEL 파싱은 CustomSpringELParser로 분리했고, 파싱 실패 시 null을 반환하도록 했습니다. 호출부에서는 null 결과를 명확한 예외로 처리해 문제를 빨리 발견할 수 있도록 했습니다.
핵심 AOP 로직에서는 tryLock()을 사용했습니다. lock()처럼 무한 대기하는 방식은 서비스 가용성 측면에서 위험할 수 있어, 일정 시간 대기 후 실패하면 다음 시도로 넘기도록 설계하는 편이 더 안전하다고 판단했습니다. 락을 안전하게 해제하기 위해서는 isHeldByCurrentThread() 체크를 통해 현재 스레드가 락을 보유하고 있을 때만 unlock()을 호출하도록 했고, IllegalMonitorStateException을 잡아서 로그로 남기되 시스템 흐름을 깨지 않도록 했습니다.
DistributedLock.java

CustomSpringELParser.java

DistributedLockAop.java


AopForTransaction(선택적 사용 사례)
API 엔드포인트에서 락을 사용하면서 동시에 트랜잭션 경계를 조절해야 하는 경우가 있습니다. 예를 들어 하나의 요청에서 DB 상태를 변경하고, 그 변경이 커밋된 이후에 락을 해제해야 다른 요청이 최신 데이터를 보게 하고 싶을 때입니다. 이런 경우 useTransaction = true로 설정하면 AOP가 트랜잭션 커밋 후에 실제 비즈니스 로직을 실행하도록 조정하는 패턴을 적용할 수 있습니다. 이 방식은 데이터 일관성을 보장하는 데 유리하지만, 구현과 테스트가 조금 복잡할 수 있으니 필요한 곳에만 선택적으로 적용하는 편이 좋습니다.

스케줄러 적용 사례
실제 운영 스케줄러에 적용한 예도 공유드립니다. 프로젝트 시작과 종료 관리 스케줄러는 하루에 한 번 새벽 4시에 실행하도록 했고, 각 스케줄러에는 명확한 락 키를 부여했습니다. 예를 들어 funding:lock:scheduler:startScheduledProjects 같은 키를 사용하면 어떤 작업이 락을 걸고 있는지 한눈에 파악할 수 있어 운영상 유리합니다. LeaseTime은 정산 API 호출이나 환불 같은 외부 연동 시간까지 고려해 30분 정도로 여유 있게 잡았습니다. 이렇게 하면 작업 중 시스템에 장애가 나더라도 leaseTime이 지나면 락이 자동 해제되어 다음 스케줄에서 다시 시도할 수 있습니다.
Outbox 재시도 스케줄러는 5분 단위로 실행되기 때문에 leaseTime은 그보다 조금 짧게, 4분으로 잡았습니다. 이 스케줄러는 재시도 정책과 최대 재시도 횟수(예: 5회)를 함께 두어 무한 재시도가 발생하지 않도록 했습니다. 최대 재시도에 도달하면 이벤트 상태를 FAILED로 바꾸고 관리자 알림 등 별도 대응을 하도록 설계했습니다.

락 키 설계와 네임스페이스
락 키 네이밍은 가능한 한 명확하게 만들었습니다. {service}:lock:{resource}:{identifier} 형식으로 통일하면 로그나 Redis 키를 보고 어떤 서비스의 어떤 리소스인지 바로 알 수 있습니다. 테스트나 로컬 환경에서는 별도의 Redis 인스턴스를 사용하므로 환경별 prefix를 코드에 넣지 않아도 무방하다고 판단했습니다. 이는 코드 단순성을 유지하는 쪽을 택한 결정입니다.
향후 개선 계획
우선 모니터링부터 강화하려고 합니다. 락 획득 실패를 Slack으로 알림 받게 하고, 작업 실행 시간에 대한 메트릭을 수집해 leaseTime과 스케줄 주기를 더 합리적으로 조정할 생각입니다. 그다음 단계로는 Kafka를 활용한 작업 분산을 검토하고 있습니다. Kafka를 도입하면 작업 병렬 처리와 장애 격리, 자동 재시도 같은 이점을 얻을 수 있습니다. 최종적으로는 필요 시 배치 서버를 분리해 완전한 격리와 독립적인 스케일링을 구현할 계획입니다.
마치며
지금 단계에서는 Redisson 기반의 분산 락이 MVP 수준에서는 가장 균형 잡힌 선택이라고 생각합니다. 구현 난이도와 유지보수성, 확장성 측면에서 장점을 가져가기 쉬우며, 필요 시 Kafka나 Spring Batch로 자연스럽게 이동할 수 있는 설계적 유연성도 확보할 수 있습니다.
이 글이 도움이 되셨다면 좋겠습니다. 문장 톤이나 구성(예: 기술 블로그 스타일, 발표 자료용 요약 등)을 더 바꾸고 싶으시면 원하시는 방향을 알려주십시오. 더 매끄럽게 다듬거나, 코드 설명을 추가하거나, 운영 사례를 더 보강해 드리겠습니다.
참고 자료
긴 글 읽어주셔서 감사합니다! 궁금하신 점이나 개선 제안이 있으시면 댓글로 남겨주세요. 😊
'Spring > MSA' 카테고리의 다른 글
| 펀딩 API 성능 개선기: TPS 61에서 100으로 (0) | 2025.12.10 |
|---|---|
| AWS ECS 기반 MSA 구축 시 서비스 디스커버리 방식 비교 (0) | 2025.11.26 |
| Java 21 Virtual Thread가 만드는 새로운 조합: MSA 서비스 간 통신의 4가지 선택지 (0) | 2025.09.18 |
| MSA에서 WebClient와 Reactive Resilience4j 선택의 기술적 이점 (0) | 2025.09.18 |