RedissonClient는 데이터베이스, 캐시, 메시지 브로커 등 일반적으로 사용되는 오픈 소스 인메모리 데이터 구조인 Redis에 사용할 수 있는 자바 클라이언트 라이브러리입니다. RedisClient는 Redis와 함께 사용하는데 도움을 주는 고급 인터페이스를 제공하는데, 분산 잠금, 스케줄링 등 기능을 제공합니다.
저는 RedisClient의 분산락 기능을 활용하여 해당 문제를 해결하였습니다.
먼저, Spring에서 RedisClient를 사용하기 위해서는 해당 라이브러리 의존성을 주입하고 config 설정을 해야 합니다.
implementation 'org.redisson:redisson-spring-boot-starter:3.17.7'
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6381");
return Redisson.create();
}
}
RedissonClient에서 분산락 설정을 위해 사용한 기능은 아래와 같습니다.
RLock: RLock은 여러 스레드 또는 프로세스 간에 공유 리소스에 대한 액세스를 동기화하는 방법을 제공하는 Redisson 분산락 기능입니다.
lock.tryLock(): RLock의 tryLock() 메서드는 즉시 잠금을 획득하려고 시도하여 잠금을 획득한 경우, true를 반환하고, 잠금을 획득하지 않은 경우 false를 반환합니다.
lock.unlock(): 해당 메소드는 이전에 획득한 잠금을 해제합니다. 보통 try-catch-finally의 finally에 작성하여 해당 락이 종료되도록 하여 데드락이 발생하는 것을 방지할 수 있습니다.
lock.isHoldByCurrentThread(): 잠금이 현재 스레드에 의해 유지되고 있는지 확인하여, 맞다면 true, 아니라면 false를 반환합니다.
// 좋아요 버튼을 눌렀을 때
@PostMapping("/posts/{postId}/like")
public ResponseEntity<?> like(@PathVariable("postId") Long postId, Authentication authentication) {
// Redis 분산 락을 postId에 기반하여 생성
RLock lock = redissonClient.getLock("post-like-lock:" + postId);
try {
// 락을 5초 동안 시도하고, 락을 얻으면 3초 후 자동 해제
boolean isLocked = lock.tryLock(5, 3, TimeUnit.SECONDS);
if (isLocked) {
if (authentication != null && authentication.isAuthenticated()) {
String username = authentication.getName();
Optional<User> userOptional = userService.findByUserName(username);
if (userOptional.isPresent()) {
User user = userOptional.get();
Optional<Post> postOptional = postService.getPostById(postId);
if (postOptional.isPresent()) {
Post post = postOptional.get();
boolean liked = likeService.like(post, user);
Long likeCount = likeService.countByPostId(postId);
String postOwnerUsername = postService.getPostOwnerUsername(postId);
if (!username.equals(postOwnerUsername)) {
notificationService.createNotification(postOwnerUsername, username + "님이 게시글에 좋아요를 눌렀습니다.");
}
return ResponseEntity.ok().body(new LikeResponse(liked, likeCount));
}
}
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("사용자를 찾을 수 없습니다.");
} else {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("잠금 획득에 실패했습니다. 잠시 후 다시 시도해 주세요.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("잠금 중 오류가 발생했습니다.");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}