-
Notifications
You must be signed in to change notification settings - Fork 1
[ICC-232] 비동기 퀴즈 응답 #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
[ICC-232] 비동기 퀴즈 응답 #124
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the
Walkthrough이 PR은 패키지 구조를 재조직하고 AI 기반 퀴즈 생성을 위한 리액티브 WebFlux 지원을 도입합니다. AWS 관련 타입을 Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ 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. Comment |
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this 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 검증이 약합니다.startsWith는https://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)에 효과가 없습니다. Primitivelong은 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" + ); }이 경우
HashMapimport도 제거할 수 있습니다.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; }
modules/quiz/api/build.gradle
Outdated
| dependencies { | ||
| implementation 'org.springframework.boot:spring-boot-starter-webflux' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
| 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*GenerationController*" | head -20Repository: q-asker/api
Length of output: 145
🏁 Script executed:
cat modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/GenerationController.javaRepository: 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.
| 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 3Repository: 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.javaRepository: 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 -100Repository: 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().
modules/quiz/impl/build.gradle
Outdated
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -nRepository: 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 -100Repository: 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-filenameRepository: q-asker/api
Length of output: 358
🏁 Script executed:
#!/bin/bash
# Check the root build.gradle for all content
cat -n build.gradle | tail -50Repository: 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.gradle의 subprojects.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.
| .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)); | ||
| } | ||
| ) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 viaonStatus(...). [1]- In
onStatus(...), returningMono.empty()suppresses treating that status as an error and lets the response continue downstream tobodyToMono(...). [2] - For an empty upstream Mono, use
defaultIfEmpty(value)(fallback value) orswitchIfEmpty(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")));onStatusmaps 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());defaultIfEmptyreplaces “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()fromonStatussuppresses error treatment for that status. [2] - Then downstream
switchIfEmptydecides 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()가 호출되지 않습니다. 그 결과 onStatus는 Mono.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.
| @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(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's find and examine the file mentioned in the review
find . -type f -name "AiWebClientConfig.java" 2>/dev/nullRepository: 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 -20Repository: 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 -30Repository: 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/nullRepository: q-asker/api
Length of output: 39
🏁 Script executed:
cat -n modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/AiWebClientConfig.javaRepository: 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
-
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 theReactorClientHttpConnector. [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.
- Set a connection timeout so dead/blackholed connects fail fast (e.g., 5–10s). Spring shows configuring this via Reactor Netty
-
Understand what Reactor Netty
responseTimeoutreally doesHttpClient.responseTimeout(Duration)is not “time to finish the whole download”; it’s the maximum interval between network read operations while reading the response (implemented viaReadTimeoutHandler). [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
responseTimeoutlong enough to cover expected gaps (or remove it) unless your stream sends heartbeats frequently. -
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.
-
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]
- Keep a conservative default for normal HTTP calls, then override for streaming endpoints (or vice versa). Reactor Netty supports per-request
-
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.)
- For streams where you want to fail if no elements arrive for N seconds (regardless of low-level TCP reads), use Reactor’s
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:
- 1: https://docs.spring.io/spring-framework/reference/web/webflux-webclient/client-builder.html?utm_source=openai
- 2: https://docs.spring.io/projectreactor/reactor-netty/docs/1.1.17/api/reactor/netty/http/client/HttpClient.html?utm_source=openai
- 3: https://docs.spring.io/projectreactor/reactor-netty/docs/current/reference/html/index.html?utm_source=openai
- 4: https://docs.spring.io/spring-framework/docs/5.1.20.RELEASE/spring-framework-reference/web-reactive.html?utm_source=openai
🌐 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_MILLISis theChannelOptionyou want for “timeout while establishing the connection”. (docs.spring.io)responseTimeout(Duration)adds aReadTimeoutHandlerfor the response receive phase (per Reactor Netty docs). (projectreactor.io)
Citations:
- 1: https://docs.spring.io/spring-framework/reference/web/webflux-webclient/client-builder.html?utm_source=openai
- 2: https://docs.spring.io/spring-framework/reference/web/webflux-webclient/client-builder.html?utm_source=openai
- 3: https://projectreactor.io/docs/netty/release/api/reactor/netty/http/client/HttpClient.html?utm_source=openai
🏁 Script executed:
# Check if there are any usages of aiStreamClient to understand streaming context
rg -A 5 -B 2 "aiStreamClient|aiGenerationClient" --type javaRepository: 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.
| @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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -50Repository: 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 -5Repository: 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 -20Repository: 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 -nRepository: 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.
| 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 bymerge, 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:
- 1: https://docs.spring.io/spring-data/jpa/reference/jpa/entity-persistence.html?utm_source=openai
- 2: https://docs.spring.io/spring-data/jpa/reference/jpa/entity-persistence.html?utm_source=openai
- 3: https://docs.huihoo.com/javadoc/spring/spring-data/commons/1.5/org/springframework/data/repository/CrudRepository.html?utm_source=openai
🏁 Script executed:
# Search for Problem.of() method to understand how it uses ProblemSet
rg "Problem\.of\(" --type java -B 2 -A 5Repository: q-asker/api
Length of output: 1993
🏁 Script executed:
# Search for ProblemSet entity definition and ID generation
fd "ProblemSet.java" --type fRepository: q-asker/api
Length of output: 129
🏁 Script executed:
# Check the GenerationServiceImpl file to see the full context
fd "GenerationServiceImpl.java" --type fRepository: 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.javaRepository: q-asker/api
Length of output: 1713
🏁 Script executed:
# Find Problem.of() method definition
fd "Problem.java" --type f | head -5Repository: 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.javaRepository: 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.javaRepository: 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.
| 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 -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "GenerationServiceImpl.java" | head -5Repository: q-asker/api
Length of output: 143
🏁 Script executed:
fd "GenerationServiceImpl.java" --type fRepository: q-asker/api
Length of output: 141
🏁 Script executed:
cat -n modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.javaRepository: 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 -80Repository: 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 -60Repository: 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
-
Prefer Reactor’s built-in schedulers unless you must integrate with an existing
ExecutorService. If you wrap an executor viafromExecutor(…), Reactor doesn’t “own” thread creation; you own the executor lifecycle (creation, shutdown, leak prevention). [1] -
Prefer
fromExecutorService(executorService, "name")over the no-name overload (especially if you use metrics), as Reactor explicitly recommends the named variant. [2] -
Treat the
Schedulerand the underlyingExecutorServiceas 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]
- If you created the executor, shut it down during app shutdown (e.g., in a Spring
-
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]
-
Shutdown style: use
disposeGracefully()when you want an orderly stop, then fall back todispose()if needed.- Reactor provides
Scheduler.disposeGracefully()and recommends putting a timeout on it; it can be followed bydispose()for a forceful shutdown. [3] - Reactor’s own guidance for implementors explicitly calls out “graceful shutdown” as using
ExecutorService.shutdown()rather thanshutdownNow()under the hood. [4]
- Reactor provides
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 executorSources: 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:
- 1: https://www.oreilly.com/library/view/hands-on-reactive-programming/9781789135794/9f7fa69a-5f1f-4998-8d1d-2b879d0dcb26.xhtml?utm_source=openai
- 2: https://docs.spring.io/projectreactor/reactor-core/docs/current/api/reactor/core/scheduler/Schedulers.html
- 3: https://docs.spring.io/projectreactor/reactor-core/docs/current/api/reactor/core/scheduler/Scheduler.html?utm_source=openai
- 4: https://github.com/reactor/reactor-core/wiki/Migration-Guides?utm_source=openai
요청마다 새 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.
| plugins { | ||
| id 'java-library' | ||
| } | ||
|
|
||
| dependencies { | ||
| implementation project(":global") | ||
| implementation project(":util:util-api") | ||
| } No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -20Repository: 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/nullRepository: 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 | uniqRepository: 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.javaRepository: 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.javaRepository: 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.javaRepository: 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/nullRepository: 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.javaRepository: 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.gradleRepository: 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.javaRepository: 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.gradleRepository: q-asker/api
Length of output: 220
:global 및 :util:util-api 의존성을 api로 변경 필요
UpdateLog 엔티티가 public 클래스이면서 :global의 CreatedAt를 상속하고 있으므로, util-impl의 공개 API를 통해 :global 타입이 노출됩니다. 또한 UpdateLogController 등의 public 메서드가 :util:util-api의 UpdateLogResponse를 반환하므로 이 의존성도 노출됩니다. 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).
📢 설명
해당 Pull Request에 대해 간략하게 설명해주세요!
설명 1
의사 선택과정(trade-off)
얻었다
잃었다
코드 설명: 코멘트 확인
✅ 체크 리스트
Summary by CodeRabbit
릴리스 노트
New Features
Refactor
Chores
✏️ Tip: You can customize this high-level summary in your review settings.