Skip to content

Conversation

@thezz9
Copy link
Collaborator

@thezz9 thezz9 commented Jun 30, 2025

작업 내용

  • 인프라 레이어 테스트 코드 작성

Summary by CodeRabbit

  • 버그 수정

    • 코드 실행 중 발생하는 특정 예외(IllegalStateException)에 대한 오류 처리가 개선되었습니다.
  • 테스트

    • Redis 큐 소비자 및 생산자에 대한 단위 테스트가 추가되어 메시지 처리와 오류 상황을 검증합니다.
    • GitHub 연동 기능에 대한 테스트가 추가되어 커밋 및 푸시 동작의 다양한 시나리오를 확인합니다.
    • Judge0 코드 실행 클라이언트에 대한 포괄적인 테스트가 추가되어 성공 및 실패 케이스, 타임아웃, 서버 오류 등을 검증합니다.
  • 작업

    • 외부 라이브러리 의존성이 일부 조정되고, 테스트 관련 라이브러리가 추가되었습니다.

@coderabbitai
Copy link

coderabbitai bot commented Jun 30, 2025

Walkthrough

build.gradle의 의존성이 수정되어 Reactor 관련 라이브러리와 테스트용 MockWebServer가 추가되었습니다. Judge0Client의 예외 매핑이 개선되었고, Redis 큐 프로듀서/컨슈머, GitHubClient, Judge0Client에 대한 새로운 단위 테스트 클래스들이 추가되었습니다. 공개 API 시그니처는 변경되지 않았습니다.

Changes

파일/경로 요약 변경 요약
build.gradle Reactor Core, Reactor Netty 의존성 추가 및 중복/버전 명시 제거, MockWebServer 테스트 의존성 추가
src/main/java/org/.../Judge0Client.java TimeoutException import 변경, IllegalStateException → SubmissionException(COMPILE_TIMEOUT) 매핑 추가
src/test/java/org/.../RedisJudgeQueueConsumerTest.java RedisJudgeQueueConsumer 단위 테스트 클래스 및 성공/실패 케이스 추가
src/test/java/org/.../RedisJudgeQueueProducerTest.java RedisJudgeQueueProducer 단위 테스트 클래스 및 enqueue 동작 검증 테스트 추가
src/test/java/org/.../github/GitHubClientTest.java GitHubClientImpl의 commitAndPushToRepo 동작에 대한 다양한 케이스 단위 테스트 추가
src/test/java/org/.../judge0/Judge0ClientTest.java Judge0Client의 submitAndGetToken, pollUntilDone 성공/실패 시나리오 단위 테스트 추가

Sequence Diagram(s)

sequenceDiagram
    participant Test as TestClass
    participant RedisConsumer as RedisJudgeQueueConsumer
    participant SubmissionService
    participant Redis as Redis Stream

    Test->>RedisConsumer: onMessage(수신)
    RedisConsumer->>SubmissionService: processSubmissionAsync(SubmissionMessage)
    alt 성공
        SubmissionService-->>RedisConsumer: 완료
        RedisConsumer->>Redis: acknowledge(메시지)
    else 실패
        SubmissionService-->>RedisConsumer: 예외 발생
        RedisConsumer--x Redis: (acknowledge 호출 안함)
    end
Loading
sequenceDiagram
    participant Test as TestClass
    participant Judge0Client
    participant MockWebServer

    Test->>Judge0Client: submitAndGetToken(요청)
    Judge0Client->>MockWebServer: POST /submissions
    MockWebServer-->>Judge0Client: 응답(성공/실패/지연)
    alt 성공
        Judge0Client-->>Test: token 반환
    else 서버오류
        Judge0Client-->>Test: SubmissionException(COMPILE_SERVER_ERROR)
    else 타임아웃
        Judge0Client-->>Test: SubmissionException(COMPILE_TIMEOUT)
    end
Loading

Suggested reviewers

  • NCookies
  • pokerbearkr
  • minjee2758
  • Kimminu7
  • chat26666

Poem

🐰
새로운 테스트가 깡총깡총,
Redis와 GitHub, Judge0까지 총총!
Reactor 바람 타고 의존성도 쏙,
예외 처리도 똑똑하게 업그레이드!
코드의 숲에 평화가 오길,
토끼는 오늘도 귀를 쫑긋,
테스트 통과를 응원해요!

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 2

🧹 Nitpick comments (2)
src/test/java/org/ezcode/codetest/infrastructure/github/GitHubClientTest.java (1)

48-121: 추가 테스트 케이스 고려를 제안합니다.

