Skip to content

[Volume-6] PG 결제 연동 및 장애 대응 설계#265

Merged
minbo2002 merged 5 commits into
loopers-labs:minbo2002from
minbo2002:feat/volume-6
Jul 2, 2026
Merged

[Volume-6] PG 결제 연동 및 장애 대응 설계#265
minbo2002 merged 5 commits into
loopers-labs:minbo2002from
minbo2002:feat/volume-6

Conversation

@minbo2002

Copy link
Copy Markdown

🧭 Context & Decision

문제 정의

  • 현재 동작/제약: 기존 결제 흐름에 로컬에서 실행 가능한 pg-simulator를 외부 결제 시스템으로 추가한다. PG 동기 응답은 접수증(PENDING + transactionKey)이고 실제 결과(승인 70% / 한도초과 20% / 잘못된카드 10%)는 1~5초 뒤 콜백으로 온다.
  • 문제(리스크): 외부 HTTP 호출은 DB 트랜잭션 ACID 밖이라, 트랜잭션에 합치면 커넥션 점유·롤백 불일치가 동시에 발생한다. 또한 요청이 성공했다고 결제가 성공한 것은 아니며, PG는 매 요청마다 새 transactionKey를 발급하는 멱등하지않은 시스템이라 재시도는 중복 결제를 유발한다.
  • 성공 기준: PG 요청/타임아웃/실패/콜백/유실 상황을 내부 결제 상태로 명확히 표현하고, Timeout·CircuitBreaker·Fallback으로 PG 장애가 commerce-api 전체로 전파되지 않으며(내부는 정상 응답), 콜백 미수신 시에도 상태 조회 기반 복구로 최종 정합성을 맞춘다.

선택지와 결정

[결정 1] 트랜잭션 구조 — 단일 트랜잭션 vs 트랜잭션 경계 분리

  • 고려한 대안:

    • A (단일 트랜잭션): 트랜잭션 하나가 createPending + PG 호출 + 결과 반영을 모두 감싼다. PG 실패 시 자동 롤백으로 PENDING 행조차 남지 않아 별도 보정 코드가 필요 없고, 주문·결제가 원자적으로 처리된다.
    • B (경계 분리 + 선삽입): PG 호출 전 PENDING을 선삽입·커밋(payments.order_id UNIQUE) → PG 호출은 트랜잭션 밖 → 결과는 콜백/폴링의 별도 트랜잭션으로 반영.
  • 최종 결정: B

  • 트레이드오프: A는 동시 요청이 둘 다 PG를 호출한 뒤 INSERT에서 하나만 실패해 이미 PG엔 이중 거래가 생긴다. 또 외부 호출이 트랜잭션 안에 있어 응답 대기 동안 커넥션을 점유한다. B는 PENDING이 먼저 커밋돼야 UNIQUE가 락 역할을 해 동시 요청 중 하나만 통과(중복 결제 차단)하고, 외부 호출을 트랜잭션 밖에 둬 풀 고갈을 막는다.


[결정 2] Retry 대상 — 모든 일시적 실패 vs 거래 미생성이 보장되는 실패만

  • 배경: PG가 40% 확률로 즉시 실패를 반환하는 환경에서 일시적 실패를 재시도하면 성공률을 보완할 수 있다. 단, PG는 멱등하지 않아 잘못 재시도하면 중복 결제를 유발한다.
  • 고려한 대안:
    • A (모든 일시적 실패 재시도): 5xx와 read timeout을 모두 재시도.
    • B (안전한 실패만 재시도): 5xx만 재시도하고 read timeout은 제외.
  • 최종 결정: B
  • 트레이드오프: 5xx는 거래 생성 전 실패라 재시도가 안전하지만, read timeout은 거래가 이미 생겼을 수 있어 재시도 시 이중 결제다. A는 성공률↑ 대신 중복 결제 위험을 안고, B는 안전한 5xx만 재시도·timeout은 역조회 복구로 돌린다. 단 현재는 취소 API가 없어 중복은 회복 불가하다.

[결정 3] 복구 경로 — 콜백 단독 vs 콜백 + 스케줄러 폴링(+ 시도 상한횟수 설정)

  • 고려한 대안:
    • A (콜백만): 단순하지만 콜백 1회·유실 시 주문이 영구 PENDING.
    • B (콜백 + 스케줄러 폴링): 폴링이 시도됐으나 아직 확정되지 않은 PENDING 결제를 PG에 역조회해 확정.
  • 최종 결정: B
  • 트레이드오프: A는 "PG는 SUCCESS인데 주문은 영구 PENDING"을 막지 못한다. B는 자가복구로 견고한 대신 스케줄러 주기 튜닝과 콜백/폴링 중복 가드가 필요하다. 서킷브레이커가 OPEN일 땐 폴링하지 않는다. 죽은 PG를 계속 조회해봐야 장애만 키운다. 복구 시도도 건당 5회까지만 하고, 넘으면 폴링 대상에서 빼고 경보를 남긴다(무한 폴링 방지). 콜백·폴링은 같은 reflect 경로로 수렴하고, 그 안에서 금액 대조와 멱등 가드(결제·주문)를 거쳐 이중 반영을 막는다.

🤔 고민한 점 / 막혔던 부분

  • 타임아웃을 실패로 단정할지 vs PENDING 유지할지 — 이중 결제 위험 때문에 재시도 대신 orderId 역조회하는 방식을 택했다.
  • 서킷브레이커 OPEN PENDING의 복구 사각지대 — 서킷 OPEN이면 PG에 요청 자체가 안 가 거래가 없으므로 역조회가 무의미하다. pgRequestAttempted 플래그로 타임아웃(복구 대상)과 미시도(재요청 대상)를 구분했다.

loopers-len and others added 5 commits June 26, 2026 03:23
결제 게이트웨이 시뮬레이터(pg-simulator) 모듈을 추가한다.
- 결제 요청/단건 조회/주문별 조회 API (POST·GET /api/v1/payments)
- 100~500ms 지연 및 40% 확률 실패로 PG 불안정성 시뮬레이션
- 이벤트 기반 비동기 처리(PENDING→SUCCESS)와 콜백 전송
- settings.gradle.kts 모듈 등록 및 http 요청 환경/예제 추가

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

변경 목적: 외부 PG 결제를 주문 생성과 분리해 비동기/복구 가능한 흐름으로 전환하고, 실패·타임아웃·콜백 유실을 안전하게 처리하려는 목적입니다.
핵심 변경점: commerce-api에 PaymentFacade, PaymentService, PaymentModel 기반의 PENDING 결제 흐름과 콜백 반영 로직을 추가하고, PG 연동용 PgPaymentGatewayAdapterPaymentRecoveryScheduler를 도입했습니다.
핵심 변경점: 주문 생성은 결제 없이 PENDING 주문까지만 처리하도록 바뀌었고, 결제 API/DTO/컨트롤러와 pg-simulator 모듈이 새로 추가되어 요청/콜백/조회 시나리오를 시뮬레이션합니다.
핵심 변경점: Resilience4j 서킷브레이커·Retry 설정과 DB 유니크 제약, 멱등성 체크, 금액 검증, 복구 시도 횟수 제한으로 중복 결제와 오염 상태를 줄이도록 구성했습니다.
리스크/주의사항: 타임아웃을 “실패”로 오해하면 이미 생성된 PG 거래를 놓칠 수 있어, callback/polling 반영이 실제로 동일하게 수렴하는지 확인이 필요합니다.
리스크/주의사항: pg-simulator의 실패 확률/지연과 복구 스케줄러가 운영 의도와 맞는지, 그리고 콜백 누락 시 최대 5회 복구 정책이 충분한지 점검이 필요합니다.
테스트/검증 방법: 추가된 PaymentModelTest, PaymentServiceUnitTest, PaymentV1ApiE2ETest, OrderV1ApiE2ETest로 상태 전이·멱등성·금액 불일치·중복 결제·콜백 반영을 검증합니다.
테스트/검증 방법: 로컬에서는 commerce-apipg-simulator를 함께 띄워 /api/v1/payments 요청, 콜백, 주문별 조회를 HTTP 예제로 재현해 보는 것이 좋습니다.

