Skip to content

Conversation

@GulSauce
Copy link
Member

@GulSauce GulSauce commented Jan 25, 2026

📢 설명

해당 Pull Request에 대해 간략하게 설명해주세요!

설명 1

의사 선택과정(trade-off)

얻었다

  • foo

잃었다

  • bar

코드 설명: 코멘트 확인

✅ 체크 리스트

  • FastAPI .env 업데이트
  • 퀴즈 생성 - problem_set 테이블에 userId값이 들어갔는지 확인
  • 로그아웃 수행
  • 퀴즈 생성 - problem_set 테이블에 null값이 들어갔는지 확인
  • 코드 리뷰

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 문제 집합 생성 응답이 이제 스트리밍 형식으로 제공됩니다.
    • NDJSON 형식의 반응형 데이터 스트리밍을 지원합니다.
  • Refactor

    • S3 서비스 구조를 개선하여 더 나은 모듈 분리를 달성했습니다.
    • API 응답 데이터 타입을 프론트엔드 포맷으로 재구성했습니다.
  • Chores

    • 새로운 유틸리티 모듈을 추가했습니다.
    • Spring WebFlux 의존성을 추가했습니다.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Walkthrough

이 PR은 패키지 구조를 재조직하고 AI 기반 퀴즈 생성을 위한 리액티브 WebFlux 지원을 도입합니다. AWS 관련 타입을 com.icc.qasker.aws에서 com.icc.qasker.util로 이동하고, DTO 패키지를 재구성(response → feResponse, request → feRequest)하며, RestClient를 WebClient로 대체하고, 새로운 util 모듈을 생성합니다.

Changes

