diff --git a/lionheart-api/build.gradle b/lionheart-api/build.gradle index 89e04b9..3dfb612 100644 --- a/lionheart-api/build.gradle +++ b/lionheart-api/build.gradle @@ -19,6 +19,7 @@ dependencies { // sqs implementation "org.springframework.cloud:spring-cloud-aws-messaging:2.2.6.RELEASE" + implementation 'org.springframework.retry:spring-retry:1.3.4' // // s3 diff --git a/lionheart-api/src/main/java/com/chiwawa/lionheart/api/config/sqs/SqsRetryListener.java b/lionheart-api/src/main/java/com/chiwawa/lionheart/api/config/sqs/SqsRetryListener.java new file mode 100644 index 0000000..08510ae --- /dev/null +++ b/lionheart-api/src/main/java/com/chiwawa/lionheart/api/config/sqs/SqsRetryListener.java @@ -0,0 +1,30 @@ +package com.chiwawa.lionheart.api.config.sqs; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class SqsRetryListener implements RetryListener { + @Override + public boolean open(RetryContext retryContext, RetryCallback retryCallback) { + log.info("SqsRetryListener Opened"); + return true; + } + + @Override + public void close(RetryContext retryContext, RetryCallback retryCallback, + Throwable throwable) { + log.info("SqsRetryListener Closed"); + } + + @Override + public void onError(RetryContext retryContext, RetryCallback retryCallback, + Throwable throwable) { + int retryCount = retryContext.getRetryCount(); + log.info("Sqs Message Produce 과정에서 에러가 발생하였습니다. :: (RetryCount: {})", retryCount, throwable); + } +} diff --git a/lionheart-api/src/main/java/com/chiwawa/lionheart/api/config/sqs/producer/SqsProducer.java b/lionheart-api/src/main/java/com/chiwawa/lionheart/api/config/sqs/producer/SqsProducer.java index 0979f34..89941a1 100644 --- a/lionheart-api/src/main/java/com/chiwawa/lionheart/api/config/sqs/producer/SqsProducer.java +++ b/lionheart-api/src/main/java/com/chiwawa/lionheart/api/config/sqs/producer/SqsProducer.java @@ -1,57 +1,73 @@ package com.chiwawa.lionheart.api.config.sqs.producer; -import java.util.Map; -import java.util.UUID; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; +import static com.chiwawa.lionheart.common.exception.ErrorCode.SQS_EXCEPTION; import com.amazonaws.services.sqs.AmazonSQS; +import com.amazonaws.services.sqs.model.AmazonSQSException; import com.amazonaws.services.sqs.model.MessageAttributeValue; import com.amazonaws.services.sqs.model.SendMessageRequest; import com.chiwawa.lionheart.common.constant.MessageType; import com.chiwawa.lionheart.common.dto.sqs.MessageDto; +import com.chiwawa.lionheart.common.exception.model.InternalServerException; import com.chiwawa.lionheart.common.util.MessageUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; - +import java.util.Map; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Component; @Slf4j @Component public class SqsProducer { - @Value("${cloud.aws.sqs.notification.url}") - private String notificationUrl; - - private static final String messageGroupId = "sqs"; - private final ObjectMapper objectMapper; - private final AmazonSQS amazonSqs; - private static final String SQS_QUEUE_REQUEST_LOG_MESSAGE = "====> [SQS Queue Request] : %s "; - - public SqsProducer(ObjectMapper objectMapper, AmazonSQS amazonSqs) { - this.objectMapper = objectMapper; - this.amazonSqs = amazonSqs; - } - - public void produce(MessageDto dto) { - try { - SendMessageRequest sendMessageRequest = new SendMessageRequest(notificationUrl, - objectMapper.writeValueAsString(dto)) - .withMessageGroupId(messageGroupId) - .withMessageDeduplicationId(UUID.randomUUID().toString()) - .withMessageAttributes(createMessageAttributes(dto.getType())); - amazonSqs.sendMessage(sendMessageRequest); - log.info(MessageUtils.generate(SQS_QUEUE_REQUEST_LOG_MESSAGE, dto)); - } catch (JsonProcessingException exception) { - log.error(exception.getMessage(), exception); - } - } - - private Map createMessageAttributes(String type) { - final String dataType = "String"; - return Map.of(MessageType.MESSAGE_TYPE_HEADER, new MessageAttributeValue() - .withDataType(dataType) - .withStringValue(type)); - } + @Value("${cloud.aws.sqs.notification.url}") + private String notificationUrl; + + private static final String messageGroupId = "sqs"; + private final ObjectMapper objectMapper; + private final AmazonSQS amazonSqs; + private static final String SQS_QUEUE_REQUEST_LOG_MESSAGE = "====> [SQS Queue Request] : %s "; + + public SqsProducer(ObjectMapper objectMapper, AmazonSQS amazonSqs) { + this.objectMapper = objectMapper; + this.amazonSqs = amazonSqs; + } + + @Retryable( + maxAttempts = 3, + backoff = @Backoff(delay = 1000), + include = {AmazonSQSException.class}, + listeners = {"SqsRetryListener"}) + public void produce(MessageDto dto) { + try { + SendMessageRequest sendMessageRequest = new SendMessageRequest(notificationUrl, + objectMapper.writeValueAsString(dto)) + .withMessageGroupId(messageGroupId) + .withMessageDeduplicationId(UUID.randomUUID().toString()) + .withMessageAttributes(createMessageAttributes(dto.getType())); + amazonSqs.sendMessage(sendMessageRequest); + log.info(MessageUtils.generate(SQS_QUEUE_REQUEST_LOG_MESSAGE, dto)); + } catch (JsonProcessingException exception) { + throw new AmazonSQSException("Message sending failed by json processing."); + } + } + + @Recover + private void recoverListener(AmazonSQSException exception, MessageDto dto) { + throw new InternalServerException( + String.format("SQS로 메시지를 Produce 하는 과정에서 에러가 발생하며 메시지 발행에 실패하였습니다. :: Message -> %s", dto.toString()) + , SQS_EXCEPTION); + } + + private Map createMessageAttributes(String type) { + final String dataType = "String"; + return Map.of(MessageType.MESSAGE_TYPE_HEADER, new MessageAttributeValue() + .withDataType(dataType) + .withStringValue(type)); + } } diff --git a/lionheart-common/src/main/java/com/chiwawa/lionheart/common/exception/ErrorCode.java b/lionheart-common/src/main/java/com/chiwawa/lionheart/common/exception/ErrorCode.java index 8a1511e..9537d02 100644 --- a/lionheart-common/src/main/java/com/chiwawa/lionheart/common/exception/ErrorCode.java +++ b/lionheart-common/src/main/java/com/chiwawa/lionheart/common/exception/ErrorCode.java @@ -36,6 +36,7 @@ public enum ErrorCode { // Internal Server Exception INTERNAL_SERVER_EXCEPTION("I001", "서버 내부에서 에러가 발생하였습니다."), + SQS_EXCEPTION("I002", "SQS 연동 과정에서 서버 내부 에러가 발생하였습니다."), // Bad Gateway Exception BAD_GATEWAY_EXCEPTION("B001", "외부 연동 중 에러가 발생하였습니다."); diff --git a/lionheart-domain/src/main/java/com/chiwawa/lionheart/domain/domain/common/BaseEntity.java b/lionheart-domain/src/main/java/com/chiwawa/lionheart/domain/domain/common/BaseEntity.java index ef207db..f47e125 100644 --- a/lionheart-domain/src/main/java/com/chiwawa/lionheart/domain/domain/common/BaseEntity.java +++ b/lionheart-domain/src/main/java/com/chiwawa/lionheart/domain/domain/common/BaseEntity.java @@ -1,19 +1,15 @@ package com.chiwawa.lionheart.domain.domain.common; +import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalDateTime; - import javax.persistence.Column; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperclass; - +import lombok.Getter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import com.fasterxml.jackson.annotation.JsonFormat; - -import lombok.Getter; - @Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class)