Walkthrough

주문 생성은 PENDING 주문만 반환하도록 바뀌고, 결제는 commerce-api의 facade/service와 pg-simulator 모듈로 분리되었다. PG 요청, 콜백 반영, 복구 폴링, 예외 처리, DTO, 설정, 테스트, 문서가 함께 추가되었다.

Changes

주문-결제 비동기화와 PG 시뮬레이터 추가

Layer / File(s) Summary
PENDING 주문 전환
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java, apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java
주문 생성은 결제 없이 PENDING 주문만 반환하고, 주문 E2E는 기대 상태를 PENDING으로 바꾼다.
결제 계약과 상태 모델
apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentCommand.java, apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentInfo.java, apps/commerce-api/src/main/java/com/loopers/domain/payment/CardType.java, apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentStatus.java, apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentModel.java, apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentGateway.java, apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentRepository.java, apps/commerce-api/src/main/java/com/loopers/domain/payment/PgPaymentCommand.java, apps/commerce-api/src/main/java/com/loopers/domain/payment/PgRequestResult.java, apps/commerce-api/src/main/java/com/loopers/domain/payment/PgTransaction.java, apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentModelTest.java
결제/PG 요청 응답의 데이터 형식, 상태 enum, PaymentModel, PaymentRepository가 추가되고 PaymentModelTest가 초기값과 전이 규칙을 검증한다.
결제 서비스와 저장소
apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentJpaRepository.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentRepositoryImpl.java, apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceUnitTest.java
PaymentService는 PENDING 생성, 결과 반영, 복구 대상 조회, 시도 기록을 처리하고 JPA 위임 저장소가 이를 저장한다.
결제 진입점과 PG 연동
apps/commerce-api/build.gradle.kts, apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java, apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentRecoveryScheduler.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgPaymentGatewayAdapter.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgProperties.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Controller.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Dto.java, apps/commerce-api/src/main/java/com/loopers/support/config/PgClientConfig.java, apps/commerce-api/src/main/java/com/loopers/support/config/SchedulingConfig.java, apps/commerce-api/src/main/resources/application.yml, apps/commerce-api/src/test/java/com/loopers/interfaces/api/PaymentV1ApiE2ETest.java
결제 facade, PG 어댑터, 복구 스케줄러, /api/v1/payments 컨트롤러, DTO, 설정, E2E가 연결된다.
PG 시뮬레이터 공통 실행과 HTTP 처리
apps/pg-simulator/build.gradle.kts, settings.gradle.kts, apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt, apps/pg-simulator/src/main/kotlin/com/loopers/config/web/WebMvcConfig.kt, apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt, apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiResponse.kt, apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.kt, apps/pg-simulator/src/main/kotlin/com/loopers/support/error/CoreException.kt, apps/pg-simulator/src/main/kotlin/com/loopers/support/error/ErrorType.kt, apps/pg-simulator/src/main/kotlin/com/loopers/domain/user/UserInfo.kt, apps/pg-simulator/src/main/resources/application.yml, http/http-client.env.json, modules/redis/src/main/resources/redis.yml
새 모듈의 부팅, 예외 응답, 요청 인자 해석, 응답 래퍼, 프로파일별 설정, Redis 타임아웃이 추가된다.
PG 결제 도메인과 저장소
apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/OrderInfo.kt, apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentCommand.kt, apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/TransactionInfo.kt, apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/*, apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/*
결제 주문/트랜잭션 데이터, 결제 엔티티, 이벤트, 저장소 인터페이스, JPA/코어 구현, 거래키 생성기가 추가된다.
PG 결제 서비스와 API
apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt, apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/*, apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/event/payment/*, apps/pg-simulator/README.md, http/pg-simulator/payments.http
결제 생성, 조회, 처리, 콜백 전송, 이벤트 후처리, API 레코드와 예시 요청이 추가된다.

Sequence Diagram(s)

commerce-api 결제 요청과 콜백

sequenceDiagram
  participant PaymentV1Controller
  participant PaymentFacade
  participant PaymentService
  participant PgPaymentGatewayAdapter
  participant PaymentApi
  PaymentV1Controller->>PaymentFacade: pay(loginId, PaymentCommand)
  PaymentFacade->>PaymentService: createPending(...)
  PaymentFacade->>PgPaymentGatewayAdapter: requestPayment(PgPaymentCommand)
  PgPaymentGatewayAdapter->>PaymentApi: POST /api/v1/payments
  PaymentV1Controller->>PaymentFacade: reflect(orderId, transactionKey, status, amount, reason)
  PaymentFacade->>PaymentService: applyResult(...)
Loading

pg-simulator 이벤트 처리

sequenceDiagram
  participant PaymentApi
  participant PaymentApplicationService
  participant PaymentCoreEventPublisher
  participant PaymentEventListener
  participant PaymentCoreRelay
  PaymentApi->>PaymentApplicationService: createTransaction(...)
  PaymentApplicationService->>PaymentCoreEventPublisher: publish(PaymentCreated)
  PaymentCoreEventPublisher->>PaymentEventListener: handle(PaymentCreated)
  PaymentEventListener->>PaymentApplicationService: handle(transactionKey)
  PaymentApplicationService->>PaymentCoreEventPublisher: publish(PaymentHandled)
  PaymentCoreEventPublisher->>PaymentEventListener: handle(PaymentHandled)
  PaymentEventListener->>PaymentApplicationService: notifyTransactionResult(transactionKey)
  PaymentApplicationService->>PaymentCoreRelay: notify(callbackUrl, transactionInfo)
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.63% 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 제목이 PG 결제 연동과 장애 대응이라는 핵심 변경을 간결하게 요약한다.
Description check ✅ Passed 필수 구조인 Context, 문제 정의, 선택지와 결정, 고민한 점이 포함되어 대부분 템플릿을 충족한다.
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.


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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (25)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Controller.java-33-42 (1)

33-42: 🔒 Security & Privacy | 🟠 Major

apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Controller.java:33-42의 콜백 원본 검증을 추가해야 한다

  • 현재는 request를 그대로 paymentFacade.reflect(...)로 전달하므로, callback URL만 알면 임의 POST가 결제 상태 변경 경로까지 도달할 수 있다.
  • PG가 합의한 HMAC 서명 헤더, 공유 시크릿, 또는 mTLS/IP allowlist 검증을 엔드포인트 진입부에 넣고, 실패 시 401/403으로 즉시 차단해야 한다.
  • 기존의 금액 불일치·중복 콜백 테스트 외에 서명 누락, 위조 서명, 변조된 bodyreflect로 전달되지 않는 테스트를 추가해야 한다.
🤖 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
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Controller.java`
around lines 33 - 42, The PaymentV1Controller.callback endpoint currently
accepts any POST and forwards the body directly to paymentFacade.reflect, so add
origin/authenticity validation at the entry point before any reflection happens.
Implement the agreed callback verification in callback using the incoming
request headers/body (for example HMAC signature, shared secret, or mTLS/IP
allowlist) and reject invalid requests with 401/403 before calling
paymentFacade.reflect. Also add tests around PaymentV1Controller and
PaymentFacade invocation to ensure missing signature, forged signature, and
tampered body cases do not reach reflect.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Controller.java-33-37 (1)

33-37: 🩺 Stability & Availability | 🟠 Major

callbackorderId를 문자열로 받지 말고 바인딩 단계에서 400으로 처리해야 한다.
현재는 Long.parseLong(request.orderId())가 컨트롤러 내부에서 터져 ApiControllerAdvice의 일반 예외 처리로 흘러 500이 된다. PG 콜백은 재시도 성격이 강하므로, 잘못된 orderId 하나가 불필요한 재전송과 미처리 상태를 만들 수 있다. CallbackRequest.orderIdLong으로 바꾸거나, 최소한 이 파싱 실패를 BAD_REQUEST로 변환하라. 숫자가 아닌 orderId로 호출했을 때 400이 내려가고 paymentFacade.reflect(...)가 호출되지 않는 테스트를 추가하라.

🤖 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
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Controller.java`
around lines 33 - 37, `PaymentV1Controller.callback` is parsing
`request.orderId()` inside the controller, which lets invalid values escape as a
server error instead of a bad request. Update `PaymentV1Dto.CallbackRequest` to
bind `orderId` as a Long at the DTO/request binding level, or convert parsing
failures in `callback` into a BAD_REQUEST response, and make sure
`paymentFacade.reflect(...)` is only invoked after successful binding. Add a
test around `callback` that sends a non-numeric `orderId`, verifies a 400
response, and confirms `paymentFacade.reflect(...)` is not called.

Source: Path instructions

apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Dto.java-16-20 (1)

16-20: 🔒 Security & Privacy | 🟠 Major

카드번호는 record component로 두지 않는 편이 안전하다.
PayRequest, CallbackRequest, PaymentCommand는 기본 toString()에 전체 PAN이 포함되므로, 객체가 로그나 예외에 찍히면 운영 로그로 민감정보가 그대로 남는다. 카드번호는 마스킹된 전용 타입으로 분리하거나 toString()을 직접 제어해야 한다. 추가로 이들 타입의 toString()과 로그 포맷에 전체 카드번호가 포함되지 않는 테스트를 넣어야 한다.

🤖 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
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Dto.java`
around lines 16 - 20, The PayRequest record currently keeps the full card number
as a component, so its default toString() can leak PAN into logs and exceptions.
Update PayRequest, and the related CallbackRequest and PaymentCommand types, to
avoid exposing the raw card number by either moving it to a masked value object
or overriding toString() to redact it. Also add tests around these types’
toString() and any logging format to ensure the full card number never appears.

Source: Path instructions

apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentModel.java-45-46 (1)

45-46: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

카드번호를 원문으로 영속화하면 안 된다.

운영 관점에서 payments 테이블, 백업, 장애 분석 덤프만으로 PAN 이 바로 노출되어 사고 반경과 PCI 범위가 크게 커진다. cardNo는 저장하지 않거나 토큰/마스킹 값만 남기고, 정말 보관이 필요하면 별도 키 관리가 있는 필드 암호화로 분리하는 편이 안전하다. 수정 후에는 저장 직후 DB 에 평문 카드번호가 남지 않는지와 조회/로그에 마스킹 값만 노출되는지를 검증하는 테스트를 추가해야 한다.

Also applies to: 54-59

🤖 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 `@apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentModel.java`
around lines 45 - 46, The PaymentModel cardNo mapping is persisting raw PAN
data, so update the payment persistence flow to stop storing the original card
number directly in the payments entity and database. Adjust the PaymentModel
field and the code that writes/reads it so only a token, masked value, or
encrypted form is retained, and make sure any logs or queries expose only masked
data. Add tests around the payment save path and retrieval path to verify no
plaintext card number remains in the DB and that only masked values are
observable.
apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgPaymentGatewayAdapter.java-65-73 (1)

65-73: 🎯 Functional Correctness | 🟠 Major

ResourceAccessException을 전부 TIMEOUT으로 보내지 마라다.

  • RestClientResourceAccessException은 read timeout뿐 아니라 connect timeout, DNS 실패, connection refused 같은 미전송 오류도 포함하므로, 이를 PgRequestResult.timeout()으로 묶으면 실제로 PG에 요청이 가지 않은 경우까지 역조회 경로로 흘러가 재시도/거절 판단이 틀어진다다.
  • read timeout 계열만 TIMEOUT으로 분리하고, 나머지는 NOT_ATTEMPTED로 보내며, 로그는 t 자체를 넘겨 원인 예외와 스택트레이스를 남겨라다.
  • ResourceAccessException의 read timeout과 connect/DNS 실패를 각각 모킹해 TIMEOUT/NOT_ATTEMPTED 분기가 유지되는 테스트를 추가해라다.
🤖 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
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgPaymentGatewayAdapter.java`
around lines 65 - 73, `PgPaymentGatewayAdapter.requestPaymentFallback` currently
treats every `ResourceAccessException` as `PgRequestResult.timeout()`, but only
read-timeout cases should map to `TIMEOUT`; adjust the fallback to distinguish
read timeout from connect/DNS/refused failures and return
`PgRequestResult.notAttempted()` for the non-read-timeout cases. Update the
`log.warn` call to pass the throwable itself rather than `t.toString()` so the
stack trace is preserved, and keep the generic fallback path for other
exceptions as `NOT_ATTEMPTED`. Add tests around `requestPaymentFallback`
covering both a read-timeout `ResourceAccessException` and a connect/DNS-style
`ResourceAccessException` to verify the `TIMEOUT` vs `NOT_ATTEMPTED` split.
apps/commerce-api/src/main/resources/application.yml-32-37 (1)

32-37: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

기본 설정에 localhost PG 엔드포인트를 두면 배포 환경에서 즉시 오동작한다.

현재 값이 공통 application.yml에 들어가 있으면 override가 빠진 환경에서 commerce-api가 자기 자신이나 로컬 호스트를 향해 결제를 시도하게 되어 장애가 난다. 이 값들은 application-local.yml로 내리거나 ${PG_SIMULATOR_BASE_URL} / ${PG_CALLBACK_URL} 같은 환경변수 바인딩으로 분리하는 편이 안전하다. 추가로 로컬 프로파일과 비로컬 프로파일에서 각각 올바른 값이 바인딩되는 설정 테스트를 넣어야 한다. As per path instructions, "**/application*.yml: 환경별 분리(프로파일)와 기본값 적절성을 점검하고 ... 운영에 영향을 주는 설정 변경은 근거와 영향 범위를 요구한다."

