Skip to content

[Fix] 결제 및 크레딧 동시성 처리 보강#71

Merged
whc9999 merged 3 commits into
mainfrom
fix/#62-payment
May 22, 2026
Merged

[Fix] 결제 및 크레딧 동시성 처리 보강#71
whc9999 merged 3 commits into
mainfrom
fix/#62-payment

Conversation

@whc9999
Copy link
Copy Markdown
Collaborator

@whc9999 whc9999 commented May 22, 2026

✨ 어떤 이유로 PR를 하셨나요?

  • feature 병합
  • 버그 수정(아래에 issue #를 남겨주세요)
  • 코드 개선
  • 코드 수정
  • 배포
  • 기타(아래에 자세한 내용 기입해주세요)

📋 세부 내용 - 왜 해당 PR이 필요한지 작업 내용을 자세하게 설명해주세요

  • 결제 승인 중복 처리를 막기 위해 Payment 조회 시 PESSIMISTIC_WRITE lock을 적용했습니다.
  • 크레딧 충전/사용/환불 시 User row lock을 적용해 동시 요청에서 credit 값이 중복 갱신되지 않도록 했습니다.
  • CreditService의 REQUIRES_NEW 전파를 제거해 상위 트랜잭션과 함께 롤백되도록 정리했습니다.
  • Toss client 설정 검증, timeout, RestClient 재사용을 추가했습니다.
  • 결제 승인/분석 크레딧 사용 동시성 테스트를 추가했습니다.

📸 작업 화면 스크린샷

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

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

🚨 관련 이슈 번호 [#62 #70 ]

Summary by CodeRabbit

  • New Features

    • Added refund support for user credits.
  • Bug Fixes

    • Prevented null dereference and removed nullable amount where applicable.
    • Reduced duplicate charges under concurrency via row-level locking and safer transaction handling.
    • Added DTO validation to reject invalid payment confirmation requests.
  • Improvements

    • Validates payment provider config at startup, caches the payment client, and trims verbose logs.
    • Tightened DB constraints to reduce nullability.
  • Tests

    • Added concurrency and credit-usage tests for analysis and payment flows.

Review Change Stack

@whc9999 whc9999 self-assigned this May 22, 2026
@whc9999 whc9999 added the fix label May 22, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 26acbd70-035e-46ff-a4a5-72020a52b7a9

📥 Commits

Reviewing files that changed from the base of the PR and between 95dcf68 and d02df8c.

📒 Files selected for processing (1)
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/TossPaymentClient.java

📝 Walkthrough

Walkthrough

Harden payment and credit flows: add DTO validation and non-nullability, introduce repository row-locking, adjust transactional behavior in CreditService, cache/configure Toss RestClient at startup, and add concurrency tests for payment confirmation and analysis credit usage.

Changes

Payment & Credit Concurrency Safety

Layer / File(s) Summary
Data contracts and nullability constraints
src/main/java/com/jobdri/jobdri_api/domain/payment/dto/response/PaymentPrepareResponse.java, src/main/java/com/jobdri/jobdri_api/domain/payment/dto/toss/TossPaymentConfirmRequest.java, src/main/java/com/jobdri/jobdri_api/domain/payment/dto/toss/TossPaymentConfirmResponse.java, src/main/java/com/jobdri/jobdri_api/domain/payment/entity/Payment.java
PaymentPrepareResponse asserts non-null user; TossPaymentConfirmRequest adds @NotBlank/@Positive validation; TossPaymentConfirmResponse.totalAmount becomes primitive int; Payment JPA columns orderId and planCode set nullable=false.
Row locking repository methods
src/main/java/com/jobdri/jobdri_api/domain/payment/repository/PaymentRepository.java, src/main/java/com/jobdri/jobdri_api/domain/user/repository/UserRepository.java
Added findByOrderIdForUpdate() and findByIdForUpdate() with @Lock(LockModeType.PESSIMISTIC_WRITE) and supporting imports/queries.
Payment service initialization and confirmation locking
src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentService.java
Added @PostConstruct validateConfig() checking toss client-key; confirm() now uses findByOrderIdForUpdate(); removed explicit null-check for response.totalAmount().
Credit service amount validation and transaction isolation
src/main/java/com/jobdri/jobdri_api/domain/payment/service/CreditService.java
charge() and use() no longer use REQUIRES_NEW, both validate positive amounts; added refund() and validatePositiveAmount(); getManagedUser() uses findByIdForUpdate().
Toss client initialization and error handling
src/main/java/com/jobdri/jobdri_api/domain/payment/service/TossPaymentClient.java
@PostConstruct init() builds/caches RestClient with timeouts and validates secret-key; confirm() uses cached client and improved logging/error truncation via truncate() helper.
Analysis service credit behavior and concurrency tests
src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java
Tests assert credit decrement and persisted USE transaction on success, verify rollback on LLM failure, and add concurrency stress test ensuring single success with 1 credit. Includes analyzeSafely(), runConcurrently(), and Result.
Payment service concurrency and persistence tests
src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java
prepare() asserts persisted Payment status PENDING; confirm() uses generated paymentKey; new parallel confirmation test ensures only one successful confirmation and one CHARGE transaction.
Test configuration for Toss Payments
src/test/resources/application-test.yaml
Added payment.toss block with client-key, secret-key, and base-url for tests.

Sequence Diagram

sequenceDiagram
  participant Client
  participant PaymentService
  participant PaymentRepository
  participant TossPaymentClient
  participant CreditService
  participant UserRepository
  participant CreditTransactionRepo

  Client->>PaymentService: confirm(orderId, paymentKey, amount)
  PaymentService->>PaymentRepository: findByOrderIdForUpdate(orderId)
  PaymentRepository-->>PaymentService: Payment (locked)
  PaymentService->>TossPaymentClient: confirm(request)
  TossPaymentClient-->>PaymentService: TossPaymentConfirmResponse
  PaymentService->>CreditService: charge(user, amount, desc, refId)
  CreditService->>UserRepository: findByIdForUpdate(userId)
  UserRepository-->>CreditService: User (locked)
  CreditService->>CreditService: validatePositiveAmount(amount)
  CreditService->>CreditTransactionRepo: save(CHARGE)
  CreditTransactionRepo-->>CreditService: persisted
  PaymentService-->>Client: confirmationResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

♻️ refactor

"I nibble at race conditions with care and a hop,
Locks in my pouch, I make the credits stop-flop.
One charge takes the prize,
The other yields, wise—
Logs and tests keep the ledger from a flop."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main focus of the PR: strengthening concurrency handling for payment and credit management with a [Fix] tag.
Description check ✅ Passed The PR description follows the template structure and includes key sections: purpose selection (code improvement + code fix), detailed technical explanation of changes, local testing confirmation, branch verification, label selection, and related issue numbers (#62, #70).
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/#62-payment

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

Copy link
Copy Markdown

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

🧹 Nitpick comments (3)
src/test/resources/application-test.yaml (1)

80-84: ⚡ Quick win

Avoid using the live Toss base URL in the test profile

src/test/resources/application-test.yaml configures payment.toss.base-url: https://api.tosspayments.com. While PaymentServiceTest mocks TossPaymentClient (@MockBean), other @SpringBootTest coverage (e.g., JobdriApiApplicationTests) still boots the real TossPaymentClient bean during context load—so keeping a live base URL risks unintended outbound calls and ties test config to external environment.

payment:
  toss:
    client-key: test-toss-client-key
    secret-key: test-toss-secret-key
    base-url: https://api.tosspayments.com

Prefer a dummy/local stub base URL for test (so the real client, if ever instantiated, never points to the production Toss endpoint).

🤖 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/resources/application-test.yaml` around lines 80 - 84, The test
profile should not point to the real Toss endpoint; update
src/test/resources/application-test.yaml so payment.toss.base-url uses a
local/dummy URL (e.g., http://localhost:PORT or http://127.0.0.1) instead of
https://api.tosspayments.com so any real TossPaymentClient (or tests that boot
the full context like JobdriApiApplicationTests) cannot make outbound calls;
keep the test keys (payment.toss.client-key and payment.toss.secret-key) as-is
and ensure tests that mock TossPaymentClient (e.g., PaymentServiceTest with
`@MockBean`) still operate normally.
src/main/java/com/jobdri/jobdri_api/domain/payment/service/CreditService.java (1)

43-50: 💤 Low value

Redundant refund when transaction rolls back.

Since use() and refund() now participate in the parent transaction (no REQUIRES_NEW), when AnalysisService.analyze() catches a RuntimeException, calls refund(), and rethrows, the entire transaction rolls back—including both the USE and REFUND operations. The explicit refund() call becomes a no-op.

This isn't a bug (the final state is correct), but the compensation pattern is now redundant. Consider either:

  1. Removing the explicit refund() call in callers and relying on rollback, or
  2. Documenting that refund() is for non-rollback scenarios (e.g., manual admin actions)
🤖 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/jobdri/jobdri_api/domain/payment/service/CreditService.java`
around lines 43 - 50, The refund() method in CreditService now participates in
the caller's transaction, so explicit compensation calls (e.g., calls to
CreditService.refund() from AnalysisService.analyze() after catching a
RuntimeException) are redundant because a rollback will undo the use() and
refund() together; either remove those explicit refund() calls from callers like
AnalysisService.analyze() and rely on transactional rollback, or update the
CreditService.refund() javadoc and any developer docs to state that refund() is
intended only for out-of-transaction/manual/admin compensations (non-rollback
scenarios) so callers understand when to invoke it.
src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java (1)

345-349: 💤 Low value

Fragile assumption about initial credit value.

saveUserWithCredit assumes User.signup() initializes credit to 1, then adds credit - 1. If the initial credit value in User.signup() changes, this helper will silently produce incorrect credit values.

Consider setting the credit directly or documenting the assumption:

 private User saveUserWithCredit(String email, int credit) {
     User user = User.signup("테스트 사용자", email, "encoded-password");
-    user.increaseCredit(credit - 1);
+    // User.signup() initializes credit to 1, so add (credit - 1) to reach target
+    int initialCredit = user.getCredit();
+    user.increaseCredit(credit - initialCredit);
     return userRepository.save(user);
 }
🤖 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/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java`
around lines 345 - 349, The helper saveUserWithCredit assumes User.signup()
starts with 1 credit; instead compute the needed delta from the actual initial
value and adjust accordingly: after creating the user with User.signup(...) call
user.getCredit() (or equivalent accessor) to get currentCredit, compute delta =
credit - currentCredit and call user.increaseCredit(delta) (or set the credit
explicitly via a setter/constructor if available) before saving; update
references to saveUserWithCredit, User.signup, user.getCredit, and
user.increaseCredit in the change.
🤖 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/jobdri/jobdri_api/domain/payment/service/TossPaymentClient.java`:
- Around line 62-71: In TossPaymentClient's exception handlers for
HttpStatusCodeException and RestClientException, stop including
e.getResponseBodyAsString() and e.getMessage() in the GeneralException thrown to
clients; instead throw a generic PAYMENT_CONFIRM_FAILED message, and log the
full exception details internally (e.g., using the class logger) after
sanitizing/truncating sensitive content for diagnostics. Update the catch blocks
around the HTTP call (the HttpStatusCodeException and RestClientException
handlers) to call logger.error(...) with the sanitized/truncated
response/message and stack trace, then throw a user-facing GeneralException with
a non-sensitive description.

---

Nitpick comments:
In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/service/CreditService.java`:
- Around line 43-50: The refund() method in CreditService now participates in
the caller's transaction, so explicit compensation calls (e.g., calls to
CreditService.refund() from AnalysisService.analyze() after catching a
RuntimeException) are redundant because a rollback will undo the use() and
refund() together; either remove those explicit refund() calls from callers like
AnalysisService.analyze() and rely on transactional rollback, or update the
CreditService.refund() javadoc and any developer docs to state that refund() is
intended only for out-of-transaction/manual/admin compensations (non-rollback
scenarios) so callers understand when to invoke it.

In
`@src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java`:
- Around line 345-349: The helper saveUserWithCredit assumes User.signup()
starts with 1 credit; instead compute the needed delta from the actual initial
value and adjust accordingly: after creating the user with User.signup(...) call
user.getCredit() (or equivalent accessor) to get currentCredit, compute delta =
credit - currentCredit and call user.increaseCredit(delta) (or set the credit
explicitly via a setter/constructor if available) before saving; update
references to saveUserWithCredit, User.signup, user.getCredit, and
user.increaseCredit in the change.

In `@src/test/resources/application-test.yaml`:
- Around line 80-84: The test profile should not point to the real Toss
endpoint; update src/test/resources/application-test.yaml so
payment.toss.base-url uses a local/dummy URL (e.g., http://localhost:PORT or
http://127.0.0.1) instead of https://api.tosspayments.com so any real
TossPaymentClient (or tests that boot the full context like
JobdriApiApplicationTests) cannot make outbound calls; keep the test keys
(payment.toss.client-key and payment.toss.secret-key) as-is and ensure tests
that mock TossPaymentClient (e.g., PaymentServiceTest with `@MockBean`) still
operate normally.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 53e6a5d4-6aee-42ad-9482-d64e17c932b6

📥 Commits

Reviewing files that changed from the base of the PR and between 34475f1 and 16a565c.

📒 Files selected for processing (12)
  • src/main/java/com/jobdri/jobdri_api/domain/payment/dto/response/PaymentPrepareResponse.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/dto/toss/TossPaymentConfirmRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/dto/toss/TossPaymentConfirmResponse.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/entity/Payment.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/repository/PaymentRepository.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/CreditService.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentService.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/TossPaymentClient.java
  • src/main/java/com/jobdri/jobdri_api/domain/user/repository/UserRepository.java
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java
  • src/test/resources/application-test.yaml

Copy link
Copy Markdown

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

🤖 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/jobdri/jobdri_api/domain/payment/service/TossPaymentClient.java`:
- Around line 67-72: In TossPaymentClient's catch blocks for
HttpStatusCodeException and RestClientException, fix the SLF4J
placeholder/argument mismatch by logging structured fields and the throwable
separately: adjust the existing log.warn calls (the one using e.getStatusCode()
and truncate(e.getResponseBodyAsString()) and the one using e.getMessage()) so
each log.warn only supplies arguments that match its {} placeholders (remove the
throwable from that varargs list), and then emit the stack trace with a separate
log.warn or log.warn("Toss payment confirm failed", e) call; reference the
existing catch blocks and the truncate(...) usage to locate where to change.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 345a13b1-7c47-4e19-82ae-b59f2f47e591

📥 Commits

Reviewing files that changed from the base of the PR and between 16a565c and 95dcf68.

📒 Files selected for processing (1)
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/TossPaymentClient.java

@whc9999 whc9999 merged commit 3f25dfe into main May 22, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant