From 74c3444aec9f3ff2a24a0d3f5559d3af10ef3e00 Mon Sep 17 00:00:00 2001 From: Oh YoungJe <139232765+GulSauce@users.noreply.github.com> Date: Wed, 21 Jan 2026 06:44:00 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[ICC-230]=20=EB=A1=9C=EA=B9=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qasker/quiz/adapter/AiServerAdapter.java | 21 +++++++++++++------ .../quiz/adapter/AiWebClientConfig.java | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java index c00452e..6a3c2ed 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java @@ -5,26 +5,25 @@ import com.icc.qasker.global.error.ExceptionMessage; import com.icc.qasker.quiz.dto.request.FeGenerationRequest; import com.icc.qasker.quiz.dto.response.AiGenerationResponse; -import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import java.net.SocketTimeoutException; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import org.springframework.web.client.HttpClientErrorException.TooManyRequests; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientResponseException; +@Slf4j @Component public class AiServerAdapter { private final RestClient aiRestClient; - public AiServerAdapter( - @Qualifier("aiGenerationRestClient") - RestClient aiRestClient) { + public AiServerAdapter(@Qualifier("aiGenerationRestClient") RestClient aiRestClient) { this.aiRestClient = aiRestClient; } - @CircuitBreaker(name = "aiServer") public AiGenerationResponse requestGenerate(FeGenerationRequest feGenerationRequest) { try { return aiRestClient.post() @@ -33,12 +32,22 @@ public AiGenerationResponse requestGenerate(FeGenerationRequest feGenerationRequ .retrieve() .body(AiGenerationResponse.class); } catch (TooManyRequests e) { + // 429 에러 발생 시 로그 출력 + log.error("AI Server Too Many Requests: {}", e.getResponseBodyAsString()); throw new ClientSideException(ExceptionMessage.AI_SERVER_TO_MANY_REQUEST); + } catch (RestClientResponseException e) { + // 그 외 4xx, 5xx 에러 발생 시 상대방 서버 메시지 로깅 (가장 중요) + log.error("AI Server Error Status: {}, Body: {}", e.getStatusCode(), + e.getResponseBodyAsString()); + throw new CustomException(ExceptionMessage.DEFAULT_ERROR); } catch (ResourceAccessException e) { + // 네트워크 연결 실패나 타임아웃은 응답 본문이 없음 + log.error("AI Server Connection Error: {}", e.getMessage()); + if (e.getCause() instanceof SocketTimeoutException) { throw new CustomException(ExceptionMessage.AI_SERVER_TIMEOUT); } throw new CustomException(ExceptionMessage.AI_SERVER_CONNECTION_FAILED); } } -} +} \ No newline at end of file diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiWebClientConfig.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiWebClientConfig.java index f261c9f..3c6dfb7 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiWebClientConfig.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiWebClientConfig.java @@ -22,7 +22,7 @@ public class AiWebClientConfig { public RestClient aiGenerationRestClient() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(Duration.ofSeconds(5)); - factory.setReadTimeout(Duration.ofSeconds(40)); + factory.setReadTimeout(Duration.ofSeconds(80)); return RestClient.builder() .baseUrl(qAskerProperties.getAiServerUrl()) From e01bcbf9cfc7b8d6fa0e1f9856d9b555c3a4e0f2 Mon Sep 17 00:00:00 2001 From: Oh YoungJe <139232765+GulSauce@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:40:02 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[ICC-230]=20=EC=97=90=EB=9F=AC=20=EC=B2=B4?= =?UTF-8?q?=EC=9D=B4=EB=8B=9D=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qasker/quiz/adapter/AiServerAdapter.java | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java index 6a3c2ed..b930bd2 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java @@ -5,14 +5,14 @@ import com.icc.qasker.global.error.ExceptionMessage; import com.icc.qasker.quiz.dto.request.FeGenerationRequest; import com.icc.qasker.quiz.dto.response.AiGenerationResponse; -import java.net.SocketTimeoutException; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; -import org.springframework.web.client.HttpClientErrorException.TooManyRequests; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClient; -import org.springframework.web.client.RestClientResponseException; @Slf4j @Component @@ -24,6 +24,7 @@ public AiServerAdapter(@Qualifier("aiGenerationRestClient") RestClient aiRestCli this.aiRestClient = aiRestClient; } + @CircuitBreaker(name = "aiServer") public AiGenerationResponse requestGenerate(FeGenerationRequest feGenerationRequest) { try { return aiRestClient.post() @@ -31,23 +32,30 @@ public AiGenerationResponse requestGenerate(FeGenerationRequest feGenerationRequ .body(feGenerationRequest) .retrieve() .body(AiGenerationResponse.class); - } catch (TooManyRequests e) { - // 429 에러 발생 시 로그 출력 - log.error("AI Server Too Many Requests: {}", e.getResponseBodyAsString()); + + // 1. 429 에러 -> 서킷 브레이커가 무시해야 함 (ignoreExceptions) + } catch (HttpClientErrorException.TooManyRequests e) { + log.error("AI Server Rate Limit Exceeded: Status={}, Body={}", e.getStatusCode(), + e.getResponseBodyAsString()); throw new ClientSideException(ExceptionMessage.AI_SERVER_TO_MANY_REQUEST); - } catch (RestClientResponseException e) { - // 그 외 4xx, 5xx 에러 발생 시 상대방 서버 메시지 로깅 (가장 중요) - log.error("AI Server Error Status: {}, Body: {}", e.getStatusCode(), + + // 2. 5xx 에러 (Server Fault) -> 서킷 브레이커가 실패로 기록해야 함 + } catch (HttpServerErrorException e) { + log.error("AI Server Server Error (5xx): Status={}, Body={}", e.getStatusCode(), e.getResponseBodyAsString()); - throw new CustomException(ExceptionMessage.DEFAULT_ERROR); + // 예외를 그대로 던지거나, 커스텀 예외(ServerException)로 감싸서 던져야 함 + throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); + + // 3. 타임아웃/연결 오류 (Network Fault) -> 서킷 브레이커가 실패로 기록해야 함 } catch (ResourceAccessException e) { - // 네트워크 연결 실패나 타임아웃은 응답 본문이 없음 - log.error("AI Server Connection Error: {}", e.getMessage()); + log.error("AI Server Connection/Timeout Error: {}", e.getMessage()); + // 타임아웃은 반드시 던져야 함 + throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); - if (e.getCause() instanceof SocketTimeoutException) { - throw new CustomException(ExceptionMessage.AI_SERVER_TIMEOUT); - } - throw new CustomException(ExceptionMessage.AI_SERVER_CONNECTION_FAILED); + // 4. 그 외의 오류 + } catch (Exception e) { + log.error("AI Server Unknown Error: {}", e.getMessage()); + throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); } } } \ No newline at end of file From 5b242dcf8a5466db146cbf5f1569069b17956565 Mon Sep 17 00:00:00 2001 From: Oh YoungJe <139232765+GulSauce@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:09:26 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[ICC-230]=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/error/ClientSideException.java | 6 ++--- .../global/error/GlobalExceptionHandler.java | 7 +++++ .../qasker/quiz/adapter/AiServerAdapter.java | 27 +++++++++++++++---- .../AiWebClientConfig.java | 4 +-- 4 files changed, 34 insertions(+), 10 deletions(-) rename modules/quiz/impl/src/main/java/com/icc/qasker/quiz/{adapter => config}/AiWebClientConfig.java (95%) diff --git a/modules/global/src/main/java/com/icc/qasker/global/error/ClientSideException.java b/modules/global/src/main/java/com/icc/qasker/global/error/ClientSideException.java index bdeb9f2..788980c 100644 --- a/modules/global/src/main/java/com/icc/qasker/global/error/ClientSideException.java +++ b/modules/global/src/main/java/com/icc/qasker/global/error/ClientSideException.java @@ -1,8 +1,8 @@ package com.icc.qasker.global.error; -public class ClientSideException extends CustomException { +public class ClientSideException extends RuntimeException { - public ClientSideException(ExceptionMessage exceptionMessage) { - super(exceptionMessage); + public ClientSideException(String message) { + super(message); } } diff --git a/modules/global/src/main/java/com/icc/qasker/global/error/GlobalExceptionHandler.java b/modules/global/src/main/java/com/icc/qasker/global/error/GlobalExceptionHandler.java index f7421dc..7a187e8 100644 --- a/modules/global/src/main/java/com/icc/qasker/global/error/GlobalExceptionHandler.java +++ b/modules/global/src/main/java/com/icc/qasker/global/error/GlobalExceptionHandler.java @@ -21,6 +21,13 @@ public ResponseEntity handleCustomException( new CustomErrorResponse(customException.getMessage())); } + @ExceptionHandler(ClientSideException.class) + public ResponseEntity handleClientException( + ClientSideException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new CustomErrorResponse(e.getMessage())); + } + @ExceptionHandler(CallNotPermittedException.class) public ResponseEntity handleCustomException( CallNotPermittedException exception) { diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java index b930bd2..3861590 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java @@ -1,5 +1,7 @@ package com.icc.qasker.quiz.adapter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.icc.qasker.global.error.ClientSideException; import com.icc.qasker.global.error.CustomException; import com.icc.qasker.global.error.ExceptionMessage; @@ -33,11 +35,26 @@ public AiGenerationResponse requestGenerate(FeGenerationRequest feGenerationRequ .retrieve() .body(AiGenerationResponse.class); - // 1. 429 에러 -> 서킷 브레이커가 무시해야 함 (ignoreExceptions) - } catch (HttpClientErrorException.TooManyRequests e) { - log.error("AI Server Rate Limit Exceeded: Status={}, Body={}", e.getStatusCode(), - e.getResponseBodyAsString()); - throw new ClientSideException(ExceptionMessage.AI_SERVER_TO_MANY_REQUEST); + // 1. 400 에러 -> 서킷 브레이커가 무시해야 함 (ignoreExceptions) + } catch (HttpClientErrorException e) { + + String messageBody = e.getResponseBodyAsString(); + log.error("[AI Server] Bad Request: Status={}, Body={}", e.getStatusCode(), + messageBody); + + String message = ""; + try { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(messageBody); + + if (rootNode.has("detail")) { + message = rootNode.get("detail").asText(); + } + } catch (Exception exception) { + message = messageBody; + } + + throw new ClientSideException(message); // 2. 5xx 에러 (Server Fault) -> 서킷 브레이커가 실패로 기록해야 함 } catch (HttpServerErrorException e) { diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiWebClientConfig.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/AiWebClientConfig.java similarity index 95% rename from modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiWebClientConfig.java rename to modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/AiWebClientConfig.java index 3c6dfb7..2552dbb 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiWebClientConfig.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/AiWebClientConfig.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.adapter; +package com.icc.qasker.quiz.config; import com.icc.qasker.global.properties.QAskerProperties; import java.time.Duration; @@ -36,7 +36,7 @@ public RestClient aiGenerationRestClient() { public RestClient aiRestClient() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(Duration.ofSeconds(5)); - factory.setReadTimeout(Duration.ofSeconds(80)); + factory.setReadTimeout(Duration.ofSeconds(40)); return RestClient.builder() .baseUrl(qAskerProperties.getAiServerUrl()) From 51db1ad58aa7cb219cadd8255dbbd068a395ba25 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 21 Jan 2026 15:23:34 +0000 Subject: [PATCH 4/4] chore: bump version to 1.6.5 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d363bb1..5a2bcd3 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { group = "com.icc.qasker" -version = "1.6.4" +version = "1.6.5" subprojects { apply plugin: 'java'