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' 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 c00452e..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,26 +1,28 @@ 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; 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.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClient; +@Slf4j @Component public class AiServerAdapter { private final RestClient aiRestClient; - public AiServerAdapter( - @Qualifier("aiGenerationRestClient") - RestClient aiRestClient) { + public AiServerAdapter(@Qualifier("aiGenerationRestClient") RestClient aiRestClient) { this.aiRestClient = aiRestClient; } @@ -32,13 +34,45 @@ public AiGenerationResponse requestGenerate(FeGenerationRequest feGenerationRequ .body(feGenerationRequest) .retrieve() .body(AiGenerationResponse.class); - } catch (TooManyRequests e) { - throw new ClientSideException(ExceptionMessage.AI_SERVER_TO_MANY_REQUEST); - } catch (ResourceAccessException e) { - if (e.getCause() instanceof SocketTimeoutException) { - throw new CustomException(ExceptionMessage.AI_SERVER_TIMEOUT); + + // 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 CustomException(ExceptionMessage.AI_SERVER_CONNECTION_FAILED); + + throw new ClientSideException(message); + + // 2. 5xx 에러 (Server Fault) -> 서킷 브레이커가 실패로 기록해야 함 + } catch (HttpServerErrorException e) { + log.error("AI Server Server Error (5xx): Status={}, Body={}", e.getStatusCode(), + e.getResponseBodyAsString()); + // 예외를 그대로 던지거나, 커스텀 예외(ServerException)로 감싸서 던져야 함 + throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); + + // 3. 타임아웃/연결 오류 (Network Fault) -> 서킷 브레이커가 실패로 기록해야 함 + } catch (ResourceAccessException e) { + log.error("AI Server Connection/Timeout Error: {}", e.getMessage()); + // 타임아웃은 반드시 던져야 함 + throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); + + // 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 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 98% 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 f261c9f..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; @@ -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()) @@ -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())