From f0fd9da6907800e084efa63c318e768993a532f6 Mon Sep 17 00:00:00 2001 From: juyeonBaeck Date: Wed, 4 Feb 2026 11:56:43 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Bug:=20PaymentKafkaConsumer=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-aws-point.yml | 2 +- .../controller/TestController.java | 40 ------------ .../kafka/PaymentKafkaConsumer.java | 60 ++++++++++++----- .../pointservice/service/PaymentConsumer.java | 65 ------------------- 4 files changed, 46 insertions(+), 121 deletions(-) delete mode 100644 cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/controller/TestController.java delete mode 100644 cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/service/PaymentConsumer.java 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..2f53737 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,65 @@ package com.techsemina.msa.pointservice.kafka; +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); + + // 1. DTO 변환 + CashResponseDTO result = objectMapper.readValue(message, CashResponseDTO.class); + + // 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 ("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 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 From 2bc053b528b1d45485ed104c301cacbe434a5eab Mon Sep 17 00:00:00 2001 From: juyeonBaeck Date: Wed, 4 Feb 2026 12:07:28 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Bug:=20PaymentKafkaConsumer=20tru-catch=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kafka/PaymentKafkaConsumer.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) 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 2f53737..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,5 +1,6 @@ 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; @@ -28,8 +29,20 @@ public void consumeWithdrawResult(String message) throws Exception { log.info("📨 [Kafka] 결과 수신: {}", message); - // 1. DTO 변환 - CashResponseDTO result = objectMapper.readValue(message, CashResponseDTO.class); + // 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())) { @@ -39,13 +52,13 @@ public void consumeWithdrawResult(String message) throws Exception { // ❌ 실패 시: 롤백(환불) 로직 진행 log.warn("🚨 결제 실패 수신 (사유: {}). 환불을 진행합니다.", result.getMessage()); - // (1) 장부(DB)에서 주문 조회 (orderId로 찾기!) + // (1) 장부(DB)에서 주문 조회 (orderId로 찾기) Payment payment = paymentRepository.findByOrderId(result.getOrderId()) .orElseThrow(() -> new RuntimeException("주문 정보를 찾을 수 없습니다.")); // (2) 이미 처리된 건인지 확인 (중복 방지) - if ("FAILED".equals(payment.getStatus())) { - log.info("이미 처리된 환불 건입니다."); + if (!"PENDING".equals(payment.getStatus())) { + log.info("⏭️ 이미 처리가 완료된 건입니다. (현재 상태: {}). 로직을 건너뜁니다.", payment.getStatus()); return; } // (3) 실제 사용했던 포인트 조회 @@ -56,7 +69,7 @@ public void consumeWithdrawResult(String message) throws Exception { // (5) 장부 상태 업데이트 (FAILED) payment.setStatus("FAILED"); - paymentRepository.save(payment); // @Transactional 있으면 자동 저장됨 (Dirty Checking) +// paymentRepository.save(payment); // @Transactional 있으면 자동 저장됨 (Dirty Checking) log.info("✅ 포인트 {}점 환불 완료.", usedPoint); }