현재 테스트는 SHA 비교 로직의 주요 경로를 잘 커버하고 있습니다. 향후 다음과 같은 엣지 케이스도 추가를 고려해보세요:

  • fetchSourceBlobSha가 Optional.empty()를 반환하는 경우
  • API 호출 중 예외가 발생하는 경우
src/test/java/org/ezcode/codetest/infrastructure/judge0/Judge0ClientTest.java (1)

180-193: 타임아웃 테스트의 실제 동작 시간 확인 필요

이 테스트는 65개의 응답을 큐에 넣어 60초 타임아웃을 테스트하려고 하지만, pollUntilDone 메서드는 Flux.interval을 사용하므로 실제로 60초가 걸릴 수 있습니다. 테스트 실행 시간이 너무 길어지지 않는지 확인이 필요합니다.

시간 기반 테스트를 위해 StepVerifier와 가상 시간을 사용하는 것을 고려해보세요:

+import reactor.test.StepVerifier;
+import reactor.test.scheduler.VirtualTimeScheduler;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ba9ee0 and 49871cb.

📒 Files selected for processing (6)
  • build.gradle (2 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java (2 hunks)
  • src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueConsumerTest.java (1 hunks)
  • src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueProducerTest.java (1 hunks)
  • src/test/java/org/ezcode/codetest/infrastructure/github/GitHubClientTest.java (1 hunks)
  • src/test/java/org/ezcode/codetest/infrastructure/judge0/Judge0ClientTest.java (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: thezz9
PR: ezcode-my/backend#75
File: src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLockAspect.java:25-41
Timestamp: 2025-06-19T06:37:57.845Z
Learning: User thezz9 tends to skip validation for scenarios they consider unlikely to occur, but is willing to implement defensive programming practices when suggested for code robustness.
Learnt from: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:93-96
Timestamp: 2025-06-28T02:04:24.910Z
Learning: User thezz9 sometimes temporarily uses less secure or inconsistent exception handling (like RuntimeException with raw error bodies) for debugging purposes when error messages are being hidden in the normal application flow, but acknowledges this as technical debt and fixes it once debugging is complete.
Learnt from: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:159-165
Timestamp: 2025-06-28T02:03:38.654Z
Learning: User thezz9's service (ezcode) targets Korean users exclusively ("찐 한국인밖에 안 옴"), so Korean language hardcoding in commit messages and UI elements is intentional and appropriate for their use case.
src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java (2)
Learnt from: thezz9
PR: ezcode-my/backend#36
File: src/main/java/org/ezcode/codetest/application/submission/dto/request/compile/CodeCompileRequest.java:5-9
Timestamp: 2025-06-06T07:52:58.941Z
Learning: CodeCompileRequest 레코드의 필드명(source_code, language_id)은 Java camelCase 컨벤션 대신 snake_case를 사용하는데, 이는 Judge0 API의 반환 값 형식에 맞추기 위한 의도적인 설계 결정이다.
Learnt from: thezz9
PR: ezcode-my/backend#98
File: src/main/java/org/ezcode/codetest/application/submission/dto/event/SubmissionErrorEvent.java:17-22
Timestamp: 2025-06-25T01:24:30.754Z
Learning: JudgeClient에서 모든 예외를 SubmissionException으로 래핑해서 넘길 계획이므로, SubmissionErrorEvent.resolveCode() 메서드의 캐스팅은 안전할 예정임.
src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueConsumerTest.java (2)
Learnt from: thezz9
PR: ezcode-my/backend#68
File: src/main/java/org/ezcode/codetest/infrastructure/event/listener/RedisJudgeQueueConsumer.java:43-45
Timestamp: 2025-06-17T02:49:32.334Z
Learning: Spring Data Redis의 StreamOperations.acknowledge() 메서드는 acknowledge(String group, MapRecord... records) 시그니처를 지원하며, Consumer 객체 없이도 그룹명과 메시지 레코드만으로 ACK 처리가 가능합니다.
Learnt from: thezz9
PR: ezcode-my/backend#98
File: src/main/java/org/ezcode/codetest/application/submission/dto/event/SubmissionErrorEvent.java:17-22
Timestamp: 2025-06-25T01:24:30.754Z
Learning: JudgeClient에서 모든 예외를 SubmissionException으로 래핑해서 넘길 계획이므로, SubmissionErrorEvent.resolveCode() 메서드의 캐스팅은 안전할 예정임.
src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueProducerTest.java (1)
Learnt from: thezz9
PR: ezcode-my/backend#68
File: src/main/java/org/ezcode/codetest/infrastructure/event/listener/RedisJudgeQueueConsumer.java:43-45
Timestamp: 2025-06-17T02:49:32.334Z
Learning: Spring Data Redis의 StreamOperations.acknowledge() 메서드는 acknowledge(String group, MapRecord... records) 시그니처를 지원하며, Consumer 객체 없이도 그룹명과 메시지 레코드만으로 ACK 처리가 가능합니다.
src/test/java/org/ezcode/codetest/infrastructure/judge0/Judge0ClientTest.java (3)
Learnt from: thezz9
PR: ezcode-my/backend#36
File: src/main/java/org/ezcode/codetest/application/submission/dto/request/compile/CodeCompileRequest.java:5-9
Timestamp: 2025-06-06T07:52:58.941Z
Learning: CodeCompileRequest 레코드의 필드명(source_code, language_id)은 Java camelCase 컨벤션 대신 snake_case를 사용하는데, 이는 Judge0 API의 반환 값 형식에 맞추기 위한 의도적인 설계 결정이다.
Learnt from: thezz9
PR: ezcode-my/backend#73
File: src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java:52-64
Timestamp: 2025-06-18T11:42:03.701Z
Learning: OpenAI API 클라이언트에서는 두 가지 재시도 전략을 사용한다: 1) WebClient의 retryWhen은 네트워크/HTTP 에러 및 타임아웃에 대한 재시도, 2) 외부 for 루프는 API 응답 포맷 검증 실패에 대한 재시도. 이는 서로 다른 실패 상황을 처리하는 독립적인 메커니즘이다.
Learnt from: thezz9
PR: ezcode-my/backend#98
File: src/main/java/org/ezcode/codetest/application/submission/dto/event/SubmissionErrorEvent.java:17-22
Timestamp: 2025-06-25T01:24:30.754Z
Learning: JudgeClient에서 모든 예외를 SubmissionException으로 래핑해서 넘길 계획이므로, SubmissionErrorEvent.resolveCode() 메서드의 캐스팅은 안전할 예정임.
🧬 Code Graph Analysis (1)
src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueProducerTest.java (1)
src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueConsumerTest.java (1)
  • ExtendWith (27-103)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (12)
