Skip to content

Feat: 포인트 결제 관련 core payment 로직 추가#39

Merged
caminobelllo merged 29 commits intomainfrom
feature/core-payment-point
Feb 3, 2026
Merged

Feat: 포인트 결제 관련 core payment 로직 추가#39
caminobelllo merged 29 commits intomainfrom
feature/core-payment-point

Conversation

@caminobelllo
Copy link
Contributor

@caminobelllo caminobelllo commented Feb 3, 2026

💡 PR 작업 내용

  • 기능 관련 작업
  • 버그 수정
  • 코드 개선 및 수정
  • 문서 작성
  • 배포
  • 브랜치
  • 기타 (아래에 자세한 내용 기입해 주세요)

📋 세부 작업 내용

  • 포인트 + 현금 복합 결제와 관련된 core payment 로직을 작성하였습니다.
  • 이를 위한 kafka consumer 로직을 수정하였습니다
  • 쿠폰 서비스와 관련하여 직접 계좌 개설 코드를 제거하고 kafka producer를 통한 메시지 발송 로직을 추가하였습니다.

📸 작업 화면 (스크린샷)

💬 리뷰 요구사항(선택)

⚠️ PR하기 전에 확인해 주세요.

  • 로컬테스트를 진행하셨나요?
  • 머지할 브랜치를 확인하셨나요?
  • 관련 label을 선택하셨나요?

⭐ 관련 이슈 번호 [#이슈번호]

🖇️ 기타

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 요청 본문 검증 강화(필수 필드·금액 유효성)
    • 비동기 결제 흐름 도입: PENDING 저장, 현금 인출 요청 발행 및 결과 처리
    • 현금 인출/응답 DTO 및 전송(프로듀서) 기능 추가
    • 주문ID 생성 및 결제 응답(202 Accepted) 반환
    • 신규 도커 이미지·헬스체크 적용
  • Improvements / Bug Fixes

    • 계좌 조회 방식 보완 및 오류 처리 일관성 강화
    • 감사(auditing) 활성화로 생성 시각 자동 기록

@caminobelllo caminobelllo self-assigned this Feb 3, 2026
@caminobelllo caminobelllo added the ✨ feat 기능 개발 시 label Feb 3, 2026
@caminobelllo caminobelllo linked an issue Feb 3, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

쿠폰 발급 로직을 제거하고 Kafka 기반 출금 처리(요청 수신 → 계좌 조회 → 출금 호출 → 결과 발행)를 도입했으며, 컨트롤러에 Bean Validation과 애플리케이션에 JPA 감사 기능을 활성화했습니다.

Changes

Cohort / File(s) Summary
애플리케이션 설정
on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/CorePaymentServiceApplication.java, on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/config/KafkaConsumerConfig.java
@EnableJpaAuditing 추가 및 Kafka 구성 파일 import 정리/주석 추가 (설정 레벨 변화).
컨트롤러 및 DTO 검증
on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/controller/PaymentController.java, .../dto/AmountRequestDto.java
@Valid 적용으로 컨트롤러에서 Bean Validation 활성화; AmountRequestDto@NotBlank, @NotNull, @Positive 추가(입력 검증 적용).
출금/응답 DTO 추가
on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/dto/CashRequestDTO.java, .../dto/CashResponseDTO.java
Kafka 출금 흐름용 DTO 신규 추가(loginId, amount, status, message 등).
리포지토리 확장
on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/repository/AccountRepository.java
Optional<Account> findByUserUuid(String userUuid) 메서드 추가(유저 UUID로 계좌 조회).
Kafka 프로듀서 도입
on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/KafkaProducerService.java
객체를 JSON으로 직렬화해 지정 토픽으로 발행하는 KafkaProducerService 신규 추가(직렬화 예외 처리 포함).
Kafka 컨슈머 흐름 변경
on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/KafkaConsumerService.java
쿠폰 관련 리스너 삭제, consumeWithdraw 추가: core-withdraw-request 수신 → PaymentService.withdrawByLoginId 호출 → 성공/실패 core-result 발행(에러 처리 분리).
결제 서비스 변화
on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/PaymentService.java, .../service/CouponManageService.java
withdrawByLoginId(String, Long) 메서드 추가 및 계좌 생성 시 쿠폰 이벤트 발행 코드 유지/조정; 기존 CouponManageService 클래스 삭제.
포인트/결제 서비스 추가·수정 (msa-point-service)
cloud-services/msa-point-service/.../Payment, PaymentRepository, PaymentService, PaymentConsumer, TestController, CashResponseDTO, CashRequestDTO
비동기 결제 흐름 추가: Payment 엔티티/레포지토리 도입, 결제 저장(PENDING) → 포인트 차감 → Kafka 출금 요청 전송 → core-withdraw-result 수신(Consumer) 시 성공이면 완료, 실패면 보상(refundPoint). 테스트용 컨트롤러 및 DTO 추가, MockCoreBanking 제거.
채널 유저 서비스 변화 (msa-channel-user-service)
cloud-services/msa-channel-user-service/src/.../BankingService.java, Dockerfile, build.gradle, JwtUtil.java
BankingService.deposit가 BigDecimal 반환으로 변경; 서비스용 Dockerfile 신규 추가; 의존성·빌드 파일·포맷팅 일부 변경.
기타 구성/빌드 변경
.github/workflows/deploy-aws-channel.yml, cloud-services/msa-point-service/build.gradle, cloud-services/msa-point-service/src/.../AppConfig.java
CI 배포 워크플로우 스크립트 정리 및 이미지/배포 변수화, Actuator 의존성 추가, ObjectMapper JavaTimeModule 등록 빈 추가.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Kafka as Kafka Topic
    participant Consumer as KafkaConsumerService
    participant Payment as PaymentService
    participant Repo as AccountRepository
    participant DB as Database
    participant Producer as KafkaProducerService

    Client->>Kafka: publish core-withdraw-request (CashRequestDTO)
    Kafka->>Consumer: deliver message -> consumeWithdraw()
    Consumer->>Payment: withdrawByLoginId(loginId, amount)
    Payment->>Repo: findByUserUuid(loginId)
    Repo->>DB: SELECT Account
    DB-->>Repo: Account
    Repo-->>Payment: Optional<Account>
    Payment->>Payment: withdraw(accountNo, amount)
    alt 성공
        Payment-->>Consumer: success
        Consumer->>Producer: send(core-result, CashResponseDTO{SUCCESS})
        Producer->>Kafka: publish core-result (SUCCESS)
    else 실패
        Payment-->>Consumer: throws or returns failure
        Consumer->>Producer: send(core-result, CashResponseDTO{FAIL})
        Producer->>Kafka: publish core-result (FAIL)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • juyeonbaeck
  • dbehdrbs0806

Poem

🎐 카프카에 실린 작은 출금 한 줄기,
🏦 UUID로 찾아가 계좌를 두드리고,
🛟 성공이면 응답을 띄우고 실패면 보상으로,
📨 프로듀서와 컨슈머가 메시지를 엮어,
✨ 서비스는 오늘도 비동기 춤을 춘다.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 변경사항의 핵심을 명확하게 반영합니다. 포인트 결제 관련 core payment 로직 추가라는 설명이 주요 변경사항과 일치합니다.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/core-payment-point

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/config/KafkaConsumerConfig.java (2)

43-44: ⚠️ Potential issue | 🟠 Major

AUTO_OFFSET_RESET_CONFIG을 외부 설정으로 분리하고 환경별로 다르게 설정하세요.

코드에서 AUTO_OFFSET_RESET_CONFIG가 "earliest"로 하드코딩되어 있는데, 이는 프로덕션 환경에서 심각한 문제를 야기할 수 있습니다.

현재 상태의 문제점:

  • 설정이 완전히 하드코딩되어 있어 모든 환경(개발, 스테이징, 프로덕션)에 동일하게 적용됨
  • 주석의 "(테스트용)"이 구현과 맞지 않음 - 실제로는 테스트만이 아닌 모든 환경에서 동작
  • bootstrap-serversgroup-id@Value로 외부화되어 있지만, 이 설정만 하드코딩되어 있어 패턴이 일관성 없음
  • 서비스 재시작 시 처리된 메시지를 다시 읽어 중복 처리 및 성능 저하 발생

해결방법:
@Value 어노테이션을 사용하여 application.yml에서 설정값을 가져오도록 수정하고, 환경별로 다른 값을 설정하세요:

`@Value`("${spring.kafka.consumer.auto-offset-reset:latest}")
private String autoOffsetReset;

// consumerFactory() 메서드 내에서
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);

프로덕션에서는 "latest"를 기본값으로, 개발 환경에서만 필요시 "earliest"를 사용하는 것을 권장합니다.


57-62: ⚠️ Potential issue | 🟠 Major

Spring Boot 자동 구성과의 ObjectMapper 빈 충돌 가능성 검토 필요

Spring Boot 3.5.10의 자동 구성(AutoConfiguration)은 클래스패스에 Jackson이 있으면 ObjectMapper를 자동으로 빈으로 등록합니다. 현재 코드에서 명시적으로 @Bean으로 ObjectMapper를 정의하면 Spring Boot의 자동 구성 빈을 오버라이드하게 되는데, 이는 다음 같은 문제를 야기할 수 있습니다:

  • 자동 구성된 ObjectMapper의 기본 설정(null 처리, 날짜 포맷 등)이 무시될 수 있음
  • 서로 다른 ObjectMapper 인스턴스 사용으로 인한 예기치 않은 동작 발생

권장사항: 다음 중 하나를 선택하세요

1️⃣ 권장: @ConditionalOnMissingBean 추가 (ObjectMapper가 없을 때만 생성)

 `@Bean`
+@ConditionalOnMissingBean
 public ObjectMapper objectMapper() {
     ObjectMapper objectMapper = new ObjectMapper();
     objectMapper.registerModule(new JavaTimeModule());
     return objectMapper;
 }

2️⃣ 더 우수한 방식: Jackson2ObjectMapperBuilderCustomizer 사용 (기존 ObjectMapper 커스터마이징)

`@Bean`
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
    return builder -> builder.modules(new JavaTimeModule());
}

이 방식은 Spring Boot의 자동 구성 빈을 활용하면서 필요한 모듈만 추가하므로 더 깔끔하고 표준적입니다.

on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/controller/PaymentController.java (1)

37-41: ⚠️ Potential issue | 🟠 Major

입금(deposit)과 출금(withdraw) 엔드포인트의 유효성 검증 일관성이 맞지 않습니다.

deposit 엔드포인트에는 @Valid가 적용되어 있지만, withdraw 엔드포인트에는 누락되어 있습니다. 동일한 AmountRequestDto를 사용하므로 출금에도 동일한 검증이 필요합니다.

🐛 수정 제안
 // 출금
 `@PostMapping`("/accounts/{accountNo}/withdraw")
