Redis를 활용한 조회수 동시성 문제 처리

김상진 ·2025년 4월 4일

Problem Solving

목록 보기
4/10

조회수 관련 이슈 해결을 위한 Redis를 활용한 동시성 문제 처리

최근 조회수 동시성 문제로 인해 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과 주기적 처리)

이후, Redis Set을 활용하여 더 효율적인 방식으로 문제를 해결했습니다. Set을 사용하여 중복 없이 프로젝트 ID를 추가하고, 이를 일정 주기로 MySQL에 반영하도록 수정했습니다. 이 방식은 조회수가 증가하는 트래픽에 따라 주기적으로 처리되므로 실시간 처리가 아닌, 일정한 시간 간격으로 일괄 업데이트가 이루어집니다.

개선된 방식 설명:

  1. Redis Set 사용: 중복 없이 프로젝트 ID를 Set에 추가하여 조회수를 추적합니다.
  2. 주기적 처리: @Scheduled 애너테이션을 이용해 4초마다 조회수를 일괄 처리합니다. 이를 통해 조회수가 실시간으로 반영되지 않더라도 안정적인 처리 성능을 보장합니다.
  3. Redis에서 삭제: 조회수가 MySQL에 반영되면 해당 키를 Redis에서 삭제하여 중복 처리를 방지합니다.

결론:

이 방법은 실시간 업데이트가 아닌 일정 시간 간격으로 조회수를 처리하여 동시성 문제를 해결하고, Redis를 활용하여 성능을 향상시킨 방식입니다. 주기적으로 처리되는 방식은 높은 트래픽에서도 안정적으로 동작할 수 있어, 실시간 처리의 부담을 덜어주고 시스템의 확장성을 높였습니다.


아래는 두 가지 방식의 성능 및 특징을 비교한 표입니다. 각 방식의 장단점과 성능 차이를 간단하게 정리했습니다.

항목초기 방식 (Redis 큐 사용)개선된 방식 (Redis Set과 주기적 처리)
조회수 처리 방식Redis 큐에 프로젝트 ID를 넣고 백그라운드 스레드에서 순차적으로 처리Redis Set에 프로젝트 ID를 넣고 주기적으로 처리
실시간 처리실시간으로 조회수 반영 가능, 하지만 큐에서 처리 시간이 소요될 수 있음실시간 처리는 아니지만 일정 주기마다 조회수 반영 (4초 간격)
동시성 처리높은 동시성에서 동시성 문제가 발생할 수 있음 (큐에서 데이터를 꺼내는 순서 문제 등)동시성 문제는 해결됨. 중복 처리가 없으며 Set을 사용하여 효율적인 처리 가능
성능큐에서 데이터를 처리하는 데 시간이 소요되어 높은 트래픽 시 지연 발생 가능일정 주기로 조회수를 처리하므로 높은 트래픽에서 성능 안정성 높음
시스템 부하실시간 큐 처리로 인한 시스템 부하가 크고, 트래픽이 많을 경우 큐가 쌓여 성능 저하 가능일정 시간 간격으로 조회수를 처리하므로 부하 분산 가능
확장성큐의 길이가 길어질 경우 성능 저하가 발생할 수 있음주기적인 배치 처리로 확장성에 유리
에러 처리큐에 대한 처리 도중 에러 발생 시, 해당 요청이 다시 큐에 남을 수 있어 재처리가 필요주기적으로 배치 처리하면서 에러 발생 시, 전체 배치에서 처리 가능
복잡도백그라운드 스레드와 큐를 사용하는 방식으로 다소 복잡비교적 간단하며, Redis Set과 주기적 처리로 구현이 직관적

결론

  • 초기 방식 (Redis 큐 사용):
    • 장점: 실시간 처리 가능, 즉시 조회수 반영이 필요할 때 유용.
    • 단점: 높은 동시성 환경에서 큐의 처리 지연이나 동시성 문제가 발생할 수 있음. 큐가 쌓일 경우 시스템 부하가 커짐.
  • 개선된 방식 (Redis Set과 주기적 처리):
    • 장점: 높은 트래픽 환경에서도 안정적으로 동작하며, 성능 부하가 분산됨. 실시간 처리의 필요성을 덜어줌으로써 시스템 안정성이 높아짐.
    • 단점: 실시간 처리가 아니므로 즉각적인 반영이 필요할 경우 불편할 수 있음.

이 표는 두 방식의 성능과 특징을 비교하여, 각각의 상황에 적합한 선택을 할 수 있도록 도와줍니다.

추가 개선 사항 (프로메테우스와 그라파나를 활용한 모니터링 기반 최적화)

  1. 주기적 처리 시간 최적화 (트래픽 기반 조정):

    • 트래픽 분석: 프로메테우스를 통해 실시간으로 시스템 트래픽을 모니터링하고, 이를 기반으로 그라파나 대시보드에서 트래픽 상태를 시각화합니다. 이를 통해 처리 주기를 자동으로 조정할 수 있습니다.
    • 트래픽에 따른 최적화: 예를 들어, 트래픽이 적을 때는 1초로, 트래픽이 많을 때는 5초로 자동으로 조정되는 방식입니다.
  2. 비동기 처리 및 이벤트 기반 처리:

    • 이벤트 기반 시스템 도입: 조회수가 발생할 때마다 이벤트를 발생시켜 비동기적으로 처리하는 방식입니다. 프로메테우스로 이벤트 처리 상태와 큐의 상태를 모니터링하고, 그라파나에서 실시간으로 시각화하여 시스템 성능을 추적할 수 있습니다.
  3. 확장성 개선 (분산 시스템 적용):

    • Redis 인스턴스 분산: 여러 Redis 인스턴스를 사용하여 분산 시스템에서 조회수 처리를 효율적으로 분배합니다. 프로메테우스를 통해 각 Redis 인스턴스의 상태를 모니터링하고, 그라파나 대시보드에서 이를 시각화하여 시스템 부하를 관리할 수 있습니다.
  4. 모니터링 및 알림 시스템 구축:

    • 프로메테우스 + 그라파나: 트래픽과 시스템 상태를 실시간으로 모니터링하는 대시보드를 구성하여, 트래픽 변화에 따른 자동 최적화나 시스템 리소스 조정이 가능합니다.
    • 자동 조정: 그라파나에서 설정한 알림 조건에 따라, 트래픽이 급증할 경우 자동으로 서버를 확장하거나 처리 주기를 변경하는 방식으로 성능을 유지할 수 있습니다.
profile
알고리즘은 백준 허브를 통해 github에 꾸준히 올리고 있습니다.🙂

0개의 댓글