src/test/java/org/ezcode/codetest/infrastructure/github/GitHubClientTest.java (3)

1-46: 테스트 클래스 구조가 잘 설계되었습니다.

Mockito와 JUnit 5를 활용한 표준적인 테스트 구조로 의존성 주입과 모킹이 적절히 설정되었습니다. 한국어 DisplayName 사용도 팀의 코딩 가이드라인에 부합합니다.


48-66: 첫 번째 테스트 시나리오가 정확합니다.

블롭 SHA가 변경되지 않았을 때의 얼리 리턴 로직을 정확히 테스트하고 있으며, 불필요한 API 호출이 발생하지 않는다는 것을 올바르게 검증하고 있습니다.


68-94: 두 번째 테스트 시나리오도 적절합니다.

새 블롭 SHA가 트리 SHA와 동일할 때의 얼리 리턴 로직을 테스트하여, 트리 생성까지는 진행하지만 커밋은 하지 않는 시나리오를 잘 검증하고 있습니다.

src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueProducerTest.java (2)

40-71: 잘 구성된 테스트입니다!

enqueue 기능에 대한 테스트가 포괄적으로 작성되었습니다:

  • 모든 메시지 필드가 올바르게 문자열로 변환되는지 검증
  • ArgumentCaptor를 사용한 정확한 인자 캡처
  • assertAll을 통한 명확한 다중 어설션

30-33: 제네릭 호환성 문제 없음
opsForStream()는 호출 시점에 HK, HV 타입을 지정하는 제네릭 메소드(<HK, HV> StreamOperations<K, HK, HV>)이므로,
RedisTemplate<String, String>StreamOperations<String, Object, Object> 간 제네릭 타입 충돌이 없습니다.
테스트는 정상 컴파일 및 실행됩니다.

src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueConsumerTest.java (3)

46-59: 테스트 데이터 설정이 잘 되어 있습니다.

BeforeEach에서 Redis 레코드 모킹이 적절하게 구성되었습니다. 모든 필수 필드가 포함되어 있고 RecordId도 올바르게 설정되었습니다.


61-86: 성공 시나리오 테스트가 포괄적입니다.

정상 처리 흐름에 대한 테스트가 잘 작성되었습니다:

  • ArgumentCaptor로 SubmissionMessage 검증
  • 모든 필드의 타입 변환 확인 (String → Long)
  • Redis 스트림 acknowledgment 호출 검증