Cohort / File(s) 변경 요약
AWS 패키지 재구조화
modules/aws/api/src/main/java/com/icc/qasker/util/*, modules/aws/impl/src/main/java/com/icc/qasker/util/*
S3Service, S3ValidateService 및 관련 DTO들을 com.icc.qasker.aws.*에서 com.icc.qasker.util.*로 이동. 설정, 컨트롤러, 서비스 구현체들의 패키지 및 임포트 경로 업데이트
Quiz DTO 패키지 재조직
modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/*, modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feRequest/*, modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiResponse/*, modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiRequest/*
Response 타입을 feResponse로 이동, Request 타입을 feRequest로 이동, AI 관련 DTO를 aiResponse/aiRequest로 재구성. GenerationResponse 삭제
API 문서 및 서비스 인터페이스 리팩토링
modules/quiz/api/src/main/java/com/icc/qasker/quiz/*.java, modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/*
GenerationService.processGenerationRequest가 Flux<ProblemSetResponse> 반환 타입으로 변경, GenerationApiDoc 새로 추가, 기존 controller.doc 위치에서 doc 패키지로 이동
리액티브 WebClient 구현
modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java, modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/AiWebClientConfig.java
RestClient에서 WebClient로 전환, NDJSON 스트리밍 지원, requestGenerate가 Flux<GenerationResponseFromAI> 반환, 반응형 에러 처리 추가
GenerationService 구현 재작성
modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java
processGenerationRequest가 리액티브 Flux 기반 처리로 변경, S3 검증 로직 통합, ProblemSet 생성 및 저장 흐름 개선, MockGenerationService 제거
Entity 및 Mapper 업데이트
modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/Problem.java, modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/ProblemSet.java, modules/quiz/impl/src/main/java/com/icc/qasker/quiz/mapper/ProblemSetResponseMapper.java
QuizGeneratedByAI → QuizGeneratedFromAI로 타입 변경, ProblemSet에 @Builder 추가, ProblemSetResponseMapper를 Spring @Component로 변환하고 해시 인코딩 로직 추가
Controller 업데이트
modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/*.java
GenerationController의 postProblemSetId가 Flux<ProblemSetResponse> 반환, NDJSON 미디어 타입 지정, MockGenerationService 의존성 제거
새로운 util 모듈 생성
modules/util/api/build.gradle, modules/util/impl/build.gradle, modules/util/impl/src/main/java/com/icc/qasker/util/*
UpdateLog 관련 기능을 quiz 패키지에서 util 패키지로 이동, HelloController 및 UpdateLogController 재위치
빌드 파일 업데이트
app/build.gradle, modules/quiz/api/build.gradle, modules/quiz/impl/build.gradle, settings.gradle
util-impl 모듈 의존성 추가, WebFlux 스타터 의존성 추가, Gradle 설정에 util-api/util-impl 모듈 등록

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant GenerationController
    participant GenerationServiceImpl
    participant AiServerAdapter
    participant WebClient as WebClient<br/>(AI Server)
    participant ProblemRepository
    participant ProblemSetResponseMapper

    Client->>GenerationController: POST /generation<br/>GenerationRequest
    GenerationController->>GenerationServiceImpl: processGenerationRequest(request, userId)
    GenerationServiceImpl->>GenerationServiceImpl: validateFile(uploadUrl)
    GenerationServiceImpl->>GenerationServiceImpl: ProblemSet 생성 및 저장
    GenerationServiceImpl->>AiServerAdapter: requestGenerate(request)
    AiServerAdapter->>WebClient: POST (NDJSON stream)
    loop NDJSON 스트림 처리
        WebClient-->>AiServerAdapter: GenerationResponseFromAI
        AiServerAdapter->>GenerationServiceImpl: Flux 항목
        GenerationServiceImpl->>ProblemRepository: Problem 저장
        GenerationServiceImpl->>ProblemSetResponseMapper: 응답 매핑
        GenerationServiceImpl-->>Client: ProblemSetResponse (NDJSON)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

기능추가, 리팩토링

Suggested reviewers

  • lhoju0158

Poem

🐰 패키지들이 새로운 집을 찾아 이동하고,
WebFlux의 흐름이 시냇물처럼 흘러가며,
AI 서버와의 대화는 NDJSON으로 속삭이네,
리액티브한 마음으로 퀴즈가 생성되고,
util 모듈의 탄생으로 코드는 한층 정돈되는구나! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
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.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목 'ICC-232 비동기 퀴즈 응답'은 변경 사항의 주요 목적(비동기 퀴즈 응답)을 명확하게 요약하고 있으며, 실제 변경 내용(Flux를 통한 비동기 처리, WebClient 도입, 리패키징)과 일치합니다.

✏️ 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 and usage tips.

@GulSauce
Copy link
Member Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

✅ Actions performed

Full review triggered.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/SpecificExplanationApiDoc.java (1)

5-9: 빈 인터페이스 메서드 정의 필요.

다른 모든 ApiDoc 인터페이스들과 달리, 이 인터페이스는 @Tag 어노테이션만 있고 메서드 정의가 없습니다. 동일 패키지의 ExplanationApiDoc, GenerationApiDoc, ProblemSetApiDoc, S3ApiDoc 등 모두 @Operation 어노테이션과 HTTP 매핑이 적용된 메서드들을 포함하고 있습니다. 해당 메서드 정의를 추가하거나, 인터페이스 자체가 불필요한 경우 제거하세요.

modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/SpecificExplanationServiceImpl.java (1)

69-70: 잠재적 NPE 위험.

node.get("specific_explanation")null을 반환할 경우 .asText() 호출 시 NPE가 발생합니다. path() 메서드를 사용하거나 null 체크를 추가하세요.

🐛 제안하는 수정
-            explanationText = node.get("specific_explanation").asText();
+            JsonNode explanationNode = node.get("specific_explanation");
+            if (explanationNode == null || explanationNode.isNull()) {
+                throw new CustomException(ExceptionMessage.AI_RESPONSE_INVALID);
+            }
+            explanationText = explanationNode.asText();

또는 path() 사용:

-            explanationText = node.get("specific_explanation").asText();
+            explanationText = node.path("specific_explanation").asText("");
modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiResponse/QuizGeneratedFromAI.java (1)

16-18: @NotNull을 primitive에 적용하면 검증이 무력화됩니다.

int는 null이 될 수 없어 @NotNull 제약이 작동하지 않습니다. JSON 역직렬화 시 누락된 필드는 0으로 설정되며 검증은 항상 통과합니다. 메시지 의도("number가 null입니다")에 맞춰 Integer로 변경하여 실제 null 값을 감지할 수 있도록 수정하세요.

🛠 제안 변경
-    `@NotNull`(message = "number가 null입니다.")
-    private int number;
+    `@NotNull`(message = "number가 null입니다.")
+    private Integer number;
modules/quiz/impl/src/main/java/com/icc/qasker/quiz/mapper/ProblemSetResponseMapper.java (1)

21-33: 선택지 순서가 비결정적입니다. 안정적인 번호 부여를 위해 정렬 메커니즘 추가 필요

Problem.selections@OrderBy 애너테이션이 없고, Selection 엔티티에도 시퀀스 필드가 없어서 조회 시점마다 선택지 순서가 변할 수 있습니다. 매퍼에서 인덱스로 번호를 부여(i+1)하므로 프론트엔드 사용자가 보는 선택지 순서가 일관성 없이 흔들릴 수 있습니다.

다음 중 하나로 해결하세요:

  • Problem 엔티티의 selections 필드에 @OrderBy 추가
  • Selection 엔티티에 명시적 시퀀스 필드 추가 후 정렬
  • 매퍼에서 ID 기준 정렬 로직 추가
modules/aws/impl/src/main/java/com/icc/qasker/util/service/S3ValidateServiceImpl.java (2)

19-22: CloudFront URL 검증이 약합니다. startsWithhttps://cdn.example.com.evil.com 같은 위장 URL을 통과시킬 수 있어 호스트/스킴을 파싱해 비교하는 방식이 안전합니다.

🔧 안전한 URL 파싱 비교 예시
-    public void checkCloudFrontUrlWithThrowing(String url) {
-        if (!url.startsWith(awsCloudFrontProperties.baseUrl())) {
+    public void checkCloudFrontUrlWithThrowing(String url) {
+        URI baseUri = URI.create(awsCloudFrontProperties.baseUrl());
+        URI targetUri = URI.create(url);
+        boolean sameHost = Objects.equals(baseUri.getHost(), targetUri.getHost());
+        boolean sameScheme = Objects.equals(baseUri.getScheme(), targetUri.getScheme());
+        if (!sameHost || !sameScheme) {
             throw new CustomException(ExceptionMessage.INVALID_URL_REQUEST);
         }
     }

27-43: MIME 검증이 부분 문자열 매칭이라 우회될 수 있습니다. contains 대신 정규화 후 정확 일치를 권장합니다.

🔧 정확 매칭으로 변경
-        String allowedExtensions = awsS3Properties.allowedExtensions();
+        String allowedExtensions = awsS3Properties.allowedExtensions();
+        String normalizedContentType = contentType.split(";")[0].trim().toLowerCase(Locale.ROOT);
+        Set<String> allowed = Arrays.stream(allowedExtensions.split(","))
+            .map(String::trim)
+            .map(s -> s.toLowerCase(Locale.ROOT))
+            .collect(Collectors.toSet());
@@
-        if (!allowedExtensions.contains(contentType)) {
+        if (!allowed.contains(normalizedContentType)) {
             throw new CustomException(ExceptionMessage.EXTENSION_INVALID);
         }
modules/aws/api/src/main/java/com/icc/qasker/util/dto/PresignRequest.java (1)

7-17: @NotNull은 primitive 타입(long)에 효과가 없습니다. Primitive long은 Java에서 절대 null이 될 수 없으므로, @NotNull 검증은 항상 통과합니다.

권장사항: 다음 중 하나를 선택하세요.

  • Long으로 변경: Long fileSize로 바꾸면 누락된 입력값을 null로 감지할 수 있습니다 (권장)
  • @NotNull 제거: 현재 @Positive가 이미 있으므로, 불필요한 @NotNull을 제거하세요
🤖 Fix all issues with AI agents
In `@modules/quiz/api/build.gradle`:
- Around line 5-6: Root build.gradle is forcing the servlet (MVC) stack by
applying 'spring-boot-starter-web' and 'springdoc-openapi-starter-webmvc-ui' to
all subprojects, which overrides the quiz/api module's WebFlux intent; either
make the root application of those dependencies conditional per-subproject (so
quiz/api does not get 'spring-boot-starter-web' or the MVC OpenAPI starter) or
explicitly set the application to reactive in the quiz/api module (e.g., set
web-application-type=reactive via application properties or configure
SpringApplication to use WebApplicationType.REACTIVE) so that symbols like
'spring-boot-starter-web', 'springdoc-openapi-starter-webmvc-ui', and the
quiz/api module's WebFlux declaration no longer conflict.

In
`@modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/GenerationApiDoc.java`:
- Around line 1-18: The GenerationApiDoc interface's postProblemSetId method is
missing the `@UserId` annotation on the userId parameter which causes
Springdoc-OpenAPI to generate incomplete docs; update the method signature in
GenerationApiDoc (postProblemSetId) to annotate the userId parameter with
`@UserId` (the same annotation used in the controller) so the interface parameter
matches the controller and Springdoc picks up the userId correctly for
GenerationRequest -> ProblemSetResponse API documentation.

In `@modules/quiz/api/src/main/java/com/icc/qasker/quiz/GenerationService.java`:
- Around line 3-10: processGenerationRequest currently returns
Flux<ProblemSetResponse> but its implementation performs blocking calls
(validateFile(), problemSetRepository.save(), problemRepository.save() inside
map()) which violates the reactive contract; refactor by moving blocking calls
out of map and into non-blocking reactive operators: wrap each blocking call
with Mono.fromCallable(...) or
Mono.fromRunnable(...).subscribeOn(Schedulers.boundedElastic()) (or switch to a
reactive repository) and replace blocking map() usages with flatMap/flatMapMany
to compose these Monos so all DB saves and file validation run on a
boundedElastic scheduler (or use reactive R2DBC repositories) while
processGenerationRequest remains a Flux; specifically change validateFile
invocations, problemSetRepository.save(...) and problemRepository.save(...) to
be executed via Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic())
and composed with flatMap instead of executing synchronously inside map().

In `@modules/quiz/impl/build.gradle`:
- Around line 9-13: This module currently pulls both WebFlux and MVC starters:
remove the duplicate by either (A) deleting the explicit
spring-boot-starter-webflux entry from modules/quiz/impl/build.gradle if you
want MVC, or (B) if you want WebFlux, override the root subprojects.dependencies
automatic inclusion of spring-boot-starter-web (referenced as
subprojects.dependencies) by excluding or removing that dependency for this
subproject (e.g., adjust the module's build.gradle to exclude
spring-boot-starter-web from the inherited configuration or change the root rule
to not apply to this subproject). Ensure only one of spring-boot-starter-webflux
or spring-boot-starter-web remains.

In
`@modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java`:
- Around line 41-63: In AiServerAdapter update the onStatus handlers so
bodyToMono(String.class) cannot return an empty Mono: call .defaultIfEmpty("")
on the Mono returned from clientResponse.bodyToMono(String.class) in both the
HttpStatusCode::is4xxClientError and HttpStatusCode::is5xxServerError branches;
this ensures parseErrorMessage(errorBody) runs and the code throws
ClientSideException (4xx) or
CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR) (5xx) even when
the response body is empty.

In
`@modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/AiWebClientConfig.java`:
- Around line 21-27: The aiGenerationClient WebClient lacks explicit connection
and response timeout settings; update the aiGenerationClient() bean to build a
WebClient using a ReactorClientHttpConnector configured with a Reactor Netty
TcpClient/HttpClient that sets a connect timeout (e.g.,
tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, ...)) and a response/read
timeout appropriate for streaming (e.g., using tcpClient.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(...)))) or HttpClient
responseTimeout, so the WebClient created in aiGenerationClient() enforces both
connect and response timeouts for streaming scenarios.

In `@modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/ProblemSet.java`:
- Around line 34-48: The two unused factory methods
ProblemSet.of(GenerationResponseFromAI) and
ProblemSet.of(GenerationResponseFromAI, String) should be removed or made
private to avoid creating ProblemSet instances with a null userId; update the
ProblemSet class to delete these static methods (or change their visibility to
private) and keep using ProblemSet.builder().userId(userId).build() as done in
GenerationServiceImpl so userId is always supplied when constructing ProblemSet.

In
`@modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java`:
- Around line 49-60: Use the managed entity returned by save() instead of the
original transient variable: replace usages of problemSet when constructing
Problem instances with the saved variable save (the managed ProblemSet returned
from problemSetRepository.save) inside GenerationServiceImpl where
Problem.of(...) and problemRepository.save(...) are called, so
Problem.of(quizGeneratedFromAI, save) is used to ensure you persist and relate
to the JPA-managed entity.
- Around line 52-56: The code creates a new Scheduler per request with
Schedulers.fromExecutor(Executors.newVirtualThreadPerTaskExecutor()) inside
GenerationServiceImpl, which leaks executors; change this to use a shared,
reusable Scheduler instance injected or created once (e.g., a `@Bean` or a private
final field) and reuse it for publishOn instead of constructing a new one in the
method; ensure the shared Scheduler is disposed on application shutdown (or
managed by the container) and replace the local scheduler variable usage around
aiServerAdapter.requestGenerate(...).publishOn(scheduler).map(...) accordingly.

In `@modules/util/impl/build.gradle`:
- Around line 1-8: The build currently declares project(":global") and
project(":util:util-api") as implementation dependencies in modules/util/impl's
build.gradle, but public types from UpdateLog and UpdateLogResponse expose those
modules on the public API; change those dependency declarations to api so the
java-library plugin treats them as exported API dependencies (replace
implementation project(":global") and implementation project(":util:util-api")
with api project(":global") and api project(":util:util-api") in the
dependencies block).
♻️ Duplicate comments (2)
modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/SpecificExplanationResponse.java (1)

1-1: 패키지 명명 규칙 이슈는 이전 파일에서 이미 언급되었습니다.

ExplanationResponse.java 리뷰에서 동일한 feResponse 패키지 명명 규칙 이슈를 지적했습니다.

modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ResultResponse.java (1)

1-1: 패키지 명명 규칙 이슈는 이전 파일에서 이미 언급되었습니다.

🧹 Nitpick comments (7)
modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiRequest/GenerationRequestToAI.java (2)

23-25: pageNumbers 요소 값 검증 추가 권장

현재는 리스트 자체만 검증되어 요소에 null/음수 값이 들어올 수 있습니다. 요소 단위 검증을 추가하는 것이 안전합니다 (Line 23-25).

🔧 제안 변경
-    List<Integer> pageNumbers
+    List<@NotNull `@Min`(value = 1, message = "pageNumbers 요소는 1 이상이어야 합니다.") Integer> pageNumbers

27-29: 빈 record 바디/세미콜론 정리

빈 바디와 불필요한 세미콜론을 축약해 가독성을 높일 수 있습니다 (Line 27-29).

🧹 제안 변경
-) {
-
-};
+) {}
modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ExplanationResponse.java (1)

1-1: Java 패키지 명명 규칙 위반: feResponse는 camelCase입니다.

Java 패키지 이름은 모두 소문자를 사용하는 것이 표준 규칙입니다. feResponse 대신 feresponse 또는 fe.response로 변경하는 것을 권장합니다.

camelCase 패키지 이름은 대소문자를 구분하지 않는 파일 시스템에서 충돌을 일으킬 수 있으며, Oracle의 Java 명명 규칙에도 어긋납니다.

modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/SpecificExplanationServiceImpl.java (1)

67-74: ObjectMapper 재사용 권장.

ObjectMapper를 메서드 내에서 매번 생성하고 있습니다. ObjectMapper는 thread-safe하므로 필드나 빈으로 재사용하는 것이 효율적입니다.

♻️ 제안하는 수정

클래스에 필드로 추가:

 public class SpecificExplanationServiceImpl implements SpecificExplanationService {
 
     private final HashUtil hashUtil;
     private final RestClient aiRestClient;
     private final ProblemRepository problemRepository;
+    private final ObjectMapper objectMapper;

생성자에서 주입받거나, Spring의 기본 ObjectMapper 빈 사용:

     public SpecificExplanationServiceImpl(
         `@Qualifier`("aiRestClient") RestClient aiRestClient,
         ProblemRepository problemRepository,
-        HashUtil hashUtil
+        HashUtil hashUtil,
+        ObjectMapper objectMapper
     ) {
         this.aiRestClient = aiRestClient;
         this.problemRepository = problemRepository;
         this.hashUtil = hashUtil;
+        this.objectMapper = objectMapper;
     }

메서드 내에서 사용:

-            ObjectMapper objectMapper = new ObjectMapper();
             JsonNode node = objectMapper.readTree(aiExplanationRaw);
modules/util/impl/src/main/java/com/icc/qasker/util/controller/HelloController.java (1)

15-23: Map.of() 사용 고려.

현재 HashMap을 사용하고 있지만, 정적인 key-value 쌍에는 불변 Map.of()가 더 간결하고 안전합니다.

♻️ 제안된 리팩터링
-    public Map<String, Object> status() {
-        Map<String, Object> response = new HashMap<>();
-
-        response.put("profile", activeProfile);
-        response.put("status", "UP");
-
-        return response;
+    public Map<String, Object> status() {
+        return Map.of(
+            "profile", activeProfile,
+            "status", "UP"
+        );
     }

이 경우 HashMap import도 제거할 수 있습니다.

modules/util/impl/src/main/java/com/icc/qasker/util/controller/UpdateLogController.java (1)

1-6: 요청 DTO는 import로 정리해 가독성을 높이는 것이 좋겠습니다.
현재 FQN 사용으로 시그니처가 길어집니다.

♻️ 제안 변경
+import com.icc.qasker.util.dto.request.UpdateLogRequest;
@@
-    public ResponseEntity<UpdateLogResponse> createUpdateLog(
-        `@RequestBody` com.icc.qasker.util.dto.request.UpdateLogRequest request) {
+    public ResponseEntity<UpdateLogResponse> createUpdateLog(
+        `@RequestBody` UpdateLogRequest request) {

Also applies to: 28-30

modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java (1)

74-84: ObjectMapper를 매 호출마다 생성하지 않도록 싱글톤 빈으로 재사용하세요.

ObjectMapper는 설정 후 스레드 세이프하며, Jackson 공식 문서에서도 무거운 객체이므로 재사용을 권장합니다. 매번 새로 생성하면 성능 저하와 GC 부담이 증가합니다. Spring Boot는 기본적으로 ObjectMapper 빈을 자동 구성하므로, 의존성 주입으로 재사용하면 됩니다.

♻️ 수정 제안
-private String parseErrorMessage(String messageBody) {
+private String parseErrorMessage(String messageBody) {
     try {
-        ObjectMapper objectMapper = new ObjectMapper();
         JsonNode rootNode = objectMapper.readTree(messageBody);
         if (rootNode.has("detail")) {
             return rootNode.get("detail").asText();
         }
     } catch (Exception ignored) {
     }
     return messageBody;
 }
-    private final WebClient webClient;
+    private final WebClient webClient;
+    private final ObjectMapper objectMapper;

-    public AiServerAdapter(`@Qualifier`("aiStreamClient") WebClient webClient) {
+    public AiServerAdapter(`@Qualifier`("aiStreamClient") WebClient webClient,
+                           ObjectMapper objectMapper) {
         this.webClient = webClient;
+        this.objectMapper = objectMapper;
     }

Comment on lines 5 to 6
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for MVC starter conflicts in build.gradle files
echo "=== Checking for spring-boot-starter-web presence ==="
rg -n "spring-boot-starter-web\b" -g 'build.gradle*' 2>/dev/null || echo "No matches found"

echo ""
echo "=== Checking web-application-type configuration ==="
rg -n "web-application-type" -g '*application*.yml' -g '*application*.properties' 2>/dev/null || echo "No matches found"

echo ""
echo "=== Examining modules/quiz/api/build.gradle ==="
fd 'build.gradle' -p '*quiz/api*' -x cat {} \;

Repository: q-asker/api

Length of output: 432


🏁 Script executed:

#!/bin/bash
# Find all build.gradle files with their paths
echo "=== All build.gradle files with spring-boot-starter-web context ==="
rg -l "spring-boot-starter-web" -g 'build.gradle*'

echo ""
echo "=== Finding modules/quiz/api/build.gradle ==="
find . -path "*/modules/quiz/api/build.gradle" -type f 2>/dev/null

