[프로젝트] RestTemplate 안정성과 성능 저하 with 리팩토링

John·2022년 11월 29일
2

개발 메모🌷

목록 보기
8/13
post-thumbnail

22년 3월 신입으로 들어와 처음으로 참여했던건 IOT 프로젝트입니다.

선배님들이 한 달 내내 새벽까지 야근하셨던 프로젝트인데 지금은 그 선배님들이 다 퇴사하시고 앱 서버 담당자가 되었습니다...😂

애증이 담긴 프로젝트인데 리팩토링하면서 고민했던 부분을 기록합니다.


RestTemplate

프로젝트의 앱 서버는 외부 서버와 HTTP 통신을 통해 API를 호출해 응답 값을 가공하여 클라이언트에 내려주는 역할을 했습니다.

여기서 API를 호출하기 위해 APIUtil을 했는데,
이 과정에서 HttpClient를 static으로 선언하여 문제가 발생했습니다.

수정 작업을 진행하던 중 몇 가지 궁금증이 생겼습니다.

"내부 try-catch 안에서 매번 생성 및 삭제를 할까? 안전하게 사용하려고 했을까?"

"의존성을 주입하지 않고 UTIL로 만든 이유는 무엇일까?"

리팩토링 하는 것을 허락 받은 후 Service 로직으로 리팩토링 작업을 진행했습니다.

참고자료
[Spring] RestTemplate는 Thread Safe할까?


API 통신

APIUtil

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객체를 매번 생성해서 사용하고 있습니다.


APIException

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 설정을 추가하려고 합니다.

참고자료
[ Springboot ] RestTemplate 객체 생성으로 인한 성능 저하 사례


RestTemplateConfig

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


APIService

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를 매번 생성할지, 싱글톤으로 생성하여 재사용할지 고민인 사람들이 이 글을 보고 도움이 됐으면 합니다!

profile
기록을 습관으로

2개의 댓글

comment-user-thumbnail
2022년 11월 29일

버텍스도 공부해주세요

1개의 답글