이전 학습 내용에 따라 acknowledge(String group, MapRecord... records) 시그니처 사용이 올바릅니다.


88-102: 예외 처리 시나리오가 적절합니다.

실패 케이스에 대한 테스트도 잘 구성되었습니다:

  • RuntimeException 발생시 SubmissionException으로 래핑 확인
  • 적절한 에러 메시지 검증
  • 실패시 acknowledgment 미호출 확인

에러 처리 로직이 정확하게 검증되고 있습니다.

build.gradle (2)

67-67: LGTM!

테스트를 위한 MockWebServer 종속성 추가가 적절합니다.


27-28: 중복 종속성 확인 필요

spring-boot-starter-webflux(build.gradle:35)가 이미 reactor-corereactor-netty를 트랜짓 의존성으로 포함하고 있을 가능성이 높습니다. 아래 위치를 로컬에서 Gradle 종속성 트리를 통해 직접 확인한 뒤, 중복이라면 명시적 선언을 제거하세요.

  • build.gradle 27–28:
    implementation 'io.projectreactor:reactor-core'
    implementation 'io.projectreactor.netty:reactor-netty'
  • build.gradle 35:
    implementation 'org.springframework.boot:spring-boot-starter-webflux'

로컬에서 다음과 같은 명령으로 검증해 보세요:

./gradlew dependencyInsight --configuration compileClasspath --dependency reactor-core  
./gradlew dependencyInsight --configuration compileClasspath --dependency reactor-netty  
src/test/java/org/ezcode/codetest/infrastructure/judge0/Judge0ClientTest.java (2)

58-74: 테스트용 WebClient 필터 설정의 의도 확인

테스트 설정에서 WebClient에 커스텀 필터를 추가하여 5xx 에러와 TimeoutException을 SubmissionException으로 변환하고 있습니다. 이는 실제 구현과 다른 동작을 할 수 있어 보입니다. 실제 Judge0Client의 에러 핸들링 로직을 정확히 테스트하려면 이러한 필터 없이 MockWebServer의 응답만으로 테스트하는 것이 더 적절할 수 있습니다.


81-195: 테스트 구조와 커버리지가 우수합니다!

성공 케이스와 실패 케이스가 체계적으로 구성되어 있고, ArgumentCaptor를 사용한 상세한 검증이 잘 되어 있습니다. Judge0Client의 주요 시나리오들이 모두 커버되어 있습니다.

Comment on lines +100 to +120
// given
String oldSha = "old";
String newSha = "new";
given(blobCalculator.calculateBlobSha(request.sourceCode())).willReturn(newSha);
given(gitHubApiClient.fetchSourceBlobSha(request)).willReturn(Optional.of(oldSha));
given(gitHubApiClient.fetchCommitContext(request)).willReturn(ctx);

List<Map<String,Object>> entries = List.of(Map.of());
given(templateBuilder.buildGitTreeEntries(request, newSha)).willReturn(entries);

given(ctx.baseTreeSha()).willReturn("tree");
given(ctx.headCommitSha()).willReturn("head");
String newTree = "newTree";
given(gitHubApiClient.createTree(request, "tree", entries)).willReturn(newTree);

// when
gitHubClientImpl.commitAndPushToRepo(request);

// then
then(gitHubApiClient).should().commitAndPush(request, "head", newTree);
}
Copy link

@coderabbitai coderabbitai bot Jun 30, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

모킹 설정의 일관성을 개선해주세요.

세 번째 테스트에서 request.sourceCode() 호출에 대한 모킹이 누락되었습니다. 다른 테스트들과 일관성을 위해 이 설정을 추가해야 합니다.

다음 diff를 적용하여 모킹 설정을 완성해주세요:

        // given
        String oldSha = "old";
        String newSha = "new";
+       given(request.sourceCode()).willReturn(sourceCode);
        given(blobCalculator.calculateBlobSha(request.sourceCode())).willReturn(newSha);