🤖 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 `@apps/commerce-api/src/main/resources/application.yml` around lines 32 - 37,
Move the pg-simulator defaults out of the shared application.yml in commerce-api
so production does not fall back to localhost; bind base-url and callback-url
from environment variables or profile-specific files such as
application-local.yml, and ensure the default profile has safe non-local values.
Update the configuration around pg-simulator to keep the existing timeout
settings, and add configuration tests around the binding so local and non-local
profiles resolve the correct URLs through the pg-simulator properties.

Source: Path instructions

apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java-51-67 (1)

51-67: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

기존 transactionKey와 다른 PG 결과를 거부하지 않아 잘못된 결제 결과가 반영될 수 있다.

이미 transactionKey가 저장된 뒤에는 다른 non-null 키가 들어오면 상태 전이를 막아야 한다. 지금처럼 무시한 채 SUCCESS/FAILED를 반영하면 잘못된 콜백이나 역조회 응답이 같은 주문의 결제 상태를 덮어써 데이터 정합성이 깨진다. payment.getTransactionKey()가 존재할 때는 입력 키와 일치하는지 먼저 검증하고, 불일치하면 예외로 차단해야 한다. 추가로 "첫 콜백은 key reconcile 허용", "이후 다른 key로 들어온 결과는 거부되고 상태가 유지된다"를 검증하는 테스트가 필요하다.

🤖 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
`@apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java`
around lines 51 - 67, `PaymentService.applyResult` currently accepts any
non-null `transactionKey` after one has already been stored, which can let a
mismatched PG callback overwrite the payment state. Update the `applyResult`
flow so that when `payment.getTransactionKey()` is already present, the incoming
`transactionKey` must match it before any status transition; otherwise throw a
`CoreException` and stop processing. Keep the existing first-time reconcile
behavior in `markRequested`, and add tests around `applyResult` to cover both
the initial key assignment and rejection of later results with a different key.
apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentRecoveryScheduler.java-42-55 (1)

42-55: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

예외 경로에서 복구 시도 횟수를 올리지 않아 동일 건을 무한 재시도하게 된다.

지금 구현은 PG 조회가 계속 예외를 던지면 recordRecoveryAttempt가 호출되지 않아 해당 결제가 영구히 recoverable 상태로 남는다. 그러면 10초마다 같은 건을 계속 두드리며 로그만 누적되고, PR에서 정의한 "결제당 최대 5회 복구 시도" 보장도 깨진다. 종결 상태를 반영하지 못한 모든 경로에서 시도 횟수가 증가하도록 catch 또는 finally에서 누적하고, 예외 로그는 e.toString()만 남기지 말고 stack trace까지 함께 남기는 편이 운영에 유리하다. 추가로 "Gateway 예외가 반복되면 recoveryAttempts가 증가하고 5회 후 조회 대상에서 빠진다"를 검증하는 테스트가 필요하다.