echo ""
echo "=== Content of modules/quiz/api/build.gradle ==="
find . -path "*/modules/quiz/api/build.gradle" -type f -exec cat -n {} \; 2>/dev/null

echo ""
echo "=== Checking for any WebFlux dependencies ==="
rg -n "webflux" -i -g 'build.gradle*'

Repository: q-asker/api

Length of output: 760


🏁 Script executed:

#!/bin/bash
# Check if quiz/api depends on quiz/impl
echo "=== Checking modules/quiz/api/build.gradle for quiz/impl dependency ==="
cat -n ./modules/quiz/api/build.gradle

echo ""
echo "=== Checking modules/quiz/impl/build.gradle ==="
cat -n ./modules/quiz/impl/build.gradle

echo ""
echo "=== Checking root build.gradle for dependency management ==="
cat -n ./build.gradle | head -100

echo ""
echo "=== Checking settings.gradle for project structure ==="
cat -n ./settings.gradle 2>/dev/null || echo "No settings.gradle found"

Repository: q-asker/api

Length of output: 4842


루트 build.gradle에서 서블릿 스택이 모든 서브프로젝트에 강제되고 있음

모듈의 WebFlux 선언에도 불구하고 루트 build.gradle의 49줄에서 모든 서브프로젝트에 spring-boot-starter-web을 적용하고 있습니다. 또한 57줄의 springdoc-openapi-starter-webmvc-ui도 MVC 스택을 강제합니다. web-application-type 설정이 없으므로 서블릿 스택이 우선되어 리액티브 스택의 의도가 무효화됩니다.

quiz/api 모듈에서 WebFlux 도입 시:

  • 루트 build.gradle에서 선택적 서브프로젝트 설정으로 변경하거나
  • web-application-type: reactive 설정을 명시하여 리액티브 우선순위를 보장해야 합니다.
