Skip to content

[REFACTOR] 결제 코드 리팩토링#15

Merged
unifolio0 merged 18 commits intodevelopfrom
refactor/#14
Feb 12, 2026
Merged

[REFACTOR] 결제 코드 리팩토링#15
unifolio0 merged 18 commits intodevelopfrom
refactor/#14

Conversation

@unifolio0
Copy link
Contributor

@unifolio0 unifolio0 commented Feb 12, 2026

closed #14

Summary by CodeRabbit

  • 새 기능

    • GitHub 이슈·PR 템플릿 추가로 보고·요청 프로세스 표준화
  • 버그 수정

    • API/결제 에러 메시지 통일 및 일관된 사용자 응답 제공
    • 결제 검증 로직 강화로 실패 케이스 처리 개선
  • 개선사항

    • CI/CD 워크플로우 단순화로 배포 제어 간소화
    • 운영 환경에 시크릿을 환경변수로 연동하여 보안 강화
  • 테스트

    • 결제 관련 단위/통합 테스트 및 테스트 픽스처 추가

@unifolio0 unifolio0 self-assigned this Feb 12, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

Warning

Rate limit exceeded

@unifolio0 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 31 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

결제 도메인·서비스 예외 처리와 메시지를 중앙화하고, 결제 확인/취소 흐름에서 에러 분기와 상태 전이를 정교화했으며, CI 워크플로우의 조건부 빌드 제거, 테스트 픽스처 및 통합 테스트 추가, Docker Compose 구성 재구성 등을 적용했습니다.

Changes

Cohort / File(s) Summary
CI/CD 워크플로우
.github/workflows/.../cd-api-*.yml, .github/workflows/.../cd-internal-*.yml, .github/workflows/.../ci-*-test.yml
각 워크플로우에서 detect-changes job을 제거하여 빌드/테스트 잡이 조건 없이 항상 실행되도록 변경. 일부 prod workflow에 환경변수(WIDGET_SECRET_KEY_PROD) 추가.
GitHub 템플릿 및 PR 템플릿
.github/ISSUE_TEMPLATE/bug_report.md, .github/ISSUE_TEMPLATE/feat-request.md, .github/ISSUE_TEMPLATE/refactor-report.md, .github/PULL_REQUEST_TEMPLATE.md
버그/기능/리팩토링 이슈 및 PR 템플릿 추가(한국어).
로컬 설정 정리
.claude/settings.local.json (삭제), .gitignore
.claude/settings.local.json 제거 및 .claude 디렉토리 .gitignore 추가.
공통 예외 타입 추가
common/src/main/java/.../InternalServerErrorException.java, common/.../NotFoundException.java
HTTP 상태 코드(500,404) 기반 예외 클래스 추가.
API 레이어 예외/메시지 중앙화
api/.../ApiErrorMessage.java, api/.../GlobalExceptionHandler.java, api/.../MemberAuthArgumentResolver.java
하드코딩 메시지를 ApiErrorMessage enum으로 대체하고 예외 핸들러/인증 인자 처리에서 해당 enum과 InternalServerErrorException 사용 및 로깅 보강.
External 모듈 에러 메시지 및 설정
external/.../ExternalErrorMessage.java, external/.../GlobalExceptionHandler.java, external/.../TossPaymentsClientBuilder.java, external/.../application-external.yml
외부 통신용 에러 메시지 enum 추가, 핸들러 메시지 교체, RestClient 변형(메서드 참조) 적용, prod secret을 env로 참조하도록 변경.
Internal(결제) 에러 메시지·핸들러
internal/.../PaymentServiceErrorMessage.java, internal/.../GlobalExceptionHandler.java
결제 서비스 전용 에러 메시지 enum 추가 및 글로벌 예외 핸들러에서 사용하도록 변경.
PaymentFacadeService 제어 흐름 개선
internal/src/.../PaymentFacadeService.java
confirm/cancel 흐름에서 HttpClientErrorException, HttpServerErrorException, ResourceAccessException 분기 처리 로직 추가(핸들러 메서드 분리), 상태 전이 및 예외 매핑을 상세화. 공용 시그니처는 유지.
도메인 에러·검증 개선
domain/.../PaymentErrorMessage.java, domain/.../TosspaymentsPayment.java
결제 검증 에러 메시지 enum 추가, validateTosspaymentsResult에서 metadata 파라미터 제거 및 불일치 시 InternalServerErrorException 사용, 로깅 추가.
서비스 NotFound 처리 개선
internal/.../TosspaymentsPaymentService.java, internal/.../TosspaymentsPaymentResultService.java
read 메서드에서 존재하지 않을 때 IllegalStateException 대신 NotFoundException과 구체적 메시지 사용 및 로깅 추가.
테스트 픽스처 및 유틸 추가
internal/src/test/.../TosspaymentsPaymentFixtureBuilder.java, .../TosspaymentsPaymentResultFixtureBuilder.java
테스트용 빌더 클래스 추가로 테스트 데이터 생성 일관화.
단위·통합 테스트 추가/수정
domain/src/test/.../TosspaymentsPaymentTest.java, .../TosspaymentsPaymentResultTest.java, internal/src/test/.../PaymentFacadeServiceTest.java, .../TosspaymentsTransactionServiceTest.java, .../TosspaymentsPaymentRepositoryTest.java, internal/src/test/.../PaymentControllerTest.java
결제 검증, 상태 변경, 저장소 제약, 확인/취소 흐름(성공/다양한 실패 케이스) 등을 검증하는 테스트 추가 및 일부 테스트 주석 정리.
테스트 도커 컴포즈 / 스크립트 분리 및 경로 보정
test-docker-compose.yml, common/test.yml (서비스 제거), common/run-test-redis.sh, domain/run-test-mysql.sh
Redis 테스트 서비스를 별도 test-docker-compose.yml로 분리, 기존 test.yml에서 서비스 제거, 스크립트에서 절대 경로(git rev-parse) 사용으로 compose 파일 참조 개선.
프로덕션 구성 업데이트
docker/prod/docker-compose-prod.yml, domain/build.gradle
prod docker-compose에 WIDGET_SECRET_KEY_PROD 환경변수 추가, domain 모듈에 implementation project(':common') 의존성 추가.
External → 내부 메시지/핸들러 일관화
external/.../GlobalExceptionHandler.java, api/.../GlobalExceptionHandler.java, internal/.../GlobalExceptionHandler.java
여러 모듈의 글로벌 핸들러에서 하드코딩 메시지를 enum 상수로 대체하고 로깅/메시지 추출 방식을 통일.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant PaymentFacade as PaymentFacadeService
    participant TossClient as TosspaymentsClient
    participant Repo as Repository
    participant DB as Database

    Client->>PaymentFacade: confirmPayment(request)
    PaymentFacade->>Repo: findPaymentById/updateState(NEED_CANCEL or ... )
    PaymentFacade->>TossClient: confirmPayment(request)
    alt Http 200 OK
        TossClient-->>PaymentFacade: successResponse
        PaymentFacade->>Repo: persistResult/updateState(COMPLETED)
        PaymentFacade-->>Client: successResponse
    else Http 4xx (client)
        TossClient-->>PaymentFacade: HttpClientErrorException
        PaymentFacade->>Repo: updateState(SERVER_BAD_REQUEST or CLIENT_BAD_REQUEST)
        PaymentFacade-->>Client: throw BadRequestException / InternalServerErrorException
    else Http 5xx
        TossClient-->>PaymentFacade: HttpServerErrorException
        PaymentFacade->>Repo: updateState(NEED_CANCEL)
        PaymentFacade-->>Client: rethrow (InternalServerErrorException)
    else Network Error
        TossClient-->>PaymentFacade: ResourceAccessException (SocketTimeout)
        PaymentFacade->>Repo: updateState(CONNECTION_TIMEOUT or NEED_CANCEL)
        PaymentFacade-->>Client: rethrow (InternalServerErrorException)
    end
