Skip to content

[FIX] 결제 멱등성 보장#357

Closed
unifolio0 wants to merge 2 commits into
developfrom
fix/error
Closed

[FIX] 결제 멱등성 보장#357
unifolio0 wants to merge 2 commits into
developfrom
fix/error

Conversation

@unifolio0

Copy link
Copy Markdown
Contributor

closed #

작업 내용

스크린샷

참고 사항

@unifolio0 unifolio0 self-assigned this May 25, 2026
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2a65685b-5acd-4ea8-8a00-f3c655b9a0f8

📥 Commits

Reviewing files that changed from the base of the PR and between 2014c72 and 37b0430.

📒 Files selected for processing (27)
  • src/main/java/com/samhap/kokomen/answer/domain/Answer.java
  • src/main/java/com/samhap/kokomen/category/domain/Category.java
  • src/main/java/com/samhap/kokomen/interview/controller/InterviewControllerV2.java
  • src/main/java/com/samhap/kokomen/interview/domain/Interview.java
  • src/main/java/com/samhap/kokomen/interview/domain/InterviewType.java
  • src/main/java/com/samhap/kokomen/interview/external/AnswerFeedbackBedrockClient.java
  • src/main/java/com/samhap/kokomen/interview/external/InterviewProceedBedrockClient.java
  • src/main/java/com/samhap/kokomen/interview/external/dto/request/InterviewBedrockRequestFactory.java
  • src/main/java/com/samhap/kokomen/interview/repository/RootQuestionRepository.java
  • src/main/java/com/samhap/kokomen/interview/service/InterviewStartFacadeService.java
  • src/main/java/com/samhap/kokomen/interview/service/core/InterviewService.java
  • src/main/java/com/samhap/kokomen/interview/service/dto/AnswerRequestV2.java
  • src/main/java/com/samhap/kokomen/interview/service/question/RootQuestionService.java
  • src/main/java/com/samhap/kokomen/interview/tool/CodingInterviewBedrockSystemMessageConstant.java
  • src/main/java/com/samhap/kokomen/interview/tool/CodingInterviewPromptFragments.java
  • src/main/java/com/samhap/kokomen/interview/tool/GptSystemMessageConstant.java
  • src/main/java/com/samhap/kokomen/interview/tool/InterviewMessagesFactory.java
  • src/main/resources/db/migration/V45__add_live_coding_category_and_seed.sql
  • src/main/resources/db/migration/V46__widen_answer_content.sql
  • src/test/java/com/samhap/kokomen/category/domain/CategoryTest.java
  • src/test/java/com/samhap/kokomen/interview/domain/InterviewTest.java
  • src/test/java/com/samhap/kokomen/interview/external/dto/request/InterviewBedrockRequestFactoryTest.java
  • src/test/java/com/samhap/kokomen/interview/repository/AnswerRepositoryTest.java
  • src/test/java/com/samhap/kokomen/interview/service/LiveCodingInterviewServiceTest.java
  • src/test/java/com/samhap/kokomen/interview/service/core/InterviewServiceTest.java
  • src/test/java/com/samhap/kokomen/interview/service/question/RootQuestionServiceTest.java
  • src/test/java/com/samhap/kokomen/interview/tool/InterviewMessagesFactoryTest.java

Summary by CodeRabbit

  • New Features

    • 라이브 코딩 인터뷰 유형 추가 및 관련 시스템 프롬프트·프롬프트 조각(진행/종료/피드백) 도입
    • 카테고리에 LIVE_CODING 추가 및 샘플 문제 시드 삽입
    • 인터뷰 생성 시 카테고리 기반으로 InterviewType 결정 및 LIVE_CODING 전용 동작(모드 검증 등) 반영
    • 답변 허용 길이 상향: 2,000 → 10,000자
  • Bug Fixes

    • 결제 확인 및 토큰 구매의 재요청(중복) 처리 개선 — 승인/완료된 건은 외부 재호출 없이 기존 데이터로 응답하여 멱등성 보장
    • 동일 요청 시 데이터 무결성 오류 발생 시 기존 결제 상태에 따라 적절히 응답/오류 처리
  • Behavior Changes

    • LIVE_CODING 카테고리의 활성 문항은 무작위 선택 대상에서 제외되고, 라이브 코딩 결과에서 참조 답변은 노출되지 않음

Walkthrough

결제 확인의 멱등성 처리 로직을 추가하고, LIVE_CODING 인터뷰 타입 및 관련 Bedrock/GPT 프롬프트·팩토리·서비스·테스트를 도입합니다.

Changes

결제 멱등성 구현

Layer / File(s) Summary
결제 상태 판별 유틸
src/main/java/com/samhap/kokomen/payment/domain/PaymentState.java, src/main/java/com/samhap/kokomen/payment/domain/TosspaymentsPayment.java
PaymentState와 TosspaymentsPayment에 isApprovedOrCompleted()isClientBadRequest()를 추가합니다.
결제 결과 서비스 접근
src/main/java/com/samhap/kokomen/payment/service/TosspaymentsPaymentResultService.java
findByTosspaymentsPaymentId(Long)를 추가해 Optional로 결과를 조회합니다.
PaymentResponse 팩토리 메서드
src/main/java/com/samhap/kokomen/payment/service/dto/PaymentResponse.java
기존 결제와 결과에서 PaymentResponse를 구성하는 fromExisting 정적 팩토리를 추가합니다.
결제 확인 멱등성 처리
src/main/java/com/samhap/kokomen/payment/service/PaymentFacadeService.java
confirmPayment 저장 단계에서 DataIntegrityViolationException을 잡아 기존 결제를 조회하고 상태에 따라 멱등 응답 또는 예외를 반환하는 로직(resolveExistingPayment, readClientFailureMessage)을 추가합니다.
결제·토큰 관련 테스트
src/test/java/com/samhap/kokomen/payment/service/PaymentFacadeServiceTest.java, src/test/java/com/samhap/kokomen/token/service/TokenFacadeServiceTest.java
재호출/중복 요청 시나리오와 멱등성(외부 호출 억제, 예외 처리, 토큰 지급 단일화)을 검증하는 테스트들을 추가합니다.

라이브 코딩 인터뷰 기능 추가

Layer / File(s) Summary
카테고리 추가 및 DB 마이그레이션
src/main/java/com/samhap/kokomen/category/domain/Category.java, src/main/resources/db/migration/V45__add_live_coding_category_and_seed.sql
Category.LIVE_CODING을 enum에 추가하고 getCategories()에서 제외하도록 변경, 마이그레이션과 시드 추가.
Interview 도메인·검증 변경
src/main/java/com/samhap/kokomen/interview/domain/Interview.java, src/main/java/com/samhap/kokomen/interview/domain/InterviewType.java, src/main/java/com/samhap/kokomen/interview/service/dto/AnswerRequestV2.java, src/main/java/com/samhap/kokomen/answer/domain/Answer.java
Interview 생성자에 InterviewType을 반영하고 isLiveCoding() 추가, Answer 입력/엔티티 길이 제한을 10,000자로 확장.
Bedrock 요청 팩토리 및 클라이언트 변경
src/main/java/com/samhap/kokomen/interview/external/dto/request/InterviewBedrockRequestFactory.java, src/main/java/com/samhap/kokomen/interview/external/InterviewProceedBedrockClient.java, src/main/java/com/samhap/kokomen/interview/external/AnswerFeedbackBedrockClient.java
System 프롬프트 생성 시 InterviewType을 전달하도록 시그니처를 변경해 인터뷰 유형별 프롬프트 선택을 가능하게 합니다.
라이브 코딩 전용 프롬프트·상수
src/main/java/com/samhap/kokomen/interview/tool/CodingInterviewBedrockSystemMessageConstant.java, src/main/java/com/samhap/kokomen/interview/tool/CodingInterviewPromptFragments.java, src/main/java/com/samhap/kokomen/interview/tool/GptSystemMessageConstant.java
라이브 코딩 전용 Bedrock/GPT 시스템 메시지와 프래그먼트 유틸을 추가합니다.
GPT 메시지 분기
src/main/java/com/samhap/kokomen/interview/tool/InterviewMessagesFactory.java
InterviewType에 따라 코딩/비코딩 시스템 메시지를 선택하도록 변경합니다.
인터뷰 시작·서비스 변경 및 루트 질문 필터링
src/main/java/com/samhap/kokomen/interview/service/InterviewStartFacadeService.java, src/main/java/com/samhap/kokomen/interview/service/core/InterviewService.java, src/main/java/com/samhap/kokomen/interview/service/question/RootQuestionService.java, src/main/java/com/samhap/kokomen/interview/repository/RootQuestionRepository.java
인터뷰 시작 시 카테고리·모드 호환성 검증, InterviewType 반영 저장, LIVE_CODING을 무작위 루트 질문 대상에서 제외하는 로직 및 저장소 메서드 추가.
테스트 추가
src/test/java/... (여러 파일)
Category, Interview 도메인·서비스, Bedrock 요청 팩토리, Answer 저장 길이 등 라이브 코딩 관련 단위/통합 테스트들을 추가했습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • nak-honest
  • kargowild

Poem

🐰 새벽 들판에 코드 한 줄,
멱등성은 단단한 껍질,
같은 열쇠로 두 번 두드려도,
결제는 한 번만 뛰어오르고,
라이브 코테는 깡총깡총 질문을 던지네.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR 설명은 완전히 비어있고 작업 내용, 스크린샷, 참고 사항 등이 모두 누락되어 있어 변경 사항에 대한 의미 있는 정보를 제공하지 않습니다. PR 설명에 변경의 목적, 멱등성 보장 방식, 관련된 이슈 번호, 그리고 테스트 검증 내용 등의 구체적인 정보를 작성하여 검토자의 이해를 돕도록 합니다.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 "[FIX] 결제 멱등성 보장"으로 변경 내용의 핵심을 명확하게 반영하고 있으며, 결제 멱등성 구현에 관한 여러 파일의 수정사항과 일치합니다.
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.

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

✨ 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 fix/error

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 and usage tips.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

본 PR은 결제 승인 과정에서 발생할 수 있는 중복 요청 문제를 해결하기 위해 멱등성을 보장하는 로직을 도입했습니다. 데이터 무결성 예외를 활용하여 이미 처리된 결제에 대한 재요청을 감지하고, 기존 결제 상태에 따라 적절한 응답을 반환하거나 예외를 던짐으로써 시스템의 안정성을 높였습니다.

Highlights

  • 결제 멱등성 보장: 결제 승인 요청 시 동일한 paymentKey로 재요청이 들어올 경우, 기존 결제 정보를 반환하여 중복 결제를 방지하도록 로직을 개선했습니다.
  • 예외 처리 강화: 결제 상태에 따라 이미 승인된 결제는 결과를 반환하고, 클라이언트 오류 발생 시 적절한 메시지를 전달하며, 비정상 상태 재요청 시 서버 오류를 발생시키도록 처리했습니다.
  • 테스트 코드 추가: 다양한 결제 상태에서의 재요청 시나리오와 토큰 구매 시의 멱등성 보장을 검증하는 테스트 케이스를 추가했습니다.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.


결제 요청 중복되어, 멱등성으로 막아내니, 데이터는 안전하고, 사용자도 안심하네.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions

Copy link
Copy Markdown

Test Results

 51 files   51 suites   1m 36s ⏱️
298 tests 297 ✅ 1 💤 0 ❌
300 runs  299 ✅ 1 💤 0 ❌

Results for commit 2014c72.

@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: 2

🤖 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 `@src/main/java/com/samhap/kokomen/payment/service/PaymentFacadeService.java`:
- Around line 48-50: The catch for DataIntegrityViolationException in
PaymentFacadeService indiscriminately treats all constraint violations as
idempotent "same paymentKey" retries; update the handler to inspect the
exception cause (e.g., exception.getMostSpecificCause() / message or
SQLState/code) to detect whether the unique constraint violated is the
payment_key constraint and only then call
resolveExistingPayment(tosspaymentsPaymentService.readByPaymentKey(request.paymentKey()));
for other constraint violations (like order_id) rethrow or handle separately
(log full exception details and propagate or map to a proper error), and ensure
the log includes the exception info to aid debugging.

In
`@src/test/java/com/samhap/kokomen/payment/service/PaymentFacadeServiceTest.java`:
- Around line 325-336: The test
CLIENT_BAD_REQUEST_상태에_failure_정보가_없으면_기본_메시지로_BadRequestException을_던진다()
currently only asserts the thrown BadRequestException message but misses
verifying idempotency by ensuring external client is not invoked; update the
test to also verify that tosspaymentsClient.confirmPayment(...) was never called
(e.g., verify(tosspaymentsClient, times(0)).confirmPayment(...)) after calling
paymentFacadeService.confirmPayment(request), keeping existing setup that sets
payment state to PaymentState.CLIENT_BAD_REQUEST and saves via
tosspaymentsPaymentRepository.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 91d0a883-1724-48e8-a701-7c1a923cd84c

📥 Commits

Reviewing files that changed from the base of the PR and between d777ff7 and 2014c72.

📒 Files selected for processing (7)
  • src/main/java/com/samhap/kokomen/payment/domain/PaymentState.java
  • src/main/java/com/samhap/kokomen/payment/domain/TosspaymentsPayment.java
  • src/main/java/com/samhap/kokomen/payment/service/PaymentFacadeService.java
  • src/main/java/com/samhap/kokomen/payment/service/TosspaymentsPaymentResultService.java
  • src/main/java/com/samhap/kokomen/payment/service/dto/PaymentResponse.java
  • src/test/java/com/samhap/kokomen/payment/service/PaymentFacadeServiceTest.java
  • src/test/java/com/samhap/kokomen/token/service/TokenFacadeServiceTest.java

Comment on lines +48 to +50
} catch (DataIntegrityViolationException e) {
log.info("동일 paymentKey 결제 재요청 - 멱등 분기 처리, paymentKey: {}", request.paymentKey());
return resolveExistingPayment(tosspaymentsPaymentService.readByPaymentKey(request.paymentKey()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

DataIntegrityViolationException 전체를 멱등 재요청으로 처리하면 오분류됩니다.

현재 분기는 모든 DataIntegrityViolationException을 동일 paymentKey 재요청으로 가정합니다. 하지만 TosspaymentsPaymentorder_id도 unique라서 다른 제약 위반까지 멱등 분기로 흡수될 수 있습니다(잘못된 상태 조회/응답 위험).

🔧 제안 수정안
 try {
     tosspaymentsPayment = tosspaymentsPaymentService.saveTosspaymentsPayment(request);
 } catch (DataIntegrityViolationException e) {
-    log.info("동일 paymentKey 결제 재요청 - 멱등 분기 처리, paymentKey: {}", request.paymentKey());
-    return resolveExistingPayment(tosspaymentsPaymentService.readByPaymentKey(request.paymentKey()));
+    TosspaymentsPayment existing = tosspaymentsPaymentService.findByPaymentKey(request.paymentKey())
+            .orElseThrow(() -> {
+                log.error("결제 저장 무결성 위반 - paymentKey로 기존 결제를 찾지 못했습니다. paymentKey: {}", request.paymentKey(), e);
+                return e;
+            });
+    log.info("동일 paymentKey 결제 재요청 - 멱등 분기 처리, paymentKey: {}", request.paymentKey());
+    return resolveExistingPayment(existing);
 }
🤖 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 `@src/main/java/com/samhap/kokomen/payment/service/PaymentFacadeService.java`
around lines 48 - 50, The catch for DataIntegrityViolationException in
PaymentFacadeService indiscriminately treats all constraint violations as
idempotent "same paymentKey" retries; update the handler to inspect the
exception cause (e.g., exception.getMostSpecificCause() / message or
SQLState/code) to detect whether the unique constraint violated is the
payment_key constraint and only then call
resolveExistingPayment(tosspaymentsPaymentService.readByPaymentKey(request.paymentKey()));
for other constraint violations (like order_id) rethrow or handle separately
(log full exception details and propagate or map to a proper error), and ensure
the log includes the exception info to aid debugging.

Comment on lines +325 to +336
void CLIENT_BAD_REQUEST_상태에_failure_정보가_없으면_기본_메시지로_BadRequestException을_던진다() {
TosspaymentsPayment payment = TosspaymentsPaymentFixtureBuilder.builder()
.paymentKey("payment_key")
.build();
payment.updateState(PaymentState.CLIENT_BAD_REQUEST);
tosspaymentsPaymentRepository.save(payment);
ConfirmRequest request = createConfirmRequest();

assertThatThrownBy(() -> paymentFacadeService.confirmPayment(request))
.isInstanceOf(BadRequestException.class)
.hasMessage(PaymentServiceErrorMessage.INVALID_REQUEST.getMessage());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

멱등성 핵심 검증(외부 호출 억제) 조건이 빠져 있습니다.

Line 325-336 테스트는 예외 메시지만 확인하고 tosspaymentsClient.confirmPayment 미호출을 확인하지 않아, 멱등성 회귀를 놓칠 수 있습니다. 외부 호출 0회를 함께 검증해 주세요.

제안 수정
     assertThatThrownBy(() -> paymentFacadeService.confirmPayment(request))
             .isInstanceOf(BadRequestException.class)
             .hasMessage(PaymentServiceErrorMessage.INVALID_REQUEST.getMessage());
+    verify(tosspaymentsClient, times(0)).confirmPayment(any(), any());
🤖 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
`@src/test/java/com/samhap/kokomen/payment/service/PaymentFacadeServiceTest.java`
around lines 325 - 336, The test
CLIENT_BAD_REQUEST_상태에_failure_정보가_없으면_기본_메시지로_BadRequestException을_던진다()
currently only asserts the thrown BadRequestException message but misses
verifying idempotency by ensuring external client is not invoked; update the
test to also verify that tosspaymentsClient.confirmPayment(...) was never called
(e.g., verify(tosspaymentsClient, times(0)).confirmPayment(...)) after calling
paymentFacadeService.confirmPayment(request), keeping existing setup that sets
payment state to PaymentState.CLIENT_BAD_REQUEST and saves via
tosspaymentsPaymentRepository.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements idempotency for the payment confirmation process by handling DataIntegrityViolationException during payment record creation. It introduces logic to resolve existing payments based on their current state, returning cached responses for successful payments or throwing specific exceptions for client-side failures. Feedback from the reviewer suggests strengthening this implementation by validating that the incoming request details (orderId and amount) match the existing record to prevent incorrect idempotent responses. Additionally, the reviewer recommends allowing the confirmation process to proceed if the payment is in a transient state such as NEED_APPROVE or CONNECTION_TIMEOUT.

Comment on lines +46 to +51
try {
tosspaymentsPayment = tosspaymentsPaymentService.saveTosspaymentsPayment(request);
} catch (DataIntegrityViolationException e) {
log.info("동일 paymentKey 결제 재요청 - 멱등 분기 처리, paymentKey: {}", request.paymentKey());
return resolveExistingPayment(tosspaymentsPaymentService.readByPaymentKey(request.paymentKey()));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

결제 승인 요청 중 DataIntegrityViolationException이 발생했을 때, 단순히 기존 결제 정보를 조회하여 반환하는 것만으로는 멱등성을 완벽하게 보장하기 어렵습니다.

  1. 재시도 허용: 기존 결제 상태가 NEED_APPROVECONNECTION_TIMEOUT인 경우, 이는 아직 토스페이먼츠 API 호출이 성공하지 않았거나 네트워크 오류로 중단된 상태임을 의미합니다. 이 경우 예외를 던지지 않고 승인 프로세스를 계속 진행할 수 있도록 처리해야 합니다.
  2. 요청 정보 검증: 멱등성 처리를 위해 기존 결제를 조회했을 때, 요청된 orderIdamount가 DB에 저장된 정보와 일치하는지 반드시 확인해야 합니다. 정보가 다를 경우 잘못된 멱등 요청으로 간주하고 예외를 발생시켜야 합니다.
        try {
            tosspaymentsPayment = tosspaymentsPaymentService.saveTosspaymentsPayment(request);
        } catch (DataIntegrityViolationException e) {
            log.info("동일 paymentKey 결제 재요청 - 멱등 분기 처리, paymentKey: {}", request.paymentKey());
            tosspaymentsPayment = tosspaymentsPaymentService.readByPaymentKey(request.paymentKey());
            PaymentResponse existingResponse = resolveExistingPayment(request, tosspaymentsPayment);
            if (existingResponse != null) {
                return existingResponse;
            }
        }

Comment on lines +66 to +79
private PaymentResponse resolveExistingPayment(TosspaymentsPayment payment) {
if (payment.isApprovedOrCompleted()) {
log.info("이미 승인된 결제 재요청 - 멱등 응답 반환, paymentKey: {}, state: {}",
payment.getPaymentKey(), payment.getState());
TosspaymentsPaymentResult result = tosspaymentsPaymentResultService.readByTosspaymentsPaymentId(payment.getId());
return PaymentResponse.fromExisting(payment, result);
}
if (payment.isClientBadRequest()) {
log.info("클라이언트 원인 실패 결제 재요청 - paymentKey: {}", payment.getPaymentKey());
throw new BadRequestException(readClientFailureMessage(payment.getId()));
}
log.error("비정상 상태 결제 재요청 - paymentKey: {}, state: {}", payment.getPaymentKey(), payment.getState());
throw new InternalServerErrorException(PaymentServiceErrorMessage.CONFIRM_SERVER_ERROR.getMessage());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

resolveExistingPayment 메서드에서 NEED_APPROVECONNECTION_TIMEOUT 상태를 처리하여 승인 프로세스를 재시작할 수 있도록 개선이 필요합니다. 또한, 요청 정보(orderId, amount)에 대한 검증 로직을 추가하여 보안 및 정합성을 강화해야 합니다.

    private PaymentResponse resolveExistingPayment(ConfirmRequest request, TosspaymentsPayment payment) {
        if (!payment.getOrderId().equals(request.orderId()) || !payment.getTotalAmount().equals(request.amount())) {
            log.warn("멱등 요청 정보 불일치 - paymentKey: {}, requestOrderId: {}, dbOrderId: {}", 
                    payment.getPaymentKey(), request.orderId(), payment.getOrderId());
            throw new BadRequestException("이미 존재하는 결제 키이나, 주문 정보가 일치하지 않습니다.");
        }

        if (payment.isApprovedOrCompleted()) {
            log.info("이미 승인된 결제 재요청 - 멱등 응답 반환, paymentKey: {}, state: {}",
                    payment.getPaymentKey(), payment.getState());
            TosspaymentsPaymentResult result = tosspaymentsPaymentResultService.readByTosspaymentsPaymentId(payment.getId());
            return PaymentResponse.fromExisting(payment, result);
        }
        if (payment.isClientBadRequest()) {
            log.info("클라이언트 원인 실패 결제 재요청 - paymentKey: {}", payment.getPaymentKey());
            throw new BadRequestException(readClientFailureMessage(payment.getId()));
        }
        if (payment.getState() == PaymentState.NEED_APPROVE || payment.getState() == PaymentState.CONNECTION_TIMEOUT) {
            log.info("결제 승인 전 단계에서 재요청 - 승인 프로세스 진행, paymentKey: {}, state: {}", 
                    payment.getPaymentKey(), payment.getState());
            return null;
        }
        log.error("비정상 상태 결제 재요청 - paymentKey: {}, state: {}", payment.getPaymentKey(), payment.getState());
        throw new InternalServerErrorException(PaymentServiceErrorMessage.CONFIRM_SERVER_ERROR.getMessage());
    }

@unifolio0 unifolio0 closed this Jun 2, 2026
@unifolio0 unifolio0 deleted the fix/error branch June 9, 2026 02:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant