22년 3월 신입으로 들어와 처음으로 참여했던건 IOT 프로젝트입니다.
선배님들이 한 달 내내 새벽까지 야근하셨던 프로젝트인데 지금은 그 선배님들이 다 퇴사하시고 앱 서버 담당자가 되었습니다...😂
애증이 담긴 프로젝트인데 리팩토링하면서 고민했던 부분을 기록합니다.
프로젝트의 앱 서버는 외부 서버와 HTTP 통신을 통해 API를 호출해 응답 값을 가공하여 클라이언트에 내려주는 역할을 했습니다.
여기서 API를 호출하기 위해 APIUtil을 했는데,
이 과정에서 HttpClient를 static으로 선언하여 문제가 발생했습니다.
수정 작업을 진행하던 중 몇 가지 궁금증이 생겼습니다.
"내부 try-catch 안에서 매번 생성 및 삭제를 할까? 안전하게 사용하려고 했을까?"
"의존성을 주입하지 않고 UTIL로 만든 이유는 무엇일까?"
리팩토링 하는 것을 허락 받은 후 Service 로직으로 리팩토링 작업을 진행했습니다.
public class APIUtil implements CodesEx {
private static Logger logger = LoggerFactory.getLogger(APIUtil.class);
public static ObjectNode sendMessage(...) throws HttpClientErrorException, URISyntaxException, JsonProcessingException, IOException {
logger.info( "Start of API transaction." );
...
try{
int setTimeout = ConfigUtil.getInt("domain.timeout", 30);
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout( setTimeout * 1000 );
factory.setReadTimeout( setTimeout * 1000 );
RestTemplate restTemplate = new RestTemplate(factory);
restTemplate.setErrorHandler(new APIException());
...
ResponseEntity<ObjectNode> response = restTemplate.exchange(url, apiType, requestEntity, ObjectNode.class);
...
RestTemplate의 Timeout 설정을 위해 HttpComponentsClientHttpRequestFactory를 사용하고 있으며, RestTemplate객체를 매번 생성해서 사용하고 있습니다.
public class APIException implements ResponseErrorHandler {
private static final Logger logger = Logger.getLogger( APIException.class );
@Override
public void handleError(ClientHttpResponse clienthttpresponse) throws IOException {
if ( clienthttpresponse.getStatusCode() != HttpStatus.OK
&& clienthttpresponse.getStatusCode() != HttpStatus.CREATED // 201 Created
&& clienthttpresponse.getStatusCode() != HttpStatus.ACCEPTED // 202 Accepted
&& clienthttpresponse.getStatusCode() != HttpStatus.NO_CONTENT // 204 NoContent
) {
...
throw new HttpClientErrorException(clienthttpresponse.getStatusCode(),"HTTP Status: " + clienthttpresponse.getStatusCode(), exceptionBody, null);
}
}
@Override
public boolean hasError(ClientHttpResponse clienthttpresponse) throws IOException {
if ( clienthttpresponse.getStatusCode() != HttpStatus.OK
&& clienthttpresponse.getStatusCode() != HttpStatus.CREATED // 201 Created
&& clienthttpresponse.getStatusCode() != HttpStatus.ACCEPTED // 202 Accepted
&& clienthttpresponse.getStatusCode() != HttpStatus.NO_CONTENT // 204 NoContent
) {
...
return true;
}
return false;
}
}
ResponseErrorHandler 인터페이스를 구현하여 에러 핸들링을 하는 코드입니다.
RestTemplate를 매번 생성하는 것은 성능의 저하를 가져올 수 있기 때문에 객체를 싱글톤으로 등록하고 호출할때마다 재사용하는 방식으로 변경하려고합니다.
그리고 서버의 스레드 문제와 함께 장애를 차단하기 위해 커넥션 수를 관리하기 위해 CloseableHttpClient 설정을 추가하려고 합니다.
public class RestTemplateConfig {
private RestTemplate restTemplate;
public RestTemplateConfig() {
int maxConnTotal = Integer.parseInt(SmartConfig.getString("httpClient.set.maxConnTotal")); // 연결을 유지할 최대 숫자
int maxConnPerRoute = Integer.parseInt(SmartConfig.getString("httpClient.set.maxConnPerRoute")); // 특정 경로당 최대 숫자
int connTimeToLive = Integer.parseInt(SmartConfig.getString("httpClient.set.connTimeToLive"));
int setTimeout = ConfigUtil.getInt("domain.timeout", 30);
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setMaxConnTotal(maxConnTotal)
.setMaxConnPerRoute(maxConnPerRoute)
.setConnectionTimeToLive(connTimeToLive, TimeUnit.SECONDS)
.build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(setTimeout * 1000);
factory.setReadTimeout(setTimeout * 1000);
factory.setHttpClient(httpClient);
RestTemplate template = new RestTemplate(factory);
template.setErrorHandler(new APIException());
this.restTemplate = template;
}
public RestTemplate getRestTemplate() {
return restTemplate;
}
커넥션 수를 관리하기 위해 factory에 HttpClient(CloseableHttpClient) 설정을 추가했습니다.
factory.setHttpClient(httpClient);
또한 빈(Bean)으로 등록 후 객체를 싱글톤으로 생성하여 getRestTemplate()를 통해 재사용 하도록 변경했습니다.
public RestTemplate getRestTemplate() {
return restTemplate;
}
참고자료
[JAVA] DefaultHttpClient와 CloseableHttpClient의 차이 1
[JAVA] DefaultHttpClient와 CloseableHttpClient의 차이 2
Spring RestTemplate Error Handling
public class APIService implements CodesEx {
@Autowired
RestTemplateConfig restTemplateConfig;
private static Logger logger = LoggerFactory.getLogger(APIUtil.class);
public ObjectNode sendMessage(...) throws HttpClientErrorException, URISyntaxException, JsonProcessingException, IOException {
logger.info( "Start of API transaction." );
...
try{
RestTemplate restTemplate = restTemplateConfig.getRestTemplate();
...
ResponseEntity<ObjectNode> response = restTemplate.exchange(url, apiType, requestEntity, ObjectNode.class);
...
Util로 사용하기 위해 Static Method로 생성되었던 SendMessage()를 Non Static Method로 변경했습니다.
그리고 RestTemplateConfig 주입, 객체를 restTemplate을 싱글톤으로 등록하고 호출하여 재사용하는 방식으로 변경했습니다.
관리가 되지 않던 에러코드라던가 하드코딩 등.. 수정을 진행했습니다.
RestTemplate은 Thread Safe하다.
RestTemplate을 매 호출마다 생성하게 될 경우 성능 저하를 유발한다.
그러므로 RestTemplate을 싱글톤으로 생성 후 재사용하는걸 고려해봐야 한다.
RestTemplate를 매번 생성할지, 싱글톤으로 생성하여 재사용할지 고민인 사람들이 이 글을 보고 도움이 됐으면 합니다!
버텍스도 공부해주세요