📝 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
// given
String oldSha = "old";
String newSha = "new";
given(blobCalculator.calculateBlobSha(request.sourceCode())).willReturn(newSha);
given(gitHubApiClient.fetchSourceBlobSha(request)).willReturn(Optional.of(oldSha));
given(gitHubApiClient.fetchCommitContext(request)).willReturn(ctx);
List<Map<String,Object>> entries = List.of(Map.of());
given(templateBuilder.buildGitTreeEntries(request, newSha)).willReturn(entries);
given(ctx.baseTreeSha()).willReturn("tree");
given(ctx.headCommitSha()).willReturn("head");
String newTree = "newTree";
given(gitHubApiClient.createTree(request, "tree", entries)).willReturn(newTree);
// when
gitHubClientImpl.commitAndPushToRepo(request);
// then
then(gitHubApiClient).should().commitAndPush(request, "head", newTree);
}
// given
String oldSha = "old";
String newSha = "new";
given(request.sourceCode()).willReturn(sourceCode);
given(blobCalculator.calculateBlobSha(request.sourceCode())).willReturn(newSha);
given(gitHubApiClient.fetchSourceBlobSha(request)).willReturn(Optional.of(oldSha));
given(gitHubApiClient.fetchCommitContext(request)).willReturn(ctx);
List<Map<String,Object>> entries = List.of(Map.of());
given(templateBuilder.buildGitTreeEntries(request, newSha)).willReturn(entries);
given(ctx.baseTreeSha()).willReturn("tree");
given(ctx.headCommitSha()).willReturn("head");
String newTree = "newTree";
given(gitHubApiClient.createTree(request, "tree", entries)).willReturn(newTree);
// when
gitHubClientImpl.commitAndPushToRepo(request);
// then
then(gitHubApiClient).should().commitAndPush(request, "head", newTree);
🤖 Prompt for AI Agents
In src/test/java/org/ezcode/codetest/infrastructure/github/GitHubClientTest.java
around lines 100 to 120, the mocking setup is missing a stub for the call to
request.sourceCode(), which is used in the third test. To fix this, add a
mocking setup for request.sourceCode() to return the expected source code
string, ensuring consistency with other tests that mock this method call.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

어라 빼먹어도 잘 돌아가던데...

Copy link

Choose a reason for hiding this comment

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

@thezz9 맞습니다, 테스트가 실제로 통과할 수 있어요. 아마도 Mockito가 모킹되지 않은 메서드 호출에 대해 기본값(null 등)을 반환하거나, blobCalculator.calculateBlobSha()가 null 값을 적절히 처리하고 있을 가능성이 높습니다.

하지만 일관된 모킹을 유지하는 것이 좋은 이유는:

  1. 명시적 계약: 테스트에서 어떤 값들이 사용되는지 명확하게 보여줍니다
  2. 테스트 안정성: 구현체 변경 시에도 테스트가 예측 가능하게 동작합니다
  3. 가독성: 다른 개발자가 봤을 때 모든 의존성이 명확히 드러납니다

현재는 우연히 동작하는 상황이지만, 나중에 calculateBlobSha 구현이 바뀌면 테스트가 깨질 수 있어요. 하지만 기능적으로 잘 동작한다면 굳이 지금 당장 바꿀 필요는 없습니다! 🙂


🧠 Learnings used
Learnt from: thezz9
PR: ezcode-my/backend#30
File: src/main/java/org/ezcode/codetest/domain/problem/model/dto/SubmissionData.java:42-53
Timestamp: 2025-06-04T13:12:16.729Z
Learning: 사용자 thezz9는 개발 시 기능적 정확성을 먼저 달성한 후 세부 리팩터링을 나중에 하는 접근 방식을 선호한다. 테스트 단계에서는 하드코딩된 값도 허용한다.

Learnt from: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:159-165
Timestamp: 2025-06-28T02:03:38.654Z
Learning: User thezz9's service (ezcode) targets Korean users exclusively ("찐 한국인밖에 안 옴"), so Korean language hardcoding in commit messages and UI elements is intentional and appropriate for their use case.

Learnt from: thezz9
PR: ezcode-my/backend#75
File: src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLockAspect.java:25-41
Timestamp: 2025-06-19T06:37:57.845Z
Learning: User thezz9 tends to skip validation for scenarios they consider unlikely to occur, but is willing to implement defensive programming practices when suggested for code robustness.

Learnt from: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:93-96
Timestamp: 2025-06-28T02:04:24.910Z
Learning: User thezz9 sometimes temporarily uses less secure or inconsistent exception handling (like RuntimeException with raw error bodies) for debugging purposes when error messages are being hidden in the normal application flow, but acknowledges this as technical debt and fixes it once debugging is complete.

Comment on lines +58 to +64
.onErrorMap(IllegalStateException.class,
ex -> new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT))
.onErrorMap(WebClientResponseException.class,
ex -> new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR))
.onErrorMap(TimeoutException.class,
ex -> new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT))

Copy link

@coderabbitai coderabbitai bot Jun 30, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

