최근 조회수 동시성 문제로 인해 DB 업데이트 과정에서 발생하는 이슈를 해결하기 위해 Redis를 활용한 방식으로 수정했습니다. 처음에는 Redis 큐를 사용하여 조회수를 실시간으로 처리하려 했지만, 동시성 문제로 인해 새로운 방식으로 해결책을 마련했습니다.
첫 번째 시도는 Redis에서 조회수를 증가시키고, 이를 큐에 넣은 후 백그라운드 프로세스에서 큐를 확인하며 MySQL에 반영하는 방식이었습니다. 하지만 이 방법은 높은 트래픽 환경에서 큐가 쌓이면서 지연이 발생하거나, 동시에 여러 개의 조회수가 업데이트될 경우 일관성 문제를 일으킬 수 있었습니다.
@Service
@RequiredArgsConstructor
public class ProjectViewService {
private final RedisTemplate<String, String> redisTemplate;
private static final String VIEW_COUNT_KEY_PREFIX = "project:viewCount:";
private static final String VIEW_COUNT_QUEUE = "project:viewCountQueue";
public void incrementViewCount(Long projectId) {
String key = VIEW_COUNT_KEY_PREFIX + projectId;
redisTemplate.opsForValue().increment(key);
redisTemplate.opsForList().leftPush(VIEW_COUNT_QUEUE, projectId.toString());
}
}
@Component
@RequiredArgsConstructor
@Slf4j
public class RedisViewCountProcessor {
private final RedisTemplate<String, String> redisTemplate;
private final ProjectRepository projectRepository;
private static final String VIEW_COUNT_QUEUE = "project:viewCountQueue";
@PostConstruct
public void processViewCountUpdates() {
new Thread(() -> {
while (true) {
try {
// 큐에서 프로젝트 ID 하나 가져오기
String projectIdStr = redisTemplate.opsForList().rightPop(VIEW_COUNT_QUEUE);
if (projectIdStr != null) {
Long projectId = Long.valueOf(projectIdStr);
String key = "project:viewCount:" + projectId;
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
int viewCount = Integer.parseInt(value);
// MySQL에 조회수 반영
projectRepository.incrementViewCount(projectId, viewCount);
// Redis에서 해당 키 삭제
redisTemplate.delete(key);
}
}
} catch (Exception e) {
log.error("조회수 동기화 중 오류 발생", e);
}
}
}).start();
}
}
이후, Redis Set을 활용하여 더 효율적인 방식으로 문제를 해결했습니다. Set을 사용하여 중복 없이 프로젝트 ID를 추가하고, 이를 일정 주기로 MySQL에 반영하도록 수정했습니다. 이 방식은 조회수가 증가하는 트래픽에 따라 주기적으로 처리되므로 실시간 처리가 아닌, 일정한 시간 간격으로 일괄 업데이트가 이루어집니다.

Set에 추가하여 조회수를 추적합니다.@Scheduled 애너테이션을 이용해 4초마다 조회수를 일괄 처리합니다. 이를 통해 조회수가 실시간으로 반영되지 않더라도 안정적인 처리 성능을 보장합니다.이 방법은 실시간 업데이트가 아닌 일정 시간 간격으로 조회수를 처리하여 동시성 문제를 해결하고, Redis를 활용하여 성능을 향상시킨 방식입니다. 주기적으로 처리되는 방식은 높은 트래픽에서도 안정적으로 동작할 수 있어, 실시간 처리의 부담을 덜어주고 시스템의 확장성을 높였습니다.
아래는 두 가지 방식의 성능 및 특징을 비교한 표입니다. 각 방식의 장단점과 성능 차이를 간단하게 정리했습니다.
| 항목 | 초기 방식 (Redis 큐 사용) | 개선된 방식 (Redis Set과 주기적 처리) |
|---|---|---|
| 조회수 처리 방식 | Redis 큐에 프로젝트 ID를 넣고 백그라운드 스레드에서 순차적으로 처리 | Redis Set에 프로젝트 ID를 넣고 주기적으로 처리 |
| 실시간 처리 | 실시간으로 조회수 반영 가능, 하지만 큐에서 처리 시간이 소요될 수 있음 | 실시간 처리는 아니지만 일정 주기마다 조회수 반영 (4초 간격) |
| 동시성 처리 | 높은 동시성에서 동시성 문제가 발생할 수 있음 (큐에서 데이터를 꺼내는 순서 문제 등) | 동시성 문제는 해결됨. 중복 처리가 없으며 Set을 사용하여 효율적인 처리 가능 |
| 성능 | 큐에서 데이터를 처리하는 데 시간이 소요되어 높은 트래픽 시 지연 발생 가능 | 일정 주기로 조회수를 처리하므로 높은 트래픽에서 성능 안정성 높음 |
| 시스템 부하 | 실시간 큐 처리로 인한 시스템 부하가 크고, 트래픽이 많을 경우 큐가 쌓여 성능 저하 가능 | 일정 시간 간격으로 조회수를 처리하므로 부하 분산 가능 |
| 확장성 | 큐의 길이가 길어질 경우 성능 저하가 발생할 수 있음 | 주기적인 배치 처리로 확장성에 유리 |
| 에러 처리 | 큐에 대한 처리 도중 에러 발생 시, 해당 요청이 다시 큐에 남을 수 있어 재처리가 필요 | 주기적으로 배치 처리하면서 에러 발생 시, 전체 배치에서 처리 가능 |
| 복잡도 | 백그라운드 스레드와 큐를 사용하는 방식으로 다소 복잡 | 비교적 간단하며, Redis Set과 주기적 처리로 구현이 직관적 |
이 표는 두 방식의 성능과 특징을 비교하여, 각각의 상황에 적합한 선택을 할 수 있도록 도와줍니다.
주기적 처리 시간 최적화 (트래픽 기반 조정):
비동기 처리 및 이벤트 기반 처리:
확장성 개선 (분산 시스템 적용):
모니터링 및 알림 시스템 구축: