Conversation
- 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 통합 테스트 추가
Summary of ChangesHello @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
Changelog
Activity
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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.
| @PostMapping("/{paymentId}/refund") | ||
| public ResponseEntity<ApiResponse<?>> refund( |
There was a problem hiding this comment.
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).
| @PostMapping("/payments/cancel") | ||
| public ResponseEntity<CancelResponse> cancel( |
There was a problem hiding this comment.
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.
| @Override | ||
| public CancelResponse cancel(CancelRequest request) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
| Refund refund = jpaRefundRepository.findByPaymentId(entity.getId()) | ||
| .map(refundMapper::toDomain) | ||
| .orElse(null); |
There was a problem hiding this comment.
매퍼 내부에서 데이터베이스 호출(jpaRefundRepository.findByPaymentId)을 통해 연관된 Refund 정보를 가져오고 있습니다. 이는 매퍼의 단일 책임 원칙을 위반하는 안티패턴이며, 특히 여러 결제 정보를 매핑할 때 N+1 쿼리 문제를 유발하여 심각한 성능 저하로 이어질 수 있습니다. 데이터 조회는 리포지토리나 서비스 계층에서 처리하고, 완전히 구성된 엔티티를 매퍼에 전달하는 것이 바람직합니다. 예를 들어, PaymentEntity와 RefundEntity 간에 @OneToOne 관계를 설정하고 PaymentRepository에서 JOIN FETCH를 사용하여 함께 로드하는 방법을 고려해볼 수 있습니다.
| 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) { | ||
| } | ||
| } |
There was a problem hiding this comment.
| public class TossPaymentProvider implements PaymentProvider { | ||
|
|
||
| private final RestClient restClient; | ||
| private final ConcurrentHashMap<String, String> paymentKeyStore = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
LocalDateTime.now()를 직접 사용하면 메서드의 동작이 시스템 시계에 의존하게 되어 테스트, 특히 환불 정책과 같이 시간에 민감한 로직의 테스트를 복잡하게 만듭니다. java.time.Clock을 주입받아 LocalDateTime.now(clock)을 사용하거나 현재 시간을 메서드 파라미터로 전달하는 것이 더 좋습니다. 이를 통해 테스트에서 시간을 제어하고 결정적인 동작을 보장할 수 있습니다.
| LocalDateTime now = LocalDateTime.now(); | |
| LocalDateTime now = LocalDateTime.now(clock); // Clock bean 주입 필요 |
| log.info("gateway cancel raw response = {}", raw); | ||
|
|
||
| try { | ||
| ObjectMapper mapper = new ObjectMapper(); |
There was a problem hiding this comment.
cancelPayment 메서드가 호출될 때마다 새로운 ObjectMapper 인스턴스가 생성됩니다. ObjectMapper는 스레드에 안전하며 생성 비용이 비교적 높은 객체이므로, 단일 인스턴스를 재사용하는 것이 좋습니다. Spring Boot가 제공하는 기본 ObjectMapper 빈을 주입받거나, private static final 필드로 선언하여 사용할 수 있습니다. 이 내용은 confirmPayment 메서드(41행)에도 동일하게 적용됩니다.
| ObjectMapper mapper = new ObjectMapper(); | |
| // ObjectMapper는 Bean으로 주입받거나 static final로 선언하여 재사용하는 것이 좋습니다. | |
| ObjectMapper mapper = new ObjectMapper(); |
| } 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; | ||
| } |
There was a problem hiding this comment.
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 exposePOST /api/v1/payments/{paymentId}/refund. - Add refund persistence layer (
refundsJPA entity/repository/mapper) and wire refund lookups into payment mapping. - Extend
payment-gatewaywith 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.
| public Payment toDomain(PaymentEntity entity) { | ||
| Refund refund = jpaRefundRepository.findByPaymentId(entity.getId()) | ||
| .map(refundMapper::toDomain) | ||
| .orElse(null); |
There was a problem hiding this comment.
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.
| // 2. PG 환불 요청 (트랜잭션 없이 외부 호출) | ||
| PaymentGateway.PaymentCancelResult cancelResult = | ||
| paymentGateway.cancelPayment(orderId, cancelAmount, reason); | ||
|
|
There was a problem hiding this comment.
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.
| @Transactional(readOnly = true) | ||
| public RefundPrepareResult prepareRefund(UUID paymentId) { | ||
| // 1. Payment 조회 | ||
| Payment payment = paymentRepository.findById(PaymentId.of(paymentId)) | ||
| .orElseThrow(PaymentNotFoundException::new); |
There was a problem hiding this comment.
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.
| public LocalDateTime getPerformanceDateTime(ReservationId reservationId) { | ||
| return jdbcTemplate.queryForObject( | ||
| "SELECT s.performance_datetime FROM reservations r " | ||
| + "JOIN schedules s ON r.schedule_id = s.id " |
There was a problem hiding this comment.
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.
| + "JOIN schedules s ON r.schedule_id = s.id " | |
| + "LEFT JOIN schedules s ON r.schedule_id = s.id " |
| private final RestClient restClient; | ||
| private final ConcurrentHashMap<String, String> paymentKeyStore = new ConcurrentHashMap<>(); | ||
|
|
There was a problem hiding this comment.
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).
| // minimumNumberOfCalls=2, failureRateThreshold=50 | ||
| // 연결 끊김 2회 → ResourceAccessException → 실패율 100% → CB OPEN | ||
| for (int i = 0; i < 2; i++) { |
There was a problem hiding this comment.
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.
| id uuid PRIMARY KEY, | ||
| payment_id uuid NOT NULL, | ||
| refund_amount integer NOT NULL, | ||
| status varchar NOT NULL, | ||
| reason varchar, |
There was a problem hiding this comment.
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).
| create table reservations | ||
| ( | ||
| id uuid primary key | ||
| id uuid primary key, | ||
| schedule_id uuid | ||
| ); |
There was a problem hiding this comment.
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.
- 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 인프라(서비스, 리포지토리, 엔티티, 스케줄러, 어드민 컨트롤러) 삭제.
🛠️ 설명 (Description)
결제 환불 기능을 구현하고, 토스페이먼츠 연동 시 외부 서비스 통신 장애 및 과부하에 대비하기 위한 복원력 패턴(서킷 브레이커, 벌크헤드)을 적용했습니다.
주요 작업 내용은 다음과 같습니다:
/payments/cancelAPI 엔드포인트 추가PaymentProvider인터페이스에cancel메서드를 정의하고, Toss 및 Stub 구현체에 환불 로직 구현ProviderRouter를 통한 환불 요청 라우팅 로직 추가confirm) 및 환불(cancel) API 연동📄 설계 문서 (Design Document)
N/A
✅ 테스트 계획 (Test Plan)
PaymentGatewayControllerTest:/payments/confirm및/payments/cancelAPI 엔드포인트의 동작을 검증합니다. Mocking을 통해ProviderRouter의 응답을 시뮬레이션합니다.ProviderRouterTest:confirm및cancel요청이 올바른PaymentProvider로 라우팅되는지, 지원하지 않는 프로바이더 요청 시 예외가 발생하는지 검증합니다.StubPaymentProviderTest: Stub Payment Provider의supports,confirm,cancel메서드가 예상대로 동작하는지 검증합니다.TossPaymentProviderTest:MockWebServer를 사용하여 토스페이먼츠 API의 성공/실패 응답을 모킹하여confirm및cancel로직을 검증합니다. 특히paymentKey저장 및 조회 로직을 확인합니다.TossPaymentProviderResilienceTest:MockWebServer를 사용하여 토스페이먼츠 API의 다양한 네트워크 오류 (타임아웃, 연결 끊김, 무응답) 및 응답 지연을 시뮬레이션하여 서킷 브레이커, 벌크헤드 등의 Resilience4j 패턴이 올바르게 동작하고 fallback 응답을 반환하는지 검증합니다.PaymentGatewayController의/payments/cancel엔드포인트,ProviderRouter의cancel라우팅,TossPaymentProvider의cancel로직 및 Resilience4j 적용 로직에 대한 테스트 커버리지를 확보했습니다.📝 변경 사항 요약 (Summary)
payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/api/PaymentGatewayController.java에POST /api/v1/gateway/payments/cancel엔드포인트를 추가하고,CancelRequest,CancelResponseDTO를 정의했습니다.payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/PaymentProvider.java에CancelResponse cancel(CancelRequest request)메서드를 추가했습니다.payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/ProviderRouter.java에cancel요청을 해당PaymentProvider로 라우팅하는 로직을 추가했습니다.payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/TossPaymentProvider.java에cancel메서드를 구현하여 토스페이먼츠 환불 API를 연동했습니다.confirm및cancel메서드에@CircuitBreaker와@Bulkhead애너테이션을 적용하여 외부 PG사 통신에 대한 복원력을 확보하고, 다양한 실패 상황에 대한 fallback 메서드를 정의했습니다.connectTimeout과readTimeout을 설정하여 통신 타임아웃을 관리합니다.TossPaymentProvider가prod프로파일 외에test프로파일에서도 활성화되도록@Profile설정을 변경했습니다.confirm성공 시paymentKey를ConcurrentHashMap에 저장하고,cancel시 이를 사용하여 환불 요청을 처리하도록paymentKeyStore를 추가했습니다.payment-gateway/src/main/java/wisoft/nextframe/paymentgateway/provider/StubPaymentProvider.java에cancel메서드를 구현하여 스텁 응답을 제공합니다..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.gradle에org.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)
TossPaymentProviderResilienceTest를 통해 다양한 실패 상황에서 Resilience4j가 의도대로 동작하는지 확인했습니다.TossPaymentProvider에서 결제 승인 시paymentKey를 메모리(ConcurrentHashMap)에 저장하고 환불 시 이를 사용하는 로직이 추가되었습니다. 이paymentKeyStore는 현재 인메모리 방식이며, 애플리케이션 재시작 시 데이터가 손실됩니다. 실제 운영 환경에서는 이paymentKey를 영속적인 저장소(예: DB)에 저장하고 관리하는 방안을 고려해야 합니다. (이 PR에서는 기능 구현 및 복원력 패턴 적용에 초점을 맞췄습니다.)Testcontainers관련 진단 스크립트와docker.sock권한 조정, 그리고TESTCONTAINERS_RYUK_DISABLED: true환경 변수 설정이 포함되었습니다. 이 변경 사항이 다른 모듈의 CI 동작에 영향을 줄 수 있는지 확인 부탁드립니다.➕ 추가 정보 (Additional Information)
N/A