diff --git a/src/main/java/com/book/backend/domain/openapi/service/OpenAPI.java b/src/main/java/com/book/backend/domain/openapi/service/OpenAPI.java index 24f7551..3c73756 100644 --- a/src/main/java/com/book/backend/domain/openapi/service/OpenAPI.java +++ b/src/main/java/com/book/backend/domain/openapi/service/OpenAPI.java @@ -14,6 +14,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.*; @Component @RequiredArgsConstructor @@ -25,14 +27,44 @@ public class OpenAPI { @Value("${openapi.authKey}") private String authKey; - private final String format = "json"; + @Value("${openapi.timeoutSeconds}") + private int TIMEOUT_SECONDS; // 타임아웃 시간 (초) + + @Value("${openapi.maxRetryCounts}") + private int MAX_RETRY_COUNTS; // 최대 재시도 횟수 + private final String format = "json"; + private final ExecutorService executorService = Executors.newCachedThreadPool(); public JSONObject connect(String subUrl, OpenAPIRequestInterface dto, OpenAPIResponseInterface responseDto) throws Exception { log.trace("OpenAPI > connect()"); - URL url = setRequest(subUrl, dto); // 요청 만들기 - InputStreamReader streamResponse = new InputStreamReader(url.openStream(), "UTF-8"); // 요청 보내기 - return readStreamToJson(streamResponse, responseDto); // 응답 stream 을 json 으로 변환 + int retryCount = 0; + + while (retryCount < MAX_RETRY_COUNTS) { + try { + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + URL url = setRequest(subUrl, dto); // 요청 만들기 + + log.trace("Request URL: " + url); + + InputStreamReader streamResponse = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8); // 요청 보내기 + return readStreamToJson(streamResponse, responseDto); // 응답 stream 을 json 으로 변환 + } catch (Exception e) { + throw new RuntimeException(e); // 커스텀 불필요, 런타임 에러로 처리 + } + }, executorService); + + // 타임아웃 설정 및 결과 가져오기 + return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (TimeoutException e) { + log.warn("OPEN API 응답을 요청하는 중 타임아웃이 발생했습니다. 재시도합니다...(" + (retryCount + 1) + "/" + MAX_RETRY_COUNTS + ")"); + retryCount++; + } + } + + // 재시도 횟수를 초과하면 예외 던지기 + throw new CustomException(ErrorCode.OPENAPI_REQUEST_TIMEOUT); } private URL setRequest(String subUrl, OpenAPIRequestInterface dto) throws Exception { @@ -57,10 +89,17 @@ private URL setRequest(String subUrl, OpenAPIRequestInterface dto) throws Except private JSONObject readStreamToJson(InputStreamReader streamResponse, OpenAPIResponseInterface responseDto) throws Exception { log.trace("OpenAPI > readStreamToJson()"); String fullResponse = new BufferedReader(streamResponse).readLine(); + JSONObject jsonObject; // response JSON 파싱 - JSONObject jsonObject = (JSONObject) (new JSONParser()).parse(fullResponse); + try { + jsonObject = (JSONObject) (new JSONParser()).parse(fullResponse); + } catch (Exception e) { + throw new CustomException(ErrorCode.INVALID_OPENAPI_RESPONSE); + } + JSONObject response = (JSONObject) jsonObject.get("response"); + // API 일일 호출 횟수 초과 에러 (일 최대 500건) if(response.get("error") != null){ throw new CustomException(ErrorCode.API_CALL_LIMIT_EXCEEDED); diff --git a/src/main/java/com/book/backend/exception/ErrorCode.java b/src/main/java/com/book/backend/exception/ErrorCode.java index 84f32ff..7b40991 100644 --- a/src/main/java/com/book/backend/exception/ErrorCode.java +++ b/src/main/java/com/book/backend/exception/ErrorCode.java @@ -52,10 +52,13 @@ public enum ErrorCode { MESSAGE_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "500", "메시지 저장에 실패했습니다."), USER_OPENTALK_NOT_FOUND(HttpStatus.NOT_FOUND, "404", "해당 오픈톡은 유저의 즐겨찾기 리스트에 없습니다."), INVALID_MESSAGE_TYPE(HttpStatus.BAD_REQUEST, "400", "text, image, goal 중 하나를 입력해주세요."), + // 외부 API 에러 KAKAO_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500", "카카오 서버에 오류가 발생했습니다."), + INVALID_OPENAPI_RESPONSE(HttpStatus.INTERNAL_SERVER_ERROR, "500", "OPEN API 서버에서 잘못된 응답을 전송했습니다."), API_CALL_LIMIT_EXCEEDED(HttpStatus.TOO_MANY_REQUESTS, "429", "OPEN API 일일 호출 횟수를 초과했습니다. (일 최대 500건)"), LIBCODE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500", "존재하는 도서관 코드인지 확인해주세요."), + OPENAPI_REQUEST_TIMEOUT(HttpStatus.REQUEST_TIMEOUT, "408", "OPEN API 응답을 요청하는 중 타임아웃이 발생했습니다."), // OAuth2 HEADER_PARSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500", "Header 파싱 중 에러가 발생했습니다."), diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index facfbff..3b13020 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -53,6 +53,8 @@ spring-doc: openapi: url: ${OPENAPI_URL} authKey: ${OPENAPI_AUTH_KEY} + timeoutSeconds: ${OPENAPI_TIMEOUT_SECONDS} + maxRetryCounts: ${OPENAPI_MAX_RETRY_COUNTS} kakao: publicKeyUri: https://kauth.kakao.com/.well-known/jwks.json