-
Notifications
You must be signed in to change notification settings - Fork 1
feat: AI 이미지 생성 DLQ 및 재시도 메커니즘 구현 #132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,8 @@ | ||||||||
| package hanium.modic.backend.common.amqp.config; | ||||||||
|
|
||||||||
| import java.util.HashMap; | ||||||||
| import java.util.Map; | ||||||||
|
|
||||||||
| import org.springframework.amqp.core.AmqpAdmin; | ||||||||
| import org.springframework.amqp.core.Binding; | ||||||||
| import org.springframework.amqp.core.BindingBuilder; | ||||||||
|
|
@@ -27,11 +30,22 @@ public class RabbitMqConfig { | |||||||
| public static final String AI_IMAGE_CREATED_EXCHANGE = "ai.image.created.exchange"; | ||||||||
| public static final String AI_IMAGE_CREATED_ROUTING_KEY = "ai.image.created"; | ||||||||
|
|
||||||||
| // DLQ (Dead Letter Queue) 및 재시도 관련 상수 | ||||||||
| public static final String AI_IMAGE_REQUEST_DLX = "ai.image.request.dlx"; | ||||||||
| public static final String AI_IMAGE_REQUEST_DLQ = "ai.image.request.dlq"; | ||||||||
| public static final String AI_IMAGE_REQUEST_RETRY_EXCHANGE = "ai.image.request.retry.exchange"; | ||||||||
| public static final String AI_IMAGE_REQUEST_RETRY_QUEUE = "ai.image.request.retry.queue"; | ||||||||
| public static final String AI_IMAGE_REQUEST_DLQ_ROUTING_KEY = "ai.image.request.dlq"; | ||||||||
| public static final String AI_IMAGE_REQUEST_RETRY_ROUTING_KEY = "ai.image.request.retry"; | ||||||||
|
|
||||||||
| private final RabbitMqProperties rabbitMqProperties; | ||||||||
|
|
||||||||
| @Bean | ||||||||
| public Queue aiImageRequestQueue() { | ||||||||
| return new Queue(AI_IMAGE_REQUEST_QUEUE, true); | ||||||||
| Map<String, Object> args = new HashMap<>(); | ||||||||
| args.put("x-dead-letter-exchange", AI_IMAGE_REQUEST_DLX); | ||||||||
| args.put("x-dead-letter-routing-key", AI_IMAGE_REQUEST_RETRY_ROUTING_KEY); // 기본적으로 재시도로 라우팅 | ||||||||
| return new Queue(AI_IMAGE_REQUEST_QUEUE, true, false, false, args); | ||||||||
|
Comment on lines
+45
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 재시도 루프가 무한 반복될 수 있습니다 – 최종 DLQ 라우팅 로직을 명확히 해주세요. 현재
과 같은 명시적 종료 지점을 추가해 주세요. 🤖 Prompt for AI Agents |
||||||||
| } | ||||||||
|
|
||||||||
| @Bean | ||||||||
|
|
@@ -63,6 +77,58 @@ public Binding aiImageCreatedBinding(Queue aiImageCreatedQueue, TopicExchange ai | |||||||
| .with(AI_IMAGE_CREATED_ROUTING_KEY); | ||||||||
| } | ||||||||
|
|
||||||||
| // DLX (Dead Letter Exchange) - 실패한 메시지를 받아서 재시도 또는 최종 처리로 라우팅 | ||||||||
| @Bean | ||||||||
| public TopicExchange aiImageRequestDlx() { | ||||||||
| return new TopicExchange(AI_IMAGE_REQUEST_DLX, true, false); | ||||||||
| } | ||||||||
|
|
||||||||
| // 최종 실패 메시지 저장소 | ||||||||
| @Bean | ||||||||
| public Queue aiImageRequestDlq() { | ||||||||
| return new Queue(AI_IMAGE_REQUEST_DLQ, true); | ||||||||
| } | ||||||||
|
|
||||||||
| // DLX에서 최종 DLQ로의 바인딩 | ||||||||
| @Bean | ||||||||
| public Binding aiImageRequestDlqBinding(Queue aiImageRequestDlq, TopicExchange aiImageRequestDlx) { | ||||||||
| return BindingBuilder.bind(aiImageRequestDlq) | ||||||||
| .to(aiImageRequestDlx) | ||||||||
| .with(AI_IMAGE_REQUEST_DLQ_ROUTING_KEY); | ||||||||
| } | ||||||||
|
|
||||||||
| // 재시도용 Exchange | ||||||||
| @Bean | ||||||||
| public TopicExchange aiImageRequestRetryExchange() { | ||||||||
| return new TopicExchange(AI_IMAGE_REQUEST_RETRY_EXCHANGE, true, false); | ||||||||
| } | ||||||||
|
|
||||||||
| // 재시도 대기 Queue (TTL 60초 설정, 만료 시 원래 exchange로 재전송) | ||||||||
| @Bean | ||||||||
| public Queue aiImageRequestRetryQueue() { | ||||||||
| Map<String, Object> args = new HashMap<>(); | ||||||||
| args.put("x-message-ttl", 60000); // 60초 | ||||||||
| args.put("x-dead-letter-exchange", AI_IMAGE_REQUEST_EXCHANGE); // 만료 시 원래 exchange로 | ||||||||
| args.put("x-dead-letter-routing-key", AI_IMAGE_REQUEST_ROUTING_KEY); | ||||||||
| return new Queue(AI_IMAGE_REQUEST_RETRY_QUEUE, true, false, false, args); | ||||||||
| } | ||||||||
|
|
||||||||
| // 재시도 Exchange와 Queue 바인딩 | ||||||||
| @Bean | ||||||||
| public Binding aiImageRequestRetryBinding(Queue aiImageRequestRetryQueue, TopicExchange aiImageRequestRetryExchange) { | ||||||||
| return BindingBuilder.bind(aiImageRequestRetryQueue) | ||||||||
| .to(aiImageRequestRetryExchange) | ||||||||
| .with(AI_IMAGE_REQUEST_RETRY_ROUTING_KEY); | ||||||||
| } | ||||||||
|
|
||||||||
| // DLX에서 재시도 Exchange로의 바인딩 | ||||||||
| @Bean | ||||||||
| public Binding aiImageRequestDlxToRetryBinding(TopicExchange aiImageRequestRetryExchange, TopicExchange aiImageRequestDlx) { | ||||||||
| return BindingBuilder.bind(aiImageRequestRetryExchange) | ||||||||
|
||||||||
| return BindingBuilder.bind(aiImageRequestRetryExchange) | |
| public Binding aiImageRequestDlxToRetryBinding(Queue aiImageRequestRetryQueue, TopicExchange aiImageRequestDlx) { | |
| return BindingBuilder.bind(aiImageRequestRetryQueue) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package hanium.modic.backend.domain.ai.listener; | ||
|
|
||
| import static hanium.modic.backend.common.amqp.config.RabbitMqConfig.*; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.springframework.amqp.core.Message; | ||
| import org.springframework.amqp.rabbit.annotation.RabbitListener; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import hanium.modic.backend.domain.ai.domain.AiRequestEntity; | ||
| import hanium.modic.backend.domain.ai.dto.AiImageRequestMessageDto; | ||
| import hanium.modic.backend.domain.ai.enums.AiImageStatus; | ||
| import hanium.modic.backend.domain.ai.repository.AiRequestRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class DlqListener { | ||
|
|
||
| private final AiRequestRepository aiRequestRepository; | ||
|
|
||
| @Transactional | ||
| @RabbitListener(queues = AI_IMAGE_REQUEST_DLQ) | ||
| public void handleFinalFailedMessage(AiImageRequestMessageDto messageDto, Message message) { | ||
| log.error("[최종 실패] AI 이미지 생성 요청 최종 실패: requestId={}", messageDto.requestId()); | ||
|
Comment on lines
+26
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 메시지 소비 실패 시 재큐잉/ACK 전략을 명시하세요
등 원하는 재시도·폐기 정책을 명확히 지정해 주세요. 🤖 Prompt for AI Agents |
||
|
|
||
| // AiRequestEntity 상태를 FAILED로 변경 | ||
| Optional<AiRequestEntity> aiRequestOpt = aiRequestRepository.findByRequestId(messageDto.requestId()); | ||
| if (aiRequestOpt.isPresent()) { | ||
| AiRequestEntity aiRequest = aiRequestOpt.get(); | ||
| aiRequest.updateStatus(AiImageStatus.FAILED); | ||
| aiRequestRepository.save(aiRequest); | ||
| log.info("[상태 업데이트] requestId={} 상태를 FAILED로 변경", messageDto.requestId()); | ||
| } else { | ||
| log.error("[데이터 오류] requestId={}에 해당하는 AI 요청을 찾을 수 없습니다", messageDto.requestId()); | ||
| } | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dead letter routing key configuration may cause infinite retry loops. When a message fails, it goes to DLX with retry routing key, but there's no mechanism to limit retry attempts or eventually route to the final DLQ. Consider adding retry count tracking or max retry limit.