From 08361dabaef448a5a08048b82dc9e95a88d10e68 Mon Sep 17 00:00:00 2001 From: 1223v <1223v@naver.com> Date: Mon, 26 May 2025 00:34:55 +0900 Subject: [PATCH 1/6] =?UTF-8?q?Feat:=20polling=20->=20sse=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ compose.yaml | 25 +++++++++++++++++++ .../src/order/OrderEventProducer.java | 20 +++++++++++++++ .../src/order/OrderServiceImpl.java | 12 +++++++++ .../src/order/dto/OrderUpdateMessage.java | 11 ++++++++ 5 files changed, 71 insertions(+) create mode 100644 compose.yaml create mode 100644 src/main/java/com/readyvery/readyverydemo/src/order/OrderEventProducer.java create mode 100644 src/main/java/com/readyvery/readyverydemo/src/order/dto/OrderUpdateMessage.java diff --git a/build.gradle b/build.gradle index ba391eb..547a881 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,9 @@ dependencies { //SOLAPI implementation 'net.nurigo:sdk:4.2.7' + //Kafka + implementation 'org.springframework.kafka:spring-kafka:3.0.7' + // //Querydsl 추가 // implementation 'com.querydsl:querydsl-core:5.0.0' // implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..1d09a62 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,25 @@ +version: '3' +services: + zookeeper: + image: confluentinc/cp-zookeeper:latest + container_name: zookeeper + ports: + - "2181:2181" + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + + kafka: + image: confluentinc/cp-kafka:latest + container_name: kafka + depends_on: + - zookeeper + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + # 클러스터 내부와 외부에서 접근할 listener 설정 (내부: kafka:29092, 외부: localhost:9092) + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 diff --git a/src/main/java/com/readyvery/readyverydemo/src/order/OrderEventProducer.java b/src/main/java/com/readyvery/readyverydemo/src/order/OrderEventProducer.java new file mode 100644 index 0000000..582d5f2 --- /dev/null +++ b/src/main/java/com/readyvery/readyverydemo/src/order/OrderEventProducer.java @@ -0,0 +1,20 @@ +package com.readyvery.readyverydemo.src.order; + +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +import com.readyvery.readyverydemo.src.order.dto.OrderUpdateMessage; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class OrderEventProducer { + + private final KafkaTemplate kafkaTemplate; + + public void sendOrderUpdate(OrderUpdateMessage message) { + // 예: "order_updates"라는 토픽 사용 + kafkaTemplate.send("order_updates", message); + } +} diff --git a/src/main/java/com/readyvery/readyverydemo/src/order/OrderServiceImpl.java b/src/main/java/com/readyvery/readyverydemo/src/order/OrderServiceImpl.java index f77f264..51dd682 100644 --- a/src/main/java/com/readyvery/readyverydemo/src/order/OrderServiceImpl.java +++ b/src/main/java/com/readyvery/readyverydemo/src/order/OrderServiceImpl.java @@ -65,6 +65,7 @@ import com.readyvery.readyverydemo.src.order.dto.HistoryDetailRes; import com.readyvery.readyverydemo.src.order.dto.HistoryRes; import com.readyvery.readyverydemo.src.order.dto.OrderMapper; +import com.readyvery.readyverydemo.src.order.dto.OrderUpdateMessage; import com.readyvery.readyverydemo.src.order.dto.PaySuccess; import com.readyvery.readyverydemo.src.order.dto.PaymentReq; import com.readyvery.readyverydemo.src.order.dto.TossCancelReq; @@ -97,6 +98,7 @@ public class OrderServiceImpl implements OrderService { private final PointRepository pointRepository; private final PointServiceFacade pointServiceFacade; private final OrderNumberSequenceRepository orderNumberSequenceRepository; + private final OrderEventProducer orderEventProducer; @Override public FoodyDetailRes getFoody(Long storeId, Long foodyId, Long inout) { @@ -332,6 +334,16 @@ public PaySuccess tossPaymentSuccess(String paymentKey, String orderId, Long amo if (!Objects.equals(order.getMessage(), TOSSPAYMENT_SUCCESS_MESSAGE)) { return orderMapper.tosspaymentDtoToPaySuccess(order.getMessage()); } + + OrderUpdateMessage message = new OrderUpdateMessage(); + message.setOrderId(order.getOrderId()); + message.setStoreId(order.getStore().getId()); // 중요! + message.setStatus(order.getProgress().name()); // "ORDER" + message.setMessage("주문 상태가 " + order.getProgress().name() + " 로 변경되었습니다."); + + // 4. 메시지 발행 + orderEventProducer.sendOrderUpdate(message); + //TODO: 영수증 처리 Receipt receipt = orderMapper.tosspaymentDtoToReceipt(tosspaymentDto, order); receiptRepository.save(receipt); diff --git a/src/main/java/com/readyvery/readyverydemo/src/order/dto/OrderUpdateMessage.java b/src/main/java/com/readyvery/readyverydemo/src/order/dto/OrderUpdateMessage.java new file mode 100644 index 0000000..519aadd --- /dev/null +++ b/src/main/java/com/readyvery/readyverydemo/src/order/dto/OrderUpdateMessage.java @@ -0,0 +1,11 @@ +package com.readyvery.readyverydemo.src.order.dto; + +import lombok.Data; + +@Data +public class OrderUpdateMessage { + private String orderId; // 주문 식별자 + private Long storeId; // 가게의 번호 + private String status; // 주문 상태 (ORDER, MAKE, COMPLETE 등) + private String message; // 기타 메시지 +} From 23d53d74c7faf9560bdb37c5c8df88bf7c8e4bec Mon Sep 17 00:00:00 2001 From: 1223v <1223v@naver.com> Date: Mon, 21 Jul 2025 10:30:22 +0900 Subject: [PATCH 2/6] =?UTF-8?q?Fix:=20OAuth=20=EB=B3=B5=ED=95=A9=20?= =?UTF-8?q?=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EC=A0=81=EC=9A=A9=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/readyvery/readyverydemo/domain/UserInfo.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/readyvery/readyverydemo/domain/UserInfo.java b/src/main/java/com/readyvery/readyverydemo/domain/UserInfo.java index 8d1b9b7..e43dcd1 100644 --- a/src/main/java/com/readyvery/readyverydemo/domain/UserInfo.java +++ b/src/main/java/com/readyvery/readyverydemo/domain/UserInfo.java @@ -29,7 +29,11 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @Builder -@Table(name = "USER") +@Table(name = "USER", indexes = { + @Index(name = "idx_email", columnList = "email"), + @Index(name = "idx_refresh_token", columnList = "refresh_token"), + @Index(name = "idx_social_type_id", columnList = "social_type, social_id") + }) @AllArgsConstructor @Slf4j public class UserInfo extends BaseTimeEntity { From 53cc66e0ad054ece34cc857f4c224ecce18f1c7a Mon Sep 17 00:00:00 2001 From: 1223v <1223v@naver.com> Date: Mon, 21 Jul 2025 10:31:29 +0900 Subject: [PATCH 3/6] =?UTF-8?q?Fix:=20=EC=9C=A0=EC=A0=80-=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20jpa=20N+1=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../readyverydemo/domain/repository/OrdersRepository.java | 4 ++++ .../readyvery/readyverydemo/src/order/OrderServiceImpl.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/readyvery/readyverydemo/domain/repository/OrdersRepository.java b/src/main/java/com/readyvery/readyverydemo/domain/repository/OrdersRepository.java index 7587241..20fc978 100644 --- a/src/main/java/com/readyvery/readyverydemo/domain/repository/OrdersRepository.java +++ b/src/main/java/com/readyvery/readyverydemo/domain/repository/OrdersRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.EntityGraph; import com.readyvery.readyverydemo.domain.Order; import com.readyvery.readyverydemo.domain.Progress; @@ -20,6 +21,9 @@ public interface OrdersRepository extends JpaRepository { Optional> findAllByUserInfo(UserInfo userInfo); + @EntityGraph(attributePaths = {"store", "cart", "userInfo", "receipt"}) + Optional> findAllWithAssociationsByUserInfo(UserInfo userInfo); + Long countByCreatedAtBetweenAndProgressNotAndStore(LocalDateTime localDateTime, LocalDateTime localDateTime1, Progress progress, Store store); } diff --git a/src/main/java/com/readyvery/readyverydemo/src/order/OrderServiceImpl.java b/src/main/java/com/readyvery/readyverydemo/src/order/OrderServiceImpl.java index 51dd682..d77f180 100644 --- a/src/main/java/com/readyvery/readyverydemo/src/order/OrderServiceImpl.java +++ b/src/main/java/com/readyvery/readyverydemo/src/order/OrderServiceImpl.java @@ -555,7 +555,7 @@ private void verifyOrderCurrent(Order order) { } private List getOrders(UserInfo user) { - return ordersRepository.findAllByUserInfo(user).orElseThrow( + return ordersRepository.findAllWithAssociationsByUserInfo(user).orElseThrow( () -> new BusinessLogicException(ExceptionCode.ORDER_NOT_FOUND)); } From 564a080f29b8192282254a1c8f7c94abd334620a Mon Sep 17 00:00:00 2001 From: 1223v <1223v@naver.com> Date: Mon, 21 Jul 2025 11:16:34 +0900 Subject: [PATCH 4/6] =?UTF-8?q?Fix:=20refreshtoken=20=EA=B3=A0=EC=9C=A0?= =?UTF-8?q?=EA=B0=92=20=EC=B6=94=EA=B0=80=EB=A1=9C=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/jwt/service/create/HmacJwtTokenGeneratorImpl.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/readyvery/readyverydemo/security/jwt/service/create/HmacJwtTokenGeneratorImpl.java b/src/main/java/com/readyvery/readyverydemo/security/jwt/service/create/HmacJwtTokenGeneratorImpl.java index 827869c..7a8d4ec 100644 --- a/src/main/java/com/readyvery/readyverydemo/security/jwt/service/create/HmacJwtTokenGeneratorImpl.java +++ b/src/main/java/com/readyvery/readyverydemo/security/jwt/service/create/HmacJwtTokenGeneratorImpl.java @@ -5,6 +5,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Date; +import java.util.UUID; import org.springframework.stereotype.Component; @@ -40,6 +41,7 @@ public String generateRefreshToken() { return JWT.create() .withSubject(REFRESH_TOKEN_SUBJECT) .withExpiresAt(Date.from(expirationTime)) + .withJWTId(UUID.randomUUID().toString()) // 고유값 추가로 중복 방지 .sign(jwtConfig.getAlgorithm()); } } From 83d0bd6f8ca2e9f481535e290d89da28f02dcf88 Mon Sep 17 00:00:00 2001 From: 1223v <1223v@naver.com> Date: Mon, 21 Jul 2025 12:23:32 +0900 Subject: [PATCH 5/6] =?UTF-8?q?Fix:=20fallback=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../refreshtoken/RefreshTokenServiceImpl.java | 44 +++++++++++++++---- .../fallback/RefreshTokenFallback.java | 21 +++++++++ .../RefreshTokenFallbackRepository.java | 6 +++ 3 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/readyvery/readyverydemo/src/refreshtoken/fallback/RefreshTokenFallback.java create mode 100644 src/main/java/com/readyvery/readyverydemo/src/refreshtoken/fallback/RefreshTokenFallbackRepository.java diff --git a/src/main/java/com/readyvery/readyverydemo/src/refreshtoken/RefreshTokenServiceImpl.java b/src/main/java/com/readyvery/readyverydemo/src/refreshtoken/RefreshTokenServiceImpl.java index cab58fd..6770335 100644 --- a/src/main/java/com/readyvery/readyverydemo/src/refreshtoken/RefreshTokenServiceImpl.java +++ b/src/main/java/com/readyvery/readyverydemo/src/refreshtoken/RefreshTokenServiceImpl.java @@ -8,27 +8,53 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import java.util.concurrent.TimeUnit; +import java.time.LocalDateTime; +import com.readyvery.readyverydemo.src.refreshtoken.fallback.RefreshTokenFallback; +import com.readyvery.readyverydemo.src.refreshtoken.fallback.RefreshTokenFallbackRepository; @Service @RequiredArgsConstructor @Slf4j public class RefreshTokenServiceImpl implements RefreshTokenService { - private final RefreshTokenRepository refreshTokenRepository; + private final RedisTemplate redisTemplate; + private final RefreshTokenFallbackRepository fallbackRepository; + private static final String REFRESH_TOKEN_PREFIX = "refreshToken:"; + private static final long REFRESH_TOKEN_TTL = 60L * 60 * 24 * 7; @Override public void removeRefreshTokenInRedis(String email) { - RefreshToken refreshToken = refreshTokenRepository.findById(email) - .orElseThrow(() -> new UsernameNotFoundException("해당 이메일이 존재하지 않습니다.")); - refreshTokenRepository.delete(refreshToken); + try { + redisTemplate.delete(REFRESH_TOKEN_PREFIX + email); + } catch (Exception e) { + log.error("Redis 삭제 실패, DB fallback 사용: {}", e.getMessage()); + fallbackRepository.deleteById(email); + } } @Override public void saveRefreshTokenInRedis(String email, String refreshToken) { - RefreshToken token = RefreshToken.builder() - .id(email) - .refreshToken(refreshToken) - .build(); - refreshTokenRepository.save(token); + try { + redisTemplate.opsForValue().set(REFRESH_TOKEN_PREFIX + email, refreshToken, REFRESH_TOKEN_TTL, TimeUnit.SECONDS); + } catch (Exception e) { + log.error("Redis 저장 실패, DB fallback 사용: {}", e.getMessage()); + fallbackRepository.save( + new RefreshTokenFallback(email, refreshToken, LocalDateTime.now().plusSeconds(REFRESH_TOKEN_TTL)) + ); + } + } + + public String getRefreshToken(String email) { + try { + return redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX + email); + } catch (Exception e) { + log.error("Redis 조회 실패, DB fallback 사용: {}", e.getMessage()); + return fallbackRepository.findById(email) + .filter(token -> token.getExpiresAt().isAfter(LocalDateTime.now())) + .map(RefreshTokenFallback::getRefreshToken) + .orElse(null); + } } } diff --git a/src/main/java/com/readyvery/readyverydemo/src/refreshtoken/fallback/RefreshTokenFallback.java b/src/main/java/com/readyvery/readyverydemo/src/refreshtoken/fallback/RefreshTokenFallback.java new file mode 100644 index 0000000..5a8cfbf --- /dev/null +++ b/src/main/java/com/readyvery/readyverydemo/src/refreshtoken/fallback/RefreshTokenFallback.java @@ -0,0 +1,21 @@ +package com.readyvery.readyverydemo.src.refreshtoken.fallback; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class RefreshTokenFallback { + @Id + private String userId; + private String refreshToken; + private LocalDateTime expiresAt; +} \ No newline at end of file diff --git a/src/main/java/com/readyvery/readyverydemo/src/refreshtoken/fallback/RefreshTokenFallbackRepository.java b/src/main/java/com/readyvery/readyverydemo/src/refreshtoken/fallback/RefreshTokenFallbackRepository.java new file mode 100644 index 0000000..a47408e --- /dev/null +++ b/src/main/java/com/readyvery/readyverydemo/src/refreshtoken/fallback/RefreshTokenFallbackRepository.java @@ -0,0 +1,6 @@ +package com.readyvery.readyverydemo.src.refreshtoken.fallback; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RefreshTokenFallbackRepository extends JpaRepository { +} \ No newline at end of file From 3665708a37bbf53f0437519b5dd0f8c54ce83d86 Mon Sep 17 00:00:00 2001 From: 1223v <1223v@naver.com> Date: Mon, 21 Jul 2025 12:24:51 +0900 Subject: [PATCH 6/6] =?UTF-8?q?Chore:=20fallback=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/oauth2/handler/OAuth2LoginSuccessHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/readyvery/readyverydemo/security/oauth2/handler/OAuth2LoginSuccessHandler.java b/src/main/java/com/readyvery/readyverydemo/security/oauth2/handler/OAuth2LoginSuccessHandler.java index a08da47..9e2b915 100644 --- a/src/main/java/com/readyvery/readyverydemo/security/oauth2/handler/OAuth2LoginSuccessHandler.java +++ b/src/main/java/com/readyvery/readyverydemo/security/oauth2/handler/OAuth2LoginSuccessHandler.java @@ -64,6 +64,7 @@ private void loginSuccess(HttpServletResponse response, CustomOAuth2User oAuth2U String refreshToken = jwtService.createRefreshToken(); jwtService.sendAccessAndRefreshToken(response, accessToken, refreshToken, role); + // RefreshTokenRepository를 직접 사용하지 말고 반드시 RefreshTokenService를 통해서만 접근하세요. (key-value 방식으로 변경됨) refreshTokenServiceImpl.saveRefreshTokenInRedis(oAuth2User.getEmail(), refreshToken); }