🤖 Prompt for AI Agents
In `@modules/quiz/api/build.gradle` around lines 5 - 6, Root build.gradle is
forcing the servlet (MVC) stack by applying 'spring-boot-starter-web' and
'springdoc-openapi-starter-webmvc-ui' to all subprojects, which overrides the
quiz/api module's WebFlux intent; either make the root application of those
dependencies conditional per-subproject (so quiz/api does not get
'spring-boot-starter-web' or the MVC OpenAPI starter) or explicitly set the
application to reactive in the quiz/api module (e.g., set
web-application-type=reactive via application properties or configure
SpringApplication to use WebApplicationType.REACTIVE) so that symbols like
'spring-boot-starter-web', 'springdoc-openapi-starter-webmvc-ui', and the
quiz/api module's WebFlux declaration no longer conflict.

Comment on lines 1 to 18
package com.icc.qasker.quiz.doc;

import com.icc.qasker.quiz.dto.feRequest.GenerationRequest;
import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import reactor.core.publisher.Flux;

@Tag(name = "Generation", description = "생성 관련 API")
public interface GenerationApiDoc {

@Operation(summary = "문제를 생성한다")
@PostMapping
Flux<ProblemSetResponse> postProblemSetId(
String userId,
@RequestBody GenerationRequest generationRequest);
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:

find . -type f -name "*GenerationController*" | head -20

Repository: q-asker/api

Length of output: 145


🏁 Script executed:

cat modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/GenerationController.java

Repository: q-asker/api

Length of output: 1291


userId에 @UserId 애너테이션 추가 필요
컨트롤러 구현에서 userId 파라미터에 @UserId 애너테이션이 적용되어 있으나, 인터페이스에는 누락되어 있습니다. Springdoc-OpenAPI가 올바른 API 문서를 생성하도록 인터페이스의 userId 파라미터에도 @UserId 애너테이션을 명시해 주세요.

🤖 Prompt for AI Agents
In `@modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/GenerationApiDoc.java`
around lines 1 - 18, The GenerationApiDoc interface's postProblemSetId method is
missing the `@UserId` annotation on the userId parameter which causes
Springdoc-OpenAPI to generate incomplete docs; update the method signature in
GenerationApiDoc (postProblemSetId) to annotate the userId parameter with
`@UserId` (the same annotation used in the controller) so the interface parameter
matches the controller and Springdoc picks up the userId correctly for
GenerationRequest -> ProblemSetResponse API documentation.

Comment on lines 3 to 10
import com.icc.qasker.quiz.dto.feRequest.GenerationRequest;
import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse;
import reactor.core.publisher.Flux;

public interface GenerationService {

GenerationResponse processGenerationRequest(FeGenerationRequest feGenerationRequest,
Flux<ProblemSetResponse> processGenerationRequest(GenerationRequest generationRequest,
String userId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all implementations of GenerationService
echo "=== GenerationService Implementations ==="
rg -nP --type=java 'implements\s+GenerationService' -A 20

echo -e "\n=== processGenerationRequest Call Sites ==="
# Find all calls to processGenerationRequest
rg -nP --type=java 'processGenerationRequest\s*\(' -B 2 -A 3

Repository: q-asker/api

Length of output: 4946


🏁 Script executed:

#!/bin/bash
cat -n modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/GenerationController.java

Repository: q-asker/api

Length of output: 1522


🏁 Script executed:

#!/bin/bash
cat -n modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java | head -100

Repository: q-asker/api

Length of output: 3992


🏁 Script executed:

#!/bin/bash
# Check if S3Service and S3ValidateService have blocking operations
echo "=== Checking validateFile context ==="
cat -n modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java | sed -n '71,77p'

echo -e "\n=== Check aiServerAdapter.requestGenerate return type ==="
rg -nP --type=java 'requestGenerate' -B 2 -A 2 modules/quiz/impl/

Repository: q-asker/api

Length of output: 1878


Flux 반환형과 실제 구현의 반응형 계약 불일치

메서드가 Flux<ProblemSetResponse>를 반환하지만 구현에서 다음과 같은 차단(blocking) 작업들이 포함되어 있습니다:

  • 47줄: validateFile() – 동기적 차단 호출
  • 50줄: problemSetRepository.save() – JPA 차단 작업
  • 60줄: problemRepository.save()map() 연산자 내부에서의 차단 작업 (반응형 파이프라인 위반)

이러한 차단 작업들은 반응형 계약을 위반합니다. 데이터베이스 작업을 반응형 API로 변경하거나, 차단 작업을 publishOn()으로 적절한 스케줄러에 오프로드하도록 리팩토링이 필요합니다.

🤖 Prompt for AI Agents
In `@modules/quiz/api/src/main/java/com/icc/qasker/quiz/GenerationService.java`
around lines 3 - 10, processGenerationRequest currently returns
Flux<ProblemSetResponse> but its implementation performs blocking calls
(validateFile(), problemSetRepository.save(), problemRepository.save() inside
map()) which violates the reactive contract; refactor by moving blocking calls
out of map and into non-blocking reactive operators: wrap each blocking call
with Mono.fromCallable(...) or
Mono.fromRunnable(...).subscribeOn(Schedulers.boundedElastic()) (or switch to a
reactive repository) and replace blocking map() usages with flatMap/flatMapMany
to compose these Monos so all DB saves and file validation run on a
boundedElastic scheduler (or use reactive R2DBC repositories) while
processGenerationRequest remains a Flux; specifically change validateFile
invocations, problemSetRepository.save(...) and problemRepository.save(...) to
be executed via Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic())
and composed with flatMap instead of executing synchronously inside map().

Comment on lines 9 to 13
implementation 'org.apache.httpcomponents.client5:httpclient5:5.3.1'
implementation "io.github.resilience4j:resilience4j-spring-boot3:2.3.0"
implementation "org.springframework.boot:spring-boot-starter-aop"
implementation 'org.springframework.boot:spring-boot-starter-webflux'
} No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for spring-boot-starter-web references in build.gradle files
rg -n "spring-boot-starter-web\b" -g 'build.gradle*'

Repository: q-asker/api

Length of output: 144


🏁 Script executed:

#!/bin/bash
# Read the full build.gradle file for modules/quiz/impl to understand all dependencies
find . -path "*/modules/quiz/impl/build.gradle" -type f | head -1 | xargs cat -n

Repository: q-asker/api

Length of output: 616


🏁 Script executed:

#!/bin/bash
# Check for root/parent build.gradle configuration
fd build.gradle -x sh -c 'echo "=== {} ===" && head -30 {}' | head -100

Repository: q-asker/api

Length of output: 2663


🏁 Script executed:

#!/bin/bash
# Find which build.gradle file has spring-boot-starter-web at line 49
rg -n "spring-boot-starter-web" -g 'build.gradle*' --with-filename

Repository: q-asker/api

Length of output: 358


🏁 Script executed:

#!/bin/bash
# Check the root build.gradle for all content
cat -n build.gradle | tail -50

Repository: q-asker/api

Length of output: 2018


🏁 Script executed:

#!/bin/bash
# Verify the full app/build.gradle
find . -path "*/app/build.gradle" -type f -exec cat -n {} \;

Repository: q-asker/api

Length of output: 2152


impl 모듈의 WebFlux/MVC 중복 유입이 확인되었습니다

modules/quiz/impl/build.gradle이 명시적으로 spring-boot-starter-webflux를 선언했지만, 루트 build.gradlesubprojects.dependencies 설정(49번 줄)으로 인해 모든 서브프로젝트에 spring-boot-starter-web (MVC)이 자동으로 추가됩니다. 이로 인해 이 모듈에는 WebFlux와 MVC 스타터가 동시에 포함되어 있으므로, 둘 중 필요한 것만 명시적으로 선택하거나 루트 설정을 조정해야 합니다.

🤖 Prompt for AI Agents
In `@modules/quiz/impl/build.gradle` around lines 9 - 13, This module currently
pulls both WebFlux and MVC starters: remove the duplicate by either (A) deleting
the explicit spring-boot-starter-webflux entry from
modules/quiz/impl/build.gradle if you want MVC, or (B) if you want WebFlux,
override the root subprojects.dependencies automatic inclusion of
spring-boot-starter-web (referenced as subprojects.dependencies) by excluding or
removing that dependency for this subproject (e.g., adjust the module's
build.gradle to exclude spring-boot-starter-web from the inherited configuration
or change the root rule to not apply to this subproject). Ensure only one of
spring-boot-starter-webflux or spring-boot-starter-web remains.

Comment on lines 41 to 63
.onStatus(HttpStatusCode::is4xxClientError, clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(
errorBody -> {
String message = parseErrorMessage(errorBody);
log.error("[AI Server] 4xx Error: {}", message);

String messageBody = e.getResponseBodyAsString();
log.error("[AI Server] Bad Request: Status={}, Body={}", e.getStatusCode(),
messageBody);
return Mono.error(new ClientSideException(message));
}
)
)
.onStatus(HttpStatusCode::is5xxServerError, clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(
errorBody -> {
String message = parseErrorMessage(errorBody);
log.error("[AI Server] 5xx Error: {}", message);

String message = "";
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(messageBody);

if (rootNode.has("detail")) {
message = rootNode.get("detail").asText();
return Mono.error(new CustomException(
ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR));
}
)
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Spring WebClient onStatus handler empty response body Mono.empty() behavior

💡 Result:

In WebClient.retrieve(), the onStatus(...) callback is only for deciding whether a given HTTP status should be turned into an error signal.

What Mono.empty() in onStatus means

If your exceptionFunction returns Mono.empty(), you are explicitly saying: “do not treat this status as an error; continue as a normal response.” Spring will then propagate the response downstream and let bodyToMono(...) (or toEntity(...), etc.) handle it normally. [1][2]

So for example, if you do:

.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, r -> Mono.empty())
.bodyToMono(MyDto.class)

then a 4xx response is not converted into an exception; the client proceeds to decode the body.

Why you see an “empty body” result

Independently of onStatus, bodyToMono(...) will complete empty when the response has no body (e.g., 204 No Content, or a response with an empty payload), because there is nothing to decode into MyDto. That appears to the caller as Mono.empty() (i.e., “no value”). [3]

If you actually want failure (or special handling) when the body is empty

Do it downstream, e.g.:

.bodyToMono(MyDto.class)
.switchIfEmpty(Mono.error(new IllegalStateException("Empty body")));

(or .single() to force an error if no element is emitted). [3]

Note about consuming the body in onStatus

If you do return an exception from onStatus, Spring’s docs note you should consume the response content in that handler if content is expected; otherwise it will be drained to release resources. [4]

Sources: Spring WebClient.ResponseSpec#onStatus Javadoc (Spring 6.x / 5.x / 5.1.x) and related bodyToMono behavior discussion. [1][2][4][3]


🌐 Web query:

Spring WebClient onStatus error handling empty body best practice

💡 Result:

Best practice: in retrieve().onStatus(...), always return a Mono<Throwable> and (when you care about the error payload) consume the response body inside the onStatus function; otherwise you risk leaking/“failed to release” scenarios when an error response actually has content. Spring’s onStatus contract explicitly calls this out (“if the response is expected to have content, the exceptionFunction should consume it”). [1][2]

Recommended patterns (including empty body)

1) Read body as String and handle empty with defaultIfEmpty

webClient.get()
  .uri("/x")
  .retrieve()
  .onStatus(HttpStatusCode::isError, resp ->
      resp.bodyToMono(String.class)
          .defaultIfEmpty("")                  // key for empty body
          .map(body -> new RemoteApiException(resp.statusCode(), body))
  )
  .bodyToMono(MyDto.class);

Why: bodyToMono(...) on an empty body completes empty (it doesn’t “fail”), so you must decide what to do when it’s empty (e.g., use an empty string, or switchIfEmpty(Mono.error(...))). This behavior is commonly observed in practice. [3]

2) If you don’t need the body, explicitly discard it

.onStatus(HttpStatusCode::isError, resp ->
    resp.bodyToMono(Void.class)
        .then(Mono.error(new RemoteApiException(resp.statusCode())))
)

Why: you’re making it explicit that you’re not using the payload, while still producing an error signal.

3) Prefer Spring’s default exception when you just need “good enough”

