package com.utic.incident.common.url; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.StringReader; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; @Slf4j public final class RequestUrlData { private static final ObjectMapper objectMapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); public static RequestUrlDataResult fetchJsonDataFromUrl(String urlString, int connectTimeout, int readTimeout, int maxRetries, int retryDelaySeconds, TypeReference responseType) { return executeRequestWithRetries(urlString, connectTimeout, readTimeout, maxRetries, retryDelaySeconds, responseType, null); } public static RequestUrlDataResult fetchXmlDataFromUrl( String urlString, int connectTimeout, int readTimeout, int maxRetries, int retryDelaySeconds, Class responseType) { return executeRequestWithRetries(urlString, connectTimeout, readTimeout, maxRetries, retryDelaySeconds, null, responseType); // ✅ rootElement 추가 } private static void retryWithDelay(int retryDelaySeconds) { if (retryDelaySeconds > 0) { try { log.info("{}초 동안 대기 후 재시도...", retryDelaySeconds); Thread.sleep(retryDelaySeconds * 1000L); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); log.error("재시도 대기 중 인터럽트 발생: {}", ex.getMessage()); } } } private static RequestUrlDataResult executeRequestWithRetries( final String urlString, final int connectTimeout, final int readTimeout, final int maxRetries, final int retryDelaySeconds, final TypeReference jsonType, final Class xmlType) { int attempt = 0; while (attempt < maxRetries) { attempt++; HttpURLConnection connection = null; try { connection = createConnection(urlString, connectTimeout, readTimeout); int responseCode = connection.getResponseCode(); String contentType = getContentType(connection); String response = readResponse(connection); if (responseCode == HttpURLConnection.HTTP_OK) { if ("application/json".equalsIgnoreCase(contentType) && jsonType != null) { // JSON 응답 처리 return new RequestUrlDataResult<>(RequestUrlDataError.SUCCESS, "SUCCESS", parseJson(response, jsonType)); } else if ("application/xml".equalsIgnoreCase(contentType) && xmlType != null) { // XML 응답 처리 return new RequestUrlDataResult<>(RequestUrlDataError.SUCCESS, "SUCCESS", parseXml(response, xmlType)); } else { String errorMessage = String.format("지원되지 않는 Content-Type: %s", contentType); log.warn("{}.", errorMessage); return new RequestUrlDataResult<>(RequestUrlDataError.UNSUPPORTED_CONTENT_TYPE, errorMessage, null); } } // 오류 응답 처리 if (attempt < maxRetries && shouldRetry(responseCode)) { int retryDelay = (responseCode == 429) ? getRetryDelay(connection, retryDelaySeconds) : retryDelaySeconds; log.info("서버에서 오류 발생[RESPONSE: {}]. {}초 후 재시도... ({}/{})", responseCode, retryDelay, attempt, maxRetries); retryWithDelay(retryDelay); } else { break; } } catch (Exception e) { log.error("요청 오류 발생 ({}/{}): [{}] -> {}", attempt, maxRetries, urlString, e.getMessage()); if (attempt < maxRetries) { log.info("네트워크 요청 오류 발생. {}초 후 재시도... ({}/{})", retryDelaySeconds, attempt, maxRetries); retryWithDelay(retryDelaySeconds); } else { return new RequestUrlDataResult<>(RequestUrlDataError.NETWORK_ERROR, e.getMessage(), null); } } finally { if (connection != null) { connection.disconnect(); } } } log.warn("최대 재시도 횟수({})를 초과했습니다: [{}]", maxRetries, urlString); return new RequestUrlDataResult<>(RequestUrlDataError.RETRY_LIMIT_EXCEEDED, "재시도 횟수 초과", null); } private static boolean shouldRetry(int responseCode) { return (responseCode >= 500 && responseCode <= 504) || responseCode == 429; } private static int getRetryDelay(HttpURLConnection connection, int retryDelaySeconds) { String retryAfter = connection.getHeaderField("Retry-After"); if (retryAfter != null) { try { int retrySeconds = Integer.parseInt(retryAfter); return Math.min(retrySeconds, retryDelaySeconds); // 최대 제한 적용 } catch (NumberFormatException e) { log.warn("Retry-After 값 파싱 오류: {}", e.getMessage()); } } return 0; // Retry-After 값이 없거나 오류가 발생한 경우 기본값 반환 } private static boolean shouldRetry(int responseCode, HttpURLConnection connection) { if (responseCode >= 500 && responseCode <= 504) { return true; // 서버 오류로 인해 재시도 가능 } if (responseCode == 429) { String retryAfter = connection.getHeaderField("Retry-After"); if (retryAfter != null) { try { int retrySeconds = Integer.parseInt(retryAfter); log.info("서버 요청 제한(429). {}초 후 재시도 권장.", retrySeconds); Thread.sleep(retrySeconds * 1000L); return true; } catch (NumberFormatException | InterruptedException e) { log.warn("Retry-After 값을 파싱하는 중 오류 발생: {}", e.getMessage()); Thread.currentThread().interrupt(); } } return false; // Retry-After 정보가 없으면 재시도하지 않음 } return false; // 기타 오류 코드에 대해서는 재시도하지 않음 } private static HttpURLConnection createConnection(String urlString, int connectTimeout, int readTimeout) throws IOException { URL url = new URL(urlString); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(connectTimeout*1000); connection.setReadTimeout(readTimeout*1000); connection.setRequestMethod("GET"); connection.setRequestProperty("Accept", "application/json, application/xml"); connection.setRequestProperty("User-Agent", "Mozilla/5.0"); return connection; } private static String getContentType(HttpURLConnection connection) { String contentType = connection.getContentType(); return contentType != null ? contentType.split(";")[0].trim() : ""; } private static String readResponse(HttpURLConnection connection) throws IOException { try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { return br.lines().collect(Collectors.joining()); } } private static T parseJson(String json, TypeReference responseType) throws JsonProcessingException { return objectMapper.readValue(json, responseType); } private static T parseXml(String xml, Class responseType) throws Exception { // String cleanedXml = xml.replaceFirst("^\\uFEFF", ""); // UTF-8 BOM 제거 try { JAXBContext jaxbContext = JAXBContext.newInstance(responseType); Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); return responseType.cast(unmarshaller.unmarshal(new StringReader(xml))); // return (T) unmarshaller.unmarshal(new StringReader(xml)); } catch (JAXBException e) { return null; // XML 파싱 실패 시 null 반환 } } }