TL;DR
좋아요 기능 개발 시 중복 요청으로 인한 문제를 방지하기 위해 DB와 애플리케이션 로직으로 멱등성을 구현했다.
하지만 여전히 동시성 문제가 남아있어 캐싱, 메시지 큐, 분산락 등의 도입을 고민 중이다.
좋아요 기능과 중복 요청 문제
사이드 프로젝트에서 좋아요 기능을 개발하면서 중복 요청 문제를 고민하게 되었다.
사용자가 버튼을 여러 번 누르거나 네트워크가 불안정하다면 요청이 반복될 가능성이 있기 때문이다.
현재 좋아요 도메인은 좋아요 등록 시 LIKE 테이블에 userId 와 productId 로 등록하고, 취소 시에는 deletedAt 컬럼으로 soft delete 하는 방식을 사용하고 있었다. 하지만 단순히 DB에 유니크 제약 조건만 설정하면 중복 요청 발생 시 DB에서 예외가 발생하고, 이를 애플리케이션 로직에서 별도로 처리해야 하는 문제가 있었다.
따라서 최대한 DB까지 가지 않고 애플리케이션 로직에서 중복 요청을 방지할 수 있도록 설계하는 것이 필요했다.
멱등성이 뭔데?
멱등성이란 같은 요청이 여러 번 반복되더라도 한 번 처리한 결과와 동일한 상태를 유지하는 것을 말한다.
좋아요 기능을 예로 들면 사용자가 좋아요 버튼을 여러 번 눌러도 실제 좋아요 수는 한 번만 증가한 상태로 유지되어야 한다는 것이다.
내가 이번 좋아요 기능 설계에서 고민한 부분도 바로 이 멱등성이다.
어떻게 처리할까?
현재 프로젝트에서는 좋아요 등록과 취소 API가 분리되어 있었기 때문에, 우선 DB와 애플리케이션 로직만 사용해 멱등성을 처리해 보기로 했다.
좋아요 등록 시에는 좋아요가 이미 있는지 확인하고 아래와 같이 처리했다.
- 없다면 추가
- 있다면 무시
- soft delete 상태라면 복구
@Transactional
public boolean add(Like like) {
Like exist = likeRepository.findDetail(like);
if (exist == null) {
likeRepository.save(like);
return true;
}
if (exist.isDeleted()) {
exist.restore();
return true;
}
return false;
}
또한 조회 성능 향상을 위해 상품 테이블에 좋아요 개수를 추가했는데, 실제로 좋아요 개수 증가 작업은 DB에 데이터가 진짜 추가된 경우에만 처리하도록 했다.
public LikeInfo.Main add(LikeCommand.Main command) {
boolean isNew = likeService.add(command.toDomain());
if (isNew) {
productService.increaseLike(command.getProductId());
}
return LikeInfo.Main.from(command.getProductId(), true);
}
앞으로 개선할 방법?
현재 방식은 DB와 애플리케이션 로직에서만 멱등성을 보장하는 구조이기 때문에 동시성 문제는 여전히 남아있다. 동시에 여러 요청이 들어올 때 find → insert 사이에서 Race Condition이 발생해 트래픽이 많아지면 정확성과 성능 문제가 발생할 가능성이 있기 때문이다.
그래서 더 확실한 멱등성 보장을 위해 다음과 같은 기술 도입을 고민하고 있다.
- Redis와 같은 캐시 시스템을 활용해 좋아요 상태를 캐싱하고 비동기 처리하기
- Kafka와 같은 메시지 큐를 이용해 비동기 처리하기
- 분산락이나 낙관적락을 사용해 동시성 문제 해결하기
실제로 실무에서는 좋아요 처리 결과나 개수 변화가 사용자에게 즉각적으로 중요한 정보는 아니기 때문에, 요청 응답을 기다리지 않고 바로 UI를 업데이트하는 "낙관적 업데이트(Optimistic Update)" 방식을 주로 사용한다는 자료를 보았다. 따라서 화면에서는 낙관적 업데이트와 함께 debounce 나 throttling 기법을 적용한다고 가정하고, 서버에서는 비동기 처리 방식을 도입하는 방향으로 설계를 고민하고 있다.
사용자 입장에서도 결과를 기다리지 않고 바로 반응을 얻을 수 있어 사용자 경험 개선에도 좋을 것이라 생각된다.
마무리
처음에는 멱등성을 간단한 조건 분기 정도로만 생각했는데, 실제로 작업을 진행하다 보니 동시성, 확장성, 성능 등 여러 측면을 함께 고려해야 하는 복잡한 문제였다. 작은 기능 하나라도 멱등성을 처음부터 충분히 고민하고 설계하는 것이 중요하다는 생각을 다시 한번 하게 됐다.
참고
https://docs.tosspayments.com/blog/what-is-idempotency
http://tecoble.techcourse.co.kr/post/2023-08-15-how-to-improve-ux-with-optimistic-update/
'Study > Architecture' 카테고리의 다른 글
| 주문과 결제, 어디까지 한 트랜잭션으로 묶어야 할까? (2) | 2025.08.29 |
|---|---|
| 장애 대응 시스템 구축하기 (0) | 2025.08.22 |
| 캐시 구조 개선 (0) | 2025.08.22 |
| 읽기 성능 개선 보고서 (4) | 2025.08.15 |
| Validation, 어디에 어떻게 두어야 할까? (3) | 2025.07.18 |