From 7b55204ae1e156b30cd6a2dc3f7194b93a5c0e92 Mon Sep 17 00:00:00 2001 From: teddy Date: Fri, 27 Feb 2026 10:06:39 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B6=94=EB=A1=A0=20api=20=ED=98=B8=EC=B6=9C?= =?UTF-8?q?=EC=8B=9C=20=EB=8C=80=EC=9A=A9=EB=9F=89=EC=9D=B4=EB=A9=B4=203?= =?UTF-8?q?=EC=B4=88=20timeout=20=EB=AC=B8=EC=A0=9C=2060=EC=B4=88=20long?= =?UTF-8?q?=20restTemplate=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/InferenceCommonService.java | 2 +- .../resttemplate/ExternalHttpClient.java | 68 ++++++++++++++----- .../resttemplate/RestTemplateConfig.java | 34 ++++------ .../service/InferenceResultService.java | 3 +- 4 files changed, 67 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/common/inference/service/InferenceCommonService.java b/src/main/java/com/kamco/cd/kamcoback/common/inference/service/InferenceCommonService.java index ae3fef7e..2776a59e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/inference/service/InferenceCommonService.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/inference/service/InferenceCommonService.java @@ -78,7 +78,7 @@ public class InferenceCommonService { // 4) 추론 실행 API 호출 ExternalCallResult result = - externalHttpClient.call(inferenceUrl, HttpMethod.POST, dto, headers, String.class); + externalHttpClient.callLong(inferenceUrl, HttpMethod.POST, dto, headers, String.class); if (result.statusCode() < 200 || result.statusCode() >= 300) { log.error("Inference API failed. status={}, body={}", result.statusCode(), result.body()); diff --git a/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/ExternalHttpClient.java b/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/ExternalHttpClient.java index e4e656d2..7bf95b9f 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/ExternalHttpClient.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/ExternalHttpClient.java @@ -3,8 +3,8 @@ package com.kamco.cd.kamcoback.config.resttemplate; import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.charset.StandardCharsets; import java.util.List; -import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -14,54 +14,86 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; -@RequiredArgsConstructor @Component @Log4j2 public class ExternalHttpClient { - private final RestTemplate restTemplate; + private final RestTemplate restTemplate; // short (@Primary) + private final RestTemplate restTemplateLong; // long private final ObjectMapper objectMapper; + public ExternalHttpClient( + RestTemplate restTemplate, + @Qualifier("restTemplateLong") RestTemplate restTemplateLong, + ObjectMapper objectMapper) { + this.restTemplate = restTemplate; + this.restTemplateLong = restTemplateLong; + this.objectMapper = objectMapper; + } + + /** 기본(짧은 timeout) 호출 */ public ExternalCallResult call( String url, HttpMethod method, Object body, HttpHeaders headers, Class responseType) { - // responseType 기반으로 Accept 동적 세팅 + return doCall(restTemplate, url, method, body, headers, responseType); + } + + /** 추론/대용량 전용 (긴 timeout) */ + public ExternalCallResult callLong( + String url, HttpMethod method, Object body, HttpHeaders headers, Class responseType) { + + return doCall(restTemplateLong, url, method, body, headers, responseType); + } + + private ExternalCallResult doCall( + RestTemplate rt, + String url, + HttpMethod method, + Object body, + HttpHeaders headers, + Class responseType) { + HttpHeaders resolvedHeaders = resolveHeaders(headers, responseType); logRequestBody(body); HttpEntity entity = new HttpEntity<>(body, resolvedHeaders); try { - // String: raw bytes -> UTF-8 string + + // String 응답은 raw byte로 받아 UTF-8 변환 if (responseType == String.class) { - ResponseEntity res = restTemplate.exchange(url, method, entity, byte[].class); + ResponseEntity res = rt.exchange(url, method, entity, byte[].class); String raw = (res.getBody() == null) ? null : new String(res.getBody(), StandardCharsets.UTF_8); @SuppressWarnings("unchecked") T casted = (T) raw; + return new ExternalCallResult<>(res.getStatusCodeValue(), true, casted, null); } - // byte[]: raw bytes로 받고, JSON이면 에러로 처리 + // byte[] 응답 처리 if (responseType == byte[].class) { - ResponseEntity res = restTemplate.exchange(url, method, entity, byte[].class); + ResponseEntity res = rt.exchange(url, method, entity, byte[].class); MediaType ct = res.getHeaders().getContentType(); byte[] bytes = res.getBody(); + // JSON이면 에러로 간주 if (isJsonLike(ct)) { String err = (bytes == null) ? null : new String(bytes, StandardCharsets.UTF_8); + return new ExternalCallResult<>(res.getStatusCodeValue(), false, null, err); } @SuppressWarnings("unchecked") T casted = (T) bytes; + return new ExternalCallResult<>(res.getStatusCodeValue(), true, casted, null); } - // DTO 등: 일반 역직렬화 - ResponseEntity res = restTemplate.exchange(url, method, entity, responseType); + // DTO 응답 + ResponseEntity res = rt.exchange(url, method, entity, responseType); return new ExternalCallResult<>(res.getStatusCodeValue(), true, res.getBody(), null); } catch (HttpStatusCodeException e) { @@ -70,29 +102,28 @@ public class ExternalHttpClient { } } - // 기존 resolveJsonHeaders를 "동적"으로 교체 + /** Accept / Content-Type 자동 처리 */ private HttpHeaders resolveHeaders(HttpHeaders headers, Class responseType) { - // 원본 headers를 그대로 쓰면 외부에서 재사용할 때 사이드이펙트 날 수 있어서 복사 권장 + HttpHeaders h = (headers == null) ? new HttpHeaders() : new HttpHeaders(headers); - // 요청 바디 기본은 JSON이라고 가정 (필요하면 호출부에서 덮어쓰기) + // 기본 Content-Type if (h.getContentType() == null) { h.setContentType(MediaType.APPLICATION_JSON); } - // 호출부에서 Accept를 명시했으면 존중 + // Accept 이미 있으면 존중 if (h.getAccept() != null && !h.getAccept().isEmpty()) { return h; } - // responseType 기반 Accept 자동 지정 + // 응답 타입 기준 Accept 자동 지정 if (responseType == byte[].class) { h.setAccept( List.of( MediaType.APPLICATION_OCTET_STREAM, MediaType.valueOf("application/zip"), - MediaType.APPLICATION_JSON // 실패(JSON 에러 바디) 대비 - )); + MediaType.APPLICATION_JSON)); } else { h.setAccept(List.of(MediaType.APPLICATION_JSON)); } @@ -100,12 +131,15 @@ public class ExternalHttpClient { return h; } + /** JSON 응답 여부 체크 */ private boolean isJsonLike(MediaType ct) { if (ct == null) return false; + return ct.includes(MediaType.APPLICATION_JSON) || "application/problem+json".equalsIgnoreCase(ct.toString()); } + /** 요청 바디 로그 */ private void logRequestBody(Object body) { try { if (body != null) { diff --git a/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/RestTemplateConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/RestTemplateConfig.java index 402f6fe7..203b3aa3 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/RestTemplateConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/RestTemplateConfig.java @@ -4,6 +4,7 @@ import lombok.extern.log4j.Log4j2; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.http.client.BufferingClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @@ -13,10 +14,20 @@ import org.springframework.web.client.RestTemplate; public class RestTemplateConfig { @Bean + @Primary public RestTemplate restTemplate(RestTemplateBuilder builder) { + return build(builder, 2000, 3000); + } + + @Bean("restTemplateLong") + public RestTemplate restTemplateLong(RestTemplateBuilder builder) { + return build(builder, 2000, 60000); + } + + private RestTemplate build(RestTemplateBuilder builder, int connectTimeoutMs, int readTimeoutMs) { SimpleClientHttpRequestFactory baseFactory = new SimpleClientHttpRequestFactory(); - baseFactory.setConnectTimeout(2000); - baseFactory.setReadTimeout(60000); + baseFactory.setConnectTimeout(connectTimeoutMs); + baseFactory.setReadTimeout(readTimeoutMs); RestTemplate rt = builder @@ -24,27 +35,8 @@ public class RestTemplateConfig { .additionalInterceptors(new RetryInterceptor()) .build(); - // byte[] 응답은 무조건 raw로 읽게 강제 (Jackson이 끼어들 여지 제거) rt.getMessageConverters() .add(0, new org.springframework.http.converter.ByteArrayHttpMessageConverter()); - return rt; } - - // @Bean(name = "restTemplateLong") - // public RestTemplate restTemplateLong(RestTemplateBuilder builder) { - // SimpleClientHttpRequestFactory baseFactory = new SimpleClientHttpRequestFactory(); - // baseFactory.setConnectTimeout(2000); - // baseFactory.setReadTimeout(60000); // 길게(예: 60초) - // - // RestTemplate rt = - // builder - // .requestFactory(() -> new BufferingClientHttpRequestFactory(baseFactory)) - // .additionalInterceptors(new RetryInterceptor()) - // .build(); - // - // rt.getMessageConverters() - // .add(0, new org.springframework.http.converter.ByteArrayHttpMessageConverter()); - // return rt; - // } } diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java b/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java index 96bfa5d7..8e8150f2 100644 --- a/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java +++ b/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java @@ -144,6 +144,7 @@ public class InferenceResultService { * @return */ public UUID runExcl(InferenceResultDto.RegReq req) { + // TODO 쿼리로 한번에 할수 있게 수정해야하나.. // 기준연도 실행가능 도엽 조회 List targetMngList = mapSheetMngCoreService.getMapSheetMngHst( @@ -267,7 +268,7 @@ public class InferenceResultService { */ @Transactional public UUID runPrev(InferenceResultDto.RegReq req) { - + // TODO 쿼리로 한번에 할수 있게 수정해야하나.. // 기준연도 실행가능 도엽 조회 List targetMngList = mapSheetMngCoreService.getMapSheetMngHst(