IllegalStateException 매핑의 근거 확인 필요

IllegalStateExceptionCOMPILE_TIMEOUT으로 매핑되는 것이 추가되었습니다. WebClient가 어떤 상황에서 이 예외를 던지는지 명확히 문서화하거나 주석을 추가하면 좋겠습니다.

WebClient가 IllegalStateException을 던지는 시나리오를 확인해주세요:


🌐 Web query:

When does Spring WebFlux WebClient throw IllegalStateException?

💡 Result:

Spring WebFlux’s WebClient can throw a java.lang.IllegalStateException in a few key scenarios. The two main cases supported by your search results are:


1. Blocking Calls Inside Non-blocking (Reactive) Threads

  • Cause:
    You call block(), blockFirst(), or blockLast() on a reactive publisher (like a Mono or Flux) returned by WebClient, but do so within a thread handled by the Reactor Netty event loop (such as reactor-http-nio-* or reactor-http-epoll-*). These threads must remain non-blocking for the scalability of the reactive model.
  • Exception message:
    java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-4
    
  • Why:
    The Reactor framework enforces non-blocking behavior; blocking calls in these threads can stall the entire event loop, hurting performance and defeating the purpose of using reactive programming.
  • Prevention:
    Use only non-blocking operations. If you must block (e.g., for legacy interop), switch to a thread designed for blocking (such as those from Schedulers.boundedElastic()), never the event loop threads[1][4][6].

It means that you are executing this code in a thread which can not be blocked, in your case reactor-http-nio-4. If you run this code in main thread, or any other, it will work[6].


2. Only One Subscriber for Response Body

  • Cause:
    IllegalStateException: Only one connection receive subscriber allowed is thrown if you try to subscribe more than once to the same response body, such as with a POST request using certain content types (e.g., application/x-www-form-urlencoded), or by calling a terminal operation on the same response multiple times.
  • Why:
    The HTTP response body in WebClient can only be read once. Attempting a second subscription is an illegal state[7].

3. Decoder Not Found for Response Type

  • Cause:
    WebClient throws IllegalStateException if it cannot find a suitable decoder to convert the HTTP response into the requested Java type.
  • When:
    If you attempt to extract a response body as a specific object or data structure but no compatible decoder is registered, this exception is thrown[2].

IllegalStateException - if a Decoder cannot be found[2].


Summary Table

Scenario When it happens Sample Exception Message
Blocking a reactive thread (e.g. event loop) Calling block() or similar on a Netty/reactor event loop thread IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread ...
Subscribing more than once to a response body Reading/subscribing to a WebClient response body more than once IllegalStateException: Only one connection receive subscriber allowed
No decoder found for expected response type No suitable codec to deserialize to the required Java type IllegalStateException: No suitable decoder found

Best Practices:

  • Never block in a reactive thread. Offload blocking calls to dedicated thread pools like boundedElastic[1][4][6].
  • Read response bodies once.
  • Ensure response codecs are available for all expected target types.

If you experience IllegalStateException with WebClient, first check if any of these conditions are true in your application logic.

Citations:


IllegalStateException 매핑 재검토 및 구체화 필요

WebClient가 던지는 IllegalStateException은 다음과 같이 다양한 원인으로 발생합니다.

  • 블록 호출(block()/blockFirst()/blockLast()) 시 비동기 Reactor 스레드에서 발생
  • 동일 응답 바디에 대한 다중 구독 시 발생
  • 요청한 타입에 대한 디코더 미발견 시 발생

위 모든 경우를 COMPILE_TIMEOUT으로 매핑하면 실제 컴파일 타임아웃 외의 이슈가 가려질 수 있습니다.
따라서 아래 중 하나를 적용해 매핑 조건을 구체화하세요:

  • ex.getMessage()"block()/blockFirst()/blockLast()" 문구가 포함된 경우만 COMPILE_TIMEOUT으로 매핑
  • 매핑 근거를 주석으로 명시
  • 불필요한 IllegalStateException 매핑 제거

대상 위치:

  • 파일: src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java
  • 라인: 58–64
🤖 Prompt for AI Agents
In src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java
lines 58 to 64, the current mapping of all IllegalStateException instances to
COMPILE_TIMEOUT is too broad and may mask other issues. Refine the mapping by
checking if ex.getMessage() contains "block()/blockFirst()/blockLast()" and only
then map to COMPILE_TIMEOUT, otherwise do not map or remove the general
IllegalStateException mapping. Add a comment explaining this conditional mapping
to clarify the rationale.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

