diff --git a/.github/workflows/dev_code_deploy.yml b/.github/workflows/dev_code_deploy.yml index 7b78b788..d586a121 100644 --- a/.github/workflows/dev_code_deploy.yml +++ b/.github/workflows/dev_code_deploy.yml @@ -39,7 +39,8 @@ jobs: - name: Add SSH fixkey to known hosts run: | mkdir -p ~/.ssh - ssh-keyscan ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts + ssh-keyscan ${{ secrets.DEV_ONE_FIRST_SSH_HOST }} >> ~/.ssh/known_hosts + ssh-keyscan ${{ secrets.DEV_ONE_SECOND_SSH_HOST }} >> ~/.ssh/known_hosts - name: Create SSH key run: | @@ -50,5 +51,7 @@ jobs: env: ACTIVE_PROFILE: dev run: | - scp -i private_key ./build/libs/*.jar ${{ secrets.SSH_FULL_INFO }}:~/ - ssh -i private_key ${{ secrets.SSH_FULL_INFO }} "sudo bash ${{ secrets.SCRIPT_PATH }} $ACTIVE_PROFILE" + scp -i private_key ./build/libs/*.jar ${{ secrets.DEV_ONE_FIRST_SSH_FULL_INFO }}:~/ + scp -i private_key ./build/libs/*.jar ${{ secrets.DEV_ONE_SECOND_SSH_FULL_INFO }}:~/ + ssh -i private_key ${{ secrets.DEV_ONE_FIRST_SSH_FULL_INFO }} "sudo bash ${{ secrets.SCRIPT_PATH }} $ACTIVE_PROFILE" + ssh -i private_key ${{ secrets.DEV_ONE_SECOND_SSH_FULL_INFO }} "sudo bash ${{ secrets.SCRIPT_PATH }} $ACTIVE_PROFILE" diff --git a/src/main/java/com/wootecam/luckyvickyauction/consumer/config/RedisStreamConfig.java b/src/main/java/com/wootecam/luckyvickyauction/consumer/config/RedisStreamConfig.java deleted file mode 100644 index 898b2d17..00000000 --- a/src/main/java/com/wootecam/luckyvickyauction/consumer/config/RedisStreamConfig.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.wootecam.luckyvickyauction.consumer.config; - -import java.util.UUID; -import lombok.Getter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.RedisStandaloneConfiguration; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; - -@Configuration -public class RedisStreamConfig { - - @Value("${spring.data.redis.host}") - private String redisHost; - - @Value("${spring.data.redis.port}") - private String redisPort; - - @Value("${spring.data.redis.password}") - private String redisPassword; - - @Getter - @Value("${stream.key}") - private String streamKey; - @Getter - @Value("${stream.consumer.groupName}") - private String consumerGroupName; - @Getter - private String consumerName = UUID.randomUUID().toString(); - - // todo [추후 서버를 분리하면 모듈이 분리될 상황을 가정한 빈입니다.] [2024-08-23] [yudonggeun] -// @Bean - public RedisConnectionFactory redisConnectionFactory() { - RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); - redisStandaloneConfiguration.setHostName(redisHost); - redisStandaloneConfiguration.setPort(Integer.parseInt(redisPort)); - redisStandaloneConfiguration.setPassword(redisPassword); - - return new LettuceConnectionFactory(redisStandaloneConfiguration); - } - -} diff --git a/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/PendingMessageConsumer.java b/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/PendingMessageConsumer.java deleted file mode 100644 index c25a9e22..00000000 --- a/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/PendingMessageConsumer.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.wootecam.luckyvickyauction.consumer.presentation; - -import com.wootecam.luckyvickyauction.consumer.config.RedisStreamConfig; -import com.wootecam.luckyvickyauction.consumer.service.MessageRouterService; -import java.time.Duration; -import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.connection.stream.MapRecord; -import org.springframework.data.redis.connection.stream.PendingMessage; -import org.springframework.data.redis.connection.stream.PendingMessages; -import org.springframework.data.redis.connection.stream.RecordId; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -@Slf4j -@EnableScheduling -@Component -@RequiredArgsConstructor -public class PendingMessageConsumer { - - private final RedisOperator redisOperator; - private final RedisStreamConfig redisStreamConfig; - private final MessageRouterService messageRouterService; - - @Scheduled(fixedRate = 1000) - public void consumePendingMessage() { - - // 처리되지 않은 메시지 조회 - PendingMessages pendingMessageInfos = redisOperator.getPendingMessage( - redisStreamConfig.getStreamKey(), - redisStreamConfig.getConsumerGroupName(), - redisStreamConfig.getConsumerName() - ); - - RecordId[] recordIds = pendingMessageInfos.stream().map( - PendingMessage::getId - ).toArray(RecordId[]::new); - - // 처리되지 않은 메시지 데이터 조회 - List> messages = redisOperator.claim( - redisStreamConfig.getStreamKey(), - redisStreamConfig.getConsumerGroupName(), - redisStreamConfig.getConsumerName(), - Duration.ofMinutes(1), - recordIds - ); - - // 메시지 처리 - messages.forEach(message -> { - messageRouterService.consume( - message, - () -> redisOperator.acknowledge(redisStreamConfig.getConsumerGroupName(), message) - ); - }); - } -} diff --git a/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/RedisOperator.java b/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/RedisOperator.java deleted file mode 100644 index 97cf67da..00000000 --- a/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/RedisOperator.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.wootecam.luckyvickyauction.consumer.presentation; - -import io.lettuce.core.api.async.RedisAsyncCommands; -import io.lettuce.core.codec.StringCodec; -import io.lettuce.core.output.StatusOutput; -import io.lettuce.core.protocol.CommandArgs; -import io.lettuce.core.protocol.CommandKeyword; -import io.lettuce.core.protocol.CommandType; -import java.time.Duration; -import java.util.Iterator; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.domain.Range; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.stream.Consumer; -import org.springframework.data.redis.connection.stream.MapRecord; -import org.springframework.data.redis.connection.stream.PendingMessages; -import org.springframework.data.redis.connection.stream.ReadOffset; -import org.springframework.data.redis.connection.stream.RecordId; -import org.springframework.data.redis.connection.stream.StreamInfo; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.data.redis.serializer.StringRedisSerializer; -import org.springframework.data.redis.stream.StreamMessageListenerContainer; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class RedisOperator { - - private static final Logger log = LoggerFactory.getLogger(RedisOperator.class); - private final RedisConnectionFactory redisConnectionFactory; - private final StringRedisTemplate redisTemplate; - - - public boolean isStreamConsumerGroupExist(String streamKey, String consumerGroupName) { - Iterator iterator = this.redisTemplate - .opsForStream().groups(streamKey).stream().iterator(); - - while (iterator.hasNext()) { - StreamInfo.XInfoGroup xInfoGroup = iterator.next(); - if (xInfoGroup.groupName().equals(consumerGroupName)) { - return true; - } - } - return false; - } - - public void createStreamConsumerGroup(String streamKey, String consumerGroupName) { - // if stream is not exist, create stream and consumer group of it - if (Boolean.FALSE.equals(this.redisTemplate.hasKey(streamKey))) { - RedisAsyncCommands commands = (RedisAsyncCommands) redisConnectionFactory - .getConnection() - .getNativeConnection(); - - CommandArgs args = new CommandArgs<>(StringCodec.UTF8) - .add(CommandKeyword.CREATE) - .add(streamKey) - .add(consumerGroupName) - .add("0") - .add("MKSTREAM"); - - commands.dispatch(CommandType.XGROUP, new StatusOutput(StringCodec.UTF8), args); - } - // stream is exist, create consumerGroup if is not exist - else { - if (!isStreamConsumerGroupExist(streamKey, consumerGroupName)) { - this.redisTemplate.opsForStream().createGroup(streamKey, ReadOffset.from("0"), consumerGroupName); - } - } - } - - public StreamMessageListenerContainer createStreamMessageListenerContainer() { - return StreamMessageListenerContainer.create(redisConnectionFactory, - StreamMessageListenerContainer - .StreamMessageListenerContainerOptions.builder() - .hashKeySerializer(new StringRedisSerializer()) - .hashValueSerializer(new StringRedisSerializer()) - .pollTimeout(Duration.ofMillis(20)) - .build() - ); - } - - public void acknowledge(String consumerGroup, MapRecord message) { - Long ack = this.redisTemplate.opsForStream().acknowledge(consumerGroup, message); - if (ack == 0) { - log.error("Acknowledge failed. MessageId: {}", message.getId()); - } else { - log.info("Acknowledge success. MessageId: {}", message.getId()); - } - } - - public PendingMessages getPendingMessage(String streamKey, String consumerGroup, String consumerName) { - return this.redisTemplate.opsForStream() - .pending(streamKey, - Consumer.from(consumerGroup, consumerName), - Range.unbounded(), - 100L - ); - } - - public List> claim(String streamKey, - String consumerGroup, String consumerName, - Duration minIdleTime, RecordId... messageIds) { - if (messageIds.length < 1) { - return List.of(); - } - - return this.redisTemplate.opsForStream() - .claim(streamKey, consumerGroup, consumerName, minIdleTime, messageIds); - } -} diff --git a/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/RedisStreamConsumer.java b/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/RedisStreamConsumer.java deleted file mode 100644 index 968396c7..00000000 --- a/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/RedisStreamConsumer.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.wootecam.luckyvickyauction.consumer.presentation; - -import com.wootecam.luckyvickyauction.consumer.config.RedisStreamConfig; -import com.wootecam.luckyvickyauction.consumer.service.MessageRouterService; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import java.time.Duration; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.connection.stream.Consumer; -import org.springframework.data.redis.connection.stream.MapRecord; -import org.springframework.data.redis.connection.stream.ReadOffset; -import org.springframework.data.redis.connection.stream.StreamOffset; -import org.springframework.data.redis.stream.StreamListener; -import org.springframework.data.redis.stream.StreamMessageListenerContainer; -import org.springframework.data.redis.stream.Subscription; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@RequiredArgsConstructor -public class RedisStreamConsumer implements StreamListener> { - - private final RedisOperator redisOperator; - private final RedisStreamConfig redisStreamConfig; - private final MessageRouterService messageRouterService; - - private StreamMessageListenerContainer> listenerContainer; - private Subscription subscription; - - @Override - public void onMessage(MapRecord message) { - messageRouterService.consume( - message, - () -> redisOperator.acknowledge(redisStreamConfig.getConsumerGroupName(), message) - ); - } - - @PostConstruct - public void init() throws InterruptedException { - // Consumer Group 설정 - this.redisOperator.createStreamConsumerGroup( - redisStreamConfig.getStreamKey(), - redisStreamConfig.getConsumerGroupName()); - - // StreamMessageListenerContainer 설정 - this.listenerContainer = this.redisOperator.createStreamMessageListenerContainer(); - - //Subscription 설정 - this.subscription = this.listenerContainer.receive( - Consumer.from( - redisStreamConfig.getConsumerGroupName(), - redisStreamConfig.getConsumerName() - ), - StreamOffset.create( - redisStreamConfig.getStreamKey(), - ReadOffset.lastConsumed() - ), - this - ); - - // redis stream 구독 생성까지 Blocking 된다. 이때의 timeout 2초다. 만약 2초보다 빠르게 구독이 생성되면 바로 다음으로 넘어간다. - this.subscription.await(Duration.ofSeconds(2)); - - // redis listen 시작 - this.listenerContainer.start(); - } - - @PreDestroy - public void destroy() { - if (this.subscription != null) { - this.subscription.cancel(); - } - if (this.listenerContainer != null) { - this.listenerContainer.stop(); - } - } -} diff --git a/src/main/java/com/wootecam/luckyvickyauction/consumer/service/MessageRouterService.java b/src/main/java/com/wootecam/luckyvickyauction/consumer/service/MessageRouterService.java deleted file mode 100644 index f513c112..00000000 --- a/src/main/java/com/wootecam/luckyvickyauction/consumer/service/MessageRouterService.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.wootecam.luckyvickyauction.consumer.service; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.wootecam.luckyvickyauction.core.auction.service.Auctioneer; -import com.wootecam.luckyvickyauction.global.dto.AuctionPurchaseRequestMessage; -import com.wootecam.luckyvickyauction.global.dto.AuctionRefundRequestMessage; -import jakarta.transaction.Transactional; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.connection.stream.MapRecord; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@RequiredArgsConstructor -public class MessageRouterService { - - private final Auctioneer auctioneer; - private final ObjectMapper objectMapper; - - @Transactional - public void consume(MapRecord mapRecord, Runnable postProcess) { - - log.debug("MessageId: {}", mapRecord.getId()); - log.debug("Stream: {}", mapRecord.getStream()); - log.debug("Body: {}", mapRecord.getValue()); - - Map message = mapRecord.getValue(); - for (Object type : message.keySet()) { - String messageType = (String) type; - - switch (messageType) { - case "purchase": - auctioneer.process(objectMapper.convertValue(message.get(messageType), - AuctionPurchaseRequestMessage.class)); - break; - case "refund": - auctioneer.refund(objectMapper.convertValue(message.get(messageType), - AuctionRefundRequestMessage.class)); - break; - default: - log.warn("Unknown message type: {}", messageType); - } - } - postProcess.run(); - } -} diff --git a/src/main/java/com/wootecam/luckyvickyauction/core/payment/entity/ReceiptEntity.java b/src/main/java/com/wootecam/luckyvickyauction/core/payment/entity/ReceiptEntity.java index a25dca41..4052d82f 100644 --- a/src/main/java/com/wootecam/luckyvickyauction/core/payment/entity/ReceiptEntity.java +++ b/src/main/java/com/wootecam/luckyvickyauction/core/payment/entity/ReceiptEntity.java @@ -2,6 +2,7 @@ import com.wootecam.luckyvickyauction.core.payment.domain.ReceiptStatus; import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.Id; @@ -12,10 +13,17 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @Entity @Table(name = "RECEIPT") @Getter +@EntityListeners(AuditingEntityListener.class) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ReceiptEntity { @Id @@ -28,7 +36,9 @@ public class ReceiptEntity { private long auctionId; private Long sellerId; private Long buyerId; + @CreatedDate private LocalDateTime createdAt; + @LastModifiedDate private LocalDateTime updatedAt; @Builder diff --git a/src/main/java/com/wootecam/luckyvickyauction/core/payment/infra/ReceiptQueryDslRepositoryImpl.java b/src/main/java/com/wootecam/luckyvickyauction/core/payment/infra/ReceiptQueryDslRepositoryImpl.java index a42f7fa6..74bff846 100644 --- a/src/main/java/com/wootecam/luckyvickyauction/core/payment/infra/ReceiptQueryDslRepositoryImpl.java +++ b/src/main/java/com/wootecam/luckyvickyauction/core/payment/infra/ReceiptQueryDslRepositoryImpl.java @@ -21,7 +21,7 @@ public List findAllByBuyerId(Long buyerId, BuyerReceiptSearchCond .select(receipt) .from(receipt) .where(receipt.buyerId.eq(buyerId)) - .orderBy(receipt.id.desc()) + .orderBy(receipt.createdAt.desc()) .limit(condition.size()) .offset(condition.offset()) .fetch(); @@ -35,7 +35,7 @@ public List findAllBySellerId(Long sellerId, SellerReceiptSearchC .select(receipt) .from(receipt) .where(receipt.sellerId.eq(sellerId)) - .orderBy(receipt.id.desc()) + .orderBy(receipt.createdAt.desc()) .limit(condition.size()) .offset(condition.offset()) .fetch(); diff --git a/src/main/java/com/wootecam/luckyvickyauction/global/config/JpaConfig.java b/src/main/java/com/wootecam/luckyvickyauction/global/config/JpaConfig.java index 398f9022..8a96c37f 100644 --- a/src/main/java/com/wootecam/luckyvickyauction/global/config/JpaConfig.java +++ b/src/main/java/com/wootecam/luckyvickyauction/global/config/JpaConfig.java @@ -5,8 +5,10 @@ import jakarta.persistence.PersistenceContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @Configuration +@EnableJpaAuditing public class JpaConfig { @PersistenceContext diff --git a/src/test/java/com/wootecam/luckyvickyauction/core/payment/infra/ReceiptCoreRepositoryTest.java b/src/test/java/com/wootecam/luckyvickyauction/core/payment/infra/ReceiptCoreRepositoryTest.java index 7c577472..2ba89b8e 100644 --- a/src/test/java/com/wootecam/luckyvickyauction/core/payment/infra/ReceiptCoreRepositoryTest.java +++ b/src/test/java/com/wootecam/luckyvickyauction/core/payment/infra/ReceiptCoreRepositoryTest.java @@ -83,8 +83,6 @@ class 거래내역_저장_시에 { .auctionId(1L) .sellerId(1L) .buyerId(2L) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) .build(); // when @@ -98,9 +96,7 @@ class 거래내역_저장_시에 { () -> assertThat(saved.getReceiptStatus()).isEqualTo(receipt.getReceiptStatus()), () -> assertThat(saved.getAuctionId()).isEqualTo(receipt.getAuctionId()), () -> assertThat(saved.getSellerId()).isEqualTo(receipt.getSellerId()), - () -> assertThat(saved.getBuyerId()).isEqualTo(receipt.getBuyerId()), - () -> assertThat(saved.getCreatedAt()).isEqualTo(receipt.getCreatedAt()), - () -> assertThat(saved.getUpdatedAt()).isEqualTo(receipt.getUpdatedAt()) + () -> assertThat(saved.getBuyerId()).isEqualTo(receipt.getBuyerId()) ); }