RequestUrlData.java 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. package com.utic.incident.common.url;
  2. import com.fasterxml.jackson.core.JsonProcessingException;
  3. import com.fasterxml.jackson.core.type.TypeReference;
  4. import com.fasterxml.jackson.databind.DeserializationFeature;
  5. import com.fasterxml.jackson.databind.ObjectMapper;
  6. import lombok.extern.slf4j.Slf4j;
  7. import javax.xml.bind.JAXBContext;
  8. import javax.xml.bind.JAXBException;
  9. import javax.xml.bind.Unmarshaller;
  10. import java.io.BufferedReader;
  11. import java.io.IOException;
  12. import java.io.InputStreamReader;
  13. import java.io.StringReader;
  14. import java.net.HttpURLConnection;
  15. import java.net.URL;
  16. import java.nio.charset.StandardCharsets;
  17. import java.util.stream.Collectors;
  18. @Slf4j
  19. public final class RequestUrlData {
  20. private static final ObjectMapper objectMapper = new ObjectMapper()
  21. .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
  22. public static <T> RequestUrlDataResult<T> fetchJsonDataFromUrl(String urlString, int connectTimeout, int readTimeout, int maxRetries, int retryDelaySeconds, TypeReference<T> responseType) {
  23. return executeRequestWithRetries(urlString, connectTimeout, readTimeout, maxRetries, retryDelaySeconds, responseType, null);
  24. }
  25. public static <T> RequestUrlDataResult<T> fetchXmlDataFromUrl(
  26. String urlString, int connectTimeout, int readTimeout, int maxRetries, int retryDelaySeconds, Class<T> responseType) {
  27. return executeRequestWithRetries(urlString, connectTimeout, readTimeout, maxRetries, retryDelaySeconds, null, responseType); // ✅ rootElement 추가
  28. }
  29. private static void retryWithDelay(int retryDelaySeconds) {
  30. if (retryDelaySeconds > 0) {
  31. try {
  32. log.info("{}초 동안 대기 후 재시도...", retryDelaySeconds);
  33. Thread.sleep(retryDelaySeconds * 1000L);
  34. } catch (InterruptedException ex) {
  35. Thread.currentThread().interrupt();
  36. log.error("재시도 대기 중 인터럽트 발생: {}", ex.getMessage());
  37. }
  38. }
  39. }
  40. private static <T> RequestUrlDataResult<T> executeRequestWithRetries(
  41. final String urlString,
  42. final int connectTimeout,
  43. final int readTimeout,
  44. final int maxRetries,
  45. final int retryDelaySeconds,
  46. final TypeReference<T> jsonType,
  47. final Class<T> xmlType) {
  48. int attempt = 0;
  49. while (attempt < maxRetries) {
  50. attempt++;
  51. HttpURLConnection connection = null;
  52. try {
  53. connection = createConnection(urlString, connectTimeout, readTimeout);
  54. int responseCode = connection.getResponseCode();
  55. String contentType = getContentType(connection);
  56. String response = readResponse(connection);
  57. if (responseCode == HttpURLConnection.HTTP_OK) {
  58. if ("application/json".equalsIgnoreCase(contentType) && jsonType != null) {
  59. // JSON 응답 처리
  60. return new RequestUrlDataResult<>(RequestUrlDataError.SUCCESS, "SUCCESS", parseJson(response, jsonType));
  61. }
  62. else if ("application/xml".equalsIgnoreCase(contentType) && xmlType != null) {
  63. // XML 응답 처리
  64. return new RequestUrlDataResult<>(RequestUrlDataError.SUCCESS, "SUCCESS", parseXml(response, xmlType));
  65. }
  66. else {
  67. String errorMessage = String.format("지원되지 않는 Content-Type: %s", contentType);
  68. log.warn("{}.", errorMessage);
  69. return new RequestUrlDataResult<>(RequestUrlDataError.UNSUPPORTED_CONTENT_TYPE, errorMessage, null);
  70. }
  71. }
  72. // 오류 응답 처리
  73. if (attempt < maxRetries && shouldRetry(responseCode)) {
  74. int retryDelay = (responseCode == 429) ? getRetryDelay(connection, retryDelaySeconds) : retryDelaySeconds;
  75. log.info("서버에서 오류 발생[RESPONSE: {}]. {}초 후 재시도... ({}/{})", responseCode, retryDelay, attempt, maxRetries);
  76. retryWithDelay(retryDelay);
  77. }
  78. else {
  79. break;
  80. }
  81. }
  82. catch (Exception e) {
  83. log.error("요청 오류 발생 ({}/{}): [{}] -> {}", attempt, maxRetries, urlString, e.getMessage());
  84. if (attempt < maxRetries) {
  85. log.info("네트워크 요청 오류 발생. {}초 후 재시도... ({}/{})", retryDelaySeconds, attempt, maxRetries);
  86. retryWithDelay(retryDelaySeconds);
  87. }
  88. else {
  89. return new RequestUrlDataResult<>(RequestUrlDataError.NETWORK_ERROR, e.getMessage(), null);
  90. }
  91. }
  92. finally {
  93. if (connection != null) {
  94. connection.disconnect();
  95. }
  96. }
  97. }
  98. log.warn("최대 재시도 횟수({})를 초과했습니다: [{}]", maxRetries, urlString);
  99. return new RequestUrlDataResult<>(RequestUrlDataError.RETRY_LIMIT_EXCEEDED, "재시도 횟수 초과", null);
  100. }
  101. private static boolean shouldRetry(int responseCode) {
  102. return (responseCode >= 500 && responseCode <= 504) || responseCode == 429;
  103. }
  104. private static int getRetryDelay(HttpURLConnection connection, int retryDelaySeconds) {
  105. String retryAfter = connection.getHeaderField("Retry-After");
  106. if (retryAfter != null) {
  107. try {
  108. int retrySeconds = Integer.parseInt(retryAfter);
  109. return Math.min(retrySeconds, retryDelaySeconds); // 최대 제한 적용
  110. } catch (NumberFormatException e) {
  111. log.warn("Retry-After 값 파싱 오류: {}", e.getMessage());
  112. }
  113. }
  114. return 0; // Retry-After 값이 없거나 오류가 발생한 경우 기본값 반환
  115. }
  116. private static boolean shouldRetry(int responseCode, HttpURLConnection connection) {
  117. if (responseCode >= 500 && responseCode <= 504) {
  118. return true; // 서버 오류로 인해 재시도 가능
  119. }
  120. if (responseCode == 429) {
  121. String retryAfter = connection.getHeaderField("Retry-After");
  122. if (retryAfter != null) {
  123. try {
  124. int retrySeconds = Integer.parseInt(retryAfter);
  125. log.info("서버 요청 제한(429). {}초 후 재시도 권장.", retrySeconds);
  126. Thread.sleep(retrySeconds * 1000L);
  127. return true;
  128. } catch (NumberFormatException | InterruptedException e) {
  129. log.warn("Retry-After 값을 파싱하는 중 오류 발생: {}", e.getMessage());
  130. Thread.currentThread().interrupt();
  131. }
  132. }
  133. return false; // Retry-After 정보가 없으면 재시도하지 않음
  134. }
  135. return false; // 기타 오류 코드에 대해서는 재시도하지 않음
  136. }
  137. private static HttpURLConnection createConnection(String urlString, int connectTimeout, int readTimeout) throws IOException {
  138. URL url = new URL(urlString);
  139. HttpURLConnection connection = (HttpURLConnection) url.openConnection();
  140. connection.setConnectTimeout(connectTimeout*1000);
  141. connection.setReadTimeout(readTimeout*1000);
  142. connection.setRequestMethod("GET");
  143. connection.setRequestProperty("Accept", "application/json, application/xml");
  144. connection.setRequestProperty("User-Agent", "Mozilla/5.0");
  145. return connection;
  146. }
  147. private static String getContentType(HttpURLConnection connection) {
  148. String contentType = connection.getContentType();
  149. return contentType != null ? contentType.split(";")[0].trim() : "";
  150. }
  151. private static String readResponse(HttpURLConnection connection) throws IOException {
  152. try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
  153. return br.lines().collect(Collectors.joining());
  154. }
  155. }
  156. private static <T> T parseJson(String json, TypeReference<T> responseType) throws JsonProcessingException {
  157. return objectMapper.readValue(json, responseType);
  158. }
  159. private static <T> T parseXml(String xml, Class<T> responseType) throws Exception {
  160. // String cleanedXml = xml.replaceFirst("^\\uFEFF", ""); // UTF-8 BOM 제거
  161. try {
  162. JAXBContext jaxbContext = JAXBContext.newInstance(responseType);
  163. Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
  164. return responseType.cast(unmarshaller.unmarshal(new StringReader(xml)));
  165. // return (T) unmarshaller.unmarshal(new StringReader(xml));
  166. } catch (JAXBException e) {
  167. return null; // XML 파싱 실패 시 null 반환
  168. }
  169. }
  170. }