feat/infer_dev_260211 #114

Merged
teddy merged 3 commits from feat/infer_dev_260211 into develop 2026-02-27 10:09:04 +09:00
6 changed files with 75 additions and 41 deletions

View File

@@ -78,7 +78,7 @@ public class InferenceCommonService {
// 4) 추론 실행 API 호출
ExternalCallResult<String> 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());

View File

@@ -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 <T> ExternalCallResult<T> call(
String url, HttpMethod method, Object body, HttpHeaders headers, Class<T> responseType) {
// responseType 기반으로 Accept 동적 세팅
return doCall(restTemplate, url, method, body, headers, responseType);
}
/** 추론/대용량 전용 (긴 timeout) */
public <T> ExternalCallResult<T> callLong(
String url, HttpMethod method, Object body, HttpHeaders headers, Class<T> responseType) {
return doCall(restTemplateLong, url, method, body, headers, responseType);
}
private <T> ExternalCallResult<T> doCall(
RestTemplate rt,
String url,
HttpMethod method,
Object body,
HttpHeaders headers,
Class<T> responseType) {
HttpHeaders resolvedHeaders = resolveHeaders(headers, responseType);
logRequestBody(body);
HttpEntity<Object> entity = new HttpEntity<>(body, resolvedHeaders);
try {
// String: raw bytes -> UTF-8 string
// String 응답은 raw byte로 받아 UTF-8 변환
if (responseType == String.class) {
ResponseEntity<byte[]> res = restTemplate.exchange(url, method, entity, byte[].class);
ResponseEntity<byte[]> 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<byte[]> res = restTemplate.exchange(url, method, entity, byte[].class);
ResponseEntity<byte[]> 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<T> res = restTemplate.exchange(url, method, entity, responseType);
// DTO 응답
ResponseEntity<T> 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) {

View File

@@ -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;
// }
}

View File

@@ -144,6 +144,7 @@ public class InferenceResultService {
* @return
*/
public UUID runExcl(InferenceResultDto.RegReq req) {
// TODO 쿼리로 한번에 할수 있게 수정해야하나..
// 기준연도 실행가능 도엽 조회
List<MngListDto> targetMngList =
mapSheetMngCoreService.getMapSheetMngHst(
@@ -267,7 +268,7 @@ public class InferenceResultService {
*/
@Transactional
public UUID runPrev(InferenceResultDto.RegReq req) {
// TODO 쿼리로 한번에 할수 있게 수정해야하나..
// 기준연도 실행가능 도엽 조회
List<MngListDto> targetMngList =
mapSheetMngCoreService.getMapSheetMngHst(

View File

@@ -351,7 +351,7 @@ public class MapSheetMngCoreService {
}
/**
* 이전 년도 도엽 조회
* 이전 년도 도엽 조회 조건이 많을 수 있으므로 chunk 줘서 끊어서 조회
*
* @param year
* @param mapIds

View File

@@ -125,5 +125,12 @@ public interface MapSheetMngRepositoryCustom {
*/
List<MngListDto> getMapSheetMngHst(Integer year, String mapSheetScope, List<String> mapSheetNum);
/**
* 비교연도 사용 가능한 이전도엽을 조회한다.
*
* @param year 연도
* @param mapIds 도엽목록
* @return 사용 가능한 이전도엽목록
*/
List<MngListDto> findFallbackCompareYearByMapSheets(Integer year, List<String> mapIds);
}