1. 개요
곽두철 프로젝트를 진행하면서 수강신청을 하는 상황에서 아래와 같은 상황이 일어날 것이라고 예상하였다.
정원이 25명인데 24명이 신청되어있는 상황이라고 가정해 보자.
이때 만약, 2명이 동시에 수강신청을 한다고 가정하면 정원이 26명이 될 것이다.
또 다른 상황으로는 3명이 신청되어 있는 상태인데 2명이 동시에 수강신청을 한다고 가정해 보자.
그러면 4번 id로 2명이 신청될 것이다.
즉, 정원을 초과하여 신청이 되는 경우, 같은 id로 동시에 수강신청이 되는 경우이다.
MySQL로도 Lock을 사용할 수 있지만, Lock의 경우 휘발성 데이터라고 판단하여 Redis를 사용하는 게 더 효율적이라고 생각하였기 때문에, Redis의 Lock을 사용해 동시성 처리를 해보려고 한다.
2. Redis Lock
Redis의 Lock은 보통 Lettuce의 setNX 명령어를 활용한 Lock과, Pub/Sub 기능을 통한 Lock으로 구현을 한다. 이 중에서 Lettuce의 setNX를 활용하여 동시성 제어를 해보려고 한다.
Lettuce의 setNX 명령어는 Redis에서 "SET if Not eXists"의 약자로, 키가 존재하지 않을 때에만 값을 설정하는 명령어다. 이를 활용하여 Lock을 구현할 수 있다.
간단히 말해, setNX는 특정 키를 만들고 그 키가 이미 존재하지 않는 경우에만 값을 설정한다. 따라서 먼저 해당 키를 만든 클라이언트가 Lock을 획득하고, 다른 클라이언트들은 그 키가 이미 존재하기 때문에 Lock을 획득하지 못하게 된다.
예를 들어, "A"라는 사용자가 먼저 수강신청을 하면, 그에 해당하는 키를 만들어 Lock을 획득한다. Lock을 획득한 사용자는 수강신청을 진행하고, 작업이 끝나면 Lock을 삭제하여 다른 사용자들이 접근할 수 있도록 한다. 그러나 Lock을 획득하지 못한 다른 사용자는 예외가 발생하거나, 다른 작업을 기다려야 한다.
이렇게 동작하는 원리를 redis cli를 통해 살펴보도록 하겠다.
127.0.0.1:6379> setnx 1 lock
(integer) 1 => Lock 획득
127.0.0.1:6379> setnx 1 lock
(integer) 0 => 1로 된 Lock이 존재하므로 Lock 획득 실패
127.0.0.1:6379> del 1
(integer) 1 => Lock 삭제
127.0.0.1:6379> setnx 1 lock
(integer) 1 => 1로 된 Lock이 존재하지 않으므로 Lock 획득
3. Lock 적용 코드
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
먼저 의존성을 받아준다.
starter-data-redis안에 Lettuce도 포함되어 있다.
RedisService
@Service
public class RedisService {
private final RedisTemplate<String, String> redisTemplate;
public RedisService(final RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void delete(final String key) {
redisTemplate.delete(key);
}
public boolean setNX(final String key, final String value, final Duration duration) {
return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, duration));
}
}
여기서 setNX 메서드를 보면 레디스에 저장된 key가 존재하지 않는 경우에 true를 반환하는 걸 볼 수 있다.
MemberShipService
@Service
@RequiredArgsConstructor
@Transactional
public class MemberShipService {
private final MemberShipRepository memberShipRepository;
private final ProductRepository productRepository;
private final UserRepository userRepository;
private final RedisService redisService;
public Long save(final Long userId, final Long productId) {
final Product product = productRepository.findById(productId).orElseThrow();
final User user = userRepository.findById(userId).orElseThrow();
final String key = Long.toString(userId);
if (redisService.setNX(key, "apply", Duration.ofSeconds(5))) {
final Long id = memberShipRepository.save(MemberShip.of(user, product, product.getCount())).getId();
redisService.delete(key);
return id;
}
throw new IllegalArgumentException();
}
}
수강신청하는 메서드이다. setNX메서드를 통해 key가 존재하지 않으면 true를 반환함으로써 조건문이 통과되고 수강신청이 된다.
만약 false를 반환하면 예외가 발생한다.
'WEB' 카테고리의 다른 글
HTML이 웹 브라우저에서 어떻게 작동할까? (1) | 2024.08.26 |
---|---|
전략 패턴을 사용해 로그인 추상화 하기 (0) | 2024.08.08 |
@NoArgsConstructor 액세스 레벨을 PROTECTED로 하는 이유 (1) | 2024.06.09 |
Redis 내부동작 파헤치기 (0) | 2024.05.05 |
인터셉터와 리졸버 (0) | 2024.04.12 |
JWT 활용기 (0) | 2024.04.12 |
Oauth를 사용해 카카오 로그인 구현 (0) | 2024.04.12 |
리사이징 적용기 with Marvin (0) | 2024.03.14 |