Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {


group = "com.icc.qasker"
version = "1.6.4"
version = "1.6.5"

subprojects {
apply plugin: 'java'
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ public ResponseEntity<CustomErrorResponse> handleCustomException(
new CustomErrorResponse(customException.getMessage()));
}

@ExceptionHandler(ClientSideException.class)
public ResponseEntity<CustomErrorResponse> handleClientException(
ClientSideException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new CustomErrorResponse(e.getMessage()));
}

@ExceptionHandler(CallNotPermittedException.class)
public ResponseEntity<CustomErrorResponse> handleCustomException(
CallNotPermittedException exception) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

Expand All @@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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())
Expand All @@ -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())
Expand Down