diff --git a/.github/workflows/deploy-aws-point.yml b/.github/workflows/deploy-aws-point.yml index e49120d..6c7f28a 100644 --- a/.github/workflows/deploy-aws-point.yml +++ b/.github/workflows/deploy-aws-point.yml @@ -5,7 +5,7 @@ on: push: branches: [ "main" ] paths: - - 'cloud-services/msa-point-service/**' # ⭐️ 핵심: msa-point 폴더 내 변경이 있을 때만 작동 + - 'cloud-services/msa-point-service/**' # ⭐️ msa-point 폴더 내 변경이 있을 때만 작동 env: DOCKER_IMAGE: ${{ secrets.DOCKER_USERNAME }}/msa-point-service diff --git a/cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/controller/TestController.java b/cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/controller/TestController.java deleted file mode 100644 index 0f0c862..0000000 --- a/cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/controller/TestController.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.techsemina.msa.pointservice.controller; -import org.springframework.context.annotation.Profile; - - -import com.techsemina.msa.pointservice.dto.CashResponseDTO; -import lombok.RequiredArgsConstructor; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/test") -@RequiredArgsConstructor -@Profile({"dev", "test"}) // 운영 환경에서는 비활성화 -public class TestController { - - private final KafkaTemplate kafkaTemplate; - - // 🕵️‍♂️ 가짜 현금 서비스: "성공했다"고 뻥치기 - // 호출 주소: POST /test/fake-success?orderId=PAY-1234 - @PostMapping("/fake-success") - public String fakeSuccess(@RequestParam String orderId) { - - // 현금 서비스가 보내줄 법한 메시지를 우리가 직접 만듭니다. - CashResponseDTO fakeResponse = new CashResponseDTO(orderId,"test-user", "SUCCESS", "정상 처리됨"); - - // 'core-withdraw-result' 토픽으로 쏩니다. - // 그러면 아까 만든 PaymentConsumer가 이걸 낚아채서 'completePayment'를 실행하겠죠? - kafkaTemplate.send("core-withdraw-result", fakeResponse); - - return "가짜 성공 메시지 전송 완료! (OrderID: " + orderId + ")"; - } - - // 🕵️‍♂️ 가짜 현금 서비스: "실패했다"고 뻥치기 (롤백 테스트) - @PostMapping("/fake-fail") - public String fakeFail(@RequestParam String orderId) { - CashResponseDTO fakeResponse = new CashResponseDTO(orderId, "test-user","FAILED", "잔액 부족"); - kafkaTemplate.send("core-withdraw-result", fakeResponse); - return "가짜 실패 메시지 전송 완료 -> 환불될 것임"; - } -} \ No newline at end of file diff --git a/cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/kafka/PaymentKafkaConsumer.java b/cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/kafka/PaymentKafkaConsumer.java index 7e6470a..2f18fa2 100644 --- a/cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/kafka/PaymentKafkaConsumer.java +++ b/cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/kafka/PaymentKafkaConsumer.java @@ -1,35 +1,78 @@ package com.techsemina.msa.pointservice.kafka; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.techsemina.msa.pointservice.domain.Payment; +import com.techsemina.msa.pointservice.dto.CashResponseDTO; import com.techsemina.msa.pointservice.dto.CoreResultEvent; +import com.techsemina.msa.pointservice.repository.PaymentRepository; +import com.techsemina.msa.pointservice.service.PaymentService; import com.techsemina.msa.pointservice.service.PointService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @Slf4j public class PaymentKafkaConsumer { - private final PointService pointService; // 직접 주입 + private final PaymentService paymentService; + private final PaymentRepository paymentRepository; // 장부 조회용 + private final ObjectMapper objectMapper; - // 온프레미스(코어뱅킹)의 응답을 듣는 리스너 - @KafkaListener(topics = "core-result", groupId = "payment-group") - public void handleCoreResult(CoreResultEvent event) { - if ("SUCCESS".equals(event.getStatus())) { - log.info("🎉 최종 결제 성공! (포인트 O, 현금 O)"); + @KafkaListener(topics = "core-result", groupId = "point-service-group") + @Transactional // 에러 발생 시 롤백 & 카프카 재시도 + public void consumeWithdrawResult(String message) throws Exception { + + log.info("📨 [Kafka] 결과 수신: {}", message); + + // DTO 변환 + CashResponseDTO result; + + try { + // 1. 여기서 에러가 나면 catch로 점프! + result = objectMapper.readValue(message, CashResponseDTO.class); + } catch (Exception e) { + // 🗑️ 2. "이 메시지는 못 쓰는 겁니다"라고 로그 남기고 + log.error("❌ 치명적 에러: JSON 형식이 잘못되어 파싱할 수 없습니다. (재시도 중단) message={}", message, e); + + // 🛑 3. 여기서 return을 안 하면 밑에서 NullPointerException 터져서 또 롤백됩니다. + // 그냥 조용히 함수를 끝내야 Kafka가 "성공했구나" 하고 다음 메시지를 줍니다. + return; + } + + // 2. 성공 여부 체크 + if ("SUCCESS".equals(result.getStatus())) { + // ✅ 성공 시: 서비스의 완료 로직 호출 + paymentService.completePayment(result.getOrderId()); } else { - log.error("🚨 온프레미스 출금 실패! -> [보상 트랜잭션] 포인트 환불 진행"); - - // --- Step 3: 포인트 롤백 (보상 트랜잭션) --- - // 🔥 핵심: Kafka 안 쓰고 직접 서비스 호출해서 롤백! - try { - pointService.refundPoint(event.getUserId(), 5000L); // 금액은 예시 - log.info("✅ 포인트 환불(롤백) 완료. 결제가 취소되었습니다."); - } catch (Exception e) { - log.error("💀 큰일 났다... 환불마저 실패함. (관리자 호출 필요)"); + // ❌ 실패 시: 롤백(환불) 로직 진행 + log.warn("🚨 결제 실패 수신 (사유: {}). 환불을 진행합니다.", result.getMessage()); + + // (1) 장부(DB)에서 주문 조회 (orderId로 찾기) + Payment payment = paymentRepository.findByOrderId(result.getOrderId()) + .orElseThrow(() -> new RuntimeException("주문 정보를 찾을 수 없습니다.")); + + // (2) 이미 처리된 건인지 확인 (중복 방지) + if (!"PENDING".equals(payment.getStatus())) { + log.info("⏭️ 이미 처리가 완료된 건입니다. (현재 상태: {}). 로직을 건너뜁니다.", payment.getStatus()); + return; } + // (3) 실제 사용했던 포인트 조회 + Long usedPoint = payment.getPointAmount(); + + // (4) 포인트 환불 + paymentService.compensatePayment(payment.getOrderId()); + + // (5) 장부 상태 업데이트 (FAILED) + payment.setStatus("FAILED"); +// paymentRepository.save(payment); // @Transactional 있으면 자동 저장됨 (Dirty Checking) + + log.info("✅ 포인트 {}점 환불 완료.", usedPoint); } + } } \ No newline at end of file diff --git a/cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/service/PaymentConsumer.java b/cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/service/PaymentConsumer.java deleted file mode 100644 index 7c168ba..0000000 --- a/cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/service/PaymentConsumer.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.techsemina.msa.pointservice.service; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.techsemina.msa.pointservice.domain.Payment; -import com.techsemina.msa.pointservice.dto.CashResponseDTO; -import com.techsemina.msa.pointservice.dto.CoreResultEvent; -import com.techsemina.msa.pointservice.repository.PaymentRepository; -import com.techsemina.msa.pointservice.service.PaymentService; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.kafka.annotation.KafkaListener; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@Slf4j -@Component -@RequiredArgsConstructor -public class PaymentConsumer { - - private final PaymentService paymentService; - private final PaymentRepository paymentRepository; // 장부 조회용 - private final ObjectMapper objectMapper; - - @KafkaListener(topics = "core-withdraw-result", groupId = "point-service-group") - @Transactional // 에러 발생 시 롤백 & 카프카 재시도 - public void consumeWithdrawResult(String message) throws Exception { - - log.info("📨 [Kafka] 결과 수신: {}", message); - - // 1. DTO 변환 - CashResponseDTO result = objectMapper.readValue(message, CashResponseDTO.class); - - // 2. 성공 여부 체크 - if ("SUCCESS".equals(result.getStatus())) { - // ✅ 성공 시: 서비스의 완료 로직 호출 - paymentService.completePayment(result.getOrderId()); - } else { - // ❌ 실패 시: 롤백(환불) 로직 진행 - log.warn("🚨 결제 실패 수신 (사유: {}). 환불을 진행합니다.", result.getMessage()); - - // (1) 장부(DB)에서 주문 조회 (orderId로 찾기!) - Payment payment = paymentRepository.findByOrderId(result.getOrderId()) - .orElseThrow(() -> new RuntimeException("주문 정보를 찾을 수 없습니다.")); - - // (2) 이미 처리된 건인지 확인 (중복 방지) - if ("FAILED".equals(payment.getStatus())) { - log.info("이미 처리된 환불 건입니다."); - return; - } - // (3) 실제 사용했던 포인트 조회 - Long usedPoint = payment.getPointAmount(); - - // (4) 포인트 환불 - paymentService.compensatePayment(payment.getOrderId()); - - // (5) 장부 상태 업데이트 (FAILED) - payment.setStatus("FAILED"); - paymentRepository.save(payment); // @Transactional 있으면 자동 저장됨 (Dirty Checking) - - log.info("✅ 포인트 {}점 환불 완료.", usedPoint); - } - - } -} \ No newline at end of file