Loading

Estimated code review effort

🎯 4 (복잡) | ⏱️ ~45분

Possibly related issues

  • [REFACTOR] 결제 코드 리팩토링 #14 — 결제 코드 리팩토링 목표와 일치: 결제 도메인·서비스 리팩토링, 에러 메시지/예외 정리, 테스트 보강 등 변경 내용이 해당 이슈 목적과 관련됩니다.

Poem

🐰 나는 토끼, 코드 숲을 훑고
문제는 이름을 얻고, 예외는 길을 찾네.
테스트는 씨앗, 상태는 싹 틔우고,
워크플로우는 한결 가벼워졌네.
당근처럼 단단한 배포로 점프! 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive 연결된 이슈 #14가 구체적인 요구사항 없이 제목만 포함되어 있어 준수 여부를 검증할 수 없습니다. 이슈 #14의 상세한 요구사항이나 체크리스트를 확인하여 코드 변경사항이 모든 요구사항을 충족하는지 검증해주세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 '[REFACTOR] 결제 코드 리팩토링'으로 변경사항의 주요 내용을 명확하게 반영합니다.
Out of Scope Changes check ✅ Passed GitHub 워크플로우 파일 수정, Docker 구성, 설정 파일, 테스트 파일 추가 등이 포함되어 있으며 모두 결제 코드 리팩토링과 관련성이 있습니다.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#14

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

❤️ Share

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

@gemini-code-assist
Copy link

Summary of Changes

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

이 Pull Request는 결제 처리 로직을 대대적으로 리팩토링하여 오류 처리의 일관성과 예외 관리의 명확성을 높이는 데 중점을 두었습니다. 오류 메시지를 중앙 집중화하고, 특정 예외 유형을 도입하며, 다양한 실패 시나리오를 보다 효과적으로 처리하도록 결제 승인 및 취소 흐름을 개선했습니다. 또한, 새로운 GitHub 템플릿 도입과 테스트 환경 설정 업데이트를 통해 개발 워크플로우를 간소화했습니다.

Highlights

  • 오류 처리 리팩토링: API, 외부 서비스, 내부 결제 서비스 전반에 걸쳐 오류 메시지를 열거형(ApiErrorMessage, ExternalErrorMessage, PaymentServiceErrorMessage)으로 중앙 집중화하고, GlobalExceptionHandler를 업데이트하여 일관된 오류 응답을 제공하도록 개선했습니다.
  • 새로운 예외 유형 도입: common 모듈에 InternalServerErrorExceptionNotFoundException을 추가하여 보다 구체적인 오류 처리가 가능하도록 했습니다.
  • 결제 로직 개선: TosspaymentsPayment의 유효성 검사 실패 시 InternalServerErrorException을 사용하도록 변경하고 로깅을 강화했습니다. PaymentFacadeService는 결제 승인 및 취소 과정에서 발생하는 다양한 오류(클라이언트, 서버, 네트워크, 타임아웃)를 더욱 견고하게 처리하고, 이에 따라 결제 상태를 업데이트하도록 로직을 개선했습니다.
  • 테스트 인프라 업데이트: domain/test.yml 파일을 test-docker-compose.yml로 이름을 변경하고, 테스트용 Redis 및 MySQL 실행 스크립트를 업데이트된 파일 이름과 더 견고한 경로 확인 로직을 사용하도록 수정했습니다.
  • GitHub 템플릿 추가: 버그 리포트, 기능 요청, 리팩토링 리포트용 GitHub 이슈 템플릿과 Pull Request 템플릿을 추가하여 개발 워크플로우를 표준화했습니다.
  • 새로운 테스트 케이스 추가: TosspaymentsPayment, TosspaymentsPaymentResult, TosspaymentsPaymentRepository, PaymentFacadeService, TosspaymentsTransactionService에 대한 포괄적인 단위 테스트를 추가하여 결제 유효성 검사, 오류 처리 및 취소 로직을 검증했습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • .claude/settings.local.json
    • Claude 관련 로컬 설정 파일을 제거했습니다.
  • .github/ISSUE_TEMPLATE/bug_report.md
    • 버그 리포트용 GitHub 이슈 템플릿을 추가했습니다.
  • .github/ISSUE_TEMPLATE/feat-request.md
    • 기능 요청용 GitHub 이슈 템플릿을 추가했습니다.
  • .github/ISSUE_TEMPLATE/refactor-report.md
    • 리팩토링 요청용 GitHub 이슈 템플릿을 추가했습니다.
  • .github/PULL_REQUEST_TEMPLATE.md
    • Pull Request 템플릿을 추가했습니다.
  • .gitignore
    • .claude 디렉토리를 Git 추적에서 제외하도록 업데이트했습니다.
  • api/src/main/java/com/samhap/kokomen/global/exception/ApiErrorMessage.java
    • API 관련 오류 메시지를 정의하는 열거형을 추가했습니다.
  • api/src/main/java/com/samhap/kokomen/global/exception/GlobalExceptionHandler.java
    • DefaultMessageSourceResolvable를 임포트하고, MethodArgumentNotValidException, MissingServletRequestParameterException, HttpMessageNotReadableException, Exception 핸들러에서 ApiErrorMessage를 사용하여 오류 메시지를 일관되게 처리하도록 업데이트했습니다.
  • api/src/main/java/com/samhap/kokomen/global/infrastructure/MemberAuthArgumentResolver.java
    • ApiErrorMessageInternalServerErrorException을 임포트하고, 인증 관련 예외 메시지를 ApiErrorMessage 열거형을 사용하도록 변경했습니다.
  • common/run-test-redis.sh
    • 테스트 Redis 컨테이너 실행 스크립트에서 test.yml 대신 test-docker-compose.yml을 사용하도록 경로를 업데이트했습니다.
  • common/src/main/java/com/samhap/kokomen/global/exception/InternalServerErrorException.java
    • 내부 서버 오류를 나타내는 InternalServerErrorException 클래스를 추가했습니다.
  • common/src/main/java/com/samhap/kokomen/global/exception/NotFoundException.java
    • 리소스를 찾을 수 없을 때 발생하는 NotFoundException 클래스를 추가했습니다.
  • common/test.yml
    • 테스트용 Redis Docker Compose 파일을 제거했습니다.
  • docker/prod/docker-compose-prod.yml
    • 프로덕션 환경 변수에 WIDGET_SECRET_KEY_PROD를 추가했습니다.
  • domain/build.gradle
    • common 모듈에 대한 의존성을 추가했습니다.
  • domain/run-test-mysql.sh
    • 테스트 MySQL 컨테이너 실행 스크립트에서 test.yml 대신 test-docker-compose.yml을 사용하도록 경로를 업데이트했습니다.
  • domain/src/main/java/com/samhap/kokomen/payment/domain/PaymentErrorMessage.java
    • 결제 관련 오류 메시지를 정의하는 열거형을 추가했습니다.
  • domain/src/main/java/com/samhap/kokomen/payment/domain/TosspaymentsPayment.java
    • InternalServerErrorException을 임포트하고, validateTosspaymentsResult 메서드에서 metadata 파라미터를 제거하고 PaymentErrorMessage를 사용하여 유효성 검사 실패 시 예외 메시지를 제공하도록 변경했습니다. 또한 로깅을 추가했습니다.
  • domain/src/main/java/com/samhap/kokomen/payment/repository/TosspaymentsPaymentRepository.java
    • 파일 끝에 누락된 개행 문자를 추가했습니다.
  • domain/src/test/java/com/samhap/kokomen/payment/domain/TosspaymentsPaymentResultTest.java
    • TosspaymentsPaymentResult의 취소 정보 업데이트 기능을 테스트하는 단위 테스트를 추가했습니다.
  • domain/src/test/java/com/samhap/kokomen/payment/domain/TosspaymentsPaymentTest.java
    • TosspaymentsPayment의 토스페이먼츠 응답 유효성 검사 및 상태 변경 기능을 테스트하는 단위 테스트를 추가했습니다.
  • external/src/main/java/com/samhap/kokomen/global/exception/ExternalErrorMessage.java
    • 외부 서비스 관련 오류 메시지를 정의하는 열거형을 추가했습니다.
  • external/src/main/java/com/samhap/kokomen/global/exception/GlobalExceptionHandler.java
    • DefaultMessageSourceResolvable를 임포트하고, MethodArgumentNotValidException, MissingServletRequestParameterException, HttpMessageNotReadableException, Exception 핸들러에서 ExternalErrorMessage를 사용하여 오류 메시지를 일관되게 처리하도록 업데이트했습니다.
  • external/src/main/java/com/samhap/kokomen/payment/external/TossPaymentsClientBuilder.java
    • 메시지 컨버터 제거 로직을 MappingJackson2HttpMessageConverter.class::isInstance를 사용하도록 개선했습니다.
  • external/src/main/resources/application-external.yml
    • 토스페이먼츠 위젯 시크릿 키를 환경 변수 ${WIDGET_SECRET_KEY_PROD}를 통해 주입받도록 변경했습니다.
  • internal/src/main/java/com/samhap/kokomen/global/exception/GlobalExceptionHandler.java
    • DefaultMessageSourceResolvable를 임포트하고, MethodArgumentNotValidException, HttpMessageNotReadableException, Exception 핸들러에서 PaymentServiceErrorMessage를 사용하여 오류 메시지를 일관되게 처리하도록 업데이트했습니다.
  • internal/src/main/java/com/samhap/kokomen/global/exception/PaymentServiceErrorMessage.java
    • 결제 서비스 관련 오류 메시지를 정의하는 열거형을 추가했습니다.
  • internal/src/main/java/com/samhap/kokomen/payment/service/PaymentFacadeService.java
    • InternalServerErrorException, KokomenException, PaymentServiceErrorMessage를 임포트하고, confirmPaymentcancelPayment 메서드에서 다양한 예외(클라이언트, 서버, 네트워크 오류 및 타임아웃)를 더 세분화하여 처리하고, 이에 따라 결제 상태를 업데이트하도록 로직을 개선했습니다.
  • internal/src/main/java/com/samhap/kokomen/payment/service/TosspaymentsPaymentResultService.java
    • NotFoundExceptionPaymentServiceErrorMessage를 임포트하고, readByTosspaymentsPaymentId 메서드에서 예외 발생 시 NotFoundException을 사용하도록 변경하고 로깅을 추가했습니다.
  • internal/src/main/java/com/samhap/kokomen/payment/service/TosspaymentsPaymentService.java
    • NotFoundExceptionPaymentServiceErrorMessage를 임포트하고, readByIdreadByPaymentKey 메서드에서 예외 발생 시 NotFoundException을 사용하도록 변경하고 로깅을 추가했습니다.
  • internal/src/test/java/com/samhap/kokomen/global/fixture/TosspaymentsPaymentFixtureBuilder.java
    • 테스트용 TosspaymentsPayment 객체를 생성하는 픽스처 빌더 클래스를 추가했습니다.
  • internal/src/test/java/com/samhap/kokomen/global/fixture/TosspaymentsPaymentResultFixtureBuilder.java
    • 테스트용 TosspaymentsPaymentResult 객체를 생성하는 픽스처 빌더 클래스를 추가했습니다.
  • internal/src/test/java/com/samhap/kokomen/payment/controller/PaymentControllerTest.java
    • 결제 컨트롤러 테스트에서 불필요한 주석을 제거했습니다.
  • internal/src/test/java/com/samhap/kokomen/payment/repository/TosspaymentsPaymentRepositoryTest.java
    • TosspaymentsPaymentRepository의 중복 paymentKeyorderId 저장 시 예외 발생을 테스트하는 단위 테스트를 추가했습니다.
  • internal/src/test/java/com/samhap/kokomen/payment/service/PaymentFacadeServiceTest.java
    • PaymentFacadeService의 결제 승인 및 취소 로직, 다양한 오류 시나리오 처리를 테스트하는 단위 테스트를 추가했습니다.
  • internal/src/test/java/com/samhap/kokomen/payment/service/TosspaymentsTransactionServiceTest.java
    • TosspaymentsTransactionService의 취소 결과 적용 로직을 테스트하는 단위 테스트를 추가했습니다.
  • test-docker-compose.yml
    • domain/test.yml 파일이 test-docker-compose.yml로 이름이 변경되었고, 테스트용 Redis 서비스 정의가 추가되었습니다.
