Skip to content

#164 feat payment refund api#172

Closed
git-mesome wants to merge 13 commits intodevelopfrom
#164-feat-payment-refund-api
Closed

#164 feat payment refund api#172
git-mesome wants to merge 13 commits intodevelopfrom
#164-feat-payment-refund-api

Conversation

@git-mesome
Copy link
Copy Markdown
Contributor

@git-mesome git-mesome commented Feb 17, 2026

🛠️ 설명 (Description)

결제 환불 기능을 구현하고, 토스페이먼츠 연동 시 외부 서비스 통신 장애 및 과부하에 대비하기 위한 복원력 패턴(서킷 브레이커, 벌크헤드)을 적용했습니다.

주요 작업 내용은 다음과 같습니다:

  • 결제 게이트웨이에 /payments/cancel API 엔드포인트 추가
  • PaymentProvider 인터페이스에 cancel 메서드를 정의하고, Toss 및 Stub 구현체에 환불 로직 구현
  • ProviderRouter를 통한 환불 요청 라우팅 로직 추가
  • 토스페이먼츠 결제 승인(confirm) 및 환불(cancel) API 연동
  • 토스페이먼츠 연동 시 Resilience4j (서킷 브레이커, 벌크헤드) 적용
  • CI 환경에서 Testcontainers 사용 시 발생할 수 있는 권한 문제 해결을 위한 워크플로우 수정 및 진단 스크립트 추가
  • 관련 컨트롤러, 라우터, 프로바이더에 대한 단위/통합 테스트 코드 추가

📄 설계 문서 (Design Document)

N/A

✅ 테스트 계획 (Test Plan)

  • 유닛 테스트:
    • PaymentGatewayControllerTest: /payments/confirm/payments/cancel API 엔드포인트의 동작을 검증합니다. Mocking을 통해 ProviderRouter의 응답을 시뮬레이션합니다.
    • ProviderRouterTest: confirmcancel 요청이 올바른 PaymentProvider로 라우팅되는지, 지원하지 않는 프로바이더 요청 시 예외가 발생하는지 검증합니다.
    • StubPaymentProviderTest: Stub Payment Provider의 supports, confirm, cancel 메서드가 예상대로 동작하는지 검증합니다.
    • TossPaymentProviderTest: MockWebServer를 사용하여 토스페이먼츠 API의 성공/실패 응답을 모킹하여 confirmcancel 로직을 검증합니다. 특히 paymentKey 저장 및 조회 로직을 확인합니다.
  • 통합 테스트:
    • TossPaymentProviderResilienceTest: MockWebServer를 사용하여 토스페이먼츠 API의 다양한 네트워크 오류 (타임아웃, 연결 끊김, 무응답) 및 응답 지연을 시뮬레이션하여 서킷 브레이커, 벌크헤드 등의 Resilience4j 패턴이 올바르게 동작하고 fallback 응답을 반환하는지 검증합니다.
  • 테스트 커버리지:
    • 신규 추가된 PaymentGatewayController/payments/cancel 엔드포인트, ProviderRoutercancel 라우팅, TossPaymentProvidercancel 로직 및 Resilience4j 적용 로직에 대한 테스트 커버리지를 확보했습니다.
    • 모든 테스트가 성공적으로 통과함을 확인했습니다.

📝 변경 사항 요약 (Summary)

  • 결제 환불 API 추가: payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/api/PaymentGatewayController.javaPOST /api/v1/gateway/payments/cancel 엔드포인트를 추가하고, CancelRequest, CancelResponse DTO를 정의했습니다.
  • PaymentProvider 인터페이스 확장: payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/PaymentProvider.javaCancelResponse cancel(CancelRequest request) 메서드를 추가했습니다.
  • ProviderRouter 환불 라우팅 추가: payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/ProviderRouter.javacancel 요청을 해당 PaymentProvider로 라우팅하는 로직을 추가했습니다.
  • TossPaymentProvider 환불 및 복원력 구현:
    • payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/TossPaymentProvider.javacancel 메서드를 구현하여 토스페이먼츠 환불 API를 연동했습니다.
    • confirmcancel 메서드에 @CircuitBreaker@Bulkhead 애너테이션을 적용하여 외부 PG사 통신에 대한 복원력을 확보하고, 다양한 실패 상황에 대한 fallback 메서드를 정의했습니다.
    • RestClient 생성 시 connectTimeoutreadTimeout을 설정하여 통신 타임아웃을 관리합니다.
    • TossPaymentProviderprod 프로파일 외에 test 프로파일에서도 활성화되도록 @Profile 설정을 변경했습니다.
    • confirm 성공 시 paymentKeyConcurrentHashMap에 저장하고, cancel 시 이를 사용하여 환불 요청을 처리하도록 paymentKeyStore를 추가했습니다.
  • StubPaymentProvider 환불 구현: payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/StubPaymentProvider.javacancel 메서드를 구현하여 스텁 응답을 제공합니다.
  • CI/CD 워크플로우 개선:
    • .github/workflows/reusable-build.yml에서 GitHub Actions Runner를 ubuntu-latest에서 ubuntu-22.04로 변경했습니다.
    • Testcontainers 사용 시 발생할 수 있는 docker.sock 권한 문제를 해결하기 위해 Docker 진단 스크립트와 sudo chmod 666 /var/run/docker.sock 명령을 추가했습니다.
    • TESTCONTAINERS_RYUK_DISABLED: true 환경 변수를 빌드 및 테스트 스텝에 추가하여 Testcontainers의 리소스 정리 컨테이너(Ryuk) 문제를 회피했습니다.
    • 테스트 리포트 업로드 조건을 if: failure()에서 if: always()로 변경하여 항상 리포트를 업로드하도록 했습니다.
  • 의존성 추가: payment-gateway/build.gradleorg.springframework.boot:spring-boot-starter-aop, io.github.resilience4j:resilience4j-spring-boot3, com.squareup.okhttp3:mockwebserver를 추가했습니다.
  • 테스트 코드 추가: 신규 기능 및 복원력 패턴 검증을 위한 PaymentGatewayControllerTest, ProviderRouterTest, StubPaymentProviderTest, TossPaymentProviderTest, TossPaymentProviderResilienceTest를 추가했습니다.
  • payment-gateway/src/test/java/wisoft/nextframe/paymentgateway/PaymentGatewayApplicationTests.java@ActiveProfiles("dev")를 추가했습니다.

