RestTemplate 라이브러리에 어떤 유용한 기능들이 있을까?

Taewoo·2023년 5월 7일
0

저는 블록체인 서비스 기업에서 거래소 서비스를 개발하며 외부 API 호출할 일이 많았습니다.

외부 API 요청을 위해 RestTemplate을 사용하며 찾은 기능과 서버 간 요청 반복작업에서 중복코드를 제거하고 코드 재사용성을 높일 수 있는 방법을 정리했습니다.

Project ⚙️

이해를 돕기 위해 적당한 OPEN API를 찾던 중 파파고 API가 좋아보여서 파파고 API를 사용했습니다.

네이버 파파고 API 바로가기

SonarQube

소나큐브는 정적 코드 분석기입니다. 코드 스멜을 확인하기 위해서 사용했습니다.

SonarQube 바로 적용하기

간단한 요청

GET 요청을 보내고 문자열 타입의 응답을 받는 방법

RestTemplate restTemplate = new RestTemplate();

String result = restTemplate.getForObject("http://helloworld.com", String.class);

// ttp://helloworld.com 에서 반환하는 문자열 리턴
log.info(result);

요청의 헤더와 바디를 설정하는 방법

    	// RestTemplate 객체 생성
		RestTemplate restTemplate = new RestTemplate();
        
        // Map으로 파라미터를 생성
		Map<String, Object> parameters = new HashMap<>();

		// 요청 헤더 설정
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_JSON);
		headers.set("Authorization", "Bearer " + "your-access-token");
		
        // 요청 바디
		parameters.put("key", "value");

		var response = restTemplate
			.postForEntity("https://helloworld.com",
            new HttpEntity<>(parameters, headers),
			Object.class);

		log.info(response.toString());

파파고 번역 API 요청 예시

요청

curl "https://openapi.naver.com/v1/papago/n2mt" \
-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
-H "X-Naver-Client-Id: {{CLIENT_ID}}" \
-H "X-Naver-Client-Secret: {{CLIENT_SECRET}}" \
-d "source={{ko}}&target={{en}}&text={{번역할 문자열 (안녕)}}" -v 

응답

{
    "message": {
        "result": {
            "srcLangType": "ko",
            "tarLangType": "en",
            "translatedText": "Hello", // 안녕 -> Hello
            "engineType": "PRETRANS"
        },
        "@type": "response",
        "@service": "naverservice.nmt.proxy",
        "@version": "1.0.0"
    }
}

RestTemplate을 사용한 요청

깃허브 바로가기

@Service
public class Papago implements Translator {

	// 파파고 API 엔드포인트
	private static final String REQUEST_URL = "https://openapi.naver.com/v1/papago/n2mt";
	
    // 인증 Key
	private static final String HEADER_CLIENT_ID = "X-Naver-Client-Id";
	private static final String HEADER_CLIENT_SECRET = "X-Naver-Client-Secret";
	
    // 파라미터 Key
	private static final String BODY_SOURCE_NAME = "source";
	private static final String BODY_TARGET_NAME = "target";
	private static final String BODY_TEXT_NAME = "text";
	
    // 인증 Value
	private final String clientId;
	private final String secret;

	// 요청 정보 초기화
	private RestTemplate request = new RestTemplate();
	private HttpHeaders header = new HttpHeaders();
	private LinkedMultiValueMap<String, String> paramter = new LinkedMultiValueMap<>();
	
    // API 인증 관련 정보 생성자 주입
	public Papago(
		@Value("${api.papago.client-id}") String clientId,
		@Value("${api.papago.secret}") String secret) {
		this.clientId = clientId;
		this.secret = secret;
	}
	
    // 헤더 설정
	public void setHeaders() {
		header.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

		header.set(HEADER_CLIENT_ID, clientId);
		header.set(HEADER_CLIENT_SECRET, secret);
	}
	