🤖 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
`@apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentRecoveryScheduler.java`
around lines 42 - 55, In PaymentRecoveryScheduler's recovery loop, the
RuntimeException path does not increment recovery attempts, so PG failures can
leave a payment in recoverable state forever; update the try/catch flow around
findByOrderId, reflect, and recordRecoveryAttempt so every non-terminal outcome
(including exceptions) still calls the attempt tracker, ideally via finally or
by duplicating the increment in the catch path. Also change the warn log to
include the exception stack trace instead of only e.toString(), and add a test
covering repeated gateway exceptions eventually pushing the payment out of
recoverable after the max attempts.
apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java-27-33 (1)

27-33: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

모든 DB 무결성 오류를 중복 결제로 번역하면 장애 원인 분리가 깨진다.

운영에서는 order_id 중복 외에도 not-null, FK, 길이 제약 위반이 모두 여기로 들어올 수 있는데, 현재 구현은 전부 409로 바꿔 실제 장애를 잘못 분류하게 만든다. order_id 고유 제약 위반만 명시적으로 409로 매핑하고, 나머지 DataIntegrityViolationException은 cause를 보존한 채 상위로 전파하거나 별도 5xx로 분리하는 편이 안전하다. 추가로 "중복 orderId는 409", "다른 제약 위반은 원인 예외가 유지된다"를 검증하는 테스트가 필요하다. As per path instructions, "**/*.java: 예외 처리 시 cause를 보존하고, 사용자 메시지와 로그 메시지를 분리하도록 제안한다."

🤖 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
`@apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java`
around lines 27 - 33, The catch block in PaymentService.createPending currently
maps every DataIntegrityViolationException to the same duplicate-payment
CoreException, which hides non-orderId integrity failures. Update the handling
so only the order_id unique constraint violation becomes the 409 conflict, while
other DataIntegrityViolationException cases preserve the original cause and are
either rethrown or mapped to a separate 5xx path; use the
PaymentService.createPending and paymentRepository.save flow to locate the
change. Add tests to verify that duplicate orderId returns 409 and that other
constraint violations keep the cause intact.

Source: Path instructions

apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentJpaRepository.java-14-14 (1)

14-14: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

복구 대상 조회를 배치 단위로 제한해야 한다.

Line 14는 조건만 있고 정렬과 상한이 없어, 적체가 생기면 스케줄러 한 번에 모든 PENDING 건을 읽어 DB 스캔과 애플리케이션 메모리 사용량이 함께 커진다. 운영에서는 폴링 주기마다 같은 부하가 반복되어 복구 경로 자체가 병목이 되기 쉽다. Pageable을 받거나 findTopNBy...OrderByIdAsc/OrderByCreatedAtAsc 형태로 배치 크기와 순서를 고정하는 쪽이 안전하다. 추가 테스트로 recoverable 데이터가 배치 크기를 초과할 때 오래된 건부터 안정적으로 잘리는 Repository 통합 테스트를 넣는 것이 좋다. As per path instructions, **/*Repository*.java: 대량 데이터에서의 병목과 정렬/인덱스 활용 가능성을 점검해야 한다.

🤖 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
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentJpaRepository.java`
at line 14, The repository query in PaymentJpaRepository currently fetches all
recoverable rows without any ordering or batch cap, which can cause large scans
and memory pressure. Update the finder to limit results per poll by either
accepting a Pageable or switching to a top-N method, and add a stable sort key
such as id or createdAt in the method name/signature. Keep the existing filter
conditions in
findByStatusAndPgRequestAttemptedTrueAndRecoveryAttemptsLessThanAndDeletedAtIsNull,
and add repository integration coverage that verifies excess recoverable rows
are returned in a bounded, deterministic order.

Source: Path instructions

apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgProperties.java-7-13 (1)

7-13: 🩺 Stability & Availability | 🟠 Major

PG 설정값에 바인딩 검증을 추가하라.

PgClientConfig.pgRestClient()PgPaymentGatewayAdapterbaseUrl, callbackUrl, connectTimeout, readTimeout를 즉시 사용하므로, 누락값이나 빈 문자열, 비정상 duration이 들어가면 문제를 배포 시점이 아니라 결제 흐름 중간에 드러내게 된다. PgProperties@Validated를 붙이고 @NotBlank, @NotNull, 필요하면 duration 하한 제약으로 기동 단계에서 실패시키라. pg-simulator.*에 누락값, 빈 문자열, 0 이하 duration을 넣었을 때 컨텍스트 초기화가 실패하는 테스트도 추가하라.

🤖 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
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgProperties.java`
around lines 7 - 13, Add startup-time binding validation to PgProperties so
pg-simulator settings fail fast instead of surfacing later in
PgClientConfig.pgRestClient() or PgPaymentGatewayAdapter. Update the
PgProperties record to use validation annotations such as `@Validated`, `@NotBlank`
for baseUrl and callbackUrl, `@NotNull` for connectTimeout and readTimeout, and
any needed duration minimum constraints. Also add tests that load the
application context with missing pg-simulator values, blank strings, and
non-positive durations to verify context initialization fails.
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentDto.kt-24-37 (1)

24-37: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

startsWith 기반 콜백 URL 검증은 우회 가능하다.

http://localhost:8080.evil.com/...http://localhost:8080@evil.com/... 같은 값도 현재 검증을 통과한다. 이후 콜백 릴레이가 이 값을 그대로 사용하면 시뮬레이터가 공격자 호스트로 outbound 호출을 보내게 된다. 문자열 prefix 비교 대신 URI로 파싱해서 scheme/host/port를 정확히 비교하고, 필요하면 허용 path까지 제한해야 한다. 위 우회 예시와 인코딩된 변형 입력을 거부하는 테스트를 추가해야 한다.

🤖 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
`@apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentDto.kt`
around lines 24 - 37, The callback URL validation in PaymentDto.validate is
unsafe because PREFIX_CALLBACK_URL prefix matching can be bypassed. Replace the
startsWith check with URI parsing and compare the scheme, host, and port
explicitly (and restrict the path if required) so values like localhost spoofing
variants are rejected. Update the validate method accordingly and add tests
around PaymentDto for bypass cases such as embedded userinfo, deceptive
subdomains, and encoded variants that should fail validation.
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt-30-31 (1)

30-31: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

요청 스레드에서 Thread.sleep으로 지연을 주면 시뮬레이터가 먼저 포화된다.

Tomcat 워커를 100~500ms씩 점유하면 동시 요청 시 결제 실패/복구 설계보다 스레드 고갈이 먼저 드러나서 E2E 결과가 왜곡된다. 지연이 필요하다면 비동기 응답(Callable/DeferredResult)으로 넘기거나, 승인 처리 이벤트 쪽에서 스케줄링으로 지연을 주는 편이 안전하다. 동시 요청 테스트를 추가해서 워커 고갈 없이 응답 지연만 시뮬레이션되는지 확인해야 한다.

🤖 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
`@apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt`
around lines 30 - 31, The issue is that PaymentApi currently blocks the request
thread with Thread.sleep, which can exhaust Tomcat workers under concurrent
load. Replace the blocking sleep in PaymentApi with an async approach such as
Callable or DeferredResult, or move the delay into the approval-processing/event
scheduling path so the request thread is released immediately. Keep the change
localized around the payment endpoint handling in PaymentApi, and add a
concurrent request test to verify delayed responses do not cause worker
starvation.
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentDto.kt-50-69 (1)

50-69: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

거래 상세 응답에 전체 카드번호를 그대로 노출하면 안 된다.

현재 TransactionDetailResponse가 원본 cardNo를 그대로 반환하고, Payment 엔티티도 전체 카드번호를 보관한다. 로컬 시뮬레이터라도 응답·저장소·로그 경로로 민감정보가 확산되어 유출 범위가 커진다. 저장은 토큰 또는 마스킹/last4 수준으로 축소하고, 응답도 ****-****-****-1234 같은 마스킹 값만 내려주도록 계약을 바꾸는 편이 안전하다. 전체 카드번호가 API 응답과 영속 저장에 남지 않는지, commerce-api 연동 DTO가 변경된 형식과 호환되는지 계약 테스트를 추가해야 한다.

🤖 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
`@apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentDto.kt`
around lines 50 - 69, 거래 상세 응답에서 전체 카드번호를 그대로 노출·저장하는 문제가 있습니다.
TransactionDto/TransactionDetailResponse의 from 매핑과 Payment 엔티티 저장 경로에서 cardNo를
원문 대신 마스킹 또는 last4/토큰 형태로 바꾸고, 응답도 동일한 마스킹 계약만 반환하도록 수정하세요. PaymentDto의
TransactionDetailResponse와 관련 저장/조회 로직을 함께 점검해 민감정보가 남지 않게 하며, commerce-api 연동
DTO가 새 형식과 맞는지 확인하는 계약 테스트를 추가하세요.
apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentCommand.kt-16-19 (1)

16-19: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

검증 규칙을 PaymentRequestCreateTransaction에 분산시키지 않는 편이 안전하다.

현재 PaymentDto.PaymentRequest.validate()orderId, cardNo, callbackUrl, amount를 검증하지만, 실제 저장 직전 PaymentApplicationService.createTransaction()이 신뢰하는 CreateTransaction.validate()는 금액만 막고 있다. 운영에서 HTTP 외 경로가 CreateTransaction을 직접 만들면 잘못된 카드번호나 callback URL이 저장되어 비동기 콜백/릴레이 단계에서 늦게 실패할 수 있다. CreateTransaction.validate()로 규칙을 모으고 PaymentRequest.validate()는 이를 재사용하도록 정리하는 편이 좋다. 추가 테스트로 PaymentApplicationService.createTransaction()에 직접 만든 command가 빈 orderId, 잘못된 cardNo, 허용되지 않은 callbackUrl일 때 모두 BAD_REQUEST로 막히는지 확인해야 한다.

🤖 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
`@apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentCommand.kt`
around lines 16 - 19, `CreateTransaction.validate()` is missing the full command
validation currently duplicated in `PaymentDto.PaymentRequest.validate()`, so
move the shared rules there and make `PaymentRequest.validate()` delegate to
`CreateTransaction.validate()` rather than maintaining its own checks. Update
`PaymentApplicationService.createTransaction()` to rely on the command-level
validation, and verify `orderId`, `cardNo`, `callbackUrl`, and `amount` are all
rejected with `BAD_REQUEST` when an invalid `CreateTransaction` is passed
directly.
apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt-15-19 (1)

15-19: 🎯 Functional Correctness | 🟠 Major

타임존 설정은 @PostConstruct가 아니라 초기 부팅 단계에서 고정해야 한다.

@PostConstruct 시점에는 일부 날짜 처리 빈이 이미 기본 타임존을 읽었을 수 있어, 운영에서 JSON 직렬화 시각과 LocalDateTime.now() 기준이 엇갈릴 수 있다. 앱 전역 KST가 필요하면 main 시작 직후에 한 번만 설정하고, Jackson은 spring.jackson.time-zone=Asia/Seoul 또는 Jackson2ObjectMapperBuilderCustomizer로 따로 고정하는 편이 안전하다. 추가로 컨텍스트 기동 직후 TimeZone.getDefault()와 날짜 직렬화 결과가 모두 기대값인지 확인하는 부트 테스트를 넣어야 한다.

🤖 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 `@apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt`
around lines 15 - 19, The timezone is being set too late in
PaymentGatewayApplication.started(), so move the global KST defaulting out of
the `@PostConstruct` hook and into the earliest startup path in main before the
Spring context is built. Also pin Jackson separately using
spring.jackson.time-zone=Asia/Seoul or a Jackson2ObjectMapperBuilderCustomizer,
and add a boot test that verifies TimeZone.getDefault() and date serialization
both reflect the expected timezone right after startup.
apps/pg-simulator/src/main/kotlin/com/loopers/support/error/ErrorType.kt-5-10 (1)

5-10: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

errorCode에 HTTP reason phrase를 쓰지 않는 편이 안전하다.

Line 7-10의 code"Bad Request" 같은 상태 문구로 내려가면 클라이언트가 안정적으로 분기하기 어렵고, 문구 변경이 곧 API 계약 변경이 된다. BAD_REQUEST, NOT_FOUND처럼 고정된 애플리케이션 코드로 분리하고, 사용자 노출 문구는 message에만 두는 편이 안전하다. 추가로 400/404 응답 직렬화 테스트에서 meta.errorCode가 상태 문구가 아니라 고정 코드로 내려가는지 검증해 두는 편이 좋다.

수정 예시
 enum class ErrorType(val status: HttpStatus, val code: String, val message: String) {
     /** 범용 에러 */