If you don’t register an onStatus handler, Spring will map >= 400 to a WebClientResponseException via ClientResponse.createException(). [1][4]

Rule of thumb: parse a structured error body only when the server reliably provides one; otherwise treat the body as optional (defaultIfEmpty) and always include at least status + request context in your exception. [1][2][4]

Sources: Spring WebClient reference/docs for retrieve()/onStatus and Javadoc contract details; Stack Overflow discussion confirming empty-body bodyToMono behavior and the need to consume error bodies. [1][2][3][4]


🌐 Web query:

Spring Reactor bodyToMono empty response defaultIfEmpty error handling pattern

💡 Result:

A common WebClient + Reactor pattern is to treat “empty body” (e.g., 204, or 200 with no content) separately from error statuses (4xx/5xx), because:

  • retrieve() turns 4xx/5xx into an error signal by default, customizable via onStatus(...). [1]
  • In onStatus(...), returning Mono.empty() suppresses treating that status as an error and lets the response continue downstream to bodyToMono(...). [2]
  • For an empty upstream Mono, use defaultIfEmpty(value) (fallback value) or switchIfEmpty(publisher) (fallback publisher, e.g., Mono.error(...)). [3]

1) If empty body should become a domain error

Mono<Foo> mono =
  webClient.get()
    .uri("/foo/{id}", id)
    .retrieve()
    .onStatus(HttpStatusCode::isError, resp ->
        resp.bodyToMono(String.class)
            .defaultIfEmpty("") // in case error body is empty
            .flatMap(body -> Mono.error(new RemoteCallException(resp.statusCode(), body)))
    )
    .bodyToMono(Foo.class)
    .switchIfEmpty(Mono.error(new EmptyBodyException("Expected Foo but got empty body")));
  • onStatus maps HTTP errors to an exception (optionally reading the error body). [1]
  • switchIfEmpty(Mono.error(..)) turns a successful-but-empty body into an error. [3]

2) If empty body should yield a default value

Mono<Foo> mono =
  webClient.get()
    .uri("/foo/{id}", id)
    .retrieve()
    .onStatus(HttpStatusCode::isError, resp -> resp.createException().flatMap(Mono::error))
    .bodyToMono(Foo.class)
    .defaultIfEmpty(Foo.empty());
  • defaultIfEmpty replaces “no element” with a value. [3]

3) If 404 should be treated as “not found” (empty), then handle downstream

Mono<Foo> mono =
  webClient.get()
    .uri("/foo/{id}", id)
    .retrieve()
    .onStatus(status -> status.value() == 404, resp -> Mono.empty()) // suppress error for 404
    .bodyToMono(Foo.class)
    .switchIfEmpty(Mono.error(new NotFoundException(id)));
  • Returning Mono.empty() from onStatus suppresses error treatment for that status. [2]
  • Then downstream switchIfEmpty decides what “empty” means for your API. [3]

Note: For Mono<Void> / “no-body” responses, defaultIfEmpty/switchIfEmpty can’t magically produce a value (there is no T to emit); you typically model it differently (status-based logic, or map to a value type). [3]

Sources: Spring WebClient retrieve()/onStatus docs [1], Spring ResponseSpec.onStatus Javadoc (suppression via Mono.empty()) [2], Reactor defaultIfEmpty / switchIfEmpty reference [3].


에러 응답 본문이 비어 있을 때 에러가 누락될 수 있습니다.

bodyToMono(String.class)가 빈 응답 본문에 대해 빈 Mono를 반환하면, flatMap이 실행되지 않아 Mono.error()가 호출되지 않습니다. 그 결과 onStatusMono.empty()를 반환하게 되어 4xx/5xx 에러가 정상 응답처럼 처리될 수 있습니다. 빈 본문에도 항상 에러로 매핑되도록 .defaultIfEmpty("")를 추가하세요.

🛠️ 수정 제안
.onStatus(HttpStatusCode::is4xxClientError, clientResponse ->
    clientResponse.bodyToMono(String.class)
+        .defaultIfEmpty("")
         .flatMap(errorBody -> {
             String message = parseErrorMessage(errorBody);
             log.error("[AI Server] 4xx Error: {}", message);
             return Mono.error(new ClientSideException(message));
         })
)
.onStatus(HttpStatusCode::is5xxServerError, clientResponse ->
    clientResponse.bodyToMono(String.class)
+        .defaultIfEmpty("")
         .flatMap(errorBody -> {
             String message = parseErrorMessage(errorBody);
             log.error("[AI Server] 5xx Error: {}", message);
             return Mono.error(new CustomException(
                 ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR));
         })
)
🤖 Prompt for AI Agents
In
`@modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java`
around lines 41 - 63, In AiServerAdapter update the onStatus handlers so
bodyToMono(String.class) cannot return an empty Mono: call .defaultIfEmpty("")
on the Mono returned from clientResponse.bodyToMono(String.class) in both the
HttpStatusCode::is4xxClientError and HttpStatusCode::is5xxServerError branches;
this ensures parseErrorMessage(errorBody) runs and the code throws
ClientSideException (4xx) or
CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR) (5xx) even when
the response body is empty.

Comment on lines 21 to 27
@Primary
@Bean("aiGenerationRestClient")
public RestClient aiGenerationRestClient() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(5));
factory.setReadTimeout(Duration.ofSeconds(80));

