좋아요 알림을 구현하는 과정에서 다음과 같은 요구사항이 있었다.
좋아요가 연속적으로 들어올 경우, 매번 알림을 보내지 말고
일정 시간 동안 좋아요 개수를 집계한 뒤
“좋아요 n개가 추가되었습니다” 형태로 하나의 알림만 전송해야 한다.
이를 위해 Redis를 사용해 좋아요 수를 집계하고, 일정 시간 후 알림을 전송하는 구조를 만들었다.
Redis는 In-Memory 기반의 Key-Value 저장소이다.
이번 문제에서는 “일시적 좋아요 집계 저장소”로 활용하였다.
• 매우 빠른 읽기/쓰기
• 원자적 연산 지원 (INCR 등)
• TTL(만료 시간) 설정 가능
• 분산 환경에서 상태 공유에 적합
• 게시글Id를 key로 사용
• 좋아요 발생 시 Redis에서 INCR
• 일정 시간 후 해당 key의 최종 count 조회
• 알림 전송 후 key 삭제
처음에는 @Async와 Thread.sleep()을 사용해 집계 시간을 구현했다.
@Async
public void scheduleLikePush(Long challengeId, Long ownerId) {
try {
Thread.sleep(AggregationDelays.CHALLENGE_LIKE_MS);
Integer likeCount = likeRepo.get(challengeId);
if (likeCount == null || likeCount <= 0) return;
notificationService.sendChallengeLikePush(challengeId, ownerId, likeCount);
likeRepo.clear(challengeId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
1. 첫 좋아요 발생
2. @Async로 별도 스레드 실행
3. Thread.sleep(5분)
4. Redis에서 최종 좋아요 수 조회
5. 푸시 전송
6. Redis key 삭제
Thread.sleep 동안 스레드는 실제로 아무 일도 하지 않고 대기만 한다. 결과적으로 이는 비효율적이고 트래픽이 증가할 경우 여러 스레드들이 대기 상태에 빠져 병목 현상으로 이어질 수 있었다.
만약 thread가 sleep 중에 서버가 재시작된다면 해당 thread가 종료되어 이후 로직이 제대로 실행되지 않을 수 있다. redis 내부에 집계 데이터는 처리도지 못한채로 남아있기 때문에 위험할 수 있다.
Thread.sleep을 제거하고, Spring의 TaskScheduler를 사용해 예약 실행 구조로 변경했다.
@Configuration
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler ts = new ThreadPoolTaskScheduler();
ts.setPoolSize(4);
ts.setThreadNamePrefix("like-agg-");
ts.initialize();
return ts;
}
}
public void scheduleLikePush(Long challengeId, Long ownerId) {
Instant runAt = Instant.now()
.plusMillis(AggregationDelays.CHALLENGE_LIKE_MS);
taskScheduler.schedule(
() -> flushLikePush(challengeId, ownerId),
runAt
);
}
private void flushLikePush(Long challengeId, Long ownerId) {
Integer likeCount = likeRepo.get(challengeId);
if (likeCount == null || likeCount <= 0) return;
notificationService.sendChallengeLikePush(
challengeId, ownerId, likeCount
);
likeRepo.clear(challengeId);
}
이를 통해 스레드를 특정 시간동안 점유하던 구조에서 실행을 예약만 해두고 해당 시점이 되면 스레드 풀에서 스레드를 가져와 실행하는 방식으로 스레드 점유를 낮추고 리소스 효율을 개선하였다.