-    INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.reasonPhrase, "일시적인 오류가 발생했습니다."),
-    BAD_REQUEST(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.reasonPhrase, "잘못된 요청입니다."),
-    NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.reasonPhrase, "존재하지 않는 요청입니다."),
-    CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.reasonPhrase, "이미 존재하는 리소스입니다."),
+    INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "일시적인 오류가 발생했습니다."),
+    BAD_REQUEST(HttpStatus.BAD_REQUEST, "BAD_REQUEST", "잘못된 요청입니다."),
+    NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND", "존재하지 않는 요청입니다."),
+    CONFLICT(HttpStatus.CONFLICT, "CONFLICT", "이미 존재하는 리소스입니다."),
 }
🤖 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 `@apps/pg-simulator/src/main/kotlin/com/loopers/support/error/ErrorType.kt`
around lines 5 - 10, `ErrorType`의 `code`가 `HttpStatus.reasonPhrase`를 사용해 상태 문구에
묶여 있으니, `INTERNAL_ERROR`, `BAD_REQUEST`, `NOT_FOUND`, `CONFLICT` 같은 enum 상수의
`code`를 고정된 애플리케이션 코드로 분리하세요. `ErrorType`와 이를 응답으로 직렬화하는 `meta.errorCode` 생성 로직을
함께 수정해 `message`는 사용자 문구만 담당하게 하고, 400/404 응답 테스트에서 `errorCode`가 reason phrase가
아닌 고정 코드로 내려오는지 검증을 추가하세요.
apps/pg-simulator/src/main/resources/application.yml-18-19 (1)

18-19: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

로컬 프로파일을 기본 활성화하지 마십시오.

Line 19의 spring.profiles.active: local은 런타임 오버라이드가 빠진 배포에서 로컬 프로파일과 로컬 DB 설정으로 기동하게 만들어 장애로 이어질 수 있다. 기본 문서에서는 이 값을 제거하고, 실행 환경에서만 SPRING_PROFILES_ACTIVE 또는 배포 설정으로 주입하는 편이 안전하다. 추가로 프로파일 미지정 상태에서 local 문서가 자동 활성화되지 않는 스모크 테스트와 dev/prd 기동 확인 테스트를 넣는 편이 좋다.

수정 예시
 spring:
   main:
     web-application-type: servlet
   application:
     name: pg-simulator
-  profiles:
-    active: local
   config:
     import:
       - jpa.yml

As per path instructions, **/application*.yml: 환경별 분리(프로파일)와 기본값 적절성을 점검한다.

🤖 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 `@apps/pg-simulator/src/main/resources/application.yml` around lines 18 - 19,
Remove the default `spring.profiles.active: local` from the `application.yml`
configuration so the app does not boot with local settings when runtime
overrides are missing. Keep profile selection externalized via
`SPRING_PROFILES_ACTIVE` or deployment config, and ensure the startup/config
path in `application.yml` does not implicitly enable `local`. Add or update
checks around the app startup behavior to verify that no profile defaults to
`local` and that `dev`/`prd` boots work only when explicitly selected.

Source: Path instructions

apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.kt-27-30 (1)

27-30: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