return RestClient.builder()
@Bean("aiStreamClient")
public WebClient aiGenerationClient() {
return WebClient.builder()
.baseUrl(qAskerProperties.getAiServerUrl())
.requestFactory(factory)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the file mentioned in the review
find . -type f -name "AiWebClientConfig.java" 2>/dev/null

Repository: q-asker/api

Length of output: 138


🏁 Script executed:

# Also search for any WebClient configurations in the codebase
fd -e java -e xml | xargs rg -l "WebClient" 2>/dev/null | head -20

Repository: q-asker/api

Length of output: 217


🏁 Script executed:

# Search for timeout configurations related to WebClient or HTTP
rg -i "timeout|connector|channel" --type java | grep -i "webclient\|httpclient\|reactor" | head -30

Repository: q-asker/api

Length of output: 332


🏁 Script executed:

# Check for any Spring HTTP client configurations
fd -e java | xargs rg -l "ReactorClientHttpConnector|HttpClient|ChannelOption" 2>/dev/null

Repository: q-asker/api

Length of output: 39


🏁 Script executed:

cat -n modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/AiWebClientConfig.java

Repository: q-asker/api

Length of output: 1803


🌐 Web query:

Spring WebClient streaming timeout configuration best practices

💡 Result:

Key ideas for streaming (SSE / application/stream+json / long polling) with Spring WebClient

  1. Separate “connect” vs “stream is alive” concerns

    • Set a connection timeout so dead/blackholed connects fail fast (e.g., 5–10s). Spring shows configuring this via Reactor Netty HttpClient.option(CONNECT_TIMEOUT_MILLIS, …) on the ReactorClientHttpConnector. [1]
    • For the stream itself, prefer an idle/read timeout (fail if no bytes arrive for N seconds) rather than a “total request duration” timeout.
  2. Understand what Reactor Netty responseTimeout really does

    • HttpClient.responseTimeout(Duration) is not “time to finish the whole download”; it’s the maximum interval between network read operations while reading the response (implemented via ReadTimeoutHandler). [2]
    • Reactor Netty explicitly recommends configuring a response timeout in general, and also shows you can override it per-request. [3]

    Best practice for streaming: set responseTimeout long enough to cover expected gaps (or remove it) unless your stream sends heartbeats frequently.

  3. Use application-level heartbeats for streaming

    • For streaming responses, Spring notes you should send data periodically (e.g., SSE comments / no-op events) to detect disconnects reliably. [4]
    • This also prevents client-side “idle” timeouts (including responseTimeout) from firing when the stream is healthy but quiet.
  4. Prefer request-specific overrides (stream vs non-stream)

    • Keep a conservative default for normal HTTP calls, then override for streaming endpoints (or vice versa). Reactor Netty supports per-request responseTimeout. [3]
  5. If you need “no items emitted” timeouts, use Reactor’s timeout()

    • For streams where you want to fail if no elements arrive for N seconds (regardless of low-level TCP reads), use Reactor’s Flux/Mono.timeout(...) at the reactive layer (often better aligned with “business” time). (Common pattern; pair with network timeouts above.)

Practical configuration pattern (global defaults + streaming override)

HttpClient httpClient =
    HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10_000)     // connect timeout [1]
        .responseTimeout(Duration.ofSeconds(30));                 // idle between reads [2][3]

WebClient webClient =
    WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();

// For a streaming call, override/remove the idle timeout (or set it > heartbeat gap):
Flux<String> stream =
    webClient.get()
        .uri("https://example.com/stream")
        .httpRequest(r -> r.getNativeRequest().responseTimeout(null)) // remove [2][3]
        .retrieve()
        .bodyToFlux(String.class);

Rule of thumb: for streaming, either (a) send heartbeats and keep a moderate idle timeout, or (b) disable/extend the idle timeout and enforce liveness with timeout() + domain-level heartbeats/keepalives. [2][4]


Sources

[1] Spring Framework Reference – WebClient configuration / timeouts (Reactor Netty connector examples) (docs.spring.io)
[2] Reactor Netty HttpClient.responseTimeout Javadoc (max interval between read ops; ReadTimeoutHandler) (docs.spring.io)
[3] Reactor Netty Reference Guide – Response Timeout best practice and per-request override (docs.spring.io)
[4] Spring “Web on Reactive Stack” – Streaming: send periodic data/heartbeats (docs.spring.io)

Citations:


🌐 Web query:

Spring WebClient ReactorClientHttpConnector ChannelOption timeout

💡 Result:

In Spring WebFlux, the ChannelOption timeout you typically set via ReactorClientHttpConnector is the TCP connect timeout (CONNECT_TIMEOUT_MILLIS). Read/write/response timeouts are configured on Reactor Netty’s HttpClient (pipeline handlers or responseTimeout). (docs.spring.io)

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import java.time.Duration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

HttpClient httpClient =
    HttpClient.create()
        // Connect timeout (TCP handshake)
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10_000)                 // 10s
        // Response timeout (max time between read operations while receiving the response)
        .responseTimeout(Duration.ofSeconds(10))                              // 10s
        // Read/Write timeouts (Netty handlers)
        .doOnConnected(conn -> conn
            .addHandlerLast(new ReadTimeoutHandler(10))
            .addHandlerLast(new WriteTimeoutHandler(10)));

WebClient webClient =
    WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();

Notes:

  • CONNECT_TIMEOUT_MILLIS is the ChannelOption you want for “timeout while establishing the connection”. (docs.spring.io)
  • responseTimeout(Duration) adds a ReadTimeoutHandler for the response receive phase (per Reactor Netty docs). (projectreactor.io)

Citations:


🏁 Script executed:

# Check if there are any usages of aiStreamClient to understand streaming context
rg -A 5 -B 2 "aiStreamClient|aiGenerationClient" --type java

Repository: q-asker/api

Length of output: 1825


스트리밍 WebClient에 연결/읽기 타임아웃 정책이 명시되지 않았습니다.

aiGenerationClient() WebClient는 기본값에만 의존하고 있어, 서버 지연이나 네트워크 문제 시 커넥션이 장시간 점유될 수 있습니다. ReactorClientHttpConnector를 통해 최소한 연결 타임아웃(connect timeout)을 명시하고, 스트리밍 특성에 맞게 응답 타임아웃(response timeout) 정책도 정의해 주세요.

🛠️ 예시 (connect timeout 설정)
+import io.netty.channel.ChannelOption;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import reactor.netty.http.client.HttpClient;
@@
     public WebClient aiGenerationClient() {
         return WebClient.builder()
             .baseUrl(qAskerProperties.getAiServerUrl())
+            .clientConnector(new ReactorClientHttpConnector(
+                HttpClient.create()
+                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
+            ))
             .build();
     }
🤖 Prompt for AI Agents
In
`@modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/AiWebClientConfig.java`
around lines 21 - 27, The aiGenerationClient WebClient lacks explicit connection
and response timeout settings; update the aiGenerationClient() bean to build a
WebClient using a ReactorClientHttpConnector configured with a Reactor Netty
TcpClient/HttpClient that sets a connect timeout (e.g.,
tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, ...)) and a response/read
timeout appropriate for streaming (e.g., using tcpClient.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(...)))) or HttpClient
responseTimeout, so the WebClient created in aiGenerationClient() enforces both
connect and response timeouts for streaming scenarios.

Comment on lines +34 to +48
@Builder
public ProblemSet(String userId) {
this.userId = userId;
}

public static ProblemSet of(GenerationResponseFromAI aiResponse) {
return of(aiResponse, null);
}

