
1. 초난감 주문 메서드
@Service
@RequiredArgsConstructor
public class OrderService {
private final RedissonClient redissonClient;
private final StockRepository stockRepository;
public void decreaseStock(Long productId, int quantity) {
RLock lock = redissonClient.getLock("stock:" + productId);
try {
boolean acquired = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!acquired) {
throw new RuntimeException("락 획득 실패");
}
// 비즈니스 로직
Stock stock = stockRepository.findById(productId).orElseThrow();
stock.decrease(quantity);
stockRepository.save(stock);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
위의 로직은 재고를 차감하는 로직입니다. 우리는 재고를 차감하기 위해 보통 비관적 락을 활용하는 경우도 있지만 비관적 락은 조회 성능에도 영향을 주기 때문에 분산락을 활용하는 예시를 만들어보았습니다. 위의 로직은 보시다시피 비즈니스 로직 외에도 많은 책임을 가지고 있습니다.
2 . 변하는 로직과 변하지 않는 로직의 분리
코드를 보면 알 수 있듯이 변하는 영역과 변하지 않는 영역이 보일 것입니다. 변하는 영역은 재고를 차감하는 비즈니스 로직일 것이고 잘 변하지 않는 로직은 레디스를 활용하여 락을 획득하고 해제하는 영역일 것입니다.
각 영역을 분리하는 이유가 중요한 이유는 자주 변하지 않는 영역은 다른 영역에서 중복하여 사용할 수 있을 가능성이 높고 자주 변하는 영역으로 인해 비즈니스 로직의 복잡성을 높이기 때문입니다. 변하는 영역을 분리하면 어떻게 구현을 할 수 있을까요?
@Component
@RequiredArgsConstructor
public class RedisLockTemplate {
private final RedissonClient redissonClient;
// 템플릿: 변하지 않는 부분
public <T> T execute(String lockKey, Supplier<T> action) {
RLock lock = redissonClient.getLock(lockKey);
try {
boolean acquired = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!acquired) {
throw new LockAcquisitionException("락 획득 실패: " + lockKey);
}
return action.get(); // 변하는 부분 실행
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LockAcquisitionException(e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock(); // 리소스 반환 보장
}
}
}
}
// 사용하는 쪽: 비즈니스 로직만 전달
public void decreaseStock(Long productId, int quantity) {
lockTemplate.execute("stock:" + productId, () -> {
Stock stock = stockRepository.findById(productId).orElseThrow();
stock.decrease(quantity);
return stockRepository.save(stock);
});
}
변경된 코드를 이제 봅시다. 템플릿 쪽 코드를 보면 코드 흐름의 제어가 역전이 되어 OrderService가 락 흐름을 직접 제어하는 것이 아니라 템플릿에서 흐름이 제어가 된다는 것을 알 수 있습니다. 저희는 템플릿에게 비즈니스 로직만 전달한다면 락의 생성과 해제까지 전부 템플릿에서 제어하고 비즈니스 로직 쪽은 그저 락이 잡힌동안 어떤 행동을 해야되는지만 전달하도록 변화하였습니다.
만약 좀 더 추상화 시켜 어노테이션을 기반으로 구현한다면 다음과 같이 구현하여 활용할 수 있을 것입니다. 중요한 것은 변하는 영역과 변하지 않는 영역을 분리하여 재사용가능한 코드를 만들어 분리하고 잘 변하는 영역의 복잡성을 최소화하는 것입니다.
@DistributedLock(key = "'stock:' + #productId")
public void decreaseStock(Long productId, int quantity) {
Stock stock = stockRepository.findById(productId).orElseThrow();
stock.decrease(quantity);
stockRepository.save(stock);
}
정리
토비의 스프링에서는 추상화된 패턴을 전략 패턴을 활용하여 구현했지만 현대 자주 사용되는 패턴 중 하나인 람다를 활용하여 구현하였습니다. 그 이유는 전략 패턴 예시를 살펴보면서 실무에서 자주 사용되는 패턴을 활용하는 것이 좀 더 오래 기억에 남을 수 있을 것 같아 다음과 같은 패턴을 활용하여 구현하였습니다.
람다는 어떻게 보면 전략 패턴의 경량화 시켜 구현된 구현이라고 볼 수 있습니다. 전략 패턴과 람다 패턴 모두 행동에 대한 정의는 외부로 부터 전해진다는 점이 같고 다른 점은 람다는 호출 시점에 전략이 정해지고 전략 패턴은 내부 클래스의 구현에 따라 정해지고 여러 상태와 메서드를 가질 수 있다는 점이 차이점이 될 수 있을 것 같습니다.
'spring' 카테고리의 다른 글
| 토비의 스프링 7편 - 스프링 예외 처리의 철학 (1) | 2026.01.28 |
|---|---|
| 토비의 스프링 정복하기 6편 - 제어의 역전 (0) | 2026.01.25 |
| 토비의 스프링 정복하기 4편 - 테스트 작성하는 법 (1) | 2026.01.17 |
| 토비의 스프링 정복하기 3편 - 테스트의 필요성 (0) | 2026.01.14 |
| 토비의 스프링 정복하기 2편 - 제어의 역전 (1) | 2026.01.11 |