RestTemplate 설정 추가 및 연결 풀, 타임아웃, Rate Limiting 기능 적용

internalApi
박성영 1 month ago
parent 36570132e4
commit e782cdac5c

@ -120,6 +120,10 @@ dependencies {
// ===== =====
// Apache Commons Text -
implementation "org.apache.commons:commons-text:${commonsTextVersion}"
// Apache HttpClient 4 - RestTemplate
implementation 'org.apache.httpcomponents:httpclient'
// Guava - Rate Limiting RateLimiter
implementation 'com.google.guava:guava:32.1.3-jre'
// ===== EXCEL =====
implementation 'org.apache.poi:poi:5.3.0'

@ -1,25 +1,112 @@
package egovframework.config;
import com.google.common.util.concurrent.RateLimiter;
import egovframework.configProperties.RestTemplateProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
/**
* RestTemplate
* API RestTemplate Bean
* Apache HttpClient 4
* Rate Limiting: application.yml
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
public class RestTemplateConfig {
private final RestTemplateProperties properties;
@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
public RestTemplate restTemplate(RestTemplateBuilder builder) {
// 타임아웃 설정 (application.yml에서 읽어옴)
int connectTimeout = properties.getTimeout().getConnectTimeoutMillis();
int readTimeout = properties.getTimeout().getReadTimeoutMillis();
log.info("RestTemplate 타임아웃 설정 - connectTimeout: {}ms, readTimeout: {}ms",
connectTimeout, readTimeout);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(connectTimeout)
.setSocketTimeout(readTimeout)
.setConnectionRequestTimeout(connectTimeout)
.build();
// 연결 풀 관리 설정 (application.yml에서 읽어옴)
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(properties.getConnectionPool().getMaxTotal());
cm.setDefaultMaxPerRoute(properties.getConnectionPool().getMaxPerRoute());
log.info("RestTemplate 연결 풀 설정 - maxTotal: {}, maxPerRoute: {}",
properties.getConnectionPool().getMaxTotal(),
properties.getConnectionPool().getMaxPerRoute());
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(cm)
.build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
// RestTemplate 생성
RestTemplate restTemplate = builder
.requestFactory(() -> requestFactory)
.build();
// Rate Limiter 인터셉터 추가 (0 이하면 제한 없음)
double permitsPerSecond = properties.getRateLimit().getPermitsPerSecond();
if (permitsPerSecond > 0) {
log.info("RestTemplate Rate Limiter 활성화 - 초당 {} 건으로 제한", permitsPerSecond);
restTemplate.getInterceptors().add(new RateLimitInterceptor(permitsPerSecond));
} else {
log.info("RestTemplate Rate Limiter 비활성화 - 제한 없음");
}
return restTemplate;
}
/**
* Rate Limiting
* Guava RateLimiter
*/
private static class RateLimitInterceptor implements ClientHttpRequestInterceptor {
private final RateLimiter rateLimiter;
private final double permitsPerSecond;
// 타임아웃 설정 (30초)
factory.setConnectTimeout(30000);
factory.setReadTimeout(30000);
public RateLimitInterceptor(double permitsPerSecond) {
this.permitsPerSecond = permitsPerSecond;
this.rateLimiter = RateLimiter.create(permitsPerSecond);
log.debug("RateLimitInterceptor 생성 - 초당 {} 건", permitsPerSecond);
}
return new RestTemplate(factory);
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
// 요청 전에 Rate Limiter를 통해 대기
double waitTime = rateLimiter.acquire();
if (waitTime > 0) {
log.debug("Rate Limiter 대기 - {}초 대기 후 요청: {} {}",
String.format("%.3f", waitTime),
request.getMethod(),
request.getURI());
}
return execution.execute(request, body);
}
}
}

@ -0,0 +1,41 @@
package egovframework.configProperties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* RestTemplate
* application.yml rest-template
*/
@Setter
@Getter
@Configuration
@ConfigurationProperties(prefix = "rest-template")
public class RestTemplateProperties {
private Timeout timeout = new Timeout();
private ConnectionPool connectionPool = new ConnectionPool();
private RateLimit rateLimit = new RateLimit();
@Setter
@Getter
public static class Timeout {
private int connectTimeoutMillis = 10000; // 기본값: 10초
private int readTimeoutMillis = 10000; // 기본값: 10초
}
@Setter
@Getter
public static class ConnectionPool {
private int maxTotal = 100; // 기본값: 최대 100 연결
private int maxPerRoute = 20; // 기본값: 경로당 20 연결
}
@Setter
@Getter
public static class RateLimit {
private double permitsPerSecond = 5.0; // 기본값: 초당 5건
}
}

@ -129,6 +129,7 @@ logging:
go.kr.project.sql: INFO # datasource-proxy
go.kr.project.sql.binding: INFO # datasource-proxy 파라미터 바인딩된 쿼리 로그
egovframework: INFO
egovframework.config.RestTemplateConfig: DEBUG
# File upload configuration
file:

@ -138,6 +138,7 @@ logging:
go.kr.project.sql: INFO # datasource-proxy
go.kr.project.sql.binding: DEBUG # datasource-proxy 파라미터 바인딩된 쿼리 로그
egovframework: DEBUG
egovframework.config.RestTemplateConfig: DEBUG
# File upload configuration
file:

@ -57,6 +57,17 @@ query-executor:
transaction:
timeout-minutes: 5 # 트랜잭션 타임아웃 (분 단위, 기본값: 5분)
# RestTemplate configuration
rest-template:
timeout:
connect-timeout-millis: 10000 # 연결 타임아웃 (밀리초, 기본값: 10초)
read-timeout-millis: 12000 # 읽기 타임아웃 (밀리초, 기본값: 12초)
connection-pool:
max-total: 100 # 최대 연결 수 (기본값: 100)
max-per-route: 20 # 경로당 최대 연결 수 (기본값: 20)
rate-limit:
permits-per-second: 5.0 # 초당 요청 제한 (0 이하: 제한 없음, 기본값: 5건/초)
# Mariadb aes_secret_key # 참고용도, 실제는 DB function에 하드코딩되어 있음
aes-secret-key: Copyright(c)2015-xit.co.kr

@ -133,7 +133,9 @@
var self = this;
// 임시 테스트용 차량번호 (실제로는 그리드에서 선택된 차량번호를 가져와야 함)
var vehicleNumbers = ["12가3456", "34나5678", "56다7890"];
var vehicleNumbers = ["12가3456", "34나5678", "56다7890", "78라1234", "90마5678",
"12바9012", "34사3456", "56아7890", "78자1234", "90차5678",
"12카9012", "34타3456", "56파7890", "78하1234", "90거5678"];
// 확인 메시지
if (!confirm("총 " + vehicleNumbers.length + "건의 차량 정보를 조회하시겠습니까?")) {

Loading…
Cancel
Save