-
Notifications
You must be signed in to change notification settings - Fork 0
Bug: SpringBoot version 4 -> 3.5.10 #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.techsemina.msa.pointservice.dto; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| public class ErrorResponse { | ||
| private String errorCode; // 예: "SERVER_ERROR", "INVALID_INPUT" | ||
| private String errorMessage; // 예: "DB 연결에 실패했습니다." | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.techsemina.msa.pointservice.exception; | ||
|
|
||
| import com.techsemina.msa.pointservice.dto.ErrorResponse; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
|
|
||
| @Slf4j | ||
| @RestControllerAdvice // 모든 컨트롤러의 에러를 여기서 잡음 | ||
| public class GlobalExceptionHandler { | ||
|
|
||
| // 1. 비즈니스 로직 에러 (예: 잘못된 입력값, 포인트 부족 등) | ||
| // 서비스에서 throw new IllegalArgumentException("포인트 부족") 했을 때 여기로 옴 | ||
| @ExceptionHandler(IllegalArgumentException.class) | ||
| public ResponseEntity<ErrorResponse> handleBadRequest(IllegalArgumentException e) { | ||
| log.warn("🚨 잘못된 요청 발생: {}", e.getMessage()); | ||
|
|
||
| ErrorResponse response = new ErrorResponse("BAD_REQUEST", e.getMessage()); | ||
| return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); | ||
| } | ||
|
|
||
| // 2. 시스템 에러 (예: DB 다운, Kafka 연결 실패, NullPointer 등) | ||
| // 위에서 안 잡힌 "나머지 모든 에러"는 여기서 잡힘 | ||
| @ExceptionHandler(Exception.class) | ||
| public ResponseEntity<ErrorResponse> handleServerException(Exception e) { | ||
| log.error("🔥 서버 내부 치명적 오류 발생", e); // 스택 트레이스 로그 남기기 | ||
|
|
||
| ErrorResponse response = new ErrorResponse("INTERNAL_SERVER_ERROR", "시스템 오류가 발생했습니다. 관리자에게 문의하세요."); | ||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,37 +1,65 @@ | ||||||||||||||||||
| 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") | ||||||||||||||||||
| public void consumeWithdrawResult(String message) { | ||||||||||||||||||
| try { | ||||||||||||||||||
| log.info("📨 [Kafka] 결과 수신: {}", message); | ||||||||||||||||||
| CashResponseDTO result = objectMapper.readValue(message, CashResponseDTO.class); | ||||||||||||||||||
|
|
||||||||||||||||||
| if ("SUCCESS".equals(result.getStatus())) { | ||||||||||||||||||
| // 성공 처리 | ||||||||||||||||||
| paymentService.completePayment(result.getOrderId()); | ||||||||||||||||||
| } else { | ||||||||||||||||||
| // 실패 -> 롤백 | ||||||||||||||||||
| paymentService.compensatePayment(result.getOrderId()); | ||||||||||||||||||
| @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; | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+47
to
50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 중복 방지 조건이
만약 이미
가드 조건을 통일하세요: 🔧 가드 조건 통일 // (2) 이미 처리된 건인지 확인 (중복 방지)
- if ("FAILED".equals(payment.getStatus())) {
- log.info("이미 처리된 환불 건입니다.");
+ if (!"PENDING".equals(payment.getStatus())) {
+ log.info("이미 처리된 건입니다. (상태: {})", payment.getStatus());
return;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| // (3) 실제 사용했던 포인트 조회 | ||||||||||||||||||
| Long usedPoint = payment.getPointAmount(); | ||||||||||||||||||
|
|
||||||||||||||||||
| // (4) 포인트 환불 | ||||||||||||||||||
| paymentService.compensatePayment(payment.getOrderId()); | ||||||||||||||||||
|
|
||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||
| log.error("❌ 메시지 처리 중 에러", e); | ||||||||||||||||||
| // (5) 장부 상태 업데이트 (FAILED) | ||||||||||||||||||
| payment.setStatus("FAILED"); | ||||||||||||||||||
| paymentRepository.save(payment); // @Transactional 있으면 자동 저장됨 (Dirty Checking) | ||||||||||||||||||
|
|
||||||||||||||||||
| log.info("✅ 포인트 {}점 환불 완료.", usedPoint); | ||||||||||||||||||
|
Comment on lines
+42
to
+61
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 중복 상태 업데이트 및 트랜잭션 범위 문제가 있습니다.
두 메서드가 각각 🔧 수정 방안: 역할 분리 명확화 // (4) 포인트 환불
paymentService.compensatePayment(payment.getOrderId());
- // (5) 장부 상태 업데이트 (FAILED)
- payment.setStatus("FAILED");
- paymentRepository.save(payment); // `@Transactional` 있으면 자동 저장됨 (Dirty Checking)
+ // compensatePayment 내부에서 이미 FAILED 상태로 변경 & 저장됨
+ // 중복 업데이트 제거
log.info("✅ 포인트 {}점 환불 완료.", usedPoint);또는 🤖 Prompt for AI Agents |
||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,7 +44,7 @@ public void processCompositePayment(PaymentRequest request) { | |
|
|
||
|
|
||
| // [Step 2] 현금 출금 요청 (Kafka) | ||
| // 1. 변수에 먼저 담습니다. | ||
| // 1. 변수에 담음 | ||
| CashRequestDTO cashMessage = new CashRequestDTO( | ||
| request.getOrderId(), | ||
| request.getLoginId(), | ||
|
|
@@ -54,7 +54,8 @@ public void processCompositePayment(PaymentRequest request) { | |
| log.info("-> [Kafka 전송] 토픽: core-withdraw-request, 데이터: {}", cashMessage); | ||
| // 3. 전송 | ||
| try { | ||
| kafkaTemplate.send("core-withdraw-request", cashMessage).get(5, TimeUnit.SECONDS); | ||
| // ❌ .get()으로 기다리지 말기! 트랜잭션 길어짐. 비동기로 보내기 | ||
| kafkaTemplate.send("core-withdraw-request", cashMessage); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kafka 토픽명이 여전히 하드코딩되어 있습니다. PR 목적 중 하나가 Kafka 하드코딩 수정인데, 🔧 토픽명 외부화 예시
kafka:
topic:
withdraw-request: core-withdraw-request서비스 클래스: + `@Value`("${kafka.topic.withdraw-request}")
+ private String withdrawRequestTopic;
// ...
- kafkaTemplate.send("core-withdraw-request", cashMessage);
+ kafkaTemplate.send(withdrawRequestTopic, cashMessage);🤖 Prompt for AI Agents |
||
| } catch (Exception e) { | ||
| log.error("Kafka 전송 실패: {}", e.getMessage()); | ||
| throw new RuntimeException("출금 요청 전송 실패", e); | ||
|
|
@@ -85,8 +86,13 @@ public void compensatePayment(String orderId) { | |
| Payment payment = paymentRepository.findByOrderId(orderId) | ||
| .orElseThrow(() -> new RuntimeException("주문 없음")); | ||
|
|
||
| // 이미 취소된 건지 체크하는 로직 등이 여기 들어가면 안전함 | ||
| if ("FAILED".equals(payment.getStatus())) return; | ||
| // 🚨 [핵심 수정] | ||
| // 기존: if ("FAILED".equals(payment.getStatus())) return; | ||
| // 수정: "PENDING(진행중)" 상태가 아니라면(이미 성공했거나 실패했으면) 건드리지 마라! | ||
| if (!"PENDING".equals(payment.getStatus())) { | ||
| log.warn("🚫 이미 처리가 완료된 주문입니다. (상태: {}) - 환불 중단", payment.getStatus()); | ||
| return; | ||
| } | ||
|
|
||
| // 포인트 환불 로직 | ||
| pointService.refundPoint(payment.getUserId(), payment.getPointAmount()); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.