| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- 문자열
- Java
- Array
- string
- javascript
- 정규식
- SpringBoot
- input
- Visual Studio Code
- IntelliJ
- 테스트자동화
- vscode
- 자바
- Eclipse
- 스프링부트
- 인텔리제이
- ArrayList
- js
- math
- 단위테스트
- 자바문법
- list
- junit
- 자바스크립트
- 배열
- java테스트
- HashMap
- junit5
- html
- CSS
- Today
- Total
어제 오늘 내일
[Spring Boot] "선착순 100명인데 101명이 당첨?" 동시성 이슈 해결: Redis 분산락 (feat. Redisson) 본문
[Spring Boot] "선착순 100명인데 101명이 당첨?" 동시성 이슈 해결: Redis 분산락 (feat. Redisson)
hi.anna 2026. 3. 21. 01:45
쇼핑몰이나 티켓팅 서비스를 개발할 때 가장 무서운 순간이 언제일까요? 바로 "재고 관리"입니다.재고 = 재고 - 1이라는 아주 간단한 로직도, 수천 명이 동시에 누르면 재앙(Race Condition)이 발생합니다.
오늘은 자바의 synchronized 키워드로는 해결할 수 없는 다중 서버 환경에서의 동시성 문제를 Redis와 Redisson 라이브러리를 사용해 완벽하게 해결해 보겠습니다.
1. 왜 동시성 문제가 생기나요? (Race Condition)
가장 흔한 재고 감소 로직을 볼까요?
// 재고 감소 로직 (문제 있음!)
public void decrease(Long id) {
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(1L); // 수량 1 감소
stockRepository.saveAndFlush(stock);
}
이 코드는 혼자 테스트할 땐 잘 됩니다. 하지만 쓰레드 A와 쓰레드 B가 동시에 들어오면 어떻게 될까요?
- 쓰레드 A: 현재 재고 100개 확인. (아직 안 줄임)
- 쓰레드 B: 현재 재고 100개 확인. (A가 줄이기 전이라 100개로 보임!)
- 쓰레드 A: 99개로 저장.
- 쓰레드 B: 99개로 저장.
결과적으로 2명이 샀는데 재고는 1개만 줄어드는 마법이 일어납니다. 이것이 바로 경쟁 상태(Race Condition)입니다.
2. 쓰면 안 되나요?
자바 개발자라면 synchronized 키워드를 먼저 떠올리실 겁니다.
// 자바의 기본 락
public synchronized void decrease(Long id) { ... }
하지만 이건 서버가 1대일 때만 통합니다.
요즘 서비스들은 트래픽 분산을 위해 서버를 여러 대(Scale-out) 띄우죠? synchronized는 각 서버(프로세스) 안에서만 동작하기 때문에, 서버 1의 락은 서버 2를 막지 못합니다.
그래서 우리는 모든 서버가 공통으로 바라보는 외부의 "락 관리자"가 필요합니다. 그게 바로 Redis입니다.
3. Redis 분산락 라이브러리: Lettuce vs Redisson
Redis 클라이언트에는 크게 두 가지가 있습니다.
- Lettuce: 스프링 부트 기본 라이브러리.
setnx명령어를 사용해 락을 구현하지만, 스핀 락(Spin Lock) 방식이라 Redis에 부하를 많이 줍니다. (계속 "락 풀렸니?" 물어봄) - Redisson: Pub/Sub 방식을 사용합니다. 락이 해제되면 "야, 락 풀렸어!"라고 알려주기 때문에 부하가 적고, 사용법이 훨씬 쉽습니다. (강력 추천!)
오늘은 Redisson을 사용하겠습니다.
4. 구현하기: Redisson으로 락 걸기
① 의존성 추가 ()
implementation 'org.redisson:redisson-spring-boot-starter:3.23.1'
② 서비스 로직 수정 ()
이제 비즈니스 로직 앞뒤로 락을 걸고 푸는 코드를 넣어줍니다.
@Service
@RequiredArgsConstructor
public class StockService {
private final RedissonClient redissonClient; // Redisson 주입
private final StockRepository stockRepository;
public void decrease(Long key, Long quantity) {
// 1. 락 이름 정의 (상품 ID마다 고유한 락 생성)
RLock lock = redissonClient.getLock("stock_lock_" + key);
try {
// 2. 락 획득 시도 (tryLock)
// waitTime: 락을 기다리는 시간 (10초)
// leaseTime: 락을 점유하는 시간 (1초 지나면 자동 해제)
boolean available = lock.tryLock(10, 1, TimeUnit.SECONDS);
if (!available) {
System.out.println("락 획득 실패! (사람이 너무 많아요)");
return;
}
// 3. ★ 핵심 로직 (안전 구역)
Stock stock = stockRepository.findById(key).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 4. 락 해제 (필수!)
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
코드 설명
getLock: 락 객체를 가져옵니다. 같은 이름("stock_lock_1")을 쓰면 같은 자물쇠를 공유하게 됩니다.tryLock(10, 1, ...):
- 첫 번째 인자(10초): 락을 얻기 위해 기다리는 시간. 10초 동안 못 얻으면
false반환. - 두 번째 인자(1초): 락을 얻고 나서 유지하는 시간. 이 시간이 지나면 서버가 죽어도 자동으로 락이 풀립니다. (데드락 방지)
finally { unlock }: 로직이 성공하든 에러가 나든 반드시 락을 반납해야 다음 사람이 들어올 수 있습니다.
5. 트랜잭션 주의사항 (AOP의 필요성)
위 코드에는 사실 미세한 구멍이 있습니다.@Transactional과 락을 같이 쓸 때, "트랜잭션이 커밋되기 전에 락이 먼저 풀려버리는" 문제가 생길 수 있습니다.
- 문제 상황:
- 락 해제 (
unlock) - 다른 쓰레드 진입 (재고 읽음)
- 트랜잭션 커밋 (DB 업데이트) -> 다른 쓰레드는 옛날 값을 읽게 됨!
- 해결책:
- 락을 거는 범위가 트랜잭션 범위보다 넓어야 합니다.
- 보통 Facade 패턴을 써서 락을 거는 클래스와 트랜잭션을 수행하는 클래스를 분리하거나, AOP를 직접 구현해서 해결합니다.
// Facade 패턴 예시
@Component
public class StockFacade {
public void decrease(Long id) {
// 1. 락 획득
try {
lock.tryLock(...)
stockService.decrease(id); // 2. 트랜잭션 메서드 호출
} finally {
lock.unlock(); // 3. 락 해제
}
}
}
마치며
오늘의 결론입니다.
- 동시성 이슈는 다중 서버 환경에서
synchronized로 해결할 수 없다. - Redis 분산락은 여러 서버가 공유하는 '외부 자물쇠' 역할을 한다.
- Redisson 라이브러리를 쓰면 Pub/Sub 방식으로 부하 없이 락을 구현할 수 있다.
이제 여러분의 서비스는 선착순 이벤트가 열려도, 재고가 꼬이지 않고 정확하게 100명에게만 혜택을 줄 수 있게 되었습니다.
다음 포스팅에서는 성능 최적화의 또 다른 축, "데이터 100만 건을 매일 밤마다 처리해야 한다면?" 대용량 데이터 처리를 위한 Spring Batch에 대해 알아보겠습니다.
도움이 되셨다면 좋아요와 댓글 부탁드립니다! 😊