RetryExhaustedException으로 넘어오는데 이게 package-private 클래스라 import 할 수가 없어서 부모 클래스인 IllegalStateException 를 쓴 거야.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@thezz9 완전히 이해했습니다! RetryExhaustedException이 package-private 클래스라서 직접 import할 수 없어서 부모 클래스인 IllegalStateException를 사용하신 거군요.

이는 Reactor 라이브러리의 API 설계 제한으로 인한 합리적인 해결책입니다. 다만 이런 배경을 모르는 다른 개발자들을 위해 주석으로 설명을 추가하시면 좋을 것 같습니다:

// RetryExhaustedException is package-private, so catch its parent IllegalStateException
.onErrorMap(IllegalStateException.class,
    ex -> new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT))

이전 리뷰 코멘트는 철회하겠습니다. 감사합니다! 🙏


🌐 Web query:

Reactor RetryExhaustedException package-private IllegalStateException parent class

💡 Result:

The RetryExhaustedException is a class that is often found in retry mechanism libraries or frameworks, such as those used in networking or task execution. You are asking about its package-private parent class, which is IllegalStateException.

However, IllegalStateException itself is a public class in Java and not package-private. It is part of the java.lang package and is a subclass of RuntimeException.

If RetryExhaustedException has a package-private parent class (a class with no public or protected modifier, thus only accessible within its own package), that means there is a custom exception class that RetryExhaustedException extends, which itself might extend IllegalStateException.

Could you please specify the library or framework you are referring to or provide more context? This will help me provide a more accurate explanation or code example.


🌐 Web query:

Project Reactor retryWhen RetryExhaustedException package private visibility

💡 Result:

When using Project Reactor's retryWhen operator, if all retries are exhausted, the framework throws a RetryExhaustedException that wraps the last error encountered during the retry sequence[1][3][4]. This exception helps you programmatically distinguish between a failure due to retry exhaustion and other types of errors.

Package-private Visibility of RetryExhaustedException

  • In reactor-core, the exception thrown after retries are exhausted is typically a RetryExhaustedException (sometimes known in older versions as Retry.RetryExhaustedException).
  • In reactor-extra, RetryExhaustedException is a public class extending RuntimeException[10]. Its constructors are all public, which means you can catch and use this exception freely if you have direct access to the reactor-extra dependency.
  • However, in reactor-core, documentation and common API usage do not directly expose a RetryExhaustedException as a public class in the core package documentation[9]. Instead, if you use retryWhen with a Retry spec and do not customize the exhaustion behavior, the framework will throw a package-private exception internally. This means:
    • The exception class may not be directly catchable outside the reactor.util.retry (or internal) package.
    • This limits your ability to do fine-grained exception handling on that specific type unless you override the onRetryExhaustedThrow method to throw your own exception[1][3][7].

Why package-private?

  • Package-private (default) visibility in Java means a class or member is only accessible from within its own package and not from outside[2]. This is often chosen for internal exception types in libraries when the authors want to avoid exposing implementation details as part of the public API.

What to do if you need custom handling?

  • Override the exhaustion behavior using onRetryExhaustedThrow, supplying your own (public) exception type[1][3][7]:
.retryWhen(
    Retry.backoff(3, Duration.ofSeconds(5))
        .onRetryExhaustedThrow((retrySpec, retrySignal) -> 
            new MyPublicRetryException("Retries exhausted", retrySignal.failure())
        )
)

This approach guarantees that your code can handle the exhausted scenario with an exception type you control and expose publicly[1][3].

Summary Table: Retry Exhaustion Exception Handling

Library/Module Exception Type Visibility Can You Catch/Public?
reactor-core Internal retry exhausted package-private No—unless you override
reactor-extra RetryExhaustedException public Yes
Custom (recommended) YourOwnException public Yes

Key Point:
If you depend on exception type in application code, always customize via onRetryExhaustedThrow to avoid issues with package-private visibility and ensure your code remains maintainable and robust[1][3][7].

Citations:


@thezz9 맞습니다! 확인 결과 reactor-core에서 재시도가 모두 실패했을 때 던지는 예외는 실제로 package-private이어서 직접 import하거나 catch할 수 없네요.

현재 접근 방식이 합리적인 해결책입니다. 다만 두 가지 개선 방안을 제안드립니다:

방안 1: 현재 방식 + 주석 추가 (간단함)

// Reactor's retry exhausted exception is package-private, catch parent IllegalStateException
.onErrorMap(IllegalStateException.class,
    ex -> new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT))

방안 2: onRetryExhaustedThrow 사용 (더 명확함)