-public BigDecimal withdraw(`@PathVariable` String accountNo, `@RequestBody` AmountRequestDto request) {
+public BigDecimal withdraw(`@PathVariable` String accountNo, `@Valid` `@RequestBody` AmountRequestDto request) {
     return paymentService.withdraw(accountNo, request.userUuid(), request.amount());
 }
🤖 Fix all issues with AI agents
In
`@on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/CorePaymentServiceApplication.java`:
- Line 8: CorePaymentServiceApplication 클래스에 붙은 `@EnableJpaAuditing` 어노테이션의
import가 빠져 컴파일 에러가 납니다; CorePaymentServiceApplication의 상단에
org.springframework.data.jpa.repository.config.EnableJpaAuditing에 대한 import를
추가하여 어노테이션을 정상 참조하도록 수정하세요.

In
`@on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/dto/CashRequestDTO.java`:
- Around line 13-15: Rename the misleading field loginId in CashRequestDTO to
userUuid and update all references/usages accordingly: change the field and its
getter/setter in class CashRequestDTO (previously loginId) to userUuid, update
any code calling requestDto.getLoginId()/setLoginId(...) to
getUserUuid()/setUserUuid(...), update mappings to CashResponseDTO so its
userUuid receives the same value, and adjust service/repository calls such as
accountRepository.findByUserUuid(requestDto.getLoginId()) to use
requestDto.getUserUuid(); ensure compilation and API contract tests across
msa-point-service and core-payment-service are updated to reflect the new name.

In
`@on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/repository/AccountRepository.java`:
- Around line 7-9: The AccountRepository interface declares Optional<Account> in
findByUserUuid but is missing the java.util.Optional import; add an import
statement for java.util.Optional near the other imports in AccountRepository
(ensuring there are no conflicting Optional types) so the findByUserUuid method
compiles correctly.

In
`@on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/KafkaConsumerService.java`:
- Around line 58-60: The current log in KafkaConsumerService logs the entire
requestDto (created via objectMapper.readValue) which exposes sensitive fields
like loginId and amount; change the logging to output only non-sensitive
identifiers (e.g., requestDto.getOrderId() or requestDto.getRequestId()) or log
a masked representation (mask loginId and redact/obfuscate amount) before
calling log.info, ensuring you update the log statement that references
requestDto and any toString usage so only the safe fields are included.
- Around line 56-70: The handler currently calls
BigDecimal.valueOf(requestDto.getAmount()) without guarding nulls and doesn't
validate loginId; update KafkaConsumerService's message handling after parsing
CashRequestDTO to first check that requestDto.getAmount() is not null (throw or
return an error/Saga failure with a clear message if null) and that
requestDto.getLoginId() is not null/blank (use StringUtils or trim check), then
only call paymentService.withdraw(account.getAccountNo(),
requestDto.getLoginId(), BigDecimal.valueOf(requestDto.getAmount())); ensure the
thrown/returned error messages clearly reference amount or loginId for easier
Saga tracing.

In
`@on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/PaymentService.java`:
- Around line 72-82: Rename the method parameter in withdrawByLoginId to
userUuid (or rename the method to match) so the parameter name matches the
repository method findByUserUuid(), then add input validation for amount inside
withdrawByLoginId: ensure amount is non-null and greater than 0 (throw
IllegalArgumentException with a clear message if not) before converting to
BigDecimal and calling account.withdraw(...); keep the existing account lookup
using accountRepository.findByUserUuid(userUuid) and preserve the log statement.
🧹 Nitpick comments (5)
on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/config/KafkaConsumerConfig.java (1)

22-22: 주석이 다소 불필요할 수 있습니다.

@Value 어노테이션의 기본 동작(application.yml에서 값을 가져오고 없으면 기본값 사용)을 설명하는 주석인데, 이는 Spring의 표준 동작이라 굳이 명시하지 않아도 됩니다. 하지만 팀 컨벤션에 따라 유지하셔도 무방합니다.

선택적 개선안: 주석 제거
-    // application.yml에서 값을 가져오되, 없으면 기본값 사용
     `@Value`("${spring.kafka.bootstrap-servers:127.0.0.1:9092}")
     private String bootstrapServers;
on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/dto/CashResponseDTO.java (1)

12-17: status 필드를 enum으로 변경하면 타입 안전성이 향상됩니다.

현재 String 타입에 주석으로 "SUCCESS" / "FAIL"을 명시하고 있는데, 이를 enum으로 정의하면 오타나 잘못된 값 사용을 컴파일 타임에 방지할 수 있습니다.

♻️ enum 사용 제안
public enum PaymentStatus {
    SUCCESS, FAIL
}
 public class CashResponseDTO {
     private String orderId;
     private String userUuid;
-    private String status;   // "SUCCESS" or "FAIL"
+    private PaymentStatus status;
     private String message;
 }
on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/KafkaProducerService.java (1)

18-27: Kafka 메시지 전송 결과 확인을 고려해보세요.

현재 kafkaTemplate.send()의 반환값을 무시하고 있어서, 메시지가 실제로 Kafka broker에 전달되었는지 확인하지 않습니다. 결제와 같은 중요한 도메인에서는 전송 실패 시 적절한 처리가 필요할 수 있습니다.

♻️ 전송 결과 확인 예시
 public void send(String topic, Object payload) {
     try {
         String jsonMessage = objectMapper.writeValueAsString(payload);
-        kafkaTemplate.send(topic, jsonMessage);
-        log.info("🚀 [Core-Producer] Sent to {}: {}", topic, jsonMessage);
+        kafkaTemplate.send(topic, jsonMessage)
+            .whenComplete((result, ex) -> {
+                if (ex != null) {
+                    log.error("❌ [Core-Producer] Failed to send to {}: {}", topic, ex.getMessage());
+                } else {
+                    log.info("🚀 [Core-Producer] Sent to {}: {}", topic, jsonMessage);
+                }
+            });
     } catch (JsonProcessingException e) {
         log.error("❌ [Core-Producer] Serialization failed: {}", payload, e);
         throw new RuntimeException("Kafka message serialization failed", e);
     }
 }
on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/PaymentService.java (1)

40-41: Kafka 토픽명을 설정 파일로 외부화하는 것을 권장합니다.

"coupon_issue" 토픽명이 하드코딩되어 있습니다. 환경별로 토픽명이 달라질 수 있고, 다른 서비스(KafkaConsumerService)와의 일관성을 위해 @Valueapplication.yml에서 관리하면 좋습니다.

♻️ 설정 외부화 예시
# application.yml
kafka:
  topics:
    coupon-issue: coupon_issue
+@Value("${kafka.topics.coupon-issue}")
+private String couponIssueTopic;

 // coupon-service가 듣고 있는 "coupon_issue" 토픽으로 쏜다
-kafkaProducerService.send("coupon_issue", couponEvent);
+kafkaProducerService.send(couponIssueTopic, couponEvent);
on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/controller/PaymentController.java (1)

19-23: 계좌 생성 엔드포인트에도 파라미터 유효성 검증을 추가하면 좋습니다.

accountNouserUuid가 빈 문자열로 전달될 수 있습니다. @NotBlank 등의 검증을 추가하면 잘못된 요청을 조기에 차단할 수 있습니다.

♻️ 검증 추가 예시
+import jakarta.validation.constraints.NotBlank;
+import org.springframework.validation.annotation.Validated;

+@Validated
 `@RestController`
 `@RequestMapping`("/api/core/payment")
 `@RequiredArgsConstructor`
 public class PaymentController {
     // ...
     
     `@PostMapping`("/accounts")
-    public String createAccount(`@RequestParam` String accountNo, `@RequestParam` String userUuid) {
+    public String createAccount(
+            `@RequestParam` `@NotBlank` String accountNo, 
+            `@RequestParam` `@NotBlank` String userUuid) {
         paymentService.createAccount(accountNo, userUuid);
         return "계좌 개설 완료";
     }

Comment on lines +13 to +15
private String orderId; // 트랜잭션 ID (Saga 추적용)
private String loginId; // 사용자 식별자 (userUuid)
private Long amount; // 출금액
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# CashRequestDTO 파일 확인
find . -name "CashRequestDTO.java" -type f

# 파일 내용 확인
cat -n on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/dto/CashRequestDTO.java

# loginId와 userUuid 사용처 검색
rg -n --type=java "loginId|userUuid" -B 2 -A 2

# core-withdraw-request 관련 코드 검색
rg -n "core-withdraw-request" -B 2 -A 2

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 50384


🏁 Script executed:

#!/bin/bash
# CashRequestDTO를 생성/수신하는 곳 찾기
rg -n "new CashRequestDTO|CashRequestDTO(" --type=java -B 2 -A 2

# 메시지 스키마나 Kafka 토픽 정의 찾기
rg -n "core-withdraw-request|cash|withdraw" --type=java -i | head -30

# CashResponseDTO 정의 확인
cat -n on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/dto/CashResponseDTO.java

# 다른 서비스의 CashRequestDTO 확인
cat -n cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/dto/CashRequestDTO.java

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 6135


🏁 Script executed:

#!/bin/bash
# PaymentRequest의 loginId가 어디서 설정되는지 확인
rg -n "PaymentRequest" --type=java -B 2 -A 2 | head -40

# MockCoreBanking에서 요청 처리 방식 확인
cat -n cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/kafka/MockCoreBanking.java

# KafkaConsumerService의 전체 흐름 확인
sed -n '51,100p' on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/KafkaConsumerService.java

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 7673


loginId 필드명이 userUuid 의미와 불일치하여 계약 혼동 위험

CashRequestDTOloginId 필드(14번 줄)는 주석에서 명시했듯이 실제로 사용자 UUID를 담고 있으나, 필드명이 로그인 ID를 의미하는 loginId로 되어 있습니다. 이로 인해 core-payment-service에서 accountRepository.findByUserUuid(requestDto.getLoginId())처럼 의미가 불명확한 코드가 발생하고, CashResponseDTOuserUuid 필드에 loginId 값이 매핑되는 등 계약이 불일치합니다.

현재는 실제 데이터가 UUID이므로 작동하지만, 차후 실제 로그인 ID가 전달될 경우 무한정 실패할 위험이 있습니다. 계약을 명확히 하기 위해 필드명을 userUuid로 통일해주세요.

🔧 제안 변경
-    private String loginId; // 사용자 식별자 (userUuid)
+    private String userUuid; // 사용자 식별자

이후 msa-point-servicecore-payment-service 모두에서 해당 필드 사용처를 업데이트해야 합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private String orderId; // 트랜잭션 ID (Saga 추적용)
private String loginId; // 사용자 식별자 (userUuid)
private Long amount; // 출금액
private String orderId; // 트랜잭션 ID (Saga 추적용)
private String userUuid; // 사용자 식별자
private Long amount; // 출금액
🤖 Prompt for AI Agents
In
`@on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/dto/CashRequestDTO.java`
around lines 13 - 15, Rename the misleading field loginId in CashRequestDTO to
userUuid and update all references/usages accordingly: change the field and its
getter/setter in class CashRequestDTO (previously loginId) to userUuid, update
any code calling requestDto.getLoginId()/setLoginId(...) to
getUserUuid()/setUserUuid(...), update mappings to CashResponseDTO so its
userUuid receives the same value, and adjust service/repository calls such as
accountRepository.findByUserUuid(requestDto.getLoginId()) to use
requestDto.getUserUuid(); ensure compilation and API contract tests across
msa-point-service and core-payment-service are updated to reflect the new name.

Comment on lines 56 to +70
try {
CouponIssueMessage couponDto = objectMapper.readValue(message, CouponIssueMessage.class);
couponService.issueCoupon(couponDto);
// (1) 메시지 파싱
requestDto = objectMapper.readValue(message, CashRequestDTO.class);
log.info("📉 [Core] 출금 요청 수신: {}", requestDto);

// (2) 계좌 조회
Account account = accountRepository.findByUserUuid(requestDto.getLoginId())
.orElseThrow(() -> new IllegalArgumentException("계좌를 찾을 수 없습니다."));

// (3) 출금 비즈니스 로직
paymentService.withdraw(
account.getAccountNo(),
requestDto.getLoginId(),
BigDecimal.valueOf(requestDto.getAmount())
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# PaymentService 클래스 찾기
find . -type f -name "*.java" | xargs grep -l "class PaymentService" | head -5

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 286


🏁 Script executed:

#!/bin/bash
# withdraw 메서드 구현 확인
rg -A 20 "public.*withdraw\(" --type=java

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 8013


🏁 Script executed:

#!/bin/bash
# CashRequestDTO 클래스 구조 확인
rg -A 10 "class CashRequestDTO\|record CashRequestDTO" --type=java

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 54


🏁 Script executed:

#!/bin/bash
# CashRequestDTO 찾기 (여러 패턴)
rg -l "CashRequestDTO" --type=java

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 597


🏁 Script executed:

#!/bin/bash
# KafkaConsumerService 파일 위치와 전체 내용 확인
find . -type f -name "*KafkaConsumerService*" -o -name "*KafkaConsumer*" | grep -i java

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 406


🏁 Script executed:

#!/bin/bash
# 파일 크기 확인 후 내용 조회
wc -l ./on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/KafkaConsumerService.java

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 187


🏁 Script executed:

#!/bin/bash
# KafkaConsumerService 전체 내용 확인
cat -n ./on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/KafkaConsumerService.java

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 4703


🏁 Script executed:

#!/bin/bash
# CashRequestDTO 내용 확인
cat -n ./on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/dto/CashRequestDTO.java

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 565


출금 요청의 amount null 검증을 추가해주세요

현재 amount가 null인 경우 BigDecimal.valueOf(requestDto.getAmount())에서 NPE가 발생할 수 있습니다. 음수/0 검증은 Account.withdraw()에서 이미 처리 중(출금액은 0원보다 커야 합니다)이지만, null 안전성을 위해 여기서 가드를 두는 편이 좋습니다.

추가로 loginId가 null/공백인 경우도 더 명확한 메시지와 함께 검증하면, PointService와의 Saga 트랜잭션 추적에 도움이 됩니다.

✅ 제안 변경
             requestDto = objectMapper.readValue(message, CashRequestDTO.class);
             log.info("📉 [Core] 출금 요청 수신: {}", requestDto);
+
+            if (requestDto.getLoginId() == null || requestDto.getLoginId().isBlank()
+                    || requestDto.getAmount() == null || requestDto.getAmount() <= 0) {
+                throw new IllegalArgumentException("유효하지 않은 출금 요청입니다.");
+            }
 
             // (2) 계좌 조회
             Account account = accountRepository.findByUserUuid(requestDto.getLoginId())
🤖 Prompt for AI Agents
In
`@on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/KafkaConsumerService.java`
around lines 56 - 70, The handler currently calls
BigDecimal.valueOf(requestDto.getAmount()) without guarding nulls and doesn't
validate loginId; update KafkaConsumerService's message handling after parsing
CashRequestDTO to first check that requestDto.getAmount() is not null (throw or
return an error/Saga failure with a clear message if null) and that
requestDto.getLoginId() is not null/blank (use StringUtils or trim check), then
only call paymentService.withdraw(account.getAccountNo(),
requestDto.getLoginId(), BigDecimal.valueOf(requestDto.getAmount())); ensure the
thrown/returned error messages clearly reference amount or loginId for easier
Saga tracing.

Comment on lines +58 to +60
requestDto = objectMapper.readValue(message, CashRequestDTO.class);
log.info("📉 [Core] 출금 요청 수신: {}", requestDto);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the file structure to locate the relevant files
find . -name "KafkaConsumerService.java" -type f

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 183


🏁 Script executed:

# Find the CashRequestDTO class
find . -name "CashRequestDTO.java" -type f

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 278


🏁 Script executed:

# Look for the directory structure of the payment service
find . -path "*core-payment-service*" -type d | head -20

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 1280


🏁 Script executed:

# Read the KafkaConsumerService.java file around lines 58-60
cat -n ./on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/KafkaConsumerService.java | sed -n '50,70p'

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 1044


🏁 Script executed:

# Read the entire CashRequestDTO class to see all fields
cat -n ./on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/dto/CashRequestDTO.java

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 565


출금 요청 로그에서 민감정보 노출 방지 필요

라인 59에서 requestDto 객체를 전체 로깅하면 @ToString 어노테이션에 의해 loginId (사용자 식별자)와 amount (출금액)가 그대로 노출됩니다. 운영 환경의 로그 정책상 개인정보와 금융정보 노출은 보안 리스크이므로, 추적 가능한 orderId 같은 최소 정보만 로깅하거나 민감정보를 마스킹하는 것을 권장합니다.

🔒 제안 변경
-            log.info("📉 [Core] 출금 요청 수신: {}", requestDto);
+            log.info("📉 [Core] 출금 요청 수신: orderId={}", requestDto.getOrderId());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
requestDto = objectMapper.readValue(message, CashRequestDTO.class);
log.info("📉 [Core] 출금 요청 수신: {}", requestDto);
requestDto = objectMapper.readValue(message, CashRequestDTO.class);
log.info("📉 [Core] 출금 요청 수신: orderId={}", requestDto.getOrderId());
🤖 Prompt for AI Agents
In
`@on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/KafkaConsumerService.java`
around lines 58 - 60, The current log in KafkaConsumerService logs the entire
requestDto (created via objectMapper.readValue) which exposes sensitive fields
like loginId and amount; change the logging to output only non-sensitive
identifiers (e.g., requestDto.getOrderId() or requestDto.getRequestId()) or log
a masked representation (mask loginId and redact/obfuscate amount) before
calling log.info, ensuring you update the log statement that references
requestDto and any toString usage so only the safe fields are included.

Comment on lines +72 to +82
// [Saga용] 계좌번호 없이 ID로 출금
@Transactional
public void withdrawByLoginId(String loginId, Long amount) {
Account account = accountRepository.findByUserUuid(loginId)
.orElseThrow(() -> new IllegalArgumentException("해당 유저의 계좌가 존재하지 않습니다."));

// Long -> BigDecimal 변환 후 출금 처리
account.withdraw(BigDecimal.valueOf(amount));

log.info("📉 [Saga] 출금 처리 완료: user={}, amount={}", loginId, amount);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

파라미터 네이밍 불일치: loginId vs userUuid

메서드 파라미터명이 loginId이지만, 실제로는 findByUserUuid()로 조회하고 있습니다. 코드 가독성과 유지보수를 위해 파라미터명을 userUuid로 변경하는 것을 권장합니다.

또한, amount에 대한 유효성 검증이 없어서 음수나 0이 전달될 수 있습니다.

🛠️ 수정 제안
 // [Saga용] 계좌번호 없이 ID로 출금
 `@Transactional`
-public void withdrawByLoginId(String loginId, Long amount) {
-    Account account = accountRepository.findByUserUuid(loginId)
+public void withdrawByLoginId(String userUuid, Long amount) {
+    if (amount == null || amount <= 0) {
+        throw new IllegalArgumentException("출금 금액은 0보다 커야 합니다.");
+    }
+    
+    Account account = accountRepository.findByUserUuid(userUuid)
             .orElseThrow(() -> new IllegalArgumentException("해당 유저의 계좌가 존재하지 않습니다."));

     // Long -> BigDecimal 변환 후 출금 처리
     account.withdraw(BigDecimal.valueOf(amount));

-    log.info("📉 [Saga] 출금 처리 완료: user={}, amount={}", loginId, amount);
+    log.info("📉 [Saga] 출금 처리 완료: user={}, amount={}", userUuid, amount);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// [Saga용] 계좌번호 없이 ID로 출금
@Transactional
public void withdrawByLoginId(String loginId, Long amount) {
Account account = accountRepository.findByUserUuid(loginId)
.orElseThrow(() -> new IllegalArgumentException("해당 유저의 계좌가 존재하지 않습니다."));
// Long -> BigDecimal 변환 후 출금 처리
account.withdraw(BigDecimal.valueOf(amount));
log.info("📉 [Saga] 출금 처리 완료: user={}, amount={}", loginId, amount);
}
// [Saga용] 계좌번호 없이 ID로 출금
`@Transactional`
public void withdrawByLoginId(String userUuid, Long amount) {
if (amount == null || amount <= 0) {
throw new IllegalArgumentException("출금 금액은 0보다 커야 합니다.");
}
Account account = accountRepository.findByUserUuid(userUuid)
.orElseThrow(() -> new IllegalArgumentException("해당 유저의 계좌가 존재하지 않습니다."));
// Long -> BigDecimal 변환 후 출금 처리
account.withdraw(BigDecimal.valueOf(amount));
log.info("📉 [Saga] 출금 처리 완료: user={}, amount={}", userUuid, amount);
}
🤖 Prompt for AI Agents
In
`@on-premise/core-payment-service/src/main/java/com/fisa/core_payment_service/service/PaymentService.java`
around lines 72 - 82, Rename the method parameter in withdrawByLoginId to
userUuid (or rename the method to match) so the parameter name matches the
repository method findByUserUuid(), then add input validation for amount inside
withdrawByLoginId: ensure amount is non-null and greater than 0 (throw
IllegalArgumentException with a clear message if not) before converting to
BigDecimal and calling account.withdraw(...); keep the existing account lookup
using accountRepository.findByUserUuid(userUuid) and preserve the log statement.

@caminobelllo
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cloud-services/msa-channel-user-service/src/main/java/com/fisa/channel_service/service/BankingService.java (1)

23-35: ⚠️ Potential issue | 🟠 Major

비동기 Kafka 전송 후 반환값의 의미가 모호합니다.

현재 deposit 메서드는 Kafka로 메시지를 전송한 후 즉시 amount를 반환하고 있는데요, 이 반환값은 메시지 전송 성공만을 의미하지 실제 입금 처리 완료를 보장하지 않습니다.

MSA 환경에서 이런 패턴은 다음과 같은 문제를 야기할 수 있어요:

  • 호출자가 반환된 amount를 보고 입금이 완료된 것으로 오해할 수 있음
  • 실제 입금 처리는 Consumer 측에서 비동기로 진행되므로 실패할 가능성 존재
  • 분산 트랜잭션 관점에서 데이터 정합성 이슈 발생 가능

고려해볼 수 있는 개선 방안:

  1. 반환 타입을 의미 있는 DTO로 변경 (예: DepositRequestResult - 요청 ID, 상태 포함)
  2. 메서드명을 requestDeposit으로 변경하여 비동기 요청임을 명시
  3. 또는 void를 유지하고, 결과는 별도 콜백/이벤트로 처리
💡 개선 방안 예시 (요청 결과 DTO 사용)
-    public BigDecimal deposit(String userUuid, String accountNo, BigDecimal amount) {
+    public DepositRequestResult requestDeposit(String userUuid, String accountNo, BigDecimal amount) {
         if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
                        throw new IllegalArgumentException("입금 금액은 0보다 커야 합니다.");
         }

         DepositMessage message = new DepositMessage(userUuid, accountNo, amount);
         try {
             kafkaProducerService.sendDepositRequest(message);
-            return amount;
+            return new DepositRequestResult(message.getRequestId(), "PENDING", amount);
         } catch (Exception e) {
             throw new RuntimeException("입금 요청 전송 실패", e);
         }
     }
🤖 Fix all issues with AI agents
In `@cloud-services/msa-channel-user-service/build.gradle`:
- Around line 48-50: The project is using jjwt 0.11.5; update the three
dependency coordinates (io.jsonwebtoken:jjwt-api, io.jsonwebtoken:jjwt-impl,
io.jsonwebtoken:jjwt-jackson) to version 0.13.0 in build.gradle, then run the
test suite and full build to catch any API/compatibility changes (especially
with Spring Boot 4.0.2) and adjust JWT-related code if any deprecated APIs
changed; if you must keep 0.11.5, add a short comment explaining the
compatibility constraint.

In
`@cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/controller/PaymentController.java`:
- Around line 34-44: PaymentController currently calls
paymentService.processCompositePayment(dto) but there is no global exception
handling, so failures (DB, Kafka, etc.) bubble up as 500s; add a
`@RestControllerAdvice` class (e.g., GlobalExceptionHandler) that defines
`@ExceptionHandler` methods for broad Exception and specific exceptions like
DataAccessException and KafkaException, log the error with context (include
dto/orderId when available), and return a consistent ResponseEntity error body
(timestamp, message, error code) and appropriate HTTP status; ensure the handler
covers exceptions thrown by PaymentController and
paymentService.processCompositePayment so clients still get the 202 when
processing starts and consistent JSON for errors.

In
`@cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/kafka/PaymentKafkaConsumer.java`:
- Around line 25-29: PaymentKafkaConsumer currently calls
pointService.refundPoint(event.getUserId(), 5000L) with a hardcoded amount;
update the flow so the refunded amount comes from the event (or a lookup)
instead: add a pointAmount field to CoreResultEvent (e.g., getPointAmount()) and
change PaymentKafkaConsumer to call pointService.refundPoint(event.getUserId(),
event.getPointAmount()), or if you choose the lookup approach call a payment
record service to fetch the exact used points before invoking
pointService.refundPoint; ensure CoreResultEvent, its producer, and consumers
are updated together if you add the field.

In
`@cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/service/PaymentConsumer.java`:
- Around line 21-34: The PaymentConsumer currently swallows all exceptions in
the try/catch (around objectMapper.readValue and calls to
paymentService.completePayment/compensatePayment) which causes Kafka to treat
the record as successfully processed and lose messages; change the catch in
PaymentConsumer to rethrow the exception (or throw a runtime/KafkaException)
instead of only logging, so the Kafka listener will trigger configured error
handling (or DeadLetter publishing), or alternatively configure a
GlobalKafkaListenerContainerFactory error handler for consistent DLQ/retry
behavior across listeners; update PaymentConsumer's catch to rethrow and/or
add/configure the global ErrorHandler in your Kafka config.

In
`@cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/service/PaymentService.java`:
- Around line 88-94: The current refund guard only skips when
payment.getStatus() is "FAILED" which allows refunds from already "COMPLETED"
payments; update the logic in PaymentService (the method handling this diff) to
only perform refund when payment.getStatus() equals "PENDING" by adding a guard
that returns unless status is "PENDING" before calling
pointService.refundPoint(payment.getUserId(), payment.getPointAmount()), and
then set payment.setStatus("FAILED") after a successful refund; ensure you
reference the existing payment.getStatus(), pointService.refundPoint, and
payment.setStatus symbols when making the change.
- Around line 46-61: The Kafka send in PaymentService (the block creating
CashRequestDTO and calling kafkaTemplate.send(...).get(5, TimeUnit.SECONDS)) can
cause DB/Kafka inconsistency on timeout; move Kafka publishing to occur only
after DB commit (implement an Outbox pattern or register a post-commit action
via TransactionSynchronization/TransactionSynchronizationManager or use an
`@TransactionalEventListener` to publish after commit) so the Payment and the
outbound message are durably coordinated; additionally, harden compensatePayment
(the compensatePayment method) to only proceed when payment.getStatus() is
exactly "PENDING" (replace the current `"FAILED"`-only check with a guard like
`if (!"PENDING".equals(payment.getStatus())) return;`) to prevent refunds for
completed payments.
🧹 Nitpick comments (14)
cloud-services/msa-channel-user-service/src/main/java/com/fisa/channel_service/service/BankingService.java (1)

37-41: depositwithdraw의 통신 패턴 불일치를 고려해주세요.

현재 구현을 보면:

  • deposit: Kafka 비동기 메시징 사용 → 입력값 amount 반환
  • withdraw: CorePaymentClient 동기 HTTP 호출 → 실제 처리 결과 반환

동일한 서비스 내에서 입금/출금이라는 유사한 금융 작업에 서로 다른 통신 패턴을 사용하고 있어요. 이러한 불일치는 향후 유지보수나 에러 핸들링 전략 수립 시 혼란을 줄 수 있습니다.

의도적인 설계라면 주석이나 문서로 그 이유를 남겨두시는 것을 추천드려요.

cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/config/KafkaProducerConfig.java (2)

20-27: 주석 내 설정 파일명 불일치

Line 20에서는 application.properties를 언급하고, Line 27에서는 application.yml을 언급하고 있어요. 실제 프로젝트에서 사용하는 설정 파일명으로 통일해주시면 좋겠습니다.


34-37: enable.idempotence=true 설정 시 retries 명시적 설정 제거 권장

ENABLE_IDEMPOTENCE_CONFIG=true를 설정하면, Kafka Producer가 retries를 자동으로 Integer.MAX_VALUE로 override합니다. 따라서 36번 라인의 RETRIES_CONFIG=3 설정은 실제로 적용되지 않습니다.

코드 동작 자체는 문제없지만, 명시적으로 설정된 retries=3이 무시되고 있어 유지보수 과정에서 혼란을 야기할 수 있습니다. 아이디포턴스 활성화 시에는 이 설정을 제거하고, 필요시 delivery.timeout.ms로 재시도 타이머를 제어하는 것이 권장됩니다.

♻️ 제안하는 수정 방안
         // 메시지 전송 신뢰성 설정
         configProps.put(ProducerConfig.ACKS_CONFIG, "all"); // 모든 replica 확인
-        configProps.put(ProducerConfig.RETRIES_CONFIG, 3);  // 재시도 횟수
         configProps.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true); // 중복 전송 방지 (retries 자동 설정됨)
.github/workflows/deploy-aws-channel.yml (1)

37-37: 테스트 스킵(-x test) 관련 참고사항

CI/CD 파이프라인에서 테스트를 스킵하는 것은 빌드 속도를 높여주지만, 별도의 테스트 단계가 있는지 확인이 필요합니다. 테스트가 다른 워크플로우나 PR 단계에서 실행된다면 괜찮습니다.

cloud-services/msa-channel-user-service/Dockerfile (2)

25-25: JAR 파일 복사 시 와일드카드 사용 주의

*.jar 와일드카드는 빌드 결과물이 여러 개일 경우 예기치 않은 동작을 유발할 수 있습니다. Spring Boot의 경우 -plain.jar와 실행 가능한 JAR이 함께 생성될 수 있습니다.

명시적인 JAR 이름을 사용하거나, build.gradle에서 plain JAR 생성을 비활성화하는 것을 권장합니다.

💡 권장 수정안

옵션 1: build.gradle에서 plain JAR 비활성화

jar {
    enabled = false
}

옵션 2: Dockerfile에서 명시적 JAR 이름 사용

-COPY --from=builder /app/build/libs/*.jar app.jar
+COPY --from=builder /app/build/libs/msa-channel-user-service-0.0.1-SNAPSHOT.jar app.jar

39-44: JVM 옵션 관련 참고사항

-XX:+UseContainerSupport는 JDK 10부터 기본값으로 활성화되어 있어 JDK 17에서는 명시적으로 지정할 필요가 없습니다. 제거해도 동작에는 영향이 없지만, 불필요한 옵션을 정리하면 더 깔끔해집니다.

나머지 옵션들(MaxRAMPercentage, urandom)은 컨테이너 환경에 적합한 좋은 설정입니다.

✨ 선택적 개선안
 ENTRYPOINT ["java", \
-    "-XX:+UseContainerSupport", \
     "-XX:MaxRAMPercentage=75.0", \
     "-Djava.security.egd=file:/dev/./urandom", \
     "-jar", \
     "app.jar"]
cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/domain/Payment.java (1)

32-33: status 필드를 enum으로 변경해 타입 안전성을 확보하세요.

현재 String으로 선언되어 있어 오타("PANDING" 등)나 유효하지 않은 상태값이 저장될 수 있습니다. PaymentStatus enum을 도입하면 컴파일 타임에 검증할 수 있고, 코드의 의도도 명확해집니다.

다만 이 변경은 PaymentService의 상태 문자열 비교(!"PENDING".equals(...)) 부분과 CashResponseDTO, PaymentResponse, CoreResultEvent 등의 DTO에도 영향을 미치므로 함께 리팩터링해야 합니다.

✏️ 리팩터링 예시

1. PaymentStatus enum 신규 작성

package com.techsemina.msa.pointservice.domain;

public enum PaymentStatus {
    PENDING,
    COMPLETED,
    FAILED
}

2. Payment.java 수정

-    // 상태 관리 (PENDING -> COMPLETED / FAILED)
-    private String status;
+    // 상태 관리 (PENDING -> COMPLETED / FAILED)
+    `@Enumerated`(EnumType.STRING)
+    `@Column`(nullable = false, length = 20)
+    private PaymentStatus status;

3. PaymentService.java 수정

- .status("PENDING")
+ .status(PaymentStatus.PENDING)

- if (!"PENDING".equals(payment.getStatus())) {
+ if (payment.getStatus() != PaymentStatus.PENDING) {

- payment.setStatus("COMPLETED");
+ payment.setStatus(PaymentStatus.COMPLETED);

- if ("FAILED".equals(payment.getStatus())) return;
+ if (payment.getStatus() == PaymentStatus.FAILED) return;

- payment.setStatus("FAILED");
+ payment.setStatus(PaymentStatus.FAILED);
cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/config/AppConfig.java (1)

11-15: Spring Boot의 Jackson 모듈 관리 패턴 개선을 권장합니다.

현재 ObjectMapper를 직접 생성하는 방식도 작동하지만, Spring Boot 4.0.2의 Jackson2ObjectMapperBuilderCustomizer를 활용하면 더욱 일관되고 유지보수하기 좋은 구조가 됩니다. 이 방식은 향후 JavaTimeModule 외에 다른 모듈이 필요할 때도 더 깔끔하게 확장할 수 있습니다.

✅ 권장 수정안
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@
     `@Bean`
-    public ObjectMapper objectMapper() {
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.registerModule(new JavaTimeModule()); // 날짜 처리 기능 추가
-        return mapper;
+    public Jackson2ObjectMapperBuilderCustomizer objectMapperCustomizer() {
+        return builder -> builder.modulesToInstall(new JavaTimeModule());
     }
cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/dto/CashResponseDTO.java (1)

7-13: 일관성 및 명확성 개선을 위해 @Setter 추가 권장

현재 코드는 @NoArgsConstructor@AllArgsConstructor로 인해 Jackson의 기본 설정에서 문제없이 역직렬화됩니다. 하지만 팀의 다른 DTO(예: PaymentRequest)가 @Setter를 사용하고 있으므로, 코드 일관성과 가독성 향상을 위해 @Setter 추가를 권장합니다.

제안: Setter 추가로 일관성 강화
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
+import lombok.Setter;

 `@Getter`
+@Setter
 `@NoArgsConstructor`
 `@AllArgsConstructor`
 public class CashResponseDTO {
cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/controller/PaymentController.java (2)

25-25: @Valid 어노테이션 누락 - 입력 검증이 수행되지 않습니다.

@RequestBody@Valid 어노테이션이 없어서 PaymentRequest DTO의 Bean Validation이 작동하지 않습니다. 악의적이거나 잘못된 입력이 서비스 레이어까지 전달될 수 있어요.

♻️ 제안된 수정
-    public ResponseEntity<PaymentResponse> pay(`@RequestBody` PaymentRequest dto) {
+    public ResponseEntity<PaymentResponse> pay(`@Valid` `@RequestBody` PaymentRequest dto) {

그리고 import 추가:

import jakarta.validation.Valid;

28-31: 요청 DTO를 직접 변경하는 것은 권장되지 않습니다.

dto.setOrderId()로 입력 DTO를 직접 수정하면 몇 가지 문제가 생길 수 있어요:

  • 로깅 시 원본 요청과 수정된 요청이 혼동될 수 있음
  • DTO가 immutable하게 설계된 경우 의도치 않은 부작용 발생 가능

별도의 변수나 새 객체를 생성하는 것이 더 안전합니다.

♻️ 제안된 수정
-        if (dto.getOrderId() == null) {
-            String newId = OrderIdGenerator.generateOrderId(); // "PAY-2026..." 생성
-            dto.setOrderId(newId); // DTO에 쏙 넣기
-        }
+        String orderId = dto.getOrderId() != null 
+            ? dto.getOrderId() 
+            : OrderIdGenerator.generateOrderId();

그리고 서비스 호출 및 응답 생성 시 orderId 변수를 사용:

-        paymentService.processCompositePayment(dto);
+        paymentService.processCompositePayment(dto, orderId);
cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/controller/TestController.java (3)

21-21: orderId 파라미터에 대한 검증이 필요합니다.

@RequestParam String orderId는 빈 문자열이나 공백만 있는 값도 허용됩니다. 잘못된 orderId가 Kafka로 전송되면 다운스트림 컨슈머에서 예상치 못한 동작이 발생할 수 있어요.

♻️ 제안된 수정
+import org.springframework.util.StringUtils;
     `@PostMapping`("/fake-success")
     public String fakeSuccess(`@RequestParam` String orderId) {
+        if (!StringUtils.hasText(orderId)) {
+            return "orderId는 필수입니다!";
+        }
 
         // 현금 서비스가 보내줄 법한 메시지를 우리가 직접 만듭니다.

또는 @NotBlank@Validated를 사용할 수도 있습니다.


26-28: Kafka 전송 실패에 대한 처리를 고려해 주세요.

kafkaTemplate.send()는 비동기로 동작하며, 현재 코드에서는 전송 실패 시 에러가 무시됩니다. 테스트 컨트롤러이긴 하지만, 메시지 전송 실패 여부를 알 수 없으면 디버깅이 어려울 수 있어요.

♻️ 제안된 수정 (콜백 추가)
-        kafkaTemplate.send("core-withdraw-result", fakeResponse);
+        kafkaTemplate.send("core-withdraw-result", fakeResponse)
+            .whenComplete((result, ex) -> {
+                if (ex != null) {
+                    // 로깅 추가
+                    System.err.println("Kafka 전송 실패: " + ex.getMessage());
+                }
+            });

26-28: 토픽 이름을 상수나 설정값으로 관리하는 것을 권장드려요.

"core-withdraw-result" 토픽 이름이 여러 곳에서 하드코딩되어 있으면 오타로 인한 버그가 발생하기 쉽고, 토픽 이름 변경 시 모든 곳을 수정해야 합니다.

♻️ 제안된 수정

상수 클래스나 application.yml에서 관리:

public class KafkaTopics {
    public static final String CORE_WITHDRAW_RESULT = "core-withdraw-result";
}

또는 @Value로 주입:

`@Value`("${kafka.topic.core-withdraw-result}")
private String withdrawResultTopic;

Also applies to: 37-37

Comment on lines +48 to +50
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

jjwt library latest version 2026 releases changelog

💡 Result:

Latest JJWT version (as of Feb 3, 2026)

  • Latest release: 0.13.0 (released Aug 20, 2025) — no newer (2026) release is shown on the upstream GitHub Releases page or Maven Central indexes. [1][2][3]

2026 releases

  • None found (no 2026-tagged JJWT releases published upstream as of Feb 3, 2026). [1][2][3]

Changelog / release notes (most recent)

0.13.0 (Aug 20, 2025)

  • Made JacksonDeserializer(ObjectMapper, Map<String,Class<?>>) constructor public (to allow registering a claims type converter on a user-supplied ObjectMapper). [1]

Prior patch (for context): 0.12.7 (Aug 14, 2025)

  • Added a Maven BOM (jjwt-bom). [4]
  • Parser builder: allowing empty nested algorithm collections to disable related features (zip/sig/enc/key). [4]
  • Fix: nested collection builders replacing algorithms with same id (bug 961). [4]
  • JWK Set printing: keys no longer fully redacted by default. [4]
  • Performance: reduced reflection by using one-time reflective factory init. [4]
  • Dependency bumps: Gson 2.11.0, BouncyCastle 1.78.1. [4]

Sources: GitHub releases [1][4], Maven Repository (Maven Central indexing) [2], javadoc.io latest-version index [3].


🌐 Web query:

jjwt 0.13.0 vs 0.11.5 security differences

💡 Result:

Security-relevant differences: JJWT 0.13.0 vs 0.11.5

1) CVE-2024-31033 / GHSA-r65j-6h5f-4f92 (“ignored characters → weaker-than-expected key”) scanners

  • Many vulnerability feeds/scanners flag JJWT through 0.12.5 for CVE-2024-31033 (aka GHSA-r65j-6h5f-4f92) as “ignores certain characters” in setSigningKey() / signWith(), possibly causing users to think they have a stronger key than they really do. NVD marks it “Disputed”. [1]
  • OSV shows the GitHub advisory as “Withdrawn” (because it’s disputed). [2]
  • Practical implication: upgrading from 0.11.5 → 0.13.0 usually stops dependency scanners from reporting this (since 0.13.0 is > 0.12.5), regardless of the dispute. [1][2]

2) Protection against the Java “Psychic Signatures” ECDSA bug (CVE-2022-21449)

  • 0.11.5 explicitly added extra guards for the JDK ECDSA verification vulnerability affecting specific Java versions (15–15.0.6, 17–17.0.2, 18). [3]
  • The safer fix is still upgrading the JDK, but 0.11.5 was specifically designed to reduce exposure if you can’t patch the JVM immediately. [3]
  • 0.13.0 release notes don’t call out additional work here; they mainly describe non-security changes. [4]

3) Supply-chain / crypto-provider updates in the 0.12.x line (picked up by 0.13.0)

  • In between your two versions, 0.12.6 upgraded BouncyCastle to 1.78 (a security-relevant dependency refresh) and included a signature-verification-related behavior change (using the application-configured Base64Url decoder output when constructing the Jws). [5]
  • By moving to 0.13.0, you inherit these 0.12.x fixes/updates (unless you were pinned to older modules). [4][5]

4) 0.13.0 itself doesn’t advertise new security fixes vs 0.12.7

  • The 0.13.0 release notes state it contains a single change (making a Jackson deserializer constructor public) and otherwise is mostly about Java 7 support policy. [4]

Bottom line

  • If your goal is “reduce security findings / get off reported CVEs”: 0.13.0 is materially better than 0.11.5 for the CVE-2024-31033 scanner noise (since it’s reported as affecting ≤0.12.5, albeit disputed/withdrawn in some feeds). [1][2]
  • If your goal is “Psychic Signatures (CVE-2022-21449) mitigation”: 0.11.5 is known to add guards, but the real fix is still a patched JVM; 0.13.0 doesn’t highlight anything new there. [3][4]

Sources: NVD CVE record [1], OSV withdrawn GHSA entry [2], JJWT 0.11.5 notes (CVE-2022-21449 guards) [3], JJWT 0.13.0 release notes [4], JJWT 0.12.6 release notes (BC upgrade & related fixes) [5].


🌐 Web query:

io.jsonwebtoken jjwt-api latest stable release

💡 Result:

The latest stable release of io.jsonwebtoken:jjwt-api is 0.13.0 (released Aug 20, 2025). [1][2][3]

Maven

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-api</artifactId>
  <version>0.13.0</version>
</dependency>

Gradle

implementation "io.jsonwebtoken:jjwt-api:0.13.0"

Sources: GitHub releases [1], javadoc.io latest listing [2], Maven Repository release details [3].


JJWT 라이브러리 버전 다운그레이드 검토 필요

현재 코드에는 jjwt 0.11.5가 지정되어 있는데, 이는 최신 안정 버전인 0.13.0(2025년 8월 릴리스)보다 이전 버전입니다. 특별한 호환성 이유가 없다면 최신 버전으로 업그레이드를 권장합니다.

0.13.0으로 업그레이드 시 얻을 수 있는 이점:

  • CVE-2024-31033 관련 스캐너 경고 제거 (0.12.5 이하 버전 대상)
  • BouncyCastle 1.78 등 보안 관련 의존성 업데이트 반영 (0.12.6+)
  • Spring Boot 4.0.2와의 호환성 검증 필요

0.11.5로 유지해야 하는 구체적인 사유가 있다면 코멘트 부탁드립니다.

🤖 Prompt for AI Agents
In `@cloud-services/msa-channel-user-service/build.gradle` around lines 48 - 50,
The project is using jjwt 0.11.5; update the three dependency coordinates
(io.jsonwebtoken:jjwt-api, io.jsonwebtoken:jjwt-impl,
io.jsonwebtoken:jjwt-jackson) to version 0.13.0 in build.gradle, then run the
test suite and full build to catch any API/compatibility changes (especially
with Spring Boot 4.0.2) and adjust JWT-related code if any deprecated APIs
changed; if you must keep 0.11.5, add a short comment explaining the
compatibility constraint.

Comment on lines 34 to +44
paymentService.processCompositePayment(dto);
return "결제 요청 처리 완료";

// 3. 응답 객체 생성
PaymentResponse response = PaymentResponse.builder()
.message("결제 요청이 정상적으로 접수되었습니다. (결과 알림 예정)")
.orderId(dto.getOrderId())
.status("PENDING") // 아직 Kafka 타고 가는 중 - PENDING
.build();

// 4. 202 Accepted 리턴
return ResponseEntity.status(HttpStatus.ACCEPTED).body(response);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: `@ControllerAdvice` 또는 `@RestControllerAdvice` 클래스 검색

# 글로벌 예외 핸들러 찾기
rg -l "@ControllerAdvice|@RestControllerAdvice" --type java

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Description: PaymentController.java 파일 위치 찾기 및 내용 확인

# PaymentController.java 찾기
fd "PaymentController.java" --type f

# 파일이 있으면 내용 확인
if fd -x sh -c 'cat -n "$1" | sed -n "30,50p"' _ PaymentController.java {} \;
then
  echo "File found and content shown above"
fi

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 15196


🏁 Script executed:

#!/bin/bash
# Description: 예외 처리 관련 try-catch 또는 throws 확인

# PaymentController에서 예외 처리 패턴 검색
find . -name "PaymentController.java" -type f -exec grep -n "throws\|try\|catch" {} + 2>/dev/null || echo "Pattern not found"

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Description: PaymentController.java 파일 읽기

cat -n "cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/controller/PaymentController.java"

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 2126


🏁 Script executed:

#!/bin/bash
# Description: msa-point-service에 GlobalExceptionHandler 존재 여부 확인

find cloud-services/msa-point-service -name "*ExceptionHandler*" -o -name "*ControllerAdvice*" | head -20

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Description: PaymentService.processCompositePayment 메서드 확인

rg "processCompositePayment" -A 10 --type java

Repository: FISA-Team-CE/tech-semina-msa

Length of output: 3456


글로벌 예외 핸들러가 구성되어야 합니다.

202 Accepted 패턴 적용은 좋으나, processCompositePayment()에서 발생 가능한 예외(DB 저장 실패, Kafka 연결 오류 등)에 대한 처리가 필요합니다. 현재 msa-point-service에는 @ControllerAdvice 또는 @RestControllerAdvice가 없어서, 예외 발생 시 클라이언트가 500 에러를 받게 됩니다.

비동기 결제 처리에서 클라이언트는 202 Accepted를 받고 이미 결과를 기다리고 있으므로, 서버 내부 오류는 적절한 로깅과 함께 Kafka 콜백 또는 별도의 알림 메커니즘으로 처리되어야 합니다. @ControllerAdvice를 통해 일관된 에러 응답을 구성해주세요.

🤖 Prompt for AI Agents
In
`@cloud-services/msa-point-service/src/main/java/com/techsemina/msa/pointservice/controller/PaymentController.java`
around lines 34 - 44, PaymentController currently calls
paymentService.processCompositePayment(dto) but there is no global exception
handling, so failures (DB, Kafka, etc.) bubble up as 500s; add a
`@RestControllerAdvice` class (e.g., GlobalExceptionHandler) that defines
`@ExceptionHandler` methods for broad Exception and specific exceptions like
DataAccessException and KafkaException, log the error with context (include
dto/orderId when available), and return a consistent ResponseEntity error body
(timestamp, message, error code) and appropriate HTTP status; ensure the handler
covers exceptions thrown by PaymentController and
paymentService.processCompositePayment so clients still get the 202 when
processing starts and consistent JSON for errors.

@caminobelllo caminobelllo merged commit 1fcb452 into main Feb 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 기능 개발 시

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feat: core-service 포인트 결제 관련 기능

3 participants