public static ProblemSet of(AiGenerationResponse aiResponse, String userId) {
public static ProblemSet of(GenerationResponseFromAI aiResponse, String userId) {
if (aiResponse == null || aiResponse.getQuiz() == null) {
throw new CustomException(ExceptionMessage.NULL_AI_RESPONSE);
}
ProblemSet problemSet = new ProblemSet();
problemSet.setUserId(userId);
List<Problem> problems = aiResponse.getQuiz().stream()
ProblemSet problemSet = ProblemSet.builder().userId(userId).build();
problemSet.problems = aiResponse.getQuiz().stream()
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:

# Search for all ProblemSet.of() calls to verify usage pattern
rg -n -C3 --type=java 'ProblemSet\.of\('

Repository: q-asker/api

Length of output: 37


🏁 Script executed:

# Find the ProblemSet file and verify it exists
find . -name "ProblemSet.java" -type f

# Also search for any usage of "of(" method more broadly
rg -n "\.of\(" --type=java modules/quiz/ | head -50

Repository: q-asker/api

Length of output: 876


🏁 Script executed:

# Read ProblemSet.java file
cat -n modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/ProblemSet.java

# Also search for any other references to ProblemSet( or new ProblemSet
rg -n "new ProblemSet|ProblemSet\(" --type=java modules/quiz/

Repository: q-asker/api

Length of output: 3380


🏁 Script executed:

# Search for actual calls to ProblemSet.of more specifically
rg -n "ProblemSet\.of" --type=java

# Also check GenerationServiceImpl around the area mentioned in previous search
cat -n modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java | sed -n '40,80p'

Repository: q-asker/api

Length of output: 2026


🏁 Script executed:

# Comprehensive search for ProblemSet.of calls - try different patterns
rg "ProblemSet\s*\.\s*of\s*\(" --type=java -n
rg 'of\(aiResponse' --type=java -n
rg 'of\(.*aiResponse' --type=java -n

# Also check if the file is even included in the built project
git log --oneline -- modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/ProblemSet.java | head -5

Repository: q-asker/api

Length of output: 658


🏁 Script executed:

# Check if the factory methods are used in tests
rg "ProblemSet\s*\.\s*of\s*\(" --type=java -n modules/quiz/

# Also look for test files that might use these methods
find modules/quiz -name "*Test.java" -o -name "*Tests.java" | head -20

Repository: q-asker/api

Length of output: 37


🏁 Script executed:

# Broader search for test files and any usage
find . -type f -name "*ProblemSet*Test*.java" -o -name "*Test*ProblemSet*.java"

# Search entire repository for ProblemSet.of calls
rg "ProblemSet\s*\.\s*of\s*\(" -n --type=java 2>/dev/null | grep -v "^modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/ProblemSet.java"

# Check if there are any comments or TODOs about these methods
rg -B2 "public static ProblemSet of" --type=java -n

Repository: q-asker/api

Length of output: 696


사용되지 않는 팩토리 메서드를 제거하세요.
ProblemSet.of(aiResponse)of(aiResponse, userId) 팩토리 메서드는 userId를 null로 저장할 수 있지만, 코드베이스 어디에서도 호출되지 않는 사용 금지 코드입니다. 현재 구현은 GenerationServiceImpl에서 ProblemSet.builder().userId(userId).build()로 직접 빌더를 사용하여 userId를 안전하게 전달합니다. 이 미사용 팩토리 메서드들을 삭제하거나 private으로 변경하세요.

🤖 Prompt for AI Agents
In `@modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/ProblemSet.java`
around lines 34 - 48, The two unused factory methods
ProblemSet.of(GenerationResponseFromAI) and
ProblemSet.of(GenerationResponseFromAI, String) should be removed or made
private to avoid creating ProblemSet instances with a null userId; update the
ProblemSet class to delete these static methods (or change their visibility to
private) and keep using ProblemSet.builder().userId(userId).build() as done in
GenerationServiceImpl so userId is always supplied when constructing ProblemSet.

Comment on lines 49 to 60
ProblemSet problemSet = ProblemSet.builder().userId(userId).build();
ProblemSet save = problemSetRepository.save(problemSet);
String id = hashUtil.encode(save.getId());
Scheduler scheduler = Schedulers.fromExecutor(Executors.newVirtualThreadPerTaskExecutor());

GenerationResponse response = new GenerationResponse(
hashUtil.encode(savedPs.getId())
);

slackNotifier.notifyText("""
✅ [퀴즈 생성 완료 알림]
ProblemSet ID: %s
""".formatted(
response.getProblemSetId()
));

return response;
return aiServerAdapter.requestGenerate(request)
.publishOn(scheduler)
.map(aiDto -> {
List<QuizForFe> quizForFeList = new ArrayList<>();
for (QuizGeneratedFromAI quizGeneratedFromAI : aiDto.getQuiz()) {
Problem problem = Problem.of(quizGeneratedFromAI, problemSet);
problemRepository.save(problem);
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

🌐 Web query:

Spring Data Repository save() method return same instance vs new instance

💡 Result:

CrudRepository.save(…) (and JpaRepository.save(…)) is only guaranteed to return “the saved entity” — not the same Java object instance you passed in.

  • If the entity is new (Spring Data detects it as new), Spring Data JPA calls EntityManager.persist(entity) and returns the same instance. (docs.spring.io)
  • If the entity is not new (typically detached with an id), Spring Data JPA calls EntityManager.merge(entity) and returns the managed instance produced/found by merge, which may be a different object than the argument. (docs.spring.io)
  • Spring Data’s own javadoc warns: “Use the returned instance for further operations as the save operation might have changed the entity instance completely.” (docs.huihoo.com)

Practical rule: always assign the result:

entity = repository.save(entity);

Citations:


🏁 Script executed:

# Search for Problem.of() method to understand how it uses ProblemSet
rg "Problem\.of\(" --type java -B 2 -A 5

Repository: q-asker/api

Length of output: 1993


🏁 Script executed:

# Search for ProblemSet entity definition and ID generation
fd "ProblemSet.java" --type f

Repository: q-asker/api

Length of output: 129


🏁 Script executed:

# Check the GenerationServiceImpl file to see the full context
fd "GenerationServiceImpl.java" --type f

Repository: q-asker/api

Length of output: 141


🏁 Script executed:

# Get ProblemSet entity definition to check ID generation strategy
cat modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/ProblemSet.java

Repository: q-asker/api

Length of output: 1713


🏁 Script executed:

# Find Problem.of() method definition
fd "Problem.java" --type f | head -5

Repository: q-asker/api

Length of output: 126


🏁 Script executed:

# Search for Problem.of() method implementation
ast-grep --pattern $'class Problem {
  $$$
  static Problem of($_,$_) {
    $$$
  }
  $$$
}'

Repository: q-asker/api

Length of output: 37


🏁 Script executed:

# Read Problem.java to see Problem.of() method
cat modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/Problem.java

Repository: q-asker/api

Length of output: 2291


🏁 Script executed:

# Also check the GenerationServiceImpl full context around lines 49-70
sed -n '45,70p' modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java

Repository: q-asker/api

Length of output: 1202


저장된 관리 엔티티 인스턴스를 사용하세요.
Spring Data의 save() 메서드는 전달된 인스턴스와 다른 인스턴스를 반환할 수 있습니다. 반환된 관리 엔티티를 사용하면 코드 안전성을 높이고 Spring Data 권장사항을 따릅니다. problemSet 대신 save 변수를 Problem 생성 시 전달하세요.

🛠️ 수정 제안
     ProblemSet problemSet = ProblemSet.builder().userId(userId).build();
-    ProblemSet save = problemSetRepository.save(problemSet);
+    ProblemSet savedProblemSet = problemSetRepository.save(problemSet);
-    String id = hashUtil.encode(save.getId());
+    String id = hashUtil.encode(savedProblemSet.getId());
     Scheduler scheduler = Schedulers.fromExecutor(Executors.newVirtualThreadPerTaskExecutor());

     return aiServerAdapter.requestGenerate(request)
         .publishOn(scheduler)
         .map(aiDto -> {
             List<QuizForFe> quizForFeList = new ArrayList<>();
             for (QuizGeneratedFromAI quizGeneratedFromAI : aiDto.getQuiz()) {
-                Problem problem = Problem.of(quizGeneratedFromAI, problemSet);
+                Problem problem = Problem.of(quizGeneratedFromAI, savedProblemSet);
                 problemRepository.save(problem);
🤖 Prompt for AI Agents
In
`@modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java`
around lines 49 - 60, Use the managed entity returned by save() instead of the
original transient variable: replace usages of problemSet when constructing
Problem instances with the saved variable save (the managed ProblemSet returned
from problemSetRepository.save) inside GenerationServiceImpl where
Problem.of(...) and problemRepository.save(...) are called, so
Problem.of(quizGeneratedFromAI, save) is used to ensure you persist and relate
to the JPA-managed entity.

Comment on lines 52 to 56
Scheduler scheduler = Schedulers.fromExecutor(Executors.newVirtualThreadPerTaskExecutor());

GenerationResponse response = new GenerationResponse(
hashUtil.encode(savedPs.getId())
);

slackNotifier.notifyText("""
✅ [퀴즈 생성 완료 알림]
ProblemSet ID: %s
""".formatted(
response.getProblemSetId()
));

return response;
return aiServerAdapter.requestGenerate(request)
.publishOn(scheduler)
.map(aiDto -> {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "GenerationServiceImpl.java" | head -5

Repository: q-asker/api

Length of output: 143


🏁 Script executed:

fd "GenerationServiceImpl.java" --type f

Repository: q-asker/api

Length of output: 141


🏁 Script executed:

cat -n modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java

Repository: q-asker/api

Length of output: 3992


🏁 Script executed:

# Check how Schedulers are used elsewhere in the codebase
rg "Schedulers\.(fromExecutor|parallel|boundedElastic|single)" --type java -A 3 -B 3 | head -80

Repository: q-asker/api

Length of output: 1047


🏁 Script executed:

# Check if there are any patterns of scheduler disposal
rg "\.dispose\(\)" --type java -B 3 -A 1 | head -60

Repository: q-asker/api

Length of output: 37


🌐 Web query:

Reactor Schedulers.fromExecutor ExecutorService lifecycle management best practices

💡 Result:

Best practices for Schedulers.fromExecutor(…) / Schedulers.fromExecutorService(…) lifecycle management

  1. Prefer Reactor’s built-in schedulers unless you must integrate with an existing ExecutorService. If you wrap an executor via fromExecutor(…), Reactor doesn’t “own” thread creation; you own the executor lifecycle (creation, shutdown, leak prevention). [1]

  2. Prefer fromExecutorService(executorService, "name") over the no-name overload (especially if you use metrics), as Reactor explicitly recommends the named variant. [2]

  3. Treat the Scheduler and the underlying ExecutorService as two lifecycles you must close deliberately.

    • If you created the executor, shut it down during app shutdown (e.g., in a Spring @PreDestroy, DisposableBean, or JVM shutdown hook). [1]
    • If the executor is container-managed/shared, don’t “own” its shutdown; only stop using it (and dispose Reactor resources you created around it). (General rule; the key point is still that you must manage ownership boundaries.) [1]
  4. Create once, reuse, and dispose once (don’t allocate a new scheduler per request/flux).

    • Repeatedly creating schedulers around executors can lead to resource leaks and “extra” worker objects even if threads are shared underneath. (Motivation: schedulers are intended to be reused; you dispose the one you created.) [3]
  5. Shutdown style: use disposeGracefully() when you want an orderly stop, then fall back to dispose() if needed.

    • Reactor provides Scheduler.disposeGracefully() and recommends putting a timeout on it; it can be followed by dispose() for a forceful shutdown. [3]
    • Reactor’s own guidance for implementors explicitly calls out “graceful shutdown” as using ExecutorService.shutdown() rather than shutdownNow() under the hood. [4]

Typical pattern

ExecutorService es = Executors.newFixedThreadPool(8);
Scheduler scheduler = Schedulers.fromExecutorService(es, "my-pool");

// use scheduler in publishOn/subscribeOn...

// on shutdown:
scheduler.disposeGracefully()
         .timeout(Duration.ofSeconds(10))
         .onErrorResume(t -> Mono.fromRunnable(scheduler::dispose))
         .block();

es.shutdown(); // and awaitTermination(...) if you own the executor

Sources: Reactor Schedulers.fromExecutorService Javadoc (naming recommendation) [2], Reactor Scheduler.disposeGracefully() Javadoc (timeout + forceful fallback) [3], Reactor migration guide (meaning of “graceful” shutdown) [4], and guidance that executor lifecycle remains the developer’s responsibility when using fromExecutor [1].

References:
[1] O’Reilly “Hands‑On Reactive Programming with Reactor” (excerpt on Schedulers.fromExecutor lifecycle ownership) (oreilly.com)
[2] Reactor Javadoc: Schedulers.fromExecutorService (“Prefer using …, String”) (docs.spring.io)
[3] Reactor Javadoc: Scheduler.disposeGracefully() (timeout + optional forceful dispose()) (docs.spring.io)
[4] Reactor GitHub Wiki Migration Guide: implementors encouraged to use ExecutorService#shutdown() vs shutdownNow() in disposeGracefully() (github.com)

Citations:


요청마다 새 Executor 생성 → 누수/리소스 고갈 위험

Schedulers.fromExecutor(Executors.newVirtualThreadPerTaskExecutor())가 매 요청마다 새로 생성되고 종료되지 않습니다. Reactor 권장 사항에 따르면 Scheduler는 한 번 생성하여 재사용하고 명시적으로 dispose 해야 하며, 개발자가 executor의 lifecycle을 관리해야 합니다. 공유 Scheduler 빈으로 주입받아 재사용하도록 수정하세요.

🛠️ 수정 제안
-    Scheduler scheduler = Schedulers.fromExecutor(Executors.newVirtualThreadPerTaskExecutor());
...
-        .publishOn(scheduler)
+        .publishOn(aiScheduler)
-    private final ProblemRepository problemRepository;
+    private final ProblemRepository problemRepository;
+    private final Scheduler aiScheduler;
🤖 Prompt for AI Agents
In
`@modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java`
around lines 52 - 56, The code creates a new Scheduler per request with
Schedulers.fromExecutor(Executors.newVirtualThreadPerTaskExecutor()) inside
GenerationServiceImpl, which leaks executors; change this to use a shared,
reusable Scheduler instance injected or created once (e.g., a `@Bean` or a private
final field) and reuse it for publishOn instead of constructing a new one in the
method; ensure the shared Scheduler is disposed on application shutdown (or
managed by the container) and replace the local scheduler variable usage around
aiServerAdapter.requestGenerate(...).publishOn(scheduler).map(...) accordingly.

Comment on lines +1 to +8
plugins {
id 'java-library'
}

dependencies {
implementation project(":global")
implementation project(":util:util-api")
} No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for public types in util-impl
echo "=== Public types in util-impl ==="
find modules/util/impl/src/main/java -type f -name "*.java" -exec grep -l "public\s\(class\|record\|interface\|enum\)" {} \; 2>/dev/null | head -20

Repository: q-asker/api

Length of output: 504


🏁 Script executed:

#!/bin/bash
# Find all source files in util-impl and check their content
echo "=== All Java files in util-impl ==="
find modules/util/impl/src/main/java -type f -name "*.java" 2>/dev/null

Repository: q-asker/api

Length of output: 594


🏁 Script executed:

#!/bin/bash
# Check imports from util or global packages in util-impl
echo "=== Imports from util or global in util-impl ==="
find modules/util/impl/src/main/java -type f -name "*.java" -exec grep -h "^import.*\(util\|global\)" {} \; 2>/dev/null | sort | uniq

Repository: q-asker/api

Length of output: 602


🏁 Script executed:

#!/bin/bash
# Check UpdateLog entity - does it expose CreatedAt?
echo "=== UpdateLog.java ==="
cat modules/util/impl/src/main/java/com/icc/qasker/util/entity/UpdateLog.java

Repository: q-asker/api

Length of output: 732


🏁 Script executed:

#!/bin/bash
# Check UpdateLogController - does it expose global types?
echo "=== UpdateLogController.java ==="
cat modules/util/impl/src/main/java/com/icc/qasker/util/controller/UpdateLogController.java

Repository: q-asker/api

Length of output: 1260


🏁 Script executed:

#!/bin/bash
# Check UpdateLogService interface
echo "=== UpdateLogService.java ==="
cat modules/util/impl/src/main/java/com/icc/qasker/util/service/UpdateLogService.java

Repository: q-asker/api

Length of output: 1451


🏁 Script executed:

#!/bin/bash
# Find UpdateLogResponse
echo "=== Searching for UpdateLogResponse ==="
find modules/util -type f -name "*.java" | xargs grep -l "class UpdateLogResponse\|record UpdateLogResponse"

Repository: q-asker/api

Length of output: 269


🏁 Script executed:

#!/bin/bash
# Check the util-api structure
echo "=== util-api contents ==="
find modules/util -type d | grep -E "util-api|api" | head -20
ls -la modules/util/ 2>/dev/null || echo "Directory not found"

Repository: q-asker/api

Length of output: 797


🏁 Script executed:

#!/bin/bash
# Check if UpdateLogResponse is in util-api
find modules/util -path "*/util-api/*" -name "*.java" -type f 2>/dev/null

Repository: q-asker/api

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Check UpdateLogResponse in util-api
echo "=== UpdateLogResponse.java ==="
cat modules/util/api/src/main/java/com/icc/qasker/util/dto/response/UpdateLogResponse.java

Repository: q-asker/api

Length of output: 317


🏁 Script executed:

#!/bin/bash
# Check util-api build.gradle
echo "=== util-api build.gradle ==="
cat modules/util/api/build.gradle

Repository: q-asker/api

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Check if :global is a dependency of :util:util-api
echo "=== Check :global dependency in util-api ==="
grep -r "implementation.*:global\|api.*:global" modules/util/api/

Repository: q-asker/api

Length of output: 99


🏁 Script executed:

#!/bin/bash
# Check the structure of UpdateLog entity and verify what's exposed
echo "=== Verify public types in util-impl expose :global dependency ==="
grep -n "public.*class\|extends\|implements" modules/util/impl/src/main/java/com/icc/qasker/util/entity/UpdateLog.java

Repository: q-asker/api

Length of output: 167


🏁 Script executed:

#!/bin/bash
# Check util-impl build.gradle structure
echo "=== util-impl build.gradle ==="
cat modules/util/impl/build.gradle

Repository: q-asker/api

Length of output: 220


:global:util:util-api 의존성을 api로 변경 필요

UpdateLog 엔티티가 public 클래스이면서 :globalCreatedAt를 상속하고 있으므로, util-impl의 공개 API를 통해 :global 타입이 노출됩니다. 또한 UpdateLogController 등의 public 메서드가 :util:util-apiUpdateLogResponse를 반환하므로 이 의존성도 노출됩니다. java-library 플러그인에서는 공개 API가 노출하는 타입의 의존성을 api 구성으로 선언해야 합니다.

dependencies {
    api project(":global")
    api project(":util:util-api")
}
🤖 Prompt for AI Agents
In `@modules/util/impl/build.gradle` around lines 1 - 8, The build currently
declares project(":global") and project(":util:util-api") as implementation
dependencies in modules/util/impl's build.gradle, but public types from
UpdateLog and UpdateLogResponse expose those modules on the public API; change
those dependency declarations to api so the java-library plugin treats them as
exported API dependencies (replace implementation project(":global") and
implementation project(":util:util-api") with api project(":global") and api
project(":util:util-api") in the dependencies block).

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