Skip to content

[PR] 결제 verify api 구현 - #606

Merged
mosungjin merged 5 commits into
Deployment_developfrom
feature/sooyoung0927-payment
Jul 13, 2026
Merged

[PR] 결제 verify api 구현#606
mosungjin merged 5 commits into
Deployment_developfrom
feature/sooyoung0927-payment

Conversation

@sooyoung0927

@sooyoung0927 sooyoung0927 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

작업내용

  • 실제 결제가 이루어지고 난 후의 검증 과정
  1. 우리가 /prepare에서 db에 저장한 예상 결제 금액과 실제 결제 금액이 같다면 결제 완료 -> user 멤버십 업데이트
  2. 만약 틀리다면 결제 취소
  3. 결제 취소 중에 실패한다면 예외처리
  4. 결제 건을 찾을 수 없거나 결제 자체가 이루어지지 않았다면 예외처리
  5. 포트원에 제대로 연결되지 않았다면 예외처리
  6. 이미 결제된 건이면 예외처리

추후 결제 관련 진행작업

  1. 동시성 toctou 작업 - 중복 결제 방지
  2. 구독 취소 (=환불) 기능
  3. 관리자 페이지 전체 매출 조회
  4. 관리자 페이지 월별 매출 조회
  5. 관리자 페이지 구독 플랜별 분포 조회
  6. 마이페이지 결제 내역 조회

#600

Summary by CodeRabbit

  • 새로운 기능
    • 결제 검증(verify) API가 추가되었습니다.
    • 결제 ID로 결제 대행사 상태와 금액을 확인해 검증을 완료합니다.
    • 검증 성공 시 멤버십 플랜과 이용 기간이 자동 갱신됩니다.
  • 오류 처리 개선
    • 결제 미존재, 금액 불일치, 이미 검증됨, 결제 시도/취소 실패 등 상황별 오류 응답을 제공합니다.
    • 취소 실패 시 별도 결제 상태가 반영됩니다.
  • API 문서
    • 준비/검증 API의 주요 응답 코드와 메시지가 문서화되었습니다.
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

결제 검증 API가 PortOne 결제 조회·취소, 결제 상태 전환, Redis 락, 멤버십 갱신 및 결제 예외 응답 처리와 연결되었다.

Changes

결제 검증 흐름

Layer / File(s) Summary
결제 검증 계약과 상태 모델
legend-momo-city/src/main/java/com/wanted/momocity/payment/application/{command,port,usecase}/*, legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/{model,repository,exception}/*
검증 커맨드·결과·포트, 결제 상태 전환, paymentId 조회 계약 및 관련 예외가 추가되었다.
PortOne 연동과 저장소 연결
legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/{adapter,persistence}/*
PortOne 결제 조회·취소와 paymentId 기반 결제 조회가 구현되었다.
검증 처리와 락·실패 상태 저장
legend-momo-city/src/main/java/com/wanted/momocity/payment/application/{service,supporter}/*, legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PaymentLockAdapter.java
결제 준비 락과 검증 결과에 따른 성공·실패·취소 실패 상태 저장이 추가되었다.
멤버십 갱신
legend-momo-city/src/main/java/com/wanted/momocity/user/infrastructure/{paymentadapter,persistence}/*
결제 성공 시 사용자 멤버십과 시작 시각을 갱신한다.
검증 API와 예외 응답
legend-momo-city/src/main/java/com/wanted/momocity/payment/presentation/api/*, legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PaymentExceptionHandler.java
POST /verify 요청·응답과 결제 예외의 HTTP 응답 매핑이 추가되었다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PaymentController
  participant PaymentCommandService
  participant PortOnePaymentAdapter
  participant SetUserMembershipAdapter
  Client->>PaymentController: POST /verify
  PaymentController->>PaymentCommandService: paymentVerify(command)
  PaymentCommandService->>PortOnePaymentAdapter: verifyPayment(paymentId)
  PortOnePaymentAdapter-->>PaymentCommandService: PortOnePaymentDetail
  PaymentCommandService->>SetUserMembershipAdapter: updateMembership(...)
  SetUserMembershipAdapter-->>PaymentCommandService: membership updated
  PaymentCommandService-->>PaymentController: PaymentVerifyResult
  PaymentController-->>Client: PaymentVerifyResponse
Loading

Possibly related PRs

Suggested labels: ⚙️ type: 기능, 🚧 status: 진행중

Poem

당근처럼 결제가 톡,
PortOne에서 금액 확인 쏙.
맞으면 멤버십 활짝,
토끼도 검증 완료 깡충! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 결제 검증 API 구현이라는 핵심 변경을 정확히 요약해 관련성이 높습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/sooyoung0927-payment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (1)
legend-momo-city/src/main/java/com/wanted/momocity/payment/presentation/api/PaymentController.java (1)

37-42: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Swagger 요약에 오타가 있습니다.

"본 결제 전 확인 단게""본 결제 전 확인 단계"로 수정해 주세요. 사용자에게 노출되는 API 문서 문자열입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/presentation/api/PaymentController.java`
around lines 37 - 42, Update the summary value in PaymentController’s `@Operation`
for the /prepare endpoint, correcting the typo from “단게” to “단계” while leaving
the description unchanged.
🧹 Nitpick comments (3)
legend-momo-city/src/main/java/com/wanted/momocity/payment/application/service/PaymentCommandService.java (1)

60-114: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

외부 API 호출이 @Transactional 범위 내에서 수행됩니다.

paymentVerify는 클래스 레벨 @Transactional로 인해 DB 트랜잭션이 열린 상태에서 PortOne API 호출(verifyPayment, cancelPayment)을 수행합니다. 외부 HTTP 호출 지연 시 DB 커넥션이 장시간 점유되어 커넥션 풀 고갈 위험이 있습니다.

외부 호출 전후로 트랜잭션을 분리하거나, 외부 호출 결과를 바탕으로 별도 트랜잭션에서 DB 갱신을 수행하는 구조를 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/application/service/PaymentCommandService.java`
around lines 60 - 114, Refactor paymentVerify so the PortOne calls verifyPayment
and cancelPayment do not execute within the class-level database transaction.
Separate external API interaction from persistence, then perform payment state
changes and membership updates through a distinct transactional method or
service using the verified result, preserving the existing success, mismatch,
and cancellation-failure outcomes.
legend-momo-city/src/main/java/com/wanted/momocity/payment/presentation/api/PaymentController.java (1)

89-99: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

주석 처리된 dead code를 제거해 주세요.

// @PatchMapping("/cancle") 등 사용하지 않는 코드가 주석으로 남아 있습니다. 향후 환불 기능은 별도 PR에서 구현할 때 추가하고, 현재는 제거하는 것이 가독성에 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/presentation/api/PaymentController.java`
around lines 89 - 99, PaymentController 주변에 남아 있는 주석 처리된 환불 엔드포인트와 관련 설명을 제거하세요.
향후 별도 PR에서 구현할 예정이므로 `@PatchMapping`("/cancle") 및 해당 주석 블록만 삭제하고, 인접한 실제 컨트롤러 코드는
유지하세요.
legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PaymentExceptionHandler.java (1)

46-122: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

에러 코드 문자열이 하드코딩되어 있습니다.

"PAYMENT_NOT_FOUND", "PORTONE_API_ERROR" 등 에러 코드가 문자열 리터럴로 직접 작성되어 있습니다. 기존 핸들러(13-45)도 동일한 패턴이므로 일관성은 맞지만, 오타 발생 시 컴파일 타임에 잡을 수 없어 유지보수 리스크가 있습니다. PaymentResponseCode처럼 상수 클래스(예: PaymentErrorCode)를 도입해 참조하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PaymentExceptionHandler.java`
around lines 46 - 122, PaymentExceptionHandler의 각 핸들러에 하드코딩된 에러 코드 문자열을 제거하고
PaymentResponseCode 또는 새 PaymentErrorCode 상수 클래스의 정의된 상수를 참조하도록 변경하세요.
PaymentNotFoundException, PortOneApiException 및 나머지 결제 예외 핸들러의 코드 값과 응답 구조는 기존
의미를 그대로 유지하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/application/service/PaymentCommandService.java`:
- Around line 63-64: PaymentCommandService의 결제 조회 직후 command.userId()와
payment.getUserId()가 일치하는지 검증하도록 수정하세요. 불일치하면 기존 도메인 예외 처리 방식에 맞는 예외를 발생시키고,
일치하는 경우에만 이후 결제 검증 및 멤버십 갱신 로직을 진행하세요. Payment에 getUserId()가 없다면 해당 접근 메서드를
추가하세요.
- Around line 82-114: 분기 내부의 paymentRepository.save 호출은 이후 RuntimeException으로
롤백되므로 실패 상태가 보존되지 않습니다. PaymentCommandService의 실패 처리 흐름에서 FAILED 및 CANCEL_FAILED
저장을 별도 REQUIRES_NEW 트랜잭션 서비스나 메서드로 분리하고, PaymentAmountMismatchException 및
PaymentCancelFailedException을 던지기 전에 해당 저장을 호출하도록 수정하세요.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/model/Payment.java`:
- Around line 43-68: Payment.markSuccess의 불완전한 주석을 “포트원 실결제금액과 DB 금액이 일치할 때”처럼
완전한 문장으로 수정하세요. Payment.markFailed 주변 주석의 pgTxId 언급은 현재 필드가 없으므로 제거하고, 실제 구현과
일치하는 금액 불일치·결제 실패 설명만 남기세요.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PortOnePaymentAdapter.java`:
- Around line 19-51: PortOnePaymentAdapter의 verifyPayment 및 cancelPayment에서
WebClient의 .block() 호출에 명시적인 Duration 타임아웃을 적용하세요. 필요한 java.time.Duration을
import하고, PortOne API 응답이 지연될 때 무기한 대기하지 않도록 두 메서드의 blocking 호출 모두 동일한 적절한 타임아웃을
사용하게 수정하세요.
- Around line 44-48: Update the amount parsing in PortOnePaymentAdapter so a
non-null amountMap with a null amountMap.get("total") produces a null total
instead of calling Long.valueOf on "null". Preserve numeric total conversion
when the field is present and avoid the resulting NumberFormatException.
- Around line 53-62: Update PortOnePaymentAdapter.cancelPayment to catch
WebClientResponseException from the PortOne cancellation request and convert it
to the existing PortOneApiException type, matching the error-handling behavior
of verifyPayment so PaymentCommandService can persist CANCEL_FAILED without
transaction rollback.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/persistence/PaymentRepositoryAdapter.java`:
- Around line 28-32: Update findPendingByUserIdAndPlan to apply the targetPlan
filter when querying pending payments, using the corresponding Spring Data
repository method and preserving the existing domain mapping. If plan filtering
is intentionally removed instead, rename this method and update
PaymentRepository plus PaymentCommandService.paymentPrepare to remove the
obsolete parameter and reflect the broader lookup.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/persistence/SpringDataPaymentRepository.java`:
- Line 11: Restore the Plan predicate in
SpringDataPaymentRepository.findFirstByUserIdAndPlanAndStatus and update
PaymentRepositoryAdapter.findPendingByUserIdAndPlan to pass targetPlan through,
ensuring /prepare retrieves the latest PENDING payment for the requested plan
rather than any plan.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/presentation/api/PaymentController.java`:
- Around line 65-87: PaymentController의 paymentVerify 흐름에서 결제 소유자 검증을 추가하세요.
paymentId로 조회한 결제의 payment.getUserId()와 PaymentVerifyCommand의 userId()를 비교하고,
불일치하면 검증 및 멤버십 갱신을 중단하도록 paymentCommandUseCase의 관련 검증 로직을 수정하세요.

---

Outside diff comments:
In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/presentation/api/PaymentController.java`:
- Around line 37-42: Update the summary value in PaymentController’s `@Operation`
for the /prepare endpoint, correcting the typo from “단게” to “단계” while leaving
the description unchanged.

---

Nitpick comments:
In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/application/service/PaymentCommandService.java`:
- Around line 60-114: Refactor paymentVerify so the PortOne calls verifyPayment
and cancelPayment do not execute within the class-level database transaction.
Separate external API interaction from persistence, then perform payment state
changes and membership updates through a distinct transactional method or
service using the verified result, preserving the existing success, mismatch,
and cancellation-failure outcomes.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PaymentExceptionHandler.java`:
- Around line 46-122: PaymentExceptionHandler의 각 핸들러에 하드코딩된 에러 코드 문자열을 제거하고
PaymentResponseCode 또는 새 PaymentErrorCode 상수 클래스의 정의된 상수를 참조하도록 변경하세요.
PaymentNotFoundException, PortOneApiException 및 나머지 결제 예외 핸들러의 코드 값과 응답 구조는 기존
의미를 그대로 유지하세요.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/presentation/api/PaymentController.java`:
- Around line 89-99: PaymentController 주변에 남아 있는 주석 처리된 환불 엔드포인트와 관련 설명을 제거하세요.
향후 별도 PR에서 구현할 예정이므로 `@PatchMapping`("/cancle") 및 해당 주석 블록만 삭제하고, 인접한 실제 컨트롤러 코드는
유지하세요.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e29b8e6a-49da-4dea-9740-4df18c375b66

📥 Commits

Reviewing files that changed from the base of the PR and between fe16e66 and e55865c.

📒 Files selected for processing (27)
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/application/command/PaymentVerifyCommand.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/application/port/PortOnePaymentPort.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/application/port/SetUserMembershipPort.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/application/service/PaymentCommandService.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/application/usecase/PaymentCommandUseCase.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PaymentAlreadyVerifiedException.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PaymentAmountMismatchException.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PaymentCancelFailedException.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PaymentExceptionHandler.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PaymentNotAttemptedException.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PaymentNotFoundException.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PortOneApiException.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/model/Payment.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/model/PaymentVerifyResult.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/model/PortOnePaymentDetail.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/model/Status.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/repository/PaymentRepository.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PortOnePaymentAdapter.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/persistence/PaymentRepositoryAdapter.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/persistence/SpringDataPaymentRepository.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/presentation/api/PaymentController.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/presentation/api/common/PaymentResponseCode.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/presentation/api/common/PaymentResponseMessage.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/presentation/api/request/PaymentVerifyRequest.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/presentation/api/response/PaymentVerifyResponse.java
  • legend-momo-city/src/main/java/com/wanted/momocity/user/infrastructure/paymentadapter/SetUserMembershipAdapter.java
  • legend-momo-city/src/main/java/com/wanted/momocity/user/infrastructure/persistence/SpringDataUserRepository.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
legend-momo-city/src/main/java/com/wanted/momocity/payment/application/service/PaymentCommandService.java (1)

66-84: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

early exit 경로에서 락이 해제되지 않습니다.

paymentPrepare에서 tryLock로 획득한 락이 paymentVerify의 다음 예외 경로에서 해제되지 않습니다:

  • PaymentNotFoundException (line 69)
  • PaymentAccessDeniedException (line 73)
  • PaymentAlreadyVerifiedException (line 79)
  • portOnePaymentPort.verifyPayment 실패 (line 84)

이 경우 TTL(5분) 만료까지 사용자가 새 결제를 시도할 수 없습니다. 메서드 전체를 try-finally로 감싸거나, 각 예외 발생 지점에서 unlock를 호출해야 합니다.

🔒️ Proposed fix
     `@Override`
     public PaymentVerifyResult paymentVerify(PaymentVerifyCommand command) {
+        Payment payment = null;
+        try {
         // 결제 건 조회
-        Payment payment = paymentRepository.findByPaymentId(command.paymentId())
+        payment = paymentRepository.findByPaymentId(command.paymentId())
                 .orElseThrow(() -> new PaymentNotFoundException("결제 정보를 찾을 수 없습니다."));

         // 정말 본인이 맞는지 확인
         if (!payment.getUserId().equals(command.userId())) {
             throw new PaymentAccessDeniedException("결제 소유자가 아닙니다.");
         }

         // 이미 검증 처리가 완료된 결제건
         if (payment.isFinalized()) {
             throw new PaymentAlreadyVerifiedException(
                     "이미 처리된 결제 건입니다. status=" + payment.getStatus()
             );
         }

         // 포트원에서 실제 결제 내역 조회
         PortOnePaymentDetail detail = portOnePaymentPort.verifyPayment(command.paymentId());

         // 검증
         boolean amountMatches = detail.isPaid() && payment.getPrice().equals(detail.amount());

         // 검증 성송
         if (amountMatches) {
             Payment result = payment.markSuccess();

             LocalDateTime membershipStart = LocalDateTime.now();
             setUserMembershipPort.updateMembership(command.userId(), payment.getPlan(), membershipStart);
             LocalDateTime membershipUntil = membershipStart.plusDays(30);

             paymentRepository.save(result);
-            paymentLockPort.unlock(command.userId(), payment.getPlan()); // 여기
+            return new PaymentVerifyResult(result.getPaymentId(), result.getStatus(), membershipUntil);
+        }

         // 금액 불일치 - 여기서부터는 전부 실패
         if (detail.isPaid()) {
             try {
                 portOnePaymentPort.cancelPayment(command.paymentId(), "결제 금액 불일치로 인한 자동 취소");
                 Payment failed = payment.markFailed();
                 paymentStatusUpdater.saveFailed(failed);
                 throw new PaymentAmountMismatchException(
                         "결제 금액이 일치하지 않아 취소 처리되었습니다. 예상 결제 금액 =" + payment.getPrice() + ", 실제 결제 금액=" + detail.amount()
                 );
             } catch (PortOneApiException e) {
                 Payment cancelFailed = payment.markCancelFailed();
                 paymentStatusUpdater.saveCancelFailed(cancelFailed);
                 throw new PaymentCancelFailedException(
                         "취소 처리 실패  paymentId=" + command.paymentId()
                 );
-            }finally {
-                paymentLockPort.unlock(command.userId(), payment.getPlan());
             }
         } else {
             Payment failed = payment.markFailed();
             paymentStatusUpdater.saveFailed(failed);
-            paymentLockPort.unlock(command.userId(), payment.getPlan());
             throw new PaymentAmountMismatchException("결제가 완료되지 않았습니다.");
         }
+        } finally {
+            if (payment != null) {
+                paymentLockPort.unlock(payment.getUserId(), payment.getPlan());
+            }
+        }
     }

unlockcommand.userId() 대신 payment.getUserId()를 사용하는 것이 더 안전합니다. PaymentAccessDeniedException 경로에서 command.userId()는 결제 소유자가 아닐 수 있기 때문입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/application/service/PaymentCommandService.java`
around lines 66 - 84, Update paymentVerify so every path after paymentPrepare’s
lock acquisition releases the lock, including payment lookup failure, ownership
denial, already-finalized payments, and verifyPayment failures. Wrap the full
flow in try-finally and unlock using the payment owner’s identifier when
available, while safely handling the PaymentNotFoundException path before a
Payment instance exists.
🧹 Nitpick comments (2)
legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PortOnePaymentAdapter.java (1)

70-91: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

cancelPayment에서 네트워크 예외(WebClientRequestException) 처리 누락

WebClientResponseException만 catch하므로, PortOne 서버가 응답하지 않거나 연결이 거부된 경우 WebClientRequestException이 그대로 전파됩니다. 이 경우 PaymentCommandServicecatch (PortOneApiException e)에서 잡히지 않아 CANCEL_FAILED 상태가 저장되지 않습니다.

필요하다면 WebClientRequestExceptionPortOneApiException으로 래핑하는 것을 고려해 보세요. 단, 현재 PR의 범위에서는 필수 사항은 아닙니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PortOnePaymentAdapter.java`
around lines 70 - 91, Update cancelPayment in PortOnePaymentAdapter to catch
WebClientRequestException in addition to WebClientResponseException, wrapping
network failures in PortOneApiException so PaymentCommandService can persist the
CANCEL_FAILED state. Preserve the existing response-status and response-body
details for WebClientResponseException.
legend-momo-city/src/main/java/com/wanted/momocity/payment/application/service/PaymentCommandService.java (1)

105-122: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

catch (PortOneApiException e)PaymentAmountMismatchException을 놓칠 수 있습니다.

try 블록에서 cancelPayment 성공 후 paymentStatusUpdater.saveFailed(failed)가 실패하면 PaymentAmountMismatchException이 아니라 다른 RuntimeException이 발생합니다. 이 경우 catch (PortOneApiException e)에 잡히지 않고 finally만 실행된 후 예외가 전파되어 CANCEL_FAILED 상태가 저장되지 않습니다.

saveFailed 호출을 catch 블록 밖으로 이동하거나, catch 범위를 Exception으로 넓히는 것을 고려하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/application/service/PaymentCommandService.java`
around lines 105 - 122, Update the payment failure handling around
cancelPayment, payment.markFailed, and paymentStatusUpdater.saveFailed so any
exception after cancellation, including saveFailed failures, records
CANCEL_FAILED through paymentStatusUpdater.saveCancelFailed before propagating
the appropriate error. Preserve the existing PaymentAmountMismatchException for
successful cancellation and ensure paymentLockPort.unlock remains in finally.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PaymentLockAdapter.java`:
- Around line 19-30: PaymentLockAdapter의 tryLock과 unlock이 서로 다른 Redis 키를 사용하고
있습니다. 두 메서드에서 동일한 키 형식을 사용하도록 수정해, unlock이 tryLock에서 설정한 락을 삭제하도록 하세요.

---

Outside diff comments:
In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/application/service/PaymentCommandService.java`:
- Around line 66-84: Update paymentVerify so every path after paymentPrepare’s
lock acquisition releases the lock, including payment lookup failure, ownership
denial, already-finalized payments, and verifyPayment failures. Wrap the full
flow in try-finally and unlock using the payment owner’s identifier when
available, while safely handling the PaymentNotFoundException path before a
Payment instance exists.

---

Nitpick comments:
In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/application/service/PaymentCommandService.java`:
- Around line 105-122: Update the payment failure handling around cancelPayment,
payment.markFailed, and paymentStatusUpdater.saveFailed so any exception after
cancellation, including saveFailed failures, records CANCEL_FAILED through
paymentStatusUpdater.saveCancelFailed before propagating the appropriate error.
Preserve the existing PaymentAmountMismatchException for successful cancellation
and ensure paymentLockPort.unlock remains in finally.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PortOnePaymentAdapter.java`:
- Around line 70-91: Update cancelPayment in PortOnePaymentAdapter to catch
WebClientRequestException in addition to WebClientResponseException, wrapping
network failures in PortOneApiException so PaymentCommandService can persist the
CANCEL_FAILED state. Preserve the existing response-status and response-body
details for WebClientResponseException.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aff66789-b7ed-4c66-b9eb-b56a89c3f735

📥 Commits

Reviewing files that changed from the base of the PR and between e55865c and c3690fa.

📒 Files selected for processing (11)
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/application/port/PaymentLockPort.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/application/service/PaymentCommandService.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/application/supporter/PaymentStatusUpdater.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PaymentAccessDeniedException.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PaymentExceptionHandler.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/model/Payment.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/repository/PaymentRepository.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PaymentLockAdapter.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PortOnePaymentAdapter.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/persistence/PaymentRepositoryAdapter.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/persistence/SpringDataPaymentRepository.java
💤 Files with no reviewable changes (3)
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/persistence/SpringDataPaymentRepository.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/persistence/PaymentRepositoryAdapter.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/repository/PaymentRepository.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/exception/PaymentExceptionHandler.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/domain/model/Payment.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (3)
legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PortOnePaymentAdapter.java (2)

39-40: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

PortOne 오류 본문을 클라이언트에 그대로 노출하지 마세요.

Line 39-40 및 Line 81-82에서 getResponseBodyAsString() 전체를 예외 메시지에 넣고 있습니다. PaymentExceptionHandlere.getMessage()를 HTTP 응답에 포함하므로 PortOne의 내부 오류 정보나 식별자가 외부에 노출될 수 있습니다. 원문은 서버 로그에 제한적으로 기록하고, 클라이언트에는 고정된 오류 메시지만 반환하는 편이 안전합니다.

Also applies to: 81-82

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PortOnePaymentAdapter.java`
around lines 39 - 40, Update both WebClientResponseException catch blocks in
PortOnePaymentAdapter to stop including getResponseBodyAsString() in the
PortOneApiException message. Log the raw response body only through the
server-side logging mechanism with appropriate limits, then throw
PortOneApiException with a fixed client-safe message while preserving the
existing failure handling.

47-65: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

PortOne 응답 파싱 실패도 PortOneApiException으로 감싸세요
status/pgTxId의 무검증 캐스팅과 amount.total 변환에서 ClassCastException·NumberFormatException이 나면 PortOneApiException으로 안 바뀌어 502가 아니라 전역 500으로 떨어집니다. 응답 파싱 전반을 검증하고 실패 시 동일한 예외로 변환하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PortOnePaymentAdapter.java`
around lines 47 - 65, Update the response parsing around status, pgTxId, and
amountMap in PortOnePaymentAdapter so invalid types and amount.total conversion
failures are caught and wrapped as PortOneApiException. Validate the required
response fields before casting or converting, preserve the existing
missing-amount error behavior, and ensure all parsing failures use the same
domain exception instead of escaping as ClassCastException or
NumberFormatException.
legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PaymentLockAdapter.java (1)

18-30: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

락 소유권을 확인한 뒤 해제해야 합니다.

Line 20-22는 모든 락에 동일한 값 "locked"를 저장하고, Line 28-29는 소유권 확인 없이 키를 삭제합니다. 기존 요청의 300초 TTL이 만료된 뒤 다른 요청이 같은 키를 획득하면, 기존 요청의 unlock이 새 요청의 락까지 삭제할 수 있습니다. 이로 인해 중복 결제 방지 락이 조기에 해제됩니다.

획득 시 고유 토큰을 저장하고, 원자적인 compare-and-delete로 동일 토큰일 때만 삭제하도록 락 포트 계약과 구현을 변경해 주세요. 또는 소유권을 지원하는 검증된 Redis 락 구현을 사용해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PaymentLockAdapter.java`
around lines 18 - 30, PaymentLockAdapter의 락 해제가 소유권을 검증하지 않아 다른 요청의 락을 삭제할 수
있습니다. tryLock에서 요청별 고유 토큰을 저장하고 호출자가 해당 토큰을 보존하도록 락 포트 계약을 변경한 뒤, unlock은 Lua
스크립트 등 원자적 compare-and-delete로 동일 토큰일 때만 삭제하도록 구현하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PaymentLockAdapter.java`:
- Around line 18-30: PaymentLockAdapter의 락 해제가 소유권을 검증하지 않아 다른 요청의 락을 삭제할 수
있습니다. tryLock에서 요청별 고유 토큰을 저장하고 호출자가 해당 토큰을 보존하도록 락 포트 계약을 변경한 뒤, unlock은 Lua
스크립트 등 원자적 compare-and-delete로 동일 토큰일 때만 삭제하도록 구현하세요.

In
`@legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PortOnePaymentAdapter.java`:
- Around line 39-40: Update both WebClientResponseException catch blocks in
PortOnePaymentAdapter to stop including getResponseBodyAsString() in the
PortOneApiException message. Log the raw response body only through the
server-side logging mechanism with appropriate limits, then throw
PortOneApiException with a fixed client-safe message while preserving the
existing failure handling.
- Around line 47-65: Update the response parsing around status, pgTxId, and
amountMap in PortOnePaymentAdapter so invalid types and amount.total conversion
failures are caught and wrapped as PortOneApiException. Validate the required
response fields before casting or converting, preserve the existing
missing-amount error behavior, and ensure all parsing failures use the same
domain exception instead of escaping as ClassCastException or
NumberFormatException.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e7a76e00-594c-4a5b-9ad3-d4867fca172b

📥 Commits

Reviewing files that changed from the base of the PR and between c3690fa and dd97622.

📒 Files selected for processing (2)
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PaymentLockAdapter.java
  • legend-momo-city/src/main/java/com/wanted/momocity/payment/infrastructure/adapter/PortOnePaymentAdapter.java
@sooyoung0927 sooyoung0927 self-assigned this Jul 13, 2026
@sooyoung0927 sooyoung0927 added ⚙️ type: 기능 프로젝트 기능 개발 🔴 priority: 높음 기능 우선순위 높음 labels Jul 13, 2026
@sooyoung0927
sooyoung0927 requested a review from mosungjin July 13, 2026 05:31

@mosungjin mosungjin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

확인했습니다. 고생하셨습니다!

@mosungjin
mosungjin merged commit 00b909b into Deployment_develop Jul 13, 2026
2 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in MoMocityproject Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔴 priority: 높음 기능 우선순위 높음 ⚙️ type: 기능 프로젝트 기능 개발

2 participants