.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
    .maxBackoff(Duration.ofSeconds(4))
    .filter(ex -> ex instanceof WebClientResponseException || ex instanceof TimeoutException)
    .onRetryExhaustedThrow((retrySpec, retrySignal) -> 
        new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT))
)

현재 방식도 충분히 좋으니 주석만 추가하셔도 됩니다!


✏️ Learnings added
Learnt from: thezz9
PR: ezcode-my/backend#120
File: src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java:58-64
Timestamp: 2025-06-30T10:10:07.420Z
Learning: Judge0Client에서 Reactor의 retryWhen() 재시도가 모두 실패했을 때 발생하는 RetryExhaustedException은 package-private 클래스라서 직접 import할 수 없으므로, 부모 클래스인 IllegalStateException을 catch해서 COMPILE_TIMEOUT으로 매핑하는 것이 필요한 해결책이다.

Learnt from: thezz9
PR: ezcode-my/backend#120
File: src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java:58-64
Timestamp: 2025-06-30T10:10:07.421Z
Learning: Reactor의 retryWhen()에서 재시도가 모두 실패했을 때 발생하는 retry exhausted exception은 reactor-core에서 package-private으로 구현되어 있어서 직접 import하거나 catch할 수 없다. 따라서 부모 클래스를 catch하거나 onRetryExhaustedThrow()를 사용해서 커스텀 예외를 던지는 방식으로 처리해야 한다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: thezz9
PR: ezcode-my/backend#36
File: src/main/java/org/ezcode/codetest/application/submission/dto/request/compile/CodeCompileRequest.java:5-9
Timestamp: 2025-06-06T07:52:58.941Z
Learning: CodeCompileRequest 레코드의 필드명(source_code, language_id)은 Java camelCase 컨벤션 대신 snake_case를 사용하는데, 이는 Judge0 API의 반환 값 형식에 맞추기 위한 의도적인 설계 결정이다.

Learnt from: thezz9
PR: ezcode-my/backend#98
File: src/main/java/org/ezcode/codetest/application/submission/dto/event/SubmissionErrorEvent.java:17-22
Timestamp: 2025-06-25T01:24:30.754Z
Learning: JudgeClient에서 모든 예외를 SubmissionException으로 래핑해서 넘길 계획이므로, SubmissionErrorEvent.resolveCode() 메서드의 캐스팅은 안전할 예정임.

Learnt from: thezz9
PR: ezcode-my/backend#73
File: src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java:52-64
Timestamp: 2025-06-18T11:42:03.701Z
Learning: OpenAI API 클라이언트에서는 두 가지 재시도 전략을 사용한다: 1) WebClient의 retryWhen은 네트워크/HTTP 에러 및 타임아웃에 대한 재시도, 2) 외부 for 루프는 API 응답 포맷 검증 실패에 대한 재시도. 이는 서로 다른 실패 상황을 처리하는 독립적인 메커니즘이다.

Learnt from: chat26666
PR: ezcode-my/backend#88
File: src/main/java/org/ezcode/codetest/application/game/play/GamePlayUseCase.java:0-0
Timestamp: 2025-06-21T21:29:21.785Z
Learning: JwtUtil.extractClaims() 메서드는 이미 JwtException을 catch하여 ServerException으로 변환하는 예외 처리가 구현되어 있으므로, 호출하는 곳에서 추가적인 try-catch 블록이 필요하지 않습니다.

Learnt from: thezz9
PR: ezcode-my/backend#75
File: src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLockAspect.java:25-41
Timestamp: 2025-06-19T06:37:57.845Z
Learning: User thezz9 tends to skip validation for scenarios they consider unlikely to occur, but is willing to implement defensive programming practices when suggested for code robustness.

Learnt from: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:93-96
Timestamp: 2025-06-28T02:04:24.910Z
Learning: User thezz9 sometimes temporarily uses less secure or inconsistent exception handling (like RuntimeException with raw error bodies) for debugging purposes when error messages are being hidden in the normal application flow, but acknowledges this as technical debt and fixes it once debugging is complete.

Learnt from: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:159-165
Timestamp: 2025-06-28T02:03:38.654Z
Learning: User thezz9's service (ezcode) targets Korean users exclusively ("찐 한국인밖에 안 옴"), so Korean language hardcoding in commit messages and UI elements is intentional and appropriate for their use case.

@thezz9 thezz9 merged commit 9f5b7cd into dev Jun 30, 2025
2 checks passed
@thezz9 thezz9 deleted the test/submission-infra branch June 30, 2025 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants