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 ecdaa7ca..ec68ef7c 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); @@ -279,11 +307,23 @@ public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(Conne return factory; } - private Binding bindFanout(Queue queue, FanoutExchange exchange) { - return BindingBuilder.bind(queue).to(exchange); + 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 8dbae6b6..80e368da 100644 --- a/src/main/java/org/example/tablenow/global/constant/RabbitConstant.java +++ b/src/main/java/org/example/tablenow/global/constant/RabbitConstant.java @@ -35,9 +35,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"; @@ -57,5 +66,7 @@ public class RabbitConstant { public static final String CHAT_DLX = CHAT_PREFIX + ".dlx"; public static final String CHAT_DLQ = CHAT_PREFIX + ".dlq"; + // 공통 public static final int TTL_MILLIS = 30000; + public static final String RETRY_HEADER = "x-retry-count"; }