    // 로직 구현
	@Override
	public String translate(String source, String target, String text) {
		setHeaders();
		
        // 파라미터 설정
		paramter.add(BODY_SOURCE_NAME, source);
		paramter.add(BODY_TARGET_NAME, target);
		paramter.add(BODY_TEXT_NAME, text);

		var response = request.postForEntity(
			REQUEST_URL,
			new HttpEntity<>(paramter, header),
			PapagoTranslateResponse.class);
        
        // PapagoTranslateResponse 객체로 파싱

		if (null == response.getBody() || null == response.getBody().getTranslatedText()) {
			throw new IllegalArgumentException(text + "에 대한 번역결과가 없습니다.");
		}

		return response.getBody().getTranslatedText();
	}
}

SonarQube 분석 결과 레포트

코드 스멜은 하나도 없지만 translate 메서드가 호출될 때마다 setHeader() 메서드를 실행됩니다. 물론 다른 방법으로도 해결할 수 있지만 RestTemplate은 이런 반복적인 작업을 간단하게 처리하기 위한 인터셉터 인터페이스를 제공합니다.

Interceptor

RestTemplate의 인터셉터는 HTTP 요청을 보내기 전/후로 특정한 처리를 추가할 수 있도록 해주는 기능입니다.

인터셉터 활용

인터셉터를 활용하면 Access-Key 같은 요청들의 공통 헤더를 인터셉터를 통해 코드 중복 없이 설정할 수 있습니다.

ClientHttpRequestInterceptor 인터페이스

package org.springframework.http.client;

import java.io.IOException;
import org.springframework.http.HttpRequest;

// 함수형 인터페이스로 선언됨 
// abstract 메서드가 한 개만 있어야 한다.
// default, static은 상관없음.

@FunctionalInterface
public interface ClientHttpRequestInterceptor {
	ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
			throws IOException;

}

파파고 API로 요청을 보내기 전 인터셉터를 사용해 인증 토큰을 추가하기

@Component
// 인터셉터를 구현
public class HeaderSettingInterceptor implements ClientHttpRequestInterceptor {

	private static final String HEADER_CLIENT_ID = "X-Naver-Client-Id";
	private static final String HEADER_CLIENT_SECRET = "X-Naver-Client-Secret";

	private final String clientId;
	private final String secret;

	public HeaderSettingInterceptor(
		@Value("${api.papago.client-id}") String clientId,
		@Value("${api.papago.secret}") String secret) {
		this.clientId = clientId;
		this.secret = secret;
	}

	@Override
	public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws
		IOException {
		
        // 헤더에 Key, Value 추가
		request.getHeaders().add(HEADER_CLIENT_ID, clientId);
		request.getHeaders().add(HEADER_CLIENT_SECRET, secret);
		request.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
		
        // 실제 요청
		return execution.execute(request, body);
	}
}

RestTemplate에 인터셉터를 구현하고 빈으로 등록

@Configuration
public class AppConfig {

	private final HeaderSettingInterceptor headerSettingInterceptor;

	public AppConfig(HeaderSettingInterceptor headerSettingInterceptor) {
		this.headerSettingInterceptor = headerSettingInterceptor;
	}

	@Bean
	public RestTemplate restTemplate() {
		RestTemplate restTemplate = new RestTemplate();
		restTemplate.setInterceptors(Collections.singletonList(headerSettingInterceptor));
		return restTemplate;
	}
}

HeaderSettingInterceptor를 주입 받아서 사용

@Service
public class Papago implements Translator {

	private static final String REQUEST_URL = "https://openapi.naver.com/v1/papago/n2mt";

	private static final String BODY_SOURCE_NAME = "source";
	private static final String BODY_TARGET_NAME = "target";
	private static final String BODY_TEXT_NAME = "text";

	private final RestTemplate request;
	private LinkedMultiValueMap<String, String> paramter = new LinkedMultiValueMap<>();

	public Papago(RestTemplate request) {
		this.request = request;
	}

	@Override
	public String translate(String source, String target, String text) {

		paramter.add(BODY_SOURCE_NAME, source);
		paramter.add(BODY_TARGET_NAME, target);
		paramter.add(BODY_TEXT_NAME, text);

		var response = request.postForEntity(
			REQUEST_URL,
			new HttpEntity<>(paramter, new HttpHeaders()),
			PapagoTranslateResponse.class);

		if (null == response.getBody() || null == response.getBody().getTranslatedText()) {
			throw new IllegalArgumentException(text + "에 대한 번역결과가 없습니다.");
		}

		return response.getBody().getTranslatedText();
	}
}

디버그

디버그 화면을 보면 RestTemplate 인스턴스에 HeaderSettingInterceptor가 등록된 것을 확인할 수 있습니다.

ErrorHandler

RestTemplate의 ErrorHandler는 RestTemplate이 HTTP 요청을 보낼 때 발생할 수 있는 예외를 처리하는 방법을 정의하는 인터페이스입니다.

예를 들어 HTTP 4xx 또는 5xx 상태 코드를 수신한 경우 RestTemplate이 예외를 던집니다.

이러한 예외를 처리하는 방법을 정의할 수 있습니다.

hasError() 메서드는 해당 응답이 오류였는지 판단하는 메서드입니다.
handleError() 메서드는 실패한 HTTP 요청에 대한 처리 방법을 결정합니다.

예시

네이버 개발자센터에서 제공하는 오류코드를 바탕으로 예외를 처리하는 ErrorHandler 만듭니다.

// 오류 예시
{
    "errorCode": "N2MT07",
    "errorMessage": "text parameter is needed (text 파라미터가 필요합니다.)"
}

5xx 오류가 발생하면 RuntimeException을 던지고 4xx 오류 발생 시 오류 메세지를 반환하는 예시입니다.


@Component
public class TranslateErrorHandler implements ResponseErrorHandler {
	
    // JSON 파싱용 ObjectMapper 주입
    
	private final ObjectMapper om;

	public TranslateErrorHandler(ObjectMapper om) {
		this.om = om;
	}

	@Override
    // 오류 판별
	public boolean hasError(ClientHttpResponse response) throws IOException {
		return response.getStatusCode().is4xxClientError()
			|| response.getStatusCode().is5xxServerError();
	}

	@Override
    // 오류 핸들링
	public void handleError(ClientHttpResponse response) throws IOException {
		var status = response.getStatusCode();

		if (status.is5xxServerError()) {
			throw new RuntimeException("예상치 못한 오류가 발생했습니다.");
		}

		if (status.is4xxClientError()) {
			ErrorResponse errorResponse = om.readValue(response.getBody(), ErrorResponse.class);

			throw new IllegalArgumentException(errorResponse.getErrorMessage());
		}
	}

	@Data
	@NoArgsConstructor
	static class ErrorResponse {
		private String errorCode;
		private String errorMessage;
	}
}

Async RestTemplate

외부 API 호출은 많은 리소스가 필요하기 때문에 Trending이나 Top같은 지표들은 캐싱을 적용할 수 있습니다.

하지만 외부 API를 사용하다 보면 캐싱이 어려워 매 번 서버 간 요청을 해야되는 상황이라면 Blocking 때문에 처리량이 낮아지는 문제가 생깁니다.

Blocking IO란?

Async RestTemplate 비동기 호출을 통해 블로킹을 방지하고 서버의 처리량과 응답성을 향상시킬 수 있습니다.

RestTemplate과 사용법은 유사하지만, 비동기 호출을 지원하고 콜백을 등록할 수 있는 메서드들이 추가되어있습니다.

사실 Spring 5.0부터는 WebClient를 지원하기 때문에 AsyncRestTemplate을 사용할 이유가 없습니다. 하지만 기존 RestTemplate을 사용하는 어플리케이션의 코드 호환성을 고려해 AsyncRestTemplate를 사용하는 것도 좋은 방법입니다.

Async RestTemplate의 기본 사용 방법

AsyncRestTemplate asyncRestTemplate = new AsyncRestTemplate();

ListenableFuture<ResponseEntity<String>> future = asyncRestTemplate.getForEntity("https://www.example.com", String.class);

future.addCallback(new ListenableFutureCallback<ResponseEntity<String>>() {
    @Override
    public void onSuccess(ResponseEntity<String> result) {
        // 비동기 호출이 성공했을 때 처리할 내용
    }

    @Override
    public void onFailure(Throwable ex) {
        // 비동기 호출이 실패했을 때 처리할 내용
    }
});

// 비동기 호출 취소
future.cancel(true);

0개의 댓글