728x90
반응형
예를 들어 대량의 이미지 합성(썸네일 생성, 포스터 합성 등) 작업을 요청-응답 동기 처리로 묶으면 API 지연, 타임아웃, 서버 부하가 한꺼번에 터집니다. 이런 이미지 합성은 단지 하나의 사례일 뿐이고, 메일 발송, 영상 인코딩, 알림톡 전송, 데이터 가공 같은 다양한 작업에도 동일한 문제가 발생합니다.
이럴 때 작업을 큐에 적재하고 워커가 비동기로 꺼내 처리하면, 처리량(Throughput)과 안정성을 모두 챙길 수 있습니다. 본 글에서는 이미지 합성을 예시로 들면서, Redis List 기반 큐를 활용해 FIFO(선입선출) 방식으로 비동기 작업을 처리하는 패턴을 설명해보겠습니다.
왜 Redis Queue인가?
- 빠름: 메모리 기반이라 초당 수만 건 수준으로 push/pop이 가볍습니다.
- 간단함: List 연산만으로 FIFO 구현이 가능—운영 복잡도가 낮음.
- 유연함: 워커 수만 늘려도 수평 확장 쉬움. 컨테이너 오토스케일과 궁합이 좋음.
- 분리: API 서버는 “작업 등록”만, 워커는 “처리”만 담당 → 응답 시간 짧고 장애 격리 쉬움.
다른 선택지?
- 정확한 소비 추적·재처리·여러 컨슈머 그룹이 필요하면 Redis Streams(XADD, XREADGROUP) 고려.
- 엄격한 영속성/트랜잭션이 중요하면 Kafka/RabbitMQ 같은 메시지 브로커.
FIFO를 보장하는 기본 큐 연산
Redis List는 양쪽에서 삽입/삭제 가능한 Deque입니다. 한쪽으로 넣고, 반대쪽에서 꺼내면 FIFO가 됩니다.
- Producer:
RPUSH queue payload(오른쪽 끝에 넣기) - Consumer:
BLPOP queue timeout(왼쪽에서 꺼내기, 없으면 블로킹)
Spring Data Redis에선 rightPush()로 넣고 leftPop() or leftPop(timeout)로 꺼냅니다.
블로킹이 필요하면 BLPOP을 직접 사용하거나, 폴링 주기/백오프를 두는 방식으로 구현합니다.
의존성 & 설정
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'com.fasterxml.jackson.core:jackson-databind'# application.yml
spring:
data:
redis:
host: localhost
port: 6379
app:
queue:
image: heritage:image:compose # 큐 키 이름
processing: heritage:image:processing # 처리중 큐(옵션)Producer 예시 (작업 등록)
@Service
@RequiredArgsConstructor
public class ImageJobProducer {
private final RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper;
@Value("${app.queue.image}")
private String queueKey;
public void enqueue(ImageJob job) {
try {
String payload = objectMapper.writeValueAsString(job);
redisTemplate.opsForList().rightPush(queueKey, payload); // FIFO: 오른쪽 push
} catch (Exception e) {
throw new IllegalStateException("Enqueue failed", e);
}
}
}Consumer(워커) 예시 (작업 처리)
- 예시는 가장 단순한 패턴인 폴링 + 타임아웃 형태로 구현했습니다.
@Service
@RequiredArgsConstructor
public class ImageJobWorker {
private final RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper;
private final ImageComposer imageComposer; // 실제 합성 서비스
@Value("${app.queue.image}")
private String queueKey;
@Scheduled(fixedDelay = 500) // 0.5초 주기로 폴링 (운영환경에 맞게 조절)
public void poll() {
String payload = redisTemplate.opsForList().leftPop(queueKey);
if (payload == null) return; // 작업 없으면 빠르게 반환
processSafely(payload);
}
private void processSafely(String payload) {
ImageJob job = null;
try {
job = objectMapper.readValue(payload, ImageJob.class);
imageComposer.compose(job); // 합성 로직 (MinIO/S3 업로드 포함)
} catch (Exception e) {
handleRetry(job, payload, e);
}
}
private void handleRetry(ImageJob job, String payload, Exception e) {
int retries = (job == null) ? 0 : job.getRetries();
if (retries < 3) {
job.setRetries(retries + 1);
backoff(job.getRetries());
requeue(job); // 재큐잉 (우측 push)
return;
}
// DLQ(사망 큐)로 이동하거나 알림 전송
// redisTemplate.opsForList().rightPush("heritage:image:dlq", payload);
// error log/alert...
}
private void backoff(int attempt) {
try {
Thread.sleep(Math.min(5000, 300 * attempt));
} catch (InterruptedException ignored) {
// exception...
}
}
private void requeue(ImageJob job) {
try {
String p = new ObjectMapper().writeValueAsString(job);
redisTemplate.opsForList().rightPush(queueKey, p);
} catch (Exception ignore) {
// exception...
}
}
}
마무리
이미지 합성과 같은 CPU/IO 집중형 작업은 요청-응답 흐름에서 분리해 큐로 처리하는 게 안정적입니다. Redis List는 빠르고 단순하게 FIFO 큐를 구성할 수 있어, 스몰/미드 스케일에서 특히 가성비가 뛰어납니다.
운영 단계에선 처리 중 유실 방지, 멱등성, 재시도/모니터링만 꼼꼼히 챙기면 충분히 견고한 파이프라인을 만들 수 있습니다!
728x90
반응형
'개발 > Spring' 카테고리의 다른 글
| 단일 폴링 스케줄러를 전용 ThreadPool로 고도화하기 (0) | 2025.09.17 |
|---|---|
| Micrometor & Metrics (2) | 2025.06.05 |
| Spring Boot Actuator (2) | 2025.06.05 |
| @Transactional 내부에서 어떻게 동작할까? (0) | 2025.06.04 |
| [MyBatis] if - else 사용하기 (choose) (0) | 2021.10.12 |