2.Redis를 사용한 동시성 제어
동시성 제어란?
- 동시성 제어는 여러 사용자 또는 프로세스가 동시에 데이터에 접근할 때 데이터의 일관성과 무결성을 유지하기 위한 기술이다.
분산 락을 이용한 동시성 제어
게시글 CRUD 를 간단하게 구현하고 게시글에 좋아요 기능을 추가
parallel() 메서드를 사용하여 병렬 처리를 통해 좋아요 기능 수행.
각 좋아요 클릭 이벤트가 게시글에 영향을 주기 전에 분산 락을 획득하여 데이터 일관성 유지.
처리가 끝난 후 락 해제하여 다음 이벤트 처리 가능하도록 함.
사용한 라이브러리
- redissonClient
실습코드
Post Entity
@Entity
@Getter
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
private int likeCount;
@CreatedDate
private LocalDateTime createdAt;
public Post(PostRequest postRequest) {
this.title = postRequest.getTitle();
this.content = postRequest.getContent();
this.likeCount = 0;
}
public void updatePost(PostRequest postRequest){
this.title = postRequest.getTitle();
this.content = postRequest.getContent();
}
public void setCount(int likeCount){
this.likeCount = likeCount;
}
}
Post Service(좋아요 관련 기능)
//좋아요 초기화
@Transactional
public void reset(){
Post post = postRepository.findById(1L).orElseThrow(()->new IllegalArgumentException("게시글이 없습니다."));
post.setCount(0);
postRepository.save(post);
}
//레디스 락을 이용한 좋아요 증가
//분산 락을 획득하는 메서드
public PostResponse updateLikeUsingLock(Long id) {
//락 키
RLock lock = redissonClient.getFairLock(LOCK_KEY);
boolean isLocked = false;
try {
// 락 획득 시도
isLocked = lock.tryLock(10, 60, TimeUnit.SECONDS);
if (isLocked) {
// 락 획득에 성공하면, 트랜잭션을 포함하는 데이터베이스 작업을 수행
return postLikeService.updateLikeCount(id);
} else {
// 락 획득에 실패하면 예외 발생
throw new IllegalStateException("잠금을 획득하지 못했습니다.");
}
} catch (InterruptedException e) {
// 현재 스레드가 잠금을 획득하는 동안 인터럽트 발생. 스레드에 인터럽트 상태를 설정하고 예외를 던짐.
Thread.currentThread().interrupt();
throw new IllegalStateException("잠금 획득 중단", e);
} finally {
if (isLocked) {
// 반드시 락 해제
lock.unlock();
}
}
}
//레디스 캐시 데이터 삭제
public void deleteCache(Long postKey) {
String Key = "post: " + postKey;
stringRedisTemplate.delete(Key);
}
//좋아요 출력 코드
public void printCount() {
System.out.println("\n\n\n====================\n");
System.out.println(postRepository.findById(1L).orElseThrow().getLikeCount());
System.out.println("\n====================\n\n\n");
}
테스트 코드
@SpringBootTest
public class PostServiceTest {
private PostRequest postRequest;
@Autowired
private PostService postService;
@BeforeEach
void set() {
postRequest = new PostRequest("test", "content");
postService.create(postRequest);
}
@AfterEach
void reset() {
postService.reset();
}
@Test
@DisplayName("단순 좋아요 증가 코드")
void test() {
IntStream.range(0, 100).parallel().forEach(i -> postService.updateLike(1L));
System.out.println("\n\n\n\n[테스트]");
postService.printCount();
}
@Test
@DisplayName("동시성 제어")
void test2() {
IntStream.range(0, 100).parallel().forEach(i -> postService.updateLikeUsingLock(1L));
System.out.println("\n\n\n\n[테스트]");
postService.printCount();
}
}
결과
단순 좋아요 증가 코드 결과
예상 좋아요 수: 100
실제 좋아요 수: X (17)
분석: 동시성 제어가 없을 경우 X회만큼의 좋아요가 정상 반영됨. 이는 병렬 처리 중 발생하는 경쟁 상황에 의해 좋아요 수가 손실됨을 알수있음.
동시성 제어 좋아요 증가 코드 결과
예상 좋아요 수: 100
실제 좋아요 수: Y (100)
분석: 분산 락을 사용한 동시성 제어가 올바르게 작동하여, 모든 병렬 처리가 정확하게 반영되어 Y회의 좋아요가 정상 반영됨.
개발 중 직면한 문제점
- 결과 값이 100이 나와야 하는데 50이 나오는 문제 발생
해결책
기존에는 락 획득과 데이터 처리 로직이 함께 있어 동시성 문제가 발생하여 결과가 50이 나왔다.
데이터 처리 로직을 별도의 서비스 클래스로 분리하고 트랜잭션을 적용하여 동시성 제어와 데이터 일관성을 보장함으로써 문제를 해결했다.
@Service
@RequiredArgsConstructor
public class PostLikeService {
private final PostRepository postRepository;
@Transactional
public PostResponse updateLikeCount(Long id) {
Post post = postRepository.findById(id)
.orElseThrow(() -> new IllegalStateException("Post not found with id: " + id));
post.setCount(post.getLikeCount() + 1);
postRepository.save(post);
return new PostResponse(post);
}
}