🔗 관련 이슈 (Related Issues)

☑️ 체크리스트 (Checklist)

  • 코드가 프로젝트 코딩 컨벤션을 따릅니다.
  • 테스트 코드가 작성되었고, 통과했습니다.
  • 변경 사항에 대한 문서화가 완료되었습니다.
  • 필요한 경우, 다른 팀원에게 리뷰를 요청했습니다.

👀 리뷰어를 위한 참고 사항 (Notes for Reviewers)

  • 토스 환불 기능 구현과 함께 Resilience4j(서킷 브레이커, 벌크헤드)를 적용했습니다. 특히 TossPaymentProviderResilienceTest를 통해 다양한 실패 상황에서 Resilience4j가 의도대로 동작하는지 확인했습니다.
  • TossPaymentProvider에서 결제 승인 시 paymentKey를 메모리(ConcurrentHashMap)에 저장하고 환불 시 이를 사용하는 로직이 추가되었습니다. 이 paymentKeyStore는 현재 인메모리 방식이며, 애플리케이션 재시작 시 데이터가 손실됩니다. 실제 운영 환경에서는 이 paymentKey를 영속적인 저장소(예: DB)에 저장하고 관리하는 방안을 고려해야 합니다. (이 PR에서는 기능 구현 및 복원력 패턴 적용에 초점을 맞췄습니다.)
  • CI 워크플로우에 Testcontainers 관련 진단 스크립트와 docker.sock 권한 조정, 그리고 TESTCONTAINERS_RYUK_DISABLED: true 환경 변수 설정이 포함되었습니다. 이 변경 사항이 다른 모듈의 CI 동작에 영향을 줄 수 있는지 확인 부탁드립니다.

➕ 추가 정보 (Additional Information)

N/A

- payment-gateway에 환불 엔드포인트 및 Toss/Stub 프로바이더 환불 기능 추가
- RefundService 트랜잭션 분리 (RefundTransactionService 신규)
- RefundEntity/RefundMapper를 domain에서 infra 레이어로 이동
- RefundRepository 포트 및 어댑터 구현
- RefundController REST API 추가
- PaymentGateway 출력 포트에 환불 메서드 추가
- 테스트 패키지 구조 정리 및 신규 테스트 추가
PG 응답 지연 시 worker 스레드 무한 대기로 인한 계단식 장애를 방지하기 위해
타임아웃, 서킷 브레이커, 벌크헤드를 적용한다.

- RestClient에 SimpleClientHttpRequestFactory 적용 (connectTimeout=2s, readTimeout=3s)
- @CIRCUITBREAKER로 연속 실패 시 빠른 실패 처리
- @bulkhead로 동시 요청 수 제한
- fallback 메서드로 PG_TEMPORARY_UNAVAILABLE, PG_NETWORK_ERROR 도메인 실패 반환
- 복원력 패턴 검증을 위한 SpringBootTest 통합 테스트 추가
@git-mesome git-mesome requested a review from Jinpyo-An February 17, 2026 06:11
@git-mesome git-mesome self-assigned this Feb 17, 2026
Copilot AI review requested due to automatic review settings February 17, 2026 06:11
@git-mesome git-mesome added status: review 코드 리뷰 중 type: feature 새로운 기능 요청 또는 구현 labels Feb 17, 2026
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @git-mesome, 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!

This pull request introduces a comprehensive payment refund capability to the payment gateway service. It integrates advanced fault tolerance mechanisms using Resilience4j for external payment provider interactions, and significantly refactors the internal refund processing logic to ensure transactional integrity and idempotency. The changes also include necessary database schema updates and a thorough suite of tests to validate the new features and system resilience.

Highlights

  • Payment Refund API: A new /payments/cancel endpoint has been added to the Payment Gateway Controller, enabling the processing of payment refund requests.
  • Resilience4j Integration: Circuit Breaker and Bulkhead patterns from Resilience4j have been applied to the TossPaymentProvider's confirm operation, enhancing fault tolerance and system stability.
  • Refactored Refund Service Logic: The RefundService has been significantly refactored to utilize a new RefundTransactionService, which manages refund preparation and completion in a transactional manner, including idempotency checks.
  • Database Schema Updates: New refunds and schedules tables have been introduced, and the reservations table has been modified to include schedule_id, supporting the new refund functionality and reservation details.
  • Comprehensive Testing: Extensive new unit and integration tests have been added for the PaymentGatewayController, ProviderRouter, StubPaymentProvider, TossPaymentProvider (including resilience tests), RefundTransactionService, PaymentIssuer, and Refund domain to ensure robustness.
Changelog
  • payment-gateway/build.gradle
    • Added spring-boot-starter-aop and resilience4j-spring-boot3 dependencies for AOP and resilience patterns.
    • Included mockwebserver for robust testing of external HTTP calls.
  • payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/api/PaymentGatewayController.java
    • Added a new POST endpoint /payments/cancel to handle payment cancellation requests.
    • Defined CancelRequest and CancelResponse records for refund data transfer objects.
  • payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/PaymentProvider.java
    • Extended the PaymentProvider interface with a new cancel method to support refund operations.
  • payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/ProviderRouter.java
    • Implemented a cancel method to route refund requests to the appropriate payment provider.
    • Refactored provider lookup logic into a dedicated private findProvider method for better encapsulation.
  • payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/StubPaymentProvider.java
    • Implemented the cancel method to provide a stubbed refund response for testing and development environments.
  • payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/TossPaymentProvider.java
    • Added @Slf4j annotation for logging.
    • Introduced a ConcurrentHashMap to store paymentKey associated with orderId for refund processing.
    • Configured RestClient with connectTimeout and readTimeout for improved network handling.
    • Applied @CircuitBreaker and @Bulkhead annotations to the confirm method, along with fallback methods, to implement resilience patterns.
    • Implemented the cancel method to interact with the Toss Payments API for refund processing.
  • payment-gateway/src/test/java/wisoft/nextframe/paymentgateway/PaymentGatewayApplicationTests.java
    • Activated the dev profile for application tests.
  • payment-gateway/src/test/java/wisoft/nextframe/paymentgateway/api/PaymentGatewayControllerTest.java
    • Added a new test file for PaymentGatewayController to cover both confirm and cancel endpoints.
  • payment-gateway/src/test/java/wisoft/nextframe/paymentgateway/provider/ProviderRouterTest.java
    • Added a new test file for ProviderRouter to verify correct routing of confirm and cancel requests.
  • payment-gateway/src/test/java/wisoft/nextframe/paymentgateway/provider/StubPaymentProviderTest.java
    • Added a new test file for StubPaymentProvider to test its supports, confirm, and cancel methods.
  • payment-gateway/src/test/java/wisoft/nextframe/paymentgateway/provider/TossPaymentProviderResilienceTest.java
    • Added a new test file for TossPaymentProvider specifically focusing on Resilience4j patterns like circuit breaker, bulkhead, and timeouts.
  • payment-gateway/src/test/java/wisoft/nextframe/paymentgateway/provider/TossPaymentProviderTest.java
    • Added a new test file for TossPaymentProvider to test its confirm and cancel logic, including payment key storage.
  • payment/src/main/java/wisoft/nextframe/payment/application/payment/port/output/PaymentGateway.java
    • Added PaymentCancelResult record and cancelPayment method to the interface.
  • payment/src/main/java/wisoft/nextframe/payment/application/payment/port/output/ReservationReader.java
    • Added getPerformanceDateTime method to retrieve performance start times for refund policy evaluation.
  • payment/src/main/java/wisoft/nextframe/payment/application/refund/RefundCancelFailedException.java
    • Added a new custom exception class, RefundCancelFailedException, for handling failures in payment gateway refund requests.
  • payment/src/main/java/wisoft/nextframe/payment/application/refund/RefundService.java
    • Refactored the RefundService to leverage PaymentGateway for external calls and RefundTransactionService for transactional logic.
    • Implemented the core refund logic, including payment gateway cancellation and transactional updates to refund records.
  • payment/src/main/java/wisoft/nextframe/payment/application/refund/RefundTransactionService.java
    • Added a new service, RefundTransactionService, responsible for transactional refund preparation and completion, including idempotency checks.
  • payment/src/main/java/wisoft/nextframe/payment/application/refund/port/output/RefundRepository.java
    • Added a new repository interface, RefundRepository, for managing Refund entities.
  • payment/src/main/java/wisoft/nextframe/payment/domain/payment/PaymentIssuer.java
    • Added @Component annotation to PaymentIssuer for Spring component scanning.
  • payment/src/main/java/wisoft/nextframe/payment/domain/refund/RefundEntity.java
    • Renamed and moved RefundEntity to payment/src/main/java/wisoft/nextframe/payment/infra/refund/RefundEntity.java.
  • payment/src/main/java/wisoft/nextframe/payment/domain/refund/RefundMapper.java
    • Renamed and moved RefundMapper to payment/src/main/java/wisoft/nextframe/payment/infra/refund/RefundMapper.java and added @Component.
  • payment/src/main/java/wisoft/nextframe/payment/infra/DbReservationReader.java
    • Implemented the getPerformanceDateTime method to fetch performance start times from the database.
  • payment/src/main/java/wisoft/nextframe/payment/infra/payment/PaymentMapper.java
    • Injected JpaRefundRepository and RefundMapper to correctly load Refund entities alongside Payment entities.
  • payment/src/main/java/wisoft/nextframe/payment/infra/payment/adaptor/HttpPaymentGatewayAdaptor.java
    • Implemented the cancelPayment method to make HTTP calls to the payment gateway's refund endpoint.
    • Defined CancelRequest and CancelResponse records for data exchange with the payment gateway.
  • payment/src/main/java/wisoft/nextframe/payment/infra/payment/adaptor/StubPaymentGatewayAdaptor.java
    • Implemented the cancelPayment method for the stub payment gateway adaptor.
  • payment/src/main/java/wisoft/nextframe/payment/infra/refund/JpaRefundRepository.java
    • Added a new JPA repository interface, JpaRefundRepository, for RefundEntity persistence.
  • payment/src/main/java/wisoft/nextframe/payment/infra/refund/RefundRepositoryImpl.java
    • Added a new implementation class, RefundRepositoryImpl, for the RefundRepository interface using JpaRefundRepository.
  • payment/src/main/java/wisoft/nextframe/payment/presentation/refund/RefundController.java
    • Added a new REST controller, RefundController, to expose refund functionality via a /payments/{paymentId}/refund endpoint, including comprehensive error handling.
  • payment/src/main/java/wisoft/nextframe/payment/presentation/refund/dto/RefundApprovedData.java
    • Added a new DTO, RefundApprovedData, for representing the response data of an approved refund.
  • payment/src/main/java/wisoft/nextframe/payment/presentation/refund/dto/RefundRequest.java
    • Added a new DTO, RefundRequest, for encapsulating the request body of a refund operation.
  • payment/src/test/java/wisoft/nextframe/payment/application/RefundServiceTest.java
    • Removed this test file as its functionality was moved or refactored.
  • payment/src/test/java/wisoft/nextframe/payment/application/refund/RefundTransactionServiceTest.java
    • Added a new test file for RefundTransactionService to verify its transactional refund preparation and completion logic.
  • payment/src/test/java/wisoft/nextframe/payment/domain/PaymentTest.java
    • Renamed and moved PaymentTest to payment/src/test/java/wisoft/nextframe/payment/domain/payment/PaymentTest.java.
  • payment/src/test/java/wisoft/nextframe/payment/domain/RefundPolicyStatusTest.java
    • Renamed and moved RefundPolicyStatusTest to payment/src/test/java/wisoft/nextframe/payment/domain/refund/RefundPolicyStatusTest.java.
  • payment/src/test/java/wisoft/nextframe/payment/domain/RefundTest.java
    • Renamed and moved RefundTest to payment/src/test/java/wisoft/nextframe/payment/domain/refund/RefundTest.java.
  • payment/src/test/java/wisoft/nextframe/payment/domain/fixture/RefundEntityFixture.java
    • Removed this test file as its functionality was moved or refactored.
  • payment/src/test/java/wisoft/nextframe/payment/domain/payment/PaymentIssuerTest.java
    • Added a new test file for PaymentIssuer to verify its refund issuance logic and policy enforcement.
  • payment/src/test/java/wisoft/nextframe/payment/infra/payment/persistence/PaymentMapperTest.java
    • Updated PaymentMapperTest to mock JpaRefundRepository and RefundMapper for isolated testing.
  • payment/src/test/java/wisoft/nextframe/payment/infra/payment/persistence/RefundMapperTest.java
    • Removed this test file as its functionality was moved or refactored.
  • payment/src/test/java/wisoft/nextframe/payment/infra/refund/RefundEntityFixture.java
    • Updated imports to reflect the new package structure.
  • payment/src/test/java/wisoft/nextframe/payment/infra/refund/RefundMapperTest.java
    • Updated imports to reflect the new package structure.
  • payment/src/test/java/wisoft/nextframe/payment/infra/refund/RefundRepositoryImplTest.java
    • Added a new test file for RefundRepositoryImpl to verify its save and find operations.
  • payment/src/test/java/wisoft/nextframe/payment/repository/PaymentRepositoryImplTest.java
    • Removed this test file as its functionality was moved or refactored.
  • payment/src/test/resources/schema-test.sql
    • Added new refunds and schedules tables to the test database schema.
    • Modified the reservations table to include a schedule_id column.
Activity
  • No specific activity logs were provided in the pull request description.
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 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 counter productive. 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.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

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.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

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 payment cancellation functionality and strengthens fault tolerance using Resilience4j. The clear transaction boundaries and thorough external API integration tests are commendable. However, critical security concerns have been identified, including missing authorization checks (IDOR) that could allow unauthorized users to cancel reservations. Additionally, the use of an unbounded in-memory map for paymentKey storage in the gateway service poses a significant risk of resource exhaustion (memory leak) and Denial of Service, especially in multi-instance environments where it also leads to data loss upon application restart. Further improvements are needed in areas such as type safety, exception handling, and reducing duplicate code. These issues require remediation, particularly implementing proper ownership verification and migrating the in-memory store to a persistent database or a cache with an expiration policy for operational stability.

Comment on lines +33 to +34
@PostMapping("/{paymentId}/refund")
public ResponseEntity<ApiResponse<?>> refund(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

The refund endpoint lacks an authorization check to verify that the authenticated user owns the payment being refunded. An attacker who obtains a valid paymentId (UUID) could potentially trigger a refund for any user's payment, leading to unauthorized cancellation of reservations. While UUIDs are difficult to guess, the absence of an ownership check is a significant security flaw (IDOR).

Comment on lines +33 to +34
@PostMapping("/payments/cancel")
public ResponseEntity<CancelResponse> cancel(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

The cancel endpoint does not perform any authorization checks on the orderId provided in the request body. This allows any caller with access to the gateway service to cancel any payment that has been previously confirmed and stored in the instance's memory. If this service is exposed or accessible from other compromised internal services, it poses a high risk of unauthorized payment cancellations.

Comment on lines +101 to +102
@Override
public CancelResponse cancel(CancelRequest request) {
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

confirm 메서드는 @CircuitBreaker@Bulkhead로 잘 보호되어 있지만, cancel 메서드에는 이러한 장애 내성 패턴이 적용되지 않았습니다. 취소를 포함한 모든 외부 API 호출은 실패할 가능성이 있으므로, 장애 전파를 막기 위해 confirm 메서드와 유사하게 보호하는 것이 좋습니다. cancel 메서드에도 동일한 장애 내성 패턴을 적용하는 것을 권장합니다. (적용 후에는 해당 fallback 메서드들도 추가해야 합니다.)

	@Override
	@CircuitBreaker(name = "tossCancel", fallbackMethod = "cancelFallback")
	@Bulkhead(name = "tossCancel")
	public CancelResponse cancel(CancelRequest request) {

"cancelAmount", request.cancelAmount())
)
.retrieve()
.body(Map.class);
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

Toss API의 JSON 응답을 Map.class로 역직렬화하는 것은 타입에 안전하지 않으며, 응답 구조가 변경되거나 예상치 못한 타입이 포함될 경우 ClassCastException과 같은 런타임 에러를 유발할 수 있습니다. 예상되는 응답 구조에 맞는 전용 record나 클래스를 정의하는 것이 훨씬 안전합니다. 이는 컴파일 시점의 타입 체크를 가능하게 하고 코드 가독성과 유지보수성을 향상시킵니다. 이 내용은 confirm 메서드(73행)에도 동일하게 적용됩니다.

Comment on lines +24 to +26
Refund refund = jpaRefundRepository.findByPaymentId(entity.getId())
.map(refundMapper::toDomain)
.orElse(null);
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

매퍼 내부에서 데이터베이스 호출(jpaRefundRepository.findByPaymentId)을 통해 연관된 Refund 정보를 가져오고 있습니다. 이는 매퍼의 단일 책임 원칙을 위반하는 안티패턴이며, 특히 여러 결제 정보를 매핑할 때 N+1 쿼리 문제를 유발하여 심각한 성능 저하로 이어질 수 있습니다. 데이터 조회는 리포지토리나 서비스 계층에서 처리하고, 완전히 구성된 엔티티를 매퍼에 전달하는 것이 바람직합니다. 예를 들어, PaymentEntityRefundEntity 간에 @OneToOne 관계를 설정하고 PaymentRepository에서 JOIN FETCH를 사용하여 함께 로드하는 방법을 고려해볼 수 있습니다.

Comment on lines 85 to +101
public record ConfirmRequest(String paymentKey, String orderId, int amount) {
}

public record ConfirmResponse(boolean isSuccess, int totalAmount, String errorCode, String errorMessage) {
}
}

public record CancelRequest(String orderId, int cancelAmount, String cancelReason) {
}

public record CancelResponse(
boolean isSuccess,
int cancelAmount,
String transactionKey,
String errorCode,
String errorMessage) {
}
}
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

ConfirmRequest, ConfirmResponse, CancelRequest, CancelResponse와 같은 요청/응답 record들이 payment-gateway 모듈에 있는 것과 중복됩니다. 이는 서비스 간의 강한 결합을 만들고 유지보수 부담을 증가시킵니다. payment-gateway의 API 명세가 변경될 경우, 이 부분을 수동으로 함께 수정해야 하므로 실수가 발생하기 쉽습니다. 두 서비스가 모두 의존하는 공통 라이브러리나 모듈로 이 DTO들을 추출하는 것이 더 나은 접근 방식입니다.

public class TossPaymentProvider implements PaymentProvider {

private final RestClient restClient;
private final ConcurrentHashMap<String, String> paymentKeyStore = new ConcurrentHashMap<>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

The paymentKeyStore is an unbounded ConcurrentHashMap that stores payment keys indefinitely. This design leads to a continuous memory leak, potentially causing an OutOfMemoryError and Denial of Service (DoS). In a multi-instance production environment, this also results in data loss upon application restart, making refund functionality unreliable. This implementation is unsafe for production use and must be replaced with a persistent and distributed storage solution like a database or Redis.

LocalDateTime performanceDateTime = reservationReader.getPerformanceDateTime(payment.getReservationId());

// 4. 환불 발급 (정책 결정 + Refund 생성 + 환불 가능 검증 + payment.assignRefund)
LocalDateTime now = LocalDateTime.now();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

LocalDateTime.now()를 직접 사용하면 메서드의 동작이 시스템 시계에 의존하게 되어 테스트, 특히 환불 정책과 같이 시간에 민감한 로직의 테스트를 복잡하게 만듭니다. java.time.Clock을 주입받아 LocalDateTime.now(clock)을 사용하거나 현재 시간을 메서드 파라미터로 전달하는 것이 더 좋습니다. 이를 통해 테스트에서 시간을 제어하고 결정적인 동작을 보장할 수 있습니다.

Suggested change
LocalDateTime now = LocalDateTime.now();
LocalDateTime now = LocalDateTime.now(clock); // Clock bean 주입 필요

log.info("gateway cancel raw response = {}", raw);

try {
ObjectMapper mapper = new ObjectMapper();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

cancelPayment 메서드가 호출될 때마다 새로운 ObjectMapper 인스턴스가 생성됩니다. ObjectMapper는 스레드에 안전하며 생성 비용이 비교적 높은 객체이므로, 단일 인스턴스를 재사용하는 것이 좋습니다. Spring Boot가 제공하는 기본 ObjectMapper 빈을 주입받거나, private static final 필드로 선언하여 사용할 수 있습니다. 이 내용은 confirmPayment 메서드(41행)에도 동일하게 적용됩니다.

Suggested change
ObjectMapper mapper = new ObjectMapper();
// ObjectMapper는 Bean으로 주입받거나 static final로 선언하여 재사용하는 것이 좋습니다.
ObjectMapper mapper = new ObjectMapper();

Comment on lines +47 to +70
} catch (PaymentNotFoundException e) {
return ResponseEntity.badRequest().body(
ApiResponse.failed("존재하지 않는 결제입니다.")
);
} catch (RefundAlreadyExistsException e) {
return ResponseEntity.badRequest().body(
ApiResponse.failed("이미 환불된 결제입니다.")
);
} catch (NotRefundableException e) {
return ResponseEntity.badRequest().body(
ApiResponse.failed(e.getMessage())
);
} catch (RefundException e) {
return ResponseEntity.badRequest().body(
ApiResponse.failed(e.getMessage())
);
} catch (RefundCancelFailedException e) {
return ResponseEntity.badRequest().body(
ApiResponse.failed("결제사 환불 처리에 실패했습니다. 잠시 후 다시 시도해주세요.")
);
} catch (Exception e) {
log.error("환불 처리 에러", e);
throw e;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

컨트롤러 메서드 내에 여러 예외를 처리하기 위한 catch 블록이 길게 나열되어 있습니다. 이러한 로직은 여러 컨트롤러에 걸쳐 반복될 수 있습니다. 코드의 명확성을 높이고 예외 처리를 중앙에서 관리하기 위해 @ControllerAdvice@ExceptionHandler를 사용하는 것을 고려해보세요. 이를 통해 컨트롤러에서 상용구 코드를 제거하고 비즈니스 로직을 더 부각시킬 수 있습니다.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an end-to-end “payment refund” capability across the payment service and the payment-gateway service, including persistence, domain/service orchestration, and a gateway cancel endpoint/provider support.

Changes:

  • Implement refund orchestration in payment (prepare → PG cancel → complete) and expose POST /api/v1/payments/{paymentId}/refund.
  • Add refund persistence layer (refunds JPA entity/repository/mapper) and wire refund lookups into payment mapping.
  • Extend payment-gateway with cancel APIs/providers and add resilience4j-based protection for Toss confirm plus new tests.

Reviewed changes

Copilot reviewed 44 out of 44 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
payment/src/test/resources/schema-test.sql Adds refunds/schedules test tables and reservations.schedule_id for refund-related reads
payment/src/test/java/wisoft/nextframe/payment/repository/PaymentRepositoryImplTest.java Removes legacy unit test
payment/src/test/java/wisoft/nextframe/payment/infra/refund/RefundRepositoryImplTest.java Adds unit tests for refund repository adapter
payment/src/test/java/wisoft/nextframe/payment/infra/refund/RefundMapperTest.java Updates imports to new infra refund mapper/entity
payment/src/test/java/wisoft/nextframe/payment/infra/refund/RefundEntityFixture.java Updates fixture to use infra refund entity
payment/src/test/java/wisoft/nextframe/payment/infra/payment/persistence/RefundMapperTest.java Removes duplicate/old refund mapper test
payment/src/test/java/wisoft/nextframe/payment/infra/payment/persistence/PaymentMapperTest.java Updates PaymentMapper construction due to new refund dependencies
payment/src/test/java/wisoft/nextframe/payment/domain/refund/RefundTest.java Fixes package + expands refund state transition tests
payment/src/test/java/wisoft/nextframe/payment/domain/refund/RefundPolicyStatusTest.java Fixes package and tidies tests
payment/src/test/java/wisoft/nextframe/payment/domain/payment/PaymentTest.java Fixes package/imports for payment domain tests
payment/src/test/java/wisoft/nextframe/payment/domain/payment/PaymentIssuerTest.java Adds domain service tests for refund issuing
payment/src/test/java/wisoft/nextframe/payment/domain/fixture/RefundEntityFixture.java Removes old fixture in domain package
payment/src/test/java/wisoft/nextframe/payment/application/refund/RefundTransactionServiceTest.java Adds unit tests for refund transaction orchestration
payment/src/test/java/wisoft/nextframe/payment/application/RefundServiceTest.java Removes obsolete test (logic moved/refactored)
payment/src/main/java/wisoft/nextframe/payment/presentation/refund/dto/RefundRequest.java Adds refund request DTO
payment/src/main/java/wisoft/nextframe/payment/presentation/refund/dto/RefundApprovedData.java Adds refund response DTO
payment/src/main/java/wisoft/nextframe/payment/presentation/refund/RefundController.java Adds refund HTTP endpoint/controller
payment/src/main/java/wisoft/nextframe/payment/infra/refund/RefundRepositoryImpl.java Implements refund persistence port (adapter)
payment/src/main/java/wisoft/nextframe/payment/infra/refund/RefundMapper.java Moves mapper to infra + makes it a Spring component
payment/src/main/java/wisoft/nextframe/payment/infra/refund/RefundEntity.java Moves entity to infra package
payment/src/main/java/wisoft/nextframe/payment/infra/refund/JpaRefundRepository.java Adds Spring Data repository for refunds
payment/src/main/java/wisoft/nextframe/payment/infra/payment/adaptor/StubPaymentGatewayAdaptor.java Adds stub PG cancel support
payment/src/main/java/wisoft/nextframe/payment/infra/payment/adaptor/HttpPaymentGatewayAdaptor.java Adds HTTP PG cancel call + response parsing
payment/src/main/java/wisoft/nextframe/payment/infra/payment/PaymentMapper.java Loads refund data when mapping PaymentEntity → Payment
payment/src/main/java/wisoft/nextframe/payment/infra/DbReservationReader.java Adds schedule datetime lookup for refund policy evaluation
payment/src/main/java/wisoft/nextframe/payment/domain/payment/PaymentIssuer.java Makes PaymentIssuer a Spring component
payment/src/main/java/wisoft/nextframe/payment/application/refund/port/output/RefundRepository.java Defines refund persistence port
payment/src/main/java/wisoft/nextframe/payment/application/refund/RefundTransactionService.java Implements transactional prepare/complete steps
payment/src/main/java/wisoft/nextframe/payment/application/refund/RefundService.java Orchestrates refund: prepare → PG cancel → complete
payment/src/main/java/wisoft/nextframe/payment/application/refund/RefundCancelFailedException.java Adds exception for PG cancel failure
payment/src/main/java/wisoft/nextframe/payment/application/payment/port/output/ReservationReader.java Adds performance datetime read API
payment/src/main/java/wisoft/nextframe/payment/application/payment/port/output/PaymentGateway.java Extends gateway port with cancel capability
payment-gateway/src/test/java/wisoft/nextframe/paymentgateway/provider/TossPaymentProviderTest.java Adds provider unit tests for confirm/cancel
payment-gateway/src/test/java/wisoft/nextframe/paymentgateway/provider/TossPaymentProviderResilienceTest.java Adds resilience-focused integration test for confirm
payment-gateway/src/test/java/wisoft/nextframe/paymentgateway/provider/StubPaymentProviderTest.java Adds stub provider unit tests
payment-gateway/src/test/java/wisoft/nextframe/paymentgateway/provider/ProviderRouterTest.java Adds router unit tests for confirm/cancel routing
payment-gateway/src/test/java/wisoft/nextframe/paymentgateway/api/PaymentGatewayControllerTest.java Adds controller tests for confirm/cancel endpoints
payment-gateway/src/test/java/wisoft/nextframe/paymentgateway/PaymentGatewayApplicationTests.java Sets test profile to dev
payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/TossPaymentProvider.java Adds timeouts, confirm resilience, and cancel support
payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/StubPaymentProvider.java Adds cancel support
payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/ProviderRouter.java Adds cancel routing + refactors provider lookup
payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/PaymentProvider.java Adds cancel API to provider interface
payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/api/PaymentGatewayController.java Adds /payments/cancel endpoint + DTOs
payment-gateway/build.gradle Adds AOP + resilience4j + mockwebserver deps

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 23 to +26
public Payment toDomain(PaymentEntity entity) {
Refund refund = jpaRefundRepository.findByPaymentId(entity.getId())
.map(refundMapper::toDomain)
.orElse(null);
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

toDomain now performs an additional DB query (findByPaymentId) as part of mapping. This couples the mapper to persistence and can introduce N+1 queries / extra round-trips on save and reads. Consider fetching refunds in the repository layer (join/projection) or populating refund separately outside the mapper.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to 36
// 2. PG 환불 요청 (트랜잭션 없이 외부 호출)
PaymentGateway.PaymentCancelResult cancelResult =
paymentGateway.cancelPayment(orderId, cancelAmount, reason);

Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

PG cancel is executed before any refund row is persisted. If two refund requests race, both can pass prepareRefund() (no DB record yet) and both may call the PG, causing duplicate cancellations. Persist a REQUESTED/PENDING refund inside prepareRefund (with payment_id UNIQUE) or otherwise lock/idempotency-key the flow before calling the PG.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +39
@Transactional(readOnly = true)
public RefundPrepareResult prepareRefund(UUID paymentId) {
// 1. Payment 조회
Payment payment = paymentRepository.findById(PaymentId.of(paymentId))
.orElseThrow(PaymentNotFoundException::new);
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

prepareRefund() is marked @Transactional(readOnly = true) but it creates a new Refund (and issueRefund mutates Payment). If the intent is to reserve/refuse concurrent refunds, this should likely be a write transaction that persists a REQUESTED refund (or acquires a lock) before returning.

Copilot uses AI. Check for mistakes.
public LocalDateTime getPerformanceDateTime(ReservationId reservationId) {
return jdbcTemplate.queryForObject(
"SELECT s.performance_datetime FROM reservations r "
+ "JOIN schedules s ON r.schedule_id = s.id "
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

This queryForObject + inner join will throw EmptyResultDataAccessException if schedule_id is null or the schedule row is missing. Consider handling that case explicitly (left join + null check) or translating it into a domain-specific exception so refund failures are deterministic.

Suggested change
+ "JOIN schedules s ON r.schedule_id = s.id "
+ "LEFT JOIN schedules s ON r.schedule_id = s.id "

Copilot uses AI. Check for mistakes.
Comment on lines 30 to 32
private final RestClient restClient;
private final ConcurrentHashMap<String, String> paymentKeyStore = new ConcurrentHashMap<>();

Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

paymentKeyStore is in-memory only. In a multi-instance deployment or after a restart, the gateway won’t find the paymentKey for an orderId, and refunds will fail with PAYMENT_KEY_NOT_FOUND. Persist this mapping (DB/Redis) or redesign cancel so it doesn’t depend on per-instance memory (e.g., store paymentKey in the payment service DB and pass it through).

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +84
// minimumNumberOfCalls=2, failureRateThreshold=50
// 연결 끊김 2회 → ResourceAccessException → 실패율 100% → CB OPEN
for (int i = 0; i < 2; i++) {
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The test assumes circuit breaker settings like minimumNumberOfCalls=2 / slidingWindowSize=4, but there’s no resilience4j configuration in the module (no application*.yml / Java config found). With defaults, the circuit breaker likely won’t OPEN after 2 failures. Add resilience4j properties for tossConfirm in the test via @DynamicPropertySource/test config, or adjust the expectations to match actual configuration.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +50
id uuid PRIMARY KEY,
payment_id uuid NOT NULL,
refund_amount integer NOT NULL,
status varchar NOT NULL,
reason varchar,
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

findByPaymentId(...) returns an Optional, which assumes one refund per payment. Without a UNIQUE constraint (and ideally an FK), duplicate rows can be inserted and later cause IncorrectResultSizeDataAccessException. Add UNIQUE (payment_id) (and consider REFERENCES payments(id) + an index).

Copilot uses AI. Check for mistakes.
Comment on lines 62 to 66
create table reservations
(
id uuid primary key
id uuid primary key,
schedule_id uuid
);
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

reservations.schedule_id is nullable and has no FK, but refund logic does an inner join on schedule_id to fetch the performance time. Either enforce schedule_id NOT NULL + REFERENCES schedules(id), or update the query/handling to support missing schedules.

Copilot uses AI. Check for mistakes.
- RestClient에 connectTimeout(3s), readTimeout(5s) 설정
- Resilience4j @CIRCUITBREAKER(paymentGateway) 적용 (confirm/cancel)
- CB fallback에서 OPEN/일반 실패 구분하여 예외 분리
- 예외 클래스를 application 레이어에 배치 (DDD 의존 방향 준수)
- GlobalExceptionHandler에 503/502 응답 매핑 추가
- MockWebServer 단위 테스트 및 CB 통합 테스트 추가
QueryDSL 기반 키워드 검색, 다중 정렬, 페이지네이션을 지원하며
향후 ELK 스택 도입 시 Adapter 교체만으로 전환 가능하도록 설계
PG 승인 후 티켓 발급을 동기 호출하고, 실패 시 PG 취소 후 FAILED 처리.
티켓 발급 Outbox 인프라(서비스, 리포지토리, 엔티티, 스케줄러, 어드민 컨트롤러) 삭제.
@git-mesome git-mesome added the type: improvement 기존 기능 개선 label Feb 17, 2026
@git-mesome git-mesome closed this Feb 18, 2026
@git-mesome git-mesome deleted the #164-feat-payment-refund-api branch February 18, 2026 07:54
@git-mesome git-mesome added status: done 완료됨 and removed status: review 코드 리뷰 중 labels Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: done 완료됨 type: feature 새로운 기능 요청 또는 구현 type: improvement 기존 기능 개선

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] 환불 API 생성

2 participants