From abc8caf531345b9c34c0c5e77f71ba1bc0193314 Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Fri, 2 May 2025 14:56:12 +0900 Subject: [PATCH] =?UTF-8?q?feat(event):=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=ED=94=88=20=EC=95=8C=EB=A6=BC=20DLQ=20+=20DLX=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20#270?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 변경점 - 상수 추가 및 누락된 곳 적용 - EventOpenConsumer에서 알림 전송 실패 시AmqpRejectAndDontRequeueException 예외 추가 - RabbitConfig에 이벤트 오픈 관련 설정 추가 - EventOpenDlqReprocessor, EventOpenRetryService 클래스 생성 - 메시지 재처리 로직 구현 - 라우팅키 오류 수정 --- .../dto/response/EventJoinResponseDto.java | 6 +- .../event/dto/response/EventResponseDto.java | 12 +-- .../message/consumer/EventOpenConsumer.java | 2 + .../consumer/EventOpenDlqReprocessor.java | 21 +++++ .../message/producer/EventOpenProducer.java | 6 +- .../event/service/EventOpenRetryService.java | 47 ++++++++++++ .../tablenow/global/config/RabbitConfig.java | 76 ++++++++++++++----- .../global/constant/RabbitConstant.java | 14 +++- 8 files changed, 156 insertions(+), 28 deletions(-) create mode 100644 src/main/java/org/example/tablenow/domain/event/message/consumer/EventOpenDlqReprocessor.java create mode 100644 src/main/java/org/example/tablenow/domain/event/service/EventOpenRetryService.java diff --git a/src/main/java/org/example/tablenow/domain/event/dto/response/EventJoinResponseDto.java b/src/main/java/org/example/tablenow/domain/event/dto/response/EventJoinResponseDto.java index 124f672f..ae434886 100644 --- a/src/main/java/org/example/tablenow/domain/event/dto/response/EventJoinResponseDto.java +++ b/src/main/java/org/example/tablenow/domain/event/dto/response/EventJoinResponseDto.java @@ -7,15 +7,17 @@ import java.time.LocalDateTime; +import static org.example.tablenow.global.constant.TimeConstants.TIME_YYYY_MM_DD_HH_MM_SS; + @Getter public class EventJoinResponseDto { private final Long eventJoinId; private final Long eventId; private final Long storeId; private final String storeName; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = TIME_YYYY_MM_DD_HH_MM_SS) private final LocalDateTime eventTime; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = TIME_YYYY_MM_DD_HH_MM_SS) private final LocalDateTime joinedAt; private final String message; diff --git a/src/main/java/org/example/tablenow/domain/event/dto/response/EventResponseDto.java b/src/main/java/org/example/tablenow/domain/event/dto/response/EventResponseDto.java index cb09c514..015263fa 100644 --- a/src/main/java/org/example/tablenow/domain/event/dto/response/EventResponseDto.java +++ b/src/main/java/org/example/tablenow/domain/event/dto/response/EventResponseDto.java @@ -8,22 +8,24 @@ import java.time.LocalDateTime; +import static org.example.tablenow.global.constant.TimeConstants.TIME_YYYY_MM_DD_HH_MM_SS; + @Getter public class EventResponseDto { private final Long eventId; private final Long storeId; private final String storeName; private final String content; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = TIME_YYYY_MM_DD_HH_MM_SS) private final LocalDateTime openAt; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = TIME_YYYY_MM_DD_HH_MM_SS) private final LocalDateTime endAt; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = TIME_YYYY_MM_DD_HH_MM_SS) private final LocalDateTime eventTime; private final int limitPeople; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = TIME_YYYY_MM_DD_HH_MM_SS) private final LocalDateTime createdAt; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = TIME_YYYY_MM_DD_HH_MM_SS) private final LocalDateTime updatedAt; private final EventStatus status; diff --git a/src/main/java/org/example/tablenow/domain/event/message/consumer/EventOpenConsumer.java b/src/main/java/org/example/tablenow/domain/event/message/consumer/EventOpenConsumer.java index 89974ce0..a60aaa87 100644 --- a/src/main/java/org/example/tablenow/domain/event/message/consumer/EventOpenConsumer.java +++ b/src/main/java/org/example/tablenow/domain/event/message/consumer/EventOpenConsumer.java @@ -8,6 +8,7 @@ import org.example.tablenow.domain.user.entity.User; import org.example.tablenow.domain.user.service.UserService; import org.example.tablenow.domain.event.message.dto.EventOpenMessage; +import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @@ -68,6 +69,7 @@ private void sendNotification(User user, EventOpenMessage message) { } catch (Exception e) { log.error("[EventOpenConsumer][Notification] 알림 전송 실패 → userId={}, eventId={}", user.getId(), message.getEventId(), e); + throw new AmqpRejectAndDontRequeueException("[DLQ] 알림 전송 실패 → DLQ로 이동", e); } } } diff --git a/src/main/java/org/example/tablenow/domain/event/message/consumer/EventOpenDlqReprocessor.java b/src/main/java/org/example/tablenow/domain/event/message/consumer/EventOpenDlqReprocessor.java new file mode 100644 index 00000000..1133165a --- /dev/null +++ b/src/main/java/org/example/tablenow/domain/event/message/consumer/EventOpenDlqReprocessor.java @@ -0,0 +1,21 @@ +package org.example.tablenow.domain.event.message.consumer; + +import lombok.RequiredArgsConstructor; +import org.example.tablenow.domain.event.service.EventOpenRetryService; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; +import org.springframework.amqp.core.Message; + +import static org.example.tablenow.global.constant.RabbitConstant.EVENT_OPEN_DLQ; + +@Component +@RequiredArgsConstructor +public class EventOpenDlqReprocessor { + + private final EventOpenRetryService eventOpenRetryService; + + @RabbitListener(queues = EVENT_OPEN_DLQ) + public void reprocess(Message message) { + eventOpenRetryService.process(message); + } +} diff --git a/src/main/java/org/example/tablenow/domain/event/message/producer/EventOpenProducer.java b/src/main/java/org/example/tablenow/domain/event/message/producer/EventOpenProducer.java index 3372fcc6..26fc812d 100644 --- a/src/main/java/org/example/tablenow/domain/event/message/producer/EventOpenProducer.java +++ b/src/main/java/org/example/tablenow/domain/event/message/producer/EventOpenProducer.java @@ -2,12 +2,13 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.example.tablenow.global.constant.RabbitConstant; import org.example.tablenow.domain.event.message.dto.EventOpenMessage; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; +import static org.example.tablenow.global.constant.RabbitConstant.EVENT_OPEN_EXCHANGE; + @Slf4j @Component @RequiredArgsConstructor @@ -17,7 +18,8 @@ public class EventOpenProducer { public void send(EventOpenMessage message) { rabbitTemplate.convertAndSend( - RabbitConstant.EVENT_OPEN_EXCHANGE, + EVENT_OPEN_EXCHANGE, + "", message ); diff --git a/src/main/java/org/example/tablenow/domain/event/service/EventOpenRetryService.java b/src/main/java/org/example/tablenow/domain/event/service/EventOpenRetryService.java new file mode 100644 index 00000000..d4410d3a --- /dev/null +++ b/src/main/java/org/example/tablenow/domain/event/service/EventOpenRetryService.java @@ -0,0 +1,47 @@ +package org.example.tablenow.domain.event.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.stereotype.Component; + +import static org.example.tablenow.global.constant.RabbitConstant.*; + +@Slf4j +@Component +@RequiredArgsConstructor +public class EventOpenRetryService { + + private final RabbitTemplate rabbitTemplate; + + private static final int MAX_RETRY_COUNT = 3; + + public void process(Message message) { + MessageProperties props = message.getMessageProperties(); + Integer retryCount = (Integer) props.getHeaders().getOrDefault(RETRY_HEADER, 0); + + if (retryCount >= MAX_RETRY_COUNT) { + log.error("[EventOpentRetryService] 재시도 최대 횟수 초과 → messageId={}, retryCount={}", + message.getMessageProperties().getMessageId(), retryCount); + return; + } + + Message retryMessage = MessageBuilder + .withBody(message.getBody()) + .copyProperties(props) + .setHeader(RETRY_HEADER, retryCount + 1) + .build(); + + rabbitTemplate.send( + EVENT_OPEN_RETRY_EXCHANGE, + EVENT_OPEN_RETRY_ROUTING_KEY, + retryMessage + ); + + log.info("[EventOpentRetryService] DLQ 메시지 재전송 완료 → retryCount={}, routingKey={}, message={}", + retryCount + 1, EVENT_OPEN_RETRY_ROUTING_KEY, new String(retryMessage.getBody())); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/tablenow/global/config/RabbitConfig.java b/src/main/java/org/example/tablenow/global/config/RabbitConfig.java index 388c3a3f..403a3766 100644 --- a/src/main/java/org/example/tablenow/global/config/RabbitConfig.java +++ b/src/main/java/org/example/tablenow/global/config/RabbitConfig.java @@ -57,7 +57,10 @@ public Binding vacancyDlqBinding(){ // 이벤트 오픈 Queue, Exchange, Binding @Bean public Queue eventOpenQueue() { - return new Queue(EVENT_OPEN_QUEUE, true); + return QueueBuilder.durable(EVENT_OPEN_QUEUE) + .withArgument("x-dead-letter-exchange", EVENT_OPEN_DLX) + .withArgument("x-dead-letter-routing-key", EVENT_OPEN_DLQ_ROUTING_KEY) + .build(); } @Bean @@ -70,6 +73,43 @@ public Binding eventOpenBinding() { return bindFanout(eventOpenQueue(), eventOpenExchange()); } + // 이벤트 오픈 DLX 및 DLQ 설정 + @Bean + public DirectExchange eventOpenDlx() { + return new DirectExchange(EVENT_OPEN_DLX); + } + + @Bean + public Queue eventOpenDlq() { + return buildDlqQueue(EVENT_OPEN_DLQ); + } + + @Bean + public Binding eventOpenDlqBinding() { + return bind(eventOpenDlq(), eventOpenDlx(), EVENT_OPEN_DLQ_ROUTING_KEY); + } + + // 이벤트 오픈 RetryQueue + @Bean + public Queue eventOpenRetryQueue() { + return QueueBuilder.durable(EVENT_OPEN_RETRY_QUEUE) + .withArgument("x-message-ttl", TTL_MILLIS) + .withArgument("x-dead-letter-exchange", EVENT_OPEN_EXCHANGE) + .build(); + } + + @Bean + public DirectExchange eventOpenRetryExchange() { + return new DirectExchange(EVENT_OPEN_RETRY_EXCHANGE); + } + + @Bean + public Binding eventOpenRetryBinding() { + return BindingBuilder.bind(eventOpenRetryQueue()) + .to(eventOpenRetryExchange()) + .with(EVENT_OPEN_RETRY_ROUTING_KEY); + } + // 예약 리마인드 등록 Queue, Exchange, Binding @Bean public Queue reminderRegisterQueue() { @@ -125,7 +165,7 @@ public Binding reminderSendDlqBinding() { @Bean public Queue reminderSendRetryQueue() { return QueueBuilder.durable(RESERVATION_REMINDER_SEND_RETRY_QUEUE) - .withArgument("x-message-ttl", 30000) + .withArgument("x-message-ttl", TTL_MILLIS) .withArgument("x-dead-letter-exchange", RESERVATION_REMINDER_SEND_EXCHANGE) .build(); } @@ -171,18 +211,6 @@ public Queue storeDeleteDlq() { return buildDlqQueue(STORE_DELETE_DLQ); } - private Queue buildMainQueue(String queueName, String dlqRoutingKey) { - return QueueBuilder.durable(queueName) - .withArgument("x-dead-letter-exchange", STORE_DLX) - .withArgument("x-dead-letter-routing-key", dlqRoutingKey) - .withArgument("x-message-ttl", TTL_MILLIS) - .build(); - } - - private Queue buildDlqQueue(String dlqName) { - return QueueBuilder.durable(dlqName).build(); - } - @Bean public DirectExchange storeExchange() { return new DirectExchange(STORE_EXCHANGE); @@ -223,10 +251,6 @@ public Binding storeDeleteDlqBinding(Queue storeDeleteDlq, DirectExchange storeD return bind(storeDeleteDlq, storeDlx, STORE_DELETE_DLQ); } - private Binding bind(Queue queue, DirectExchange exchange, String routingKey) { - return BindingBuilder.bind(queue).to(exchange).with(routingKey); - } - // 채팅 알림 Queue, Exchange, Binding @Bean public Queue chatQueue() { @@ -268,6 +292,22 @@ public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(Conne return factory; } + private Queue buildMainQueue(String queueName, String dlqRoutingKey) { + return QueueBuilder.durable(queueName) + .withArgument("x-dead-letter-exchange", STORE_DLX) + .withArgument("x-dead-letter-routing-key", dlqRoutingKey) + .withArgument("x-message-ttl", TTL_MILLIS) + .build(); + } + + private Queue buildDlqQueue(String dlqName) { + return QueueBuilder.durable(dlqName).build(); + } + + private Binding bind(Queue queue, DirectExchange exchange, String routingKey) { + return BindingBuilder.bind(queue).to(exchange).with(routingKey); + } + private Binding bindFanout(Queue queue, FanoutExchange exchange) { return BindingBuilder.bind(queue).to(exchange); } diff --git a/src/main/java/org/example/tablenow/global/constant/RabbitConstant.java b/src/main/java/org/example/tablenow/global/constant/RabbitConstant.java index fccfa91e..f0f6eec4 100644 --- a/src/main/java/org/example/tablenow/global/constant/RabbitConstant.java +++ b/src/main/java/org/example/tablenow/global/constant/RabbitConstant.java @@ -32,9 +32,18 @@ public class RabbitConstant { public static final String RESERVATION_REMINDER_REGISTER_QUEUE = RESERVATION_REMINDER_PREFIX + ".register.queue"; // 이벤트 오픈 알림 - public static final String EVENT_OPEN_EXCHANGE = EVENT_OPEN_PREFIX + ".fanout"; + public static final String EVENT_OPEN_EXCHANGE = EVENT_OPEN_PREFIX + ".exchange"; public static final String EVENT_OPEN_QUEUE = EVENT_OPEN_PREFIX + ".queue"; + public static final String EVENT_OPEN_DLX = EVENT_OPEN_PREFIX + ".dlx"; + public static final String EVENT_OPEN_DLQ = EVENT_OPEN_PREFIX + ".dlq"; + public static final String EVENT_OPEN_DLQ_ROUTING_KEY = EVENT_OPEN_PREFIX + ".dlq.key"; + + public static final String EVENT_OPEN_RETRY_QUEUE = EVENT_OPEN_PREFIX + ".retry.queue"; + public static final String EVENT_OPEN_RETRY_EXCHANGE = EVENT_OPEN_PREFIX + ".retry.exchange"; + public static final String EVENT_OPEN_RETRY_ROUTING_KEY = EVENT_OPEN_PREFIX + ".retry.key"; + + // 가게 public static final String STORE_EXCHANGE = "store.exchange"; public static final String STORE_CREATE = "store.create"; public static final String STORE_UPDATE = "store.update"; @@ -47,9 +56,12 @@ public class RabbitConstant { public static final String STORE_UPDATE_DLQ = "store.update.dlq"; public static final String STORE_DELETE_DLQ = "store.delete.dlq"; + // 채팅 public static final String CHAT_EXCHANGE = "chat.exchange"; public static final String CHAT_QUEUE = "chat.queue"; public static final String CHAT_ROUTING_KEY = "chat.key"; + // 공통 public static final int TTL_MILLIS = 30000; + public static final String RETRY_HEADER = "x-retry-count"; }