X-USER-ID도 거부해야 한다.

Line 27-30은 헤더가 없는 경우만 막고 공백 문자열은 통과시킨다. 그러면 잘못된 사용자 컨텍스트가 정상 요청처럼 흘러가 조회 격리나 결제 추적에서 운영 장애를 만들 수 있다. isNullOrBlank()로 검사하고, 통과시키더라도 trim()한 값만 UserInfo로 생성하는 편이 안전하다. 추가로 missing, empty, whitespace-only 헤더에 대해 모두 400이 내려가는 MVC 테스트를 넣는 편이 좋다.

수정 예시
     override fun resolveArgument(
         parameter: MethodParameter,
         mavContainer: ModelAndViewContainer?,
         webRequest: NativeWebRequest,
         binderFactory: WebDataBinderFactory?,
     ): UserInfo {
-        val userId = webRequest.getHeader(KEY_USER_ID)
-            ?: throw CoreException(ErrorType.BAD_REQUEST, "유저 ID 헤더는 필수입니다.")
-
-        return UserInfo(userId)
+        val userId = webRequest.getHeader(KEY_USER_ID)
+        if (userId.isNullOrBlank()) {
+            throw CoreException(ErrorType.BAD_REQUEST, "유저 ID 헤더는 필수입니다.")
+        }
+
+        return UserInfo(userId.trim())
     }

As per path instructions, **/*.kt: null-safety를 최우선으로 점검한다.

🤖 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
`@apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.kt`
around lines 27 - 30, The UserInfoArgumentResolver currently only rejects a
missing X-USER-ID header, so empty or whitespace-only values can still pass
through. Update the getHeader(KEY_USER_ID) handling in UserInfoArgumentResolver
to validate with isNullOrBlank(), and when accepted, pass only the trimmed value
into UserInfo. Also add MVC tests covering missing, empty, and whitespace-only
X-USER-ID headers to verify they all return 400.

Source: Path instructions

apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt-12-18 (1)

12-18: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

현재 transactionKey 생성 방식은 충돌 여지가 크다.

날짜 prefix 뒤에 6자리 hex 문자열만 붙이면 하루 기준 키 공간이 16^6으로 제한된다. 동시 요청이나 테스트 반복이 늘면 충돌로 다른 결제건 조회·콜백 반영이 섞이거나 저장 실패가 발생할 수 있어 운영 안정성이 떨어진다. 전체 UUID/ULID를 사용하거나 충분한 길이의 난수와 시각 정보를 포함하고, 가능하면 저장소 unique 제약과 재생성 전략도 함께 두는 편이 안전하다. 추가로 대량 생성 테스트에서 중복이 발생하지 않는지와 포맷이 유지되는지를 검증해야 한다.

🤖 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
`@apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt`
around lines 12 - 18, 현재 TransactionKeyGenerator.generate()는 날짜 prefix 뒤에 UUID의
6자리만 붙여 충돌 가능성이 큽니다. generate()에서 사용하는 uuid 생성 방식을 전체 UUID/ULID 또는 더 긴 난수+시각 정보
조합으로 바꾸고, KEY_TRANSACTION 포맷은 유지하되 충분히 유일한 키가 되도록 수정하세요. 필요하면 저장소의 unique 제약과
재시도 로직도 함께 고려하고, 대량 생성 시 중복이 없는지와 문자열 포맷이 유지되는지 검증하는 테스트를 추가하세요.
apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/TransactionInfo.kt-18-37 (1)

18-37: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

콜백 payload에 전체 카드번호를 싣지 마시기 바란다.

PaymentApplicationService.notifyTransactionResult에서 이 DTO가 그대로 외부 callback payload로 전송된다. 전체 카드번호를 넘기면 downstream 로그·추적 계층까지 민감정보 범위가 넓어져 운영 사고 시 영향이 커진다. cardNo는 제거하거나 마지막 4자리만 남긴 마스킹 필드로 바꾸는 편이 안전하다. 추가로 직렬화 테스트에서 callback payload에 원본 카드번호가 포함되지 않는지 검증해야 한다. As per path instructions, "로깅 시 민감정보 노출 가능성을 점검한다."

🤖 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
`@apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/TransactionInfo.kt`
around lines 18 - 37, `TransactionInfo`는
`PaymentApplicationService.notifyTransactionResult`를 통해 외부 callback payload로 그대로
나가므로 전체 카드번호를 담지 않도록 수정해야 합니다. `TransactionInfo.from`과 `TransactionInfo` 필드에서
`cardNo`를 제거하거나 마지막 4자리만 남기는 마스킹 값으로 바꾸고, `notifyTransactionResult`가 사용하는
payload 직렬화 경로도 함께 확인하세요. 또한 callback payload 직렬화 테스트를 추가해 원본 카드번호가 포함되지 않는지
`TransactionInfo` 기준으로 검증하세요.

Source: Path instructions

apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRelay.kt-3-6 (1)

3-6: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

도메인 포트가 application DTO를 직접 의존하지 않도록 분리하시기 바란다.

PaymentRelayTransactionInfo를 받으면서 도메인 계층이 application.payment에 역의존하고 있다. 이 구조는 callback payload 형식이 바뀔 때마다 도메인 계약도 함께 흔들려 운영 중 변경 범위를 불필요하게 키운다. 도메인 포트에는 도메인 이벤트나 도메인 전용 결과 모델만 두고, 외부 전송용 DTO 매핑은 application/infrastructure 어댑터에서 수행하는 편이 안전하다. 추가로 패키지 의존 규칙 테스트나 어댑터 단위 테스트로 도메인 패키지가 application 패키지를 참조하지 않는지 검증해야 한다. As per path instructions, "비즈니스 규칙은 도메인에 두고, 인프라 관심사가 섞이면 분리하도록 제안한다."

🤖 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 `@apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRelay.kt`
around lines 3 - 6, PaymentRelay currently depends directly on the application
DTO TransactionInfo, so the domain port should be refactored to use a
domain-only contract instead. Update the notify signature in PaymentRelay to
accept a domain event/result model defined in the domain layer, then move the
TransactionInfo-to-domain mapping into the application or infrastructure adapter
that calls this port. Also add/adjust a dependency rule or adapter test to
ensure the domain package no longer references com.loopers.application.payment.

Source: Path instructions

apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt-32-41 (1)

32-41: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

원본 카드번호를 그대로 저장·전파하지 않는 편이 안전하다.

Line 38에서 원본 cardNo를 그대로 영속화하면 DB 덤프, 장애 대응 로그, 직렬화된 callback payload를 통해 PAN 노출면이 커진다. 제공된 TransactionInfo.from(payment)도 이 값을 그대로 실어 보내고, 콜백 소비자 apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Controller.java:33-43orderId, transactionKey, status, amount, reason만 사용하므로 운영상 보관 이점도 거의 없다. 마지막 4자리만 남긴 마스킹 값이나 토큰만 저장하고, callback 전용 DTO에서는 카드 정보를 제외해주기 바란다. 추가 테스트로 저장된 값과 callback JSON 모두에 전체 카드번호가 포함되지 않는지 검증이 필요하다.

🤖 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
`@apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt`
around lines 32 - 41, The `PaymentApplicationService` flow is persisting and
propagating the full `cardNo`, which should be avoided; update the `save` path
so `Payment` does not store the raw PAN, and ensure
`TransactionInfo.from(payment)` no longer includes card data in the callback
payload. Keep only a masked value or token in the domain model if needed, and
verify via tests that both the persisted record and the serialized callback JSON
do not contain the full card number.
apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRelay.kt-11-19 (1)

11-19: 🩺 Stability & Availability | 🟠 Major

콜백용 RestTemplate에 connect/read timeout을 명시해야 한다다

RestTemplate() 기본 설정은 타임아웃이 없어 느리거나 응답 없는 callback URL이 호출 스레드를 장시간 점유할 수 있다다. RestTemplateBuilder 또는 주입된 Bean으로 timeout을 설정하고, 지연 응답과 연결 실패를 구분해 기록하도록 바꾸는 것이 맞다다. 추가로 지연 응답 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
`@apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRelay.kt`
around lines 11 - 19, The callback RestTemplate in PaymentCoreRelay currently
uses default settings with no connect/read timeout, so update the notify flow to
use a timeout-configured RestTemplate (preferably via RestTemplateBuilder or an
injected bean) instead of the static RestTemplate instance. Keep the change
localized to PaymentCoreRelay and its companion object/notify method, and adjust
error logging so connection failures and slow response timeouts are
distinguishable in the logger.error path.
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/event/payment/PaymentEventListener.kt-16-20 (1)

16-20: 🩺 Stability & Availability | 🟠 Major

인터럽트 예외를 처리해 결제 이벤트 유실을 막아야 한다.
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/event/payment/PaymentEventListener.kt:16-20
Thread.sleep가 인터럽트되면 paymentApplicationService.handle(event.transactionKey)까지 도달하지 못해 결제 상태가 PENDING에 남을 수 있다. InterruptedException을 잡아 Thread.currentThread().interrupt()를 복원하고, 경고 로그와 재처리 가능한 경로를 추가해야 한다. 인터럽트 발생 시 로그가 남고 보상 또는 재시도 흐름으로 이어지는 테스트를 추가해야 한다.

🤖 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
`@apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/event/payment/PaymentEventListener.kt`
around lines 16 - 20, In PaymentEventListener.handle, Thread.sleep can interrupt
before paymentApplicationService.handle(event.transactionKey) runs, so add
InterruptedException handling around the sleep in the handle method. Catch the
interruption, restore the interrupt status with
Thread.currentThread().interrupt(), log a warning with enough context, and route
the event into a retryable or compensating path so the payment is not left in
PENDING. Also add a test covering an interrupted handle path to verify the
log/flow still leads to reprocessing or compensation.
🟡 Minor comments (2)
apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt-56-85 (1)

56-85: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

상태 전이 후에도 updatedAt이 갱신되지 않는다.

지금 구현이면 승인/실패로 상태가 바뀌어도 updatedAt은 생성 시각에 고정된다. 비동기 결제 흐름에서 마지막 변경 시각이 틀리면 장애 추적과 후속 복구 판단이 왜곡된다. 상태 변경 메서드의 공통 경로에서 updatedAt = LocalDateTime.now()를 함께 갱신하거나, 엔티티 라이프사이클 콜백으로 일관되게 처리하는 편이 안전하다. 상태 전이 후 updatedAt이 증가하는지 확인하는 단위 테스트를 추가해야 한다.

🤖 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 `@apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt`
around lines 56 - 85, Payment 엔티티의 상태 변경 메서드인 approve, invalidCard,
limitExceeded에서 status와 reason만 바꾸고 updatedAt을 갱신하지 않는 문제가 있습니다. 상태 전이 공통 경로에서
updatedAt을 현재 시각으로 함께 업데이트하도록 처리하거나, Payment 엔티티의 라이프사이클 콜백으로 일관되게 반영되게 수정하세요.
또한 상태 변경 후 updatedAt이 실제로 증가하는지 검증하는 테스트를 추가해 주세요.
http/pg-simulator/payments.http-6-11 (1)

6-11: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

예제 요청 본문에서 amount 타입을 DTO 계약과 맞추는 편이 낫다.

PaymentDto.PaymentRequest.amountLong인데 예제는 문자열을 보내고 있다. 지금은 coercion 설정에 따라 우연히 통과할 수 있어도, 설정이 엄격해지면 문서 예제를 그대로 호출한 사용자가 바로 400을 받게 된다. 예제는 "5000" 대신 5000으로 맞추고, 추가 테스트로 문서 스모크 테스트나 E2E 요청 예제가 실제 DTO 타입과 일치하는지 확인하는 편이 좋다.

🤖 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 `@http/pg-simulator/payments.http` around lines 6 - 11, The example request
body uses a string for amount, but PaymentDto.PaymentRequest.amount expects a
Long, so update the HTTP example in payments.http to send amount as a numeric
value and not a quoted string. Make sure the sample payload matches the DTO
contract used by PaymentDto.PaymentRequest, and if there are request examples or
smoke/E2E checks tied to this payload, align them with the same amount type.
🧹 Nitpick comments (6)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Dto.java (1)

16-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

요청 검증을 DTO 내부 수동 예외 대신 Bean Validation으로 올려야 한다.

현재 toCommand()가 검증과 매핑을 동시에 수행해서 바인딩 오류, 필드 누락, 형식 오류의 400 계약이 컨트롤러별로 흩어진다. 운영에서는 같은 잘못된 요청도 예외 종류에 따라 응답 포맷과 로깅 지점이 달라져 장애 분석이 어려워진다. PayRequest 컴포넌트에 @NotNull, @Pattern을 선언하고 컨트롤러에 @Valid를 붙여 toCommand()는 순수 매핑만 담당하게 바꾸는 편이 안전하다. 추가로 주문 ID 누락, 카드 종류 누락, 카드번호 형식 오류가 모두 동일한 400 응답 스키마로 내려가는 API 테스트를 보강해야 한다. As per path instructions, **/*Controller*.java: Controller는 요청 검증(Bean Validation)과 응답 조립에 집중하고 상태 코드와 에러 응답 포맷이 일관되는지 점검한다.