Ignored Files
  • Ignored by pattern: .github/workflows/** (6)
    • .github/workflows/cd-api-dev.yml
    • .github/workflows/cd-api-prod.yml
    • .github/workflows/cd-internal-dev.yml
    • .github/workflows/cd-internal-prod.yml
    • .github/workflows/ci-api-test.yml
    • .github/workflows/ci-internal-test.yml
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.

@github-actions
Copy link

github-actions bot commented Feb 12, 2026

Test Results

1 tests   1 ✅  0s ⏱️
1 suites  0 💤
1 files    0 ❌

Results for commit 2f29348.

♻️ This comment has been updated with latest results.

Copy link

@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 PR performs a large-scale refactoring of payment-related code. However, it introduces significant security risks related to broken access control and insecure configuration in the payment module. The internal payment service lacks ownership checks for payment cancellation and confirmation and is exposed to the host network without authentication, potentially allowing attackers to manipulate or cancel payments. It is highly recommended to implement proper authorization checks and restrict access to internal services to mitigate these risks. On the code quality front, error handling logic is robust, and consistency and maintainability have improved with standardized exceptions and error messages, along with extensive high-quality test coverage. A few minor improvements were also suggested during the review.

}
}

public void cancelPayment(CancelRequest request) {

Choose a reason for hiding this comment

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

security-high high

The cancelPayment method accepts a paymentKey and proceeds to cancel the payment without verifying if the payment belongs to the authenticated user. This is an Insecure Direct Object Reference (IDOR) vulnerability. An attacker who knows or guesses a paymentKey can cancel any user's payment, leading to a Denial of Service on the payment system.

Comment on lines 56 to 62
if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
String fieldName = invalidFormatException.getPath().get(0).getFieldName();
String invalidValue = String.valueOf(invalidFormatException.getValue());
message = String.format(
"JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: '%s')",
fieldName,
invalidValue
);
log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(ApiErrorMessage.JSON_PARSE_ERROR.getMessage()));
}

Choose a reason for hiding this comment

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

medium

InvalidFormatException 발생 시 API 응답으로 일반적인 오류 메시지를 반환하도록 변경되었는데, 이는 API를 사용하는 개발자 입장에서 디버깅을 어렵게 만들 수 있습니다.

이전 구현처럼 어떤 필드에서 어떤 값 때문에 오류가 발생했는지에 대한 구체적인 정보를 포함하여 응답하는 것을 고려해 보세요. 민감한 정보를 노출하지 않는 선에서, 개발자에게 유용한 정보를 제공하는 것이 좋습니다.

예를 들어, 다음과 같이 상세 메시지를 생성하여 반환할 수 있습니다.

if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
    String fieldName = invalidFormatException.getPath().get(0).getFieldName();
    String invalidValue = String.valueOf(invalidFormatException.getValue());
    log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
    String detailedMessage = String.format("JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: %s)", fieldName, invalidValue);
    return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .body(new ErrorResponse(detailedMessage));
}
Suggested change
if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
String fieldName = invalidFormatException.getPath().get(0).getFieldName();
String invalidValue = String.valueOf(invalidFormatException.getValue());
message = String.format(
"JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: '%s')",
fieldName,
invalidValue
);
log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(ApiErrorMessage.JSON_PARSE_ERROR.getMessage()));
}
if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
String fieldName = invalidFormatException.getPath().get(0).getFieldName();
String invalidValue = String.valueOf(invalidFormatException.getValue());
log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
String detailedMessage = String.format("JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: %s)", fieldName, invalidValue);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(detailedMessage));
}

Comment on lines +1 to +19
package com.samhap.kokomen.global.exception;

import lombok.Getter;

@Getter
public enum ExternalErrorMessage {

INVALID_REQUEST("잘못된 요청입니다."),
MISSING_REQUEST_PARAMETER("필수 요청 파라미터가 누락되었습니다."),
INVALID_REQUEST_FORMAT("잘못된 요청 형식입니다. JSON 형식을 확인해주세요."),
JSON_PARSE_ERROR("JSON 파싱 오류: 유효하지 않은 값이 전달되었습니다."),
INTERNAL_SERVER_ERROR("서버에 문제가 발생하였습니다.");

private final String message;

ExternalErrorMessage(String message) {
this.message = message;
}
}

Choose a reason for hiding this comment

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

medium

ExternalErrorMessage 열거형의 내용이 api 모듈의 ApiErrorMessage와 거의 중복됩니다. 이러한 공통 오류 메시지는 common 모듈로 옮겨서 중복을 제거하는 것이 좋겠습니다.

예를 들어, common 모듈에 CommonErrorMessage를 만들고, 각 모듈에서 이를 사용하거나 확장하는 방식을 고려해볼 수 있습니다. 이렇게 하면 향후 오류 메시지를 일관되게 관리하기 용이해집니다.

Comment on lines 56 to 62
if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
String fieldName = invalidFormatException.getPath().get(0).getFieldName();
String invalidValue = String.valueOf(invalidFormatException.getValue());
message = String.format(
"JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: '%s')",
fieldName,
invalidValue
);
log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(ExternalErrorMessage.JSON_PARSE_ERROR.getMessage()));
}

Choose a reason for hiding this comment

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

medium

api 모듈의 GlobalExceptionHandler와 동일한 문제입니다. InvalidFormatException 발생 시 API 응답으로 일반적인 오류 메시지를 반환하고 있어 디버깅이 어려울 수 있습니다.

어떤 필드에서 오류가 발생했는지 구체적인 정보를 응답에 포함하는 것을 고려해 보세요.

if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
    String fieldName = invalidFormatException.getPath().get(0).getFieldName();
    String invalidValue = String.valueOf(invalidFormatException.getValue());
    log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
    String detailedMessage = String.format("JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: %s)", fieldName, invalidValue);
    return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .body(new ErrorResponse(detailedMessage));
}
Suggested change
if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
String fieldName = invalidFormatException.getPath().get(0).getFieldName();
String invalidValue = String.valueOf(invalidFormatException.getValue());
message = String.format(
"JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: '%s')",
fieldName,
invalidValue
);
log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(ExternalErrorMessage.JSON_PARSE_ERROR.getMessage()));
}
if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
String fieldName = invalidFormatException.getPath().get(0).getFieldName();
String invalidValue = String.valueOf(invalidFormatException.getValue());
log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
String detailedMessage = String.format("JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: %s)", fieldName, invalidValue);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(detailedMessage));
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@api/src/main/java/com/samhap/kokomen/global/exception/ApiErrorMessage.java`:
- Line 9: The enum constant LOGIN_REQUIRED in ApiErrorMessage is missing a
trailing period; update the enum entry LOGIN_REQUIRED("로그인이 필요합니다") to include a
final period in its message so it matches the punctuation style of the other
enum values (e.g., LOGIN_REQUIRED("로그인이 필요합니다.")). Ensure no other enum usages
or tests rely on the exact string without the period.

In
`@api/src/main/java/com/samhap/kokomen/global/exception/GlobalExceptionHandler.java`:
- Around line 56-62: In GlobalExceptionHandler where you inspect
InvalidFormatException (the block using e.getCause() instanceof
InvalidFormatException invalidFormatException), guard against an empty path
before calling invalidFormatException.getPath().get(0): check if
invalidFormatException.getPath() is null or empty and only extract fieldName and
value when present; otherwise set a safe fallback (e.g., "unknown" or omit
fieldName) and still log the invalidValue. Update the log.warn call to use the
fallback fieldName and keep returning the same BAD_REQUEST ErrorResponse so
behavior is preserved.

In `@domain/build.gradle`:
- Line 2: Change the dependency declaration in the domain module from an
implementation to an api dependency so types from common are exposed to domain
consumers: replace the current implementation project(':common') in domain's
build.gradle with an api configuration; this ensures the domain method
TosspaymentsPayment.validateTosspaymentsResult() can legitimately throw and
expose common.InternalServerErrorException to modules depending on domain.

In
`@external/src/main/java/com/samhap/kokomen/global/exception/GlobalExceptionHandler.java`:
- Around line 56-62: In GlobalExceptionHandler where you handle
HttpMessageNotReadableException and cast e.getCause() to InvalidFormatException
(invalidFormatException), guard against an empty path before calling
invalidFormatException.getPath().get(0) by checking
invalidFormatException.getPath() != null &&
!invalidFormatException.getPath().isEmpty(); if empty, use a safe fallback
(e.g., "unknown" or skip fieldName/invalidValue extraction) and still log and
return the same ErrorResponse; update the logic around fieldName and
invalidValue extraction in that block to avoid IndexOutOfBoundsException.

In
`@internal/src/main/java/com/samhap/kokomen/payment/service/PaymentFacadeService.java`:
- Around line 69-82: handleConfirmClientError (and the similar logic in
cancelPayment) can NPE when e.getResponseBodyAs(Failure.class) returns null; add
a null check after calling getResponseBodyAs and treat a null Failure as an
unknown/empty response by (1) logging that the response body was missing or
unparsable along with the original exception, (2) using a safe default for
code/message (e.g. "UNKNOWN" and e.getMessage() or a default message) when
evaluating TosspaymentsInternalServerErrorCode and when creating the returned
exception, and (3) still updating tosspaymentsPaymentService.updateState(...)
appropriately (choose SERVER_BAD_REQUEST only if code matches, otherwise
CLIENT_BAD_REQUEST or a generic error state). Apply the same null-guard pattern
to the cancelPayment handling at the referenced lines.
- Around line 96-108: handleConfirmNetworkError currently updates state only for
SocketTimeoutException and leaves other network failures (e.g.,
ConnectException, UnknownHostException, generic IOException) unhandled, so
confirmPayment rethrows without state change; modify handleConfirmNetworkError
to detect other network-related root causes (check e.getRootCause() instanceof
ConnectException, UnknownHostException or IOException) and call
tosspaymentsPaymentService.updateState(tosspaymentsPayment.getId(),
PaymentState.CONNECTION_TIMEOUT) (or PaymentState.NEED_CANCEL where appropriate)
before allowing the exception to propagate; ensure the method
(handleConfirmNetworkError) performs the state update for all network IO
failures so confirmPayment’s exception path doesn’t leave the payment in the
initial state.
🧹 Nitpick comments (9)
common/run-test-redis.sh (1)

1-30: run-test-mysql.shrun-test-redis.sh의 헬스체크 로직 중복 고려.

두 스크립트의 헬스체크 대기 루프가 거의 동일한 구조입니다. 향후 서비스가 더 추가될 경우, 공통 헬스체크 함수를 별도 스크립트로 추출하면 유지보수가 편해질 수 있습니다. 현재 수준에서는 허용 가능한 중복입니다.

.github/workflows/cd-internal-prod.yml (1)

82-93: REDIS_PRIMARY_HOST_PROD 환경 변수가 deploy 단계의 env 블록에 누락되어 있습니다.

docker-compose-prod.yml에서 kokomen-payment-apikokomen-payment-internal 모두 REDIS_PRIMARY_HOST_PROD를 참조하고 있지만, 이 워크플로우의 env 블록(Line 84-89)에는 해당 변수가 포함되어 있지 않습니다. 이 변수가 self-hosted runner의 시스템 환경에 별도로 설정되어 있다면 문제없겠지만, 그렇지 않다면 Redis 연결이 실패할 수 있습니다.

이 PR에서 추가된 변경은 아니지만, 관련 블록을 수정하는 김에 함께 확인해 보시면 좋겠습니다.

common/src/main/java/com/samhap/kokomen/global/exception/NotFoundException.java (1)

3-7: InternalServerErrorException와의 일관성 부족: Throwable cause 생성자 누락 및 들여쓰기 차이.

InternalServerErrorException(String message, Throwable cause) 생성자를 제공하지만, NotFoundException에는 해당 생성자가 없습니다. 원인 체이닝이 필요한 경우를 대비해 추가를 고려해주세요. 또한 들여쓰기가 2칸으로, InternalServerErrorException의 4칸과 다릅니다.

external/src/main/java/com/samhap/kokomen/global/exception/ExternalErrorMessage.java (1)

8-12: ApiErrorMessage와 메시지 중복.

INVALID_REQUEST, MISSING_REQUEST_PARAMETER, INVALID_REQUEST_FORMAT, JSON_PARSE_ERROR, INTERNAL_SERVER_ERROR 5개 항목이 ApiErrorMessage와 동일한 메시지 문자열을 가지고 있습니다. 추후 메시지 변경 시 동기화 누락 위험이 있으므로, 공통 메시지를 common 모듈로 추출하는 것을 고려해주세요.

api/src/main/java/com/samhap/kokomen/global/infrastructure/MemberAuthArgumentResolver.java (2)

37-38: getNativeRequest()null을 반환할 경우 NPE 발생 가능.

webRequest.getNativeRequest(HttpServletRequest.class)는 이론적으로 null을 반환할 수 있으며, Line 38에서 request.getSession(false) 호출 시 NPE가 발생합니다. Spring MVC 환경에서는 일반적으로 null이 아니지만, 방어적 null 체크 또는 Objects.requireNonNull 사용을 고려해주세요.


56-62: memberIdnull이고 인증이 필수가 아닌 경우에도 error 레벨로 로깅.

Line 57-58에서 memberId == null이면 authenticationRequired 여부와 관계없이 log.error를 호출합니다. 선택적 인증(optional auth) 시 세션은 있지만 MEMBER_ID가 없는 것이 정상 시나리오일 수 있다면, authenticationRequired에 따라 로그 레벨을 분리하는 것이 적절합니다.

♻️ 수정 제안
     private void validateAuthentication(Long memberId, boolean authenticationRequired) {
-        if (memberId == null) {
-            log.error(ApiErrorMessage.MEMBER_ID_NOT_IN_SESSION.getMessage());
-        }
         if (memberId == null && authenticationRequired) {
+            log.error(ApiErrorMessage.MEMBER_ID_NOT_IN_SESSION.getMessage());
             throw new UnauthorizedException(ApiErrorMessage.MEMBER_ID_NOT_IN_SESSION.getMessage());
+        } else if (memberId == null) {
+            log.warn(ApiErrorMessage.MEMBER_ID_NOT_IN_SESSION.getMessage());
         }
     }
domain/src/test/java/com/samhap/kokomen/payment/domain/TosspaymentsPaymentResultTest.java (1)

13-21: TosspaymentsPaymentResult 생성 시 23개의 파라미터 사용 — Fixture Builder 도입 고려

TosspaymentsPaymentRepository 테스트에서는 TosspaymentsPaymentFixtureBuilder를 사용하고 있지만, 여기서는 23개의 생성자 파라미터를 직접 전달하고 있어 가독성이 떨어지고 유지보수가 어렵습니다. TosspaymentsPaymentResultFixtureBuilder 도입을 고려해 보세요.

domain/src/main/java/com/samhap/kokomen/payment/domain/TosspaymentsPayment.java (1)

17-19: 도메인 엔티티에 @Slf4j 사용 — 설계 트레이드오프 인지

도메인 엔티티에 로깅 프레임워크 의존성을 추가하는 것은 순수 도메인 모델 원칙에서 벗어나지만, 결제 검증 실패라는 심각한 상황에서의 진단 로깅이라는 점에서 실용적인 선택입니다. 향후 도메인 순수성이 중요해지면, 검증 로직을 도메인 서비스로 분리하는 것도 방법입니다.

internal/src/main/java/com/samhap/kokomen/global/exception/PaymentServiceErrorMessage.java (1)

6-16: api 모듈의 ApiErrorMessage와 중복되는 에러 메시지 상수 존재

INVALID_REQUEST, INTERNAL_SERVER_ERROR 상수가 api 모듈의 ApiErrorMessage에 동일한 메시지로 존재합니다. 두 모듈에서 이 메시지들을 공통으로 사용한다면, common 모듈로 추출하여 일관성을 유지하는 것을 권장합니다. 다만 INVALID_REQUEST_FORMAT은 각 모듈에서 제공하는 메시지가 다르므로(API 모듈은 JSON 형식 안내 포함), 필요에 따라 구분해서 사용할 수 있습니다.

public enum ApiErrorMessage {

AUTHENTICATION_ANNOTATION_REQUIRED("MemberAuth 파라미터는 @Authentication 어노테이션이 있어야 합니다."),
LOGIN_REQUIRED("로그인이 필요합니다"),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

LOGIN_REQUIRED 메시지에 마침표(.) 누락.

다른 메시지들은 모두 마침표로 끝나지만, LOGIN_REQUIRED("로그인이 필요합니다")만 마침표가 없습니다.

✏️ 수정 제안
-    LOGIN_REQUIRED("로그인이 필요합니다"),
+    LOGIN_REQUIRED("로그인이 필요합니다."),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LOGIN_REQUIRED("로그인이 필요합니다"),
LOGIN_REQUIRED("로그인이 필요합니다."),
🤖 Prompt for AI Agents
In `@api/src/main/java/com/samhap/kokomen/global/exception/ApiErrorMessage.java`
at line 9, The enum constant LOGIN_REQUIRED in ApiErrorMessage is missing a
trailing period; update the enum entry LOGIN_REQUIRED("로그인이 필요합니다") to include a
final period in its message so it matches the punctuation style of the other
enum values (e.g., LOGIN_REQUIRED("로그인이 필요합니다.")). Ensure no other enum usages
or tests rely on the exact string without the period.

Comment on lines 56 to 62
if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
String fieldName = invalidFormatException.getPath().get(0).getFieldName();
String invalidValue = String.valueOf(invalidFormatException.getValue());
message = String.format(
"JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: '%s')",
fieldName,
invalidValue
);
log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(ApiErrorMessage.JSON_PARSE_ERROR.getMessage()));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

invalidFormatException.getPath().get(0)에서 IndexOutOfBoundsException 발생 가능

InvalidFormatExceptiongetPath()가 빈 리스트를 반환하는 경우 get(0) 호출 시 IndexOutOfBoundsException이 발생할 수 있습니다. path가 비어있는 경우에 대한 방어 로직이 필요합니다.

🛡️ 방어 코드 제안
     if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
-        String fieldName = invalidFormatException.getPath().get(0).getFieldName();
-        String invalidValue = String.valueOf(invalidFormatException.getValue());
-        log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
+        var path = invalidFormatException.getPath();
+        if (!path.isEmpty()) {
+            String fieldName = path.get(0).getFieldName();
+            String invalidValue = String.valueOf(invalidFormatException.getValue());
+            log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
+        } else {
+            log.warn("HttpMessageNotReadableException :: invalidValue: {}", invalidFormatException.getValue());
+        }
         return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                 .body(new ErrorResponse(ApiErrorMessage.JSON_PARSE_ERROR.getMessage()));
     }
🤖 Prompt for AI Agents
In
`@api/src/main/java/com/samhap/kokomen/global/exception/GlobalExceptionHandler.java`
around lines 56 - 62, In GlobalExceptionHandler where you inspect
InvalidFormatException (the block using e.getCause() instanceof
InvalidFormatException invalidFormatException), guard against an empty path
before calling invalidFormatException.getPath().get(0): check if
invalidFormatException.getPath() is null or empty and only extract fieldName and
value when present; otherwise set a safe fallback (e.g., "unknown" or omit
fieldName) and still log the invalidValue. Update the log.warn call to use the
fallback fieldName and keep returning the same BAD_REQUEST ErrorResponse so
behavior is preserved.

@@ -1,4 +1,5 @@
dependencies {
implementation project(':common')
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== Checking domain module imports from common ==="
if [ -d "domain/src/main" ]; then
  find domain/src/main -type f \( -name "*.java" -o -name "*.kt" \) -exec grep -l "import.*common\." {} \; 2>/dev/null || echo "No imports found"
else
  echo "domain/src/main directory not found"
fi

echo ""
echo "=== All modules that depend on common ==="
rg -n "project.*:common" --glob 'build.gradle*' 

echo ""
echo "=== Domain module build.gradle content ==="
if [ -f "domain/build.gradle" ]; then
  cat -n domain/build.gradle
fi

echo ""
echo "=== Checking domain module structure ==="
find domain/src -type f -name "*.java" -o -name "*.kt" | head -20

Repository: samhap-soft/kokomen-payment

Length of output: 1930


🏁 Script executed:

#!/bin/bash

echo "=== Common module structure and content ==="
if [ -d "common/src/main" ]; then
  find common/src/main -type f -name "*.java" -o -name "*.kt" | head -20
fi

echo ""
echo "=== Checking all imports in domain module files ==="
find domain/src/main -type f \( -name "*.java" -o -name "*.kt" \) -exec echo "File: {}" \; -exec head -30 {} \; | head -100

echo ""
echo "=== Looking for any reference to 'common' in domain ==="
rg -r "common" domain/src/main --no-filename 2>/dev/null || echo "No references found"

echo ""
echo "=== Checking domain BaseEntity class ==="
cat domain/src/main/java/com/samhap/kokomen/global/domain/BaseEntity.java

Repository: samhap-soft/kokomen-payment

Length of output: 5215


🏁 Script executed:

#!/bin/bash

echo "=== Full TosspaymentsPayment class to check public API signatures ==="
cat domain/src/main/java/com/samhap/kokomen/payment/domain/TosspaymentsPayment.java

echo ""
echo "=== Checking TosspaymentsPaymentResult class ==="
cat domain/src/main/java/com/samhap/kokomen/payment/domain/TosspaymentsPaymentResult.java

echo ""
echo "=== Checking repository interfaces ==="
cat domain/src/main/java/com/samhap/kokomen/payment/repository/TosspaymentsPaymentRepository.java

Repository: samhap-soft/kokomen-payment

Length of output: 8929


api 대신 implementation 사용으로 인한 API 노출 문제

domain 모듈의 TosspaymentsPayment.validateTosspaymentsResult() 메서드는 common 모듈의 InternalServerErrorException을 throws합니다. 이는 public API의 일부이므로, domain에 의존하는 모듈들이 이 예외 타입에 접근할 수 있어야 합니다.

현재 implementation으로 선언했기 때문에 common 모듈 타입이 transitively 노출되지 않습니다. 다행히 모든 소비 모듈(internal, external, api)이 이미 common을 직접 의존하고 있어 실제 문제는 없지만, 의존성 구조 관점에서는 api를 사용하는 것이 더 적절합니다.

🤖 Prompt for AI Agents
In `@domain/build.gradle` at line 2, Change the dependency declaration in the
domain module from an implementation to an api dependency so types from common
are exposed to domain consumers: replace the current implementation
project(':common') in domain's build.gradle with an api configuration; this
ensures the domain method TosspaymentsPayment.validateTosspaymentsResult() can
legitimately throw and expose common.InternalServerErrorException to modules
depending on domain.

Comment on lines 56 to 62
if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
String fieldName = invalidFormatException.getPath().get(0).getFieldName();
String invalidValue = String.valueOf(invalidFormatException.getValue());
message = String.format(
"JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: '%s')",
fieldName,
invalidValue
);
log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(ExternalErrorMessage.JSON_PARSE_ERROR.getMessage()));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

invalidFormatException.getPath()가 비어있을 경우 IndexOutOfBoundsException 발생 가능.

getPath().get(0)은 path 리스트가 비어있을 때 IndexOutOfBoundsException을 발생시킵니다. 방어적으로 빈 리스트 체크를 추가하는 것을 권장합니다.

🛡️ 수정 제안
     if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
-            String fieldName = invalidFormatException.getPath().get(0).getFieldName();
+            var path = invalidFormatException.getPath();
+            String fieldName = path.isEmpty() ? "unknown" : path.get(0).getFieldName();
             String invalidValue = String.valueOf(invalidFormatException.getValue());
🤖 Prompt for AI Agents
In
`@external/src/main/java/com/samhap/kokomen/global/exception/GlobalExceptionHandler.java`
around lines 56 - 62, In GlobalExceptionHandler where you handle
HttpMessageNotReadableException and cast e.getCause() to InvalidFormatException
(invalidFormatException), guard against an empty path before calling
invalidFormatException.getPath().get(0) by checking
invalidFormatException.getPath() != null &&
!invalidFormatException.getPath().isEmpty(); if empty, use a safe fallback
(e.g., "unknown" or skip fieldName/invalidValue extraction) and still log and
return the same ErrorResponse; update the logic around fieldName and
invalidValue extraction in that block to avoid IndexOutOfBoundsException.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@internal/src/test/java/com/samhap/kokomen/payment/service/PaymentFacadeServiceTest.java`:
- Around line 237-277: Cancel flow lacks the catch-all exception handler present
in confirmPayment, so add a generic Exception catch in
PaymentFacadeService.cancelPayment that mirrors confirmPayment's behavior: set
payment status to NEED_CANCEL (use the same NEED_CANCEL enum/constant) and throw
an InternalServerErrorException with the same error message/handling used for
unexpected errors in confirmPayment; then add a unit test (e.g.,
결제_취소_시_예상치_못한_예외가_발생하면_InternalServerErrorException을_던진다) in
PaymentFacadeServiceTest that makes tosspaymentsClient.cancelPayment throw a
RuntimeException and asserts InternalServerErrorException is thrown and the
payment status transitions to NEED_CANCEL.
🧹 Nitpick comments (3)
internal/src/main/java/com/samhap/kokomen/payment/service/PaymentFacadeService.java (2)

89-99: handleConfirmServerError에서 getResponseBodyAs 반환값 null 가능성

Line 92의 e.getResponseBodyAs(TosspaymentsPaymentResponse.class)null을 반환하면 Line 93에서 NPE가 발생합니다. 현재 catch (Exception parseException) 블록으로 잡히기 때문에 기능적으로는 안전하지만, 파싱 실패가 아닌 NPE로 처리되는 것은 의도가 불명확합니다.

♻️ 명시적 null 체크 추가 제안
     try {
         TosspaymentsPaymentResponse tosspaymentsConfirmResponse = e.getResponseBodyAs(TosspaymentsPaymentResponse.class);
+        if (tosspaymentsConfirmResponse == null) {
+            throw new IllegalStateException("5xx 응답 본문이 비어 있습니다.");
+        }
         TosspaymentsPaymentResult tosspaymentsPaymentResult = tosspaymentsConfirmResponse.toTosspaymentsPaymentResult(tosspaymentsPayment);

117-137: cancelPayment의 400 에러 처리가 confirmPayment와 불일치합니다.

confirmPayment에서는 TosspaymentsInternalServerErrorCode.contains(code)로 서버 원인 400과 클라이언트 원인 400을 구분하여, 서버 원인이면 InternalServerErrorException, 클라이언트 원인이면 BadRequestException을 던집니다. 반면 cancelPayment에서는 모든 non-null FailureBadRequestException으로 처리합니다.

취소 시에도 INVALID_API_KEY 같은 서버 원인 에러 코드가 반환될 수 있으므로, confirmPayment와 동일한 분기 로직을 적용하거나 공통 헬퍼로 추출하는 것을 권장합니다.

♻️ 공통 헬퍼 추출 제안
     } catch (HttpClientErrorException e) {
         Failure failure = e.getResponseBodyAs(Failure.class);
         if (failure == null) {
             log.error("결제 취소 실패(400) - 응답 파싱 실패, paymentKey: {}", request.paymentKey(), e);
             throw new InternalServerErrorException(PaymentServiceErrorMessage.CANCEL_SERVER_ERROR.getMessage(), e);
         }
-        log.error("결제 취소 실패(400) - paymentKey: {}, code: {}, message: {}", request.paymentKey(), failure.code(), failure.message());
-        throw new BadRequestException(failure.message(), e);
+        if (TosspaymentsInternalServerErrorCode.contains(failure.code())) {
+            log.error("결제 취소 실패(서버 원인 400) - paymentKey: {}, code: {}, message: {}", request.paymentKey(), failure.code(), failure.message());
+            throw new InternalServerErrorException(PaymentServiceErrorMessage.CANCEL_SERVER_ERROR.getMessage(), e);
+        }
+        log.info("결제 취소 실패(클라이언트 원인 400) - paymentKey: {}, code: {}, message: {}", request.paymentKey(), failure.code(), failure.message());
+        throw new BadRequestException(failure.message(), e);

또한, Line 128의 클라이언트 원인 400 에러 로그 레벨이 error로 되어 있습니다. confirmPayment에서는 클라이언트 원인 400을 log.info(Line 84)로 기록하고 있으므로, 일관성을 위해 로그 레벨도 통일하는 것이 좋습니다.

internal/src/test/java/com/samhap/kokomen/payment/service/PaymentFacadeServiceTest.java (1)

198-235: 취소 성공 테스트의 응답 객체 구성을 헬퍼로 추출하면 가독성이 향상됩니다.

TosspaymentsCancelTosspaymentsPaymentResponse를 인라인으로 생성하고 있어 테스트 의도 파악이 어렵습니다. 승인 테스트처럼 createCancelResponse() 같은 헬퍼 메서드로 추출하면 가독성과 유지보수성이 향상됩니다.

Comment on lines +237 to +277
@Test
void 결제_취소_시_400_에러가_발생하면_BadRequestException을_던진다() {
HttpClientErrorException clientError = mock(HttpClientErrorException.class);
when(clientError.getResponseBodyAs(Failure.class))
.thenReturn(new Failure("ALREADY_CANCELED_PAYMENT", "이미 취소된 결제입니다."));
when(tosspaymentsClient.cancelPayment(any(), any())).thenThrow(clientError);

assertThatThrownBy(() -> paymentFacadeService.cancelPayment(new CancelRequest("payment_key", "단순 변심")))
.isInstanceOf(BadRequestException.class);
}

@Test
void 결제_취소_시_400_에러_응답_파싱에_실패하면_InternalServerErrorException을_던진다() {
HttpClientErrorException clientError = mock(HttpClientErrorException.class);
when(clientError.getResponseBodyAs(Failure.class)).thenReturn(null);
when(tosspaymentsClient.cancelPayment(any(), any())).thenThrow(clientError);

assertThatThrownBy(() -> paymentFacadeService.cancelPayment(new CancelRequest("payment_key", "단순 변심")))
.isInstanceOf(InternalServerErrorException.class)
.hasMessage(PaymentServiceErrorMessage.CANCEL_SERVER_ERROR.getMessage());
}

@Test
void 결제_취소_시_5xx_에러가_발생하면_InternalServerErrorException을_던진다() {
when(tosspaymentsClient.cancelPayment(any(), any()))
.thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR));

assertThatThrownBy(() -> paymentFacadeService.cancelPayment(new CancelRequest("payment_key", "단순 변심")))
.isInstanceOf(InternalServerErrorException.class)
.hasMessage(PaymentServiceErrorMessage.CANCEL_SERVER_ERROR.getMessage());
}

@Test
void 결제_취소_시_네트워크_에러가_발생하면_InternalServerErrorException을_던진다() {
when(tosspaymentsClient.cancelPayment(any(), any()))
.thenThrow(new ResourceAccessException("네트워크 오류"));

assertThatThrownBy(() -> paymentFacadeService.cancelPayment(new CancelRequest("payment_key", "단순 변심")))
.isInstanceOf(InternalServerErrorException.class)
.hasMessage(PaymentServiceErrorMessage.CANCEL_NETWORK_ERROR.getMessage());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the test file and check its structure
find . -name "PaymentFacadeServiceTest.java" -type f

Repository: samhap-soft/kokomen-payment

Length of output: 160


🏁 Script executed:

#!/bin/bash
# Locate the PaymentFacadeService implementation
find . -name "PaymentFacadeService.java" -type f | grep -v Test

Repository: samhap-soft/kokomen-payment

Length of output: 156


🏁 Script executed:

#!/bin/bash
# Search for the approval flow test mentioned in the comment
rg -n "결제_승인_시_예상치_못한_예외가_발생하면" --type=java

Repository: samhap-soft/kokomen-payment

Length of output: 220


🏁 Script executed:

#!/bin/bash
# Read the test file to see the approval flow test and full context
wc -l "./internal/src/test/java/com/samhap/kokomen/payment/service/PaymentFacadeServiceTest.java"

Repository: samhap-soft/kokomen-payment

Length of output: 164


🏁 Script executed:

#!/bin/bash
# Read the approval flow test around line 187
sed -n '180,220p' "./internal/src/test/java/com/samhap/kokomen/payment/service/PaymentFacadeServiceTest.java"

Repository: samhap-soft/kokomen-payment

Length of output: 2084


🏁 Script executed:

#!/bin/bash
# Read the PaymentFacadeService implementation
wc -l "./internal/src/main/java/com/samhap/kokomen/payment/service/PaymentFacadeService.java"

Repository: samhap-soft/kokomen-payment

Length of output: 160


🏁 Script executed:

#!/bin/bash
# Read the full PaymentFacadeService implementation
cat -n "./internal/src/main/java/com/samhap/kokomen/payment/service/PaymentFacadeService.java"

Repository: samhap-soft/kokomen-payment

Length of output: 9112


🏁 Script executed:

#!/bin/bash
# Read the cancellation tests from line 230 to end
sed -n '230,292p' "./internal/src/test/java/com/samhap/kokomen/payment/service/PaymentFacadeServiceTest.java"

Repository: samhap-soft/kokomen-payment

Length of output: 3262


취소 흐름에서 예상치 못한 예외 발생 시 처리가 승인 흐름과 다릅니다.

승인 흐름(confirmPayment)에는 HttpClientErrorException, HttpServerErrorException, ResourceAccessException 외의 예외가 발생하면 NEED_CANCEL 상태로 변경하는 catch-all 핸들러(43~47줄)가 있습니다. 그러나 취소 흐름(cancelPayment)은 위 세 가지 예외만 처리하고 catch-all 핸들러가 없어, 예상치 못한 RuntimeException 등의 예외는 그대로 전파됩니다.

일관성을 위해 취소 흐름에도 같은 패턴의 catch-all 핸들러를 추가하고, 이에 대한 테스트 케이스(결제_취소_시_예상치_못한_예외가_발생하면_InternalServerErrorException을_던진다 등)를 추가하는 것을 검토하세요.

🤖 Prompt for AI Agents
In
`@internal/src/test/java/com/samhap/kokomen/payment/service/PaymentFacadeServiceTest.java`
around lines 237 - 277, Cancel flow lacks the catch-all exception handler
present in confirmPayment, so add a generic Exception catch in
PaymentFacadeService.cancelPayment that mirrors confirmPayment's behavior: set
payment status to NEED_CANCEL (use the same NEED_CANCEL enum/constant) and throw
an InternalServerErrorException with the same error message/handling used for
unexpected errors in confirmPayment; then add a unit test (e.g.,
결제_취소_시_예상치_못한_예외가_발생하면_InternalServerErrorException을_던진다) in
PaymentFacadeServiceTest that makes tosspaymentsClient.cancelPayment throw a
RuntimeException and asserts InternalServerErrorException is thrown and the
payment status transitions to NEED_CANCEL.

@unifolio0 unifolio0 merged commit 7b8ab43 into develop Feb 12, 2026
5 checks passed
@unifolio0 unifolio0 deleted the refactor/#14 branch February 12, 2026 06:35
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.

[REFACTOR] 결제 코드 리팩토링

1 participant