[Spring MVC] ApiGateWay 코드

식빵·2022년 4월 11일
0

Spring Lab

목록 보기
9/35
post-thumbnail

회사 선배님이 작성하신 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 라는 게 클라우드 관련되서 용어가 쓰이는 거 같은데...
아무래도 공부를 더 해봐야 위 코드의 쓰임새를 더 정확히 알 수 있을 거 같다.
일단은 코드만 남겨놓고 글을 마치겠다.

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글