🤖 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
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Dto.java`
around lines 16 - 32, `PayRequest` in `PaymentV1Dto` is doing both validation
and mapping inside `toCommand()`, so move the request checks to Bean Validation
annotations on the record fields and keep `toCommand()` as pure conversion only.
Add `@Valid` to the payment controller request binding so `orderId`, `cardType`,
and `cardNo` failures are handled consistently before mapping, and remove the
manual `CoreException` throws from `toCommand()`. Update the controller/tests to
verify missing order ID, missing card type, and invalid card number all return
the same 400 response shape.

Source: Path instructions

apps/commerce-api/src/test/java/com/loopers/interfaces/api/PaymentV1ApiE2ETest.java (1)

157-173: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

NOT_ATTEMPTED 경로 테스트도 고정해야 한다.

운영 관점에서 TIMEOUTNOT_ATTEMPTED는 재시도 정책이 다르므로 한쪽만 검증하면 리팩터링 시 안전 재시도 규칙이 쉽게 무너진다. PgRequestResult.notAttempted() 시나리오를 추가해 transactionKey == null, pgRequestAttempted == false, 그리고 같은 주문에 대한 후속 요청이 현재 계약대로 동작하는지까지 검증하는 편이 좋다. 수정 후에는 두 번째 결제 요청의 상태 코드와 저장된 결제 상태를 함께 확인하는 테스트를 추가해야 한다. As per path instructions, "**/*Test*.java: Java 테스트 리뷰 기준이다. 단위 테스트는 경계값/실패 케이스/예외 흐름을 포함하는지 점검한다."

🤖 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
`@apps/commerce-api/src/test/java/com/loopers/interfaces/api/PaymentV1ApiE2ETest.java`
around lines 157 - 173, The payment E2E test currently covers only the TIMEOUT
path, but the NOT_ATTEMPTED contract also needs to be locked down. Extend the
`PaymentV1ApiE2ETest.timeout_keepsPendingWithoutKey` coverage by adding a
`PgRequestResult.notAttempted()` scenario that asserts
`PaymentModel.getTransactionKey()` stays null,
`PaymentModel.isPgRequestAttempted()` remains false, and the follow-up request
for the same order behaves as expected. Use the existing `requestPayment`,
`paymentRepository.findByOrderId`, and `paymentGateway.requestPayment` flow to
verify both the response status and persisted payment state.

Source: Path instructions

apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceUnitTest.java (1)

50-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

실패 콜백 반영 경로도 테스트해야 한다.

현재 applyResult 테스트는 SUCCESS 중심이고, 이번 PR의 핵심인 실패 결과 반영(FAILED + reason) 경로는 비어 있다. 운영에서는 이 경로가 깨져도 결제가 계속 PENDING으로 남아 복구 스케줄러가 불필요하게 재시도할 수 있다. PaymentStatus.FAILED가 들어왔을 때 상태와 사유가 정확히 저장되는 케이스를 추가하는 편이 안전하다. 추가 테스트로 실패 반영 후 뒤늦은 SUCCESS가 와도 상태가 뒤집히지 않는 시나리오까지 함께 검증하는 것이 좋다. As per path instructions, **/*Test*.java: 단위 테스트는 경계값/실패 케이스/예외 흐름을 포함하는지 점검해야 한다.

🤖 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
`@apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceUnitTest.java`
around lines 50 - 100, `PaymentServiceUnitTest.ApplyResult`에서 실패 콜백 반영 경로가 빠져
있으므로, `paymentService.applyResult`에 `PaymentStatus.FAILED`와 `reason`이 들어왔을 때
`PaymentModel`의 상태와 사유가 정확히 저장되는 테스트를 추가하세요. `rejectsAmountMismatch`,
`appliesSuccess`, `reconcilesMissingTransactionKey`, `ignoresWhenAlreadyFinal`와
같은 기존 패턴을 따라 `FAILED` 처리 후 `getStatus()`와 실패 사유가 기대값인지 검증하고, 이어서 뒤늦은 `SUCCESS`가
와도 상태가 `FAILED`에서 뒤집히지 않는 멱등 시나리오도 함께 추가하세요.

Source: Path instructions

apps/pg-simulator/src/main/kotlin/com/loopers/support/error/CoreException.kt (1)

3-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

CoreException가 원인 예외와 내부 문맥을 보존하지 못한다.

현재 구조에서는 customMessage가 곧 RuntimeException.message가 되고, ApiControllerAdvice가 그 값을 응답과 로그에 함께 사용한다. 운영 중 인프라 예외를 감싸는 순간 근본 원인과 내부 진단 문맥이 사라져 장애 분석이 어려워진다. userMessage, logMessage, cause를 분리한 생성자로 바꾸고, 응답에는 사용자 메시지만, 로그에는 내부 메시지와 cause를 사용하도록 분리하는 편이 안전하다. CoreException 래핑 시 cause가 유지되는지와, 핸들러가 사용자 메시지만 응답에 노출하는지 테스트를 추가해야 한다.

🤖 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 `@apps/pg-simulator/src/main/kotlin/com/loopers/support/error/CoreException.kt`
around lines 3 - 6, CoreException currently collapses user-facing text, internal
diagnostics, and the original cause into a single RuntimeException message, so
update the CoreException constructor to accept separate userMessage, logMessage,
and cause values and preserve the cause when wrapping errors. Then adjust
ApiControllerAdvice to return only the userMessage in the API response while
logging the logMessage plus the underlying cause for diagnostics, using the
CoreException and ApiControllerAdvice symbols to locate the changes. Add tests
to verify wrapped exceptions keep their cause and that the handler exposes only
the user-facing message externally.
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt (1)

26-29: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

CoreException 경로에서는 사용자 메시지와 운영 로그를 분리하는 편이 낫다.

지금처럼 4xx성 CoreException마다 사용자 응답 문구와 스택트레이스를 함께 WARN으로 남기면 운영 로그가 과도하게 쌓이고, 요청값이 포함된 메시지가 있으면 그대로 로그에 노출될 수 있다. 예상 가능한 비즈니스 예외는 errorType.code와 식별자 정도만 구조화해서 남기고 스택트레이스는 제외하며, 원인 예외가 있는 5xx만 cause를 보존해 별도로 로깅하는 방식으로 분리하는 편이 안전하다. 추가 테스트로 CoreException 발생 시 응답 메시지는 유지되되, 로그 appender 테스트에서 4xx 경로에 스택트레이스가 찍히지 않는지 확인하는 편이 좋다. As per path instructions, "예외 처리 시 cause를 보존하고, 사용자 메시지와 로그 메시지를 분리하도록 제안한다. 로깅 시 민감정보 노출 가능성을 점검한다."

🤖 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
`@apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt`
around lines 26 - 29, The CoreException handling in ApiControllerAdvice.handle
should separate user-facing failure text from operational logging: keep
failureResponse using e.customMessage, but change the WARN log to record only a
structured business identifier such as e.errorType.code and a minimal identifier
context, without the full exception or stacktrace. Reserve cause-preserving
logging for unexpected 5xx paths in the same advice class, and add/adjust tests
so CoreException still returns the same response while the 4xx log path does not
emit a stacktrace or sensitive request text.

Source: Path instructions

apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentJpaRepository.kt (1)

8-8: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

주문별 정렬을 조회 계약에 포함시키는 편이 안전하다.

현재 시그니처는 정렬 없는 리스트를 반환하게 해서 호출부가 전건 로드 후 메모리에서 updatedAt 기준 정렬을 하게 만든다. 복구 조회가 반복되는 경로에서는 거래 수가 늘수록 불필요한 I/O와 힙 사용이 커진다. findByUserIdAndOrderIdOrderByUpdatedAtDesc(...)처럼 DB 정렬을 계약으로 올리고 호출부 정렬은 제거해주기 바란다. 추가 테스트로 동일 orderId에 여러 거래가 있을 때 최신 updatedAt 순으로 반환되는 저장소 테스트가 필요하다. As per path instructions, **/*Repository*.kt: 정렬/인덱스 활용 가능성, 대량 데이터에서의 성능 병목을 점검한다.

🤖 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
`@apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentJpaRepository.kt`
at line 8, The repository method on PaymentJpaRepository currently exposes an
unsorted query contract, which pushes ordering work into callers. Update
findByUserIdAndOrderId to a derived query that includes DB-side ordering by
updatedAt descending, and remove any in-memory sort at the call site so the
latest payment is returned first by default. Add or update a repository test
around PaymentJpaRepository to verify that multiple Payment records for the same
userId and orderId come back in updatedAt-desc order.

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b1be570-1501-457a-8407-123ea5e929e3

📥 Commits

Reviewing files that changed from the base of the PR and between e2d10a1 and 0e60f65.

📒 Files selected for processing (66)
  • apps/commerce-api/build.gradle.kts
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentCommand.java
  • apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentRecoveryScheduler.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/PaymentCommand.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/PaymentGateway.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/PaymentResult.java
  • apps/commerce-api/src/main/java/com/loopers/domain/payment/CardType.java
  • apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentGateway.java
  • apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentModel.java
  • apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentStatus.java
  • apps/commerce-api/src/main/java/com/loopers/domain/payment/PgPaymentCommand.java
  • apps/commerce-api/src/main/java/com/loopers/domain/payment/PgRequestResult.java
  • apps/commerce-api/src/main/java/com/loopers/domain/payment/PgTransaction.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/FakePaymentGatewayAdapter.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgPaymentGatewayAdapter.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgProperties.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/support/config/PgClientConfig.java
  • apps/commerce-api/src/main/java/com/loopers/support/config/SchedulingConfig.java
  • apps/commerce-api/src/main/resources/application.yml
  • apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentModelTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceUnitTest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/PaymentV1ApiE2ETest.java
  • apps/pg-simulator/README.md
  • apps/pg-simulator/build.gradle.kts
  • apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/OrderInfo.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentCommand.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/TransactionInfo.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/config/web/WebMvcConfig.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/CardType.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentEvent.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentEventPublisher.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRelay.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRepository.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionStatus.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/domain/user/UserInfo.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreEventPublisher.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRelay.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRepository.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentJpaRepository.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiResponse.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentDto.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/event/payment/PaymentEventListener.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/support/error/CoreException.kt
  • apps/pg-simulator/src/main/kotlin/com/loopers/support/error/ErrorType.kt
  • apps/pg-simulator/src/main/resources/application.yml
  • http/http-client.env.json
  • http/pg-simulator/payments.http
  • modules/redis/src/main/resources/redis.yml
  • settings.gradle.kts
💤 Files with no reviewable changes (4)
  • apps/commerce-api/src/main/java/com/loopers/domain/order/PaymentCommand.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/FakePaymentGatewayAdapter.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/PaymentResult.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/PaymentGateway.java

@minbo2002
minbo2002 merged commit a13472b into loopers-labs:minbo2002 Jul 2, 2026
2 checks passed
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.

2 participants