회사 선배님이 작성하신 ApiGateWay
라는 @Controller
클래스가 있었는데,
조금 더 알아보기 쉽게 리팩토링을 했다.
참고로 난 ApiGateWay 클래스가 왜 필요한지 모르고 있는 상태다.
하지만 Spring MVC
프레임워크에 대한 사용법을 좀 더 익히고,
추후에 쓸 수도 있을 거 같아서 기록을 남긴다.
기존 선배님의 코드는 내가 작성한 것이 아니므로 함부로 올리지 못한다.
그러니 그냥 내가 리팩토링한 코드만 올린다.
아래 코드는 static 필드를 많이 사용했는데, 그냥 @PostConsturct를 사용해서
static 필드가 아닌 인스턴스 필드로 작업을 해도 된다.
그리고 사실 그게 더 낫다.
Spring DI를 적극적으로 쓰기 위해서는 인스턴스 필드를 쓰는 게 더 편하기 때문이다.
package me.dailycode
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.stereotype.Controller;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import egovframework.com.cmm.service.EgovProperties;
import lombok.extern.slf4j.Slf4j;
@Controller
@Slf4j
public class ApiGateway {
private static final String API_SERVER_SCHEME = "http";
private static final String API_SERVER_IP = "api.server.ip";
private static final String API_SERVER_PORT = "8080";
private static final String API_SERVER_CONTEXT_PATH = "api.context";
private static final RestTemplate REST_TEMPLATE;
private static final HttpHeaders DEFAULT_HEADERS;
private static final ObjectMapper JACKSON_MAPPER;
private static final JsonNodeFactory FACTORY;
private static final UriComponentsBuilder defaultComponentBuilder;
// Object Mapper 의 convertValue에서 가장 흔하게 쓰이는 Type 지정
public static final TypeReference<Map<String, Object>> MAP_TYPE
= new TypeReference<Map<String, Object>>(){};
static {
// RestTemplate 기본 설정을 위한 Factory 생성
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
// 설정 안한 상태에서 API 서버가 죽으면, 거의 10~15초 정도 기다려야 한다.
factory.setConnectTimeout(1500);
// 설정 안한 상태에서 API 서버가 죽으면, 거의 10~15초 정도 기다려야 한다.
factory.setReadTimeout(1500);
// out of memory 예방, 사실 완벽하게 예방하고 싶으면 stream을 쓰는 방식을
// 고안해야 한다. resttemplate은 stream 을 통한 반환에 최적화 되어 있지 않다.
factory.setBufferRequestBody(false);
// 위에서 만든 factory 를 사용할 RestTemplate을 생성한다.
REST_TEMPLATE = new RestTemplate(factory);
// Http Response body 의 한글 깨짐 방지
REST_TEMPLATE.getMessageConverters()
.add(0, new StringHttpMessageConverter(Charset.forName("UTF-8")));
// 헤더 생성
DEFAULT_HEADERS = new HttpHeaders();
// application/json;charset=UTF-8 을 위해 특화되어 있다.
DEFAULT_HEADERS.setContentType(MediaType.APPLICATION_JSON_UTF8);
// Accept 헤더 값도 application/json;charset=UTF-8 을 받도록 수정
DEFAULT_HEADERS
.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
// 계속 쓰이는 JacksonMapper를 미리 초기화한다. Thread-Safe 하다.
JACKSON_MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 복잡한 json을 http body 전송 시 필요한 objectNode, arrayNode 생성용
FACTORY = JsonNodeFactory.instance;
defaultComponentBuilder =
UriComponentsBuilder.newInstance()
.scheme(API_SERVER_SCHEME)
.host(API_SERVER_IP)
.port(API_SERVER_PORT)
.path(API_SERVER_CONTEXT_PATH);
// 만약 URI의 스키마, 아이피, 포트번호, 컨텍스트 경로를 다 합친 문자열이 있다면
// 아래처럼 대체해서 defaultComponentBuilder 에 값을 할당할 수 있다.
// defaultComponentBuilder = UriComponentsBuilder.fromUriString(API_URL);
}
/**
* 브라우저 단에서는 아래처럼 요청을 하면 된다. <br/>
*
* <pre>
* $.ajax({
* url: contextPath
* + "/{swagger API URL을 그대로 입력하세요! 단 , 쿼리 스트링은 X!!}/apiGateway.do"
* + "?{API에서 필요한 쿼리 스트링은 여기에 작성하세요}",
* dataType: 'json',
* contentType: 'application/json',
* data: JSON.stringify({good:'day'}),
* type: 'post',
* success: function(result) {
* console.log(result)
* },
* error: function(jqXHR, status, error) {
* console.log(jqXHR, status, error);
* }
*})
* </pre>
*/
@RequestMapping(
value = "/**/apiGateway.do",
consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE,
MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.APPLICATION_JSON_UTF8_VALUE,
MediaType.APPLICATION_JSON_VALUE}
)
public ResponseEntity<?> mirrorRestWithRestTemplate(
HttpEntity<JsonNode> requestHeadersAndBody,
HttpMethod httpMethod,
@RequestParam MultiValueMap<String, String> queryParam,
HttpMethod method, HttpServletRequest request) {
// 요청 api 주소 획득
String requestApiUri = request.getRequestURI()
.substring(request.getContextPath().length(),
request.getRequestURI()
.lastIndexOf("/apiGateway.do"));
// api에 필요한 method, queryString 등을 그대로 전송하기 위해서 아래와 같이 지정한다.
UriComponents build = getDefaultComponentBuilder()
.path(requestApiUri)
.queryParams(queryParam)
.build();
try {
ResponseEntity<JsonNode> response
= REST_TEMPLATE.exchange(build.toUri(), method,
requestHeadersAndBody,
JsonNode.class);
return response;
} catch (HttpStatusCodeException e) {
log.error("error: {}", new String(e.getResponseBodyAsByteArray()));
return ResponseEntity.status(e.getRawStatusCode())
.headers(e.getResponseHeaders())
.body(e.getResponseBodyAsByteArray());
}
}
/**
* GET 방식도 허용
*/
@GetMapping(
value = "/**/apiGateway.do",
produces = {MediaType.APPLICATION_JSON_UTF8_VALUE,
MediaType.APPLICATION_JSON_VALUE}
)
public ResponseEntity<?> mirrorRestWithRestTemplateGetMapping(
@RequestParam MultiValueMap<String, String> queryParam,
@RequestHeader MultiValueMap<String , String> headers,
HttpMethod method, HttpServletRequest request) {
// 요청 api 주소 획득
HttpEntity<JsonNode> httpEntity = new HttpEntity<>(headers);
return mirrorRestWithRestTemplate(httpEntity,
HttpMethod.GET,
queryParam,
method,
request);
}
private UriComponentsBuilder getDefaultComponentBuilder() {
return defaultComponentBuilder.cloneBuilder();
}
}
뭔가 ApiGateWay 라는 게 클라우드 관련되서 용어가 쓰이는 거 같은데...
아무래도 공부를 더 해봐야 위 코드의 쓰임새를 더 정확히 알 수 있을 거 같다.
일단은 코드만 남겨놓고 글을 마치겠다.