티스토리 뷰

웹 개발/wanFramework

Proxy API

0bliviat3 2024. 8. 25. 18:21

 

개발 동기

 

회사에서 근래에 대응개발을 해야할일들이 많아졌다.

현재 운영중인 서비스에서 타 서비스와 연동하는 부분이 생겼고 그에 따라 Restful API를 통해

데이터를 주고 받아야 할일들이 발생했다.

 

현재 담당하는 서비스가 엔드포인트로서 사용자들에게 보여지는 부분들이 많이 있다보니

보통 대응개발이면 다른 서비스의 데이터를 받아와 사용자들에게 화면으로 보여줘야 할 부분들이 좀 있다.

 

문제는 현재 타서비스와 연동하려면 CORS 정책에 걸려 브라우져에서 요청 할 수 없는 경우가 많다.

결국 우리 서버에서 타서비스 서버로 요청을 보내야하므로 대응개발건이 생길때마다

API 요청 서비스 개발, 데이터를 보여줄 화면단 개발, 배포등 일련의 과정을 거쳐야 하고

또 문서로 정리해둬야 하는 등의 공수가 발생한다.

 

대응 개발 기본 흐름

 

 

이때 서비스개발이 필요하기 때문에 애플리케이션 서버를 한번 내렸다가 올려야 하는 상황이 발생한다.

 

운영에서 서버 재기동이 이뤄지는건 고객입장에서는 불안한 요소중 하나이다.

또 개발하는 입장에서도 매 대응개발시마다 거의 동일한 서비스를 만들어 배포하는것 또한 귀찮은 일인건 맞다.

반복되는 개발을 줄이고 더 안정적인 방법이 있다면 그걸 택해야 하는 법

 

그래서 생각해낸것은 우리 애플리케이션 서버를 프록시 서버 역할을 할수 있도록 Proxy API 서비스를 개발하는것이였다.

 

 

 

흐름

 

말이 거창해서 Proxy API 서비스 개발이지

원리만 조금 알면 누구나 구현 가능한 개발이다.

 

우선 프록시 서버의 시퀀스 다이어그램을 한번 보면 바로 이해 할 수 있다.

 

 

CORS정책에서 자유로우려면 사용자가 데이터 요청을 보냈을때 우리 서버를 경유해서 가야하므로

타서비스 요청이 필요한 상황이라면 일단 우리 서비스를 한번 태워 우리 서버에서 요청을 하면 된다.

이때 타서비스마다 요청시 필요한 데이터의 형식이나 데이터가 다르기 때문에

이부분에 대해 공통모듈로 만들어 주면된다.

 

 

구현

 

먼저 공통모듈의 기능부터 정의하자면,

타서비스의 요청URL, 요청타입, 요청 파라미터, 요청 header를 받아 적절히 파싱해 프록시서버에서

사용할 파라미터만 제외하고 나머지는 다시 감싸서 타 서비스에 요청을 보내야 한다.

 

그러기 위해서는 우선 Util먼저 정의할 필요가 있다.

 

util에서는 파라미터에 대한 유효성 체크와 요청 타입 생성에 대한 기능을 구현한다.

 

package ...;

import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import org.apache.log4j.Logger;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

public class ProxyAPIUtils {
	
	
	private static final Logger LOGGER = Logger.getLogger(ProxyAPIUtils.class);
	
	/**
     * 파라미터 빈값 제거
     * @param params
     * @return
     */
    public static Map<String, String> convertNotNullParams(Map<String, String>... params) {
        return Arrays.stream(params)
        	.filter(map -> map != null)
            .flatMap(map -> map.entrySet().stream())
            .filter(f -> !Optional.ofNullable(f.getValue()).orElse("").isEmpty())
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v1));
    }

    /**
     * Map -> Json 변환
     * @param map
     * @return String
     */
    public static String mapToJson(Map<String, String> map) {
        ObjectMapper mapper = new ObjectMapper();
        try {
			return mapper.writeValueAsString(map);
		} catch (Exception e) {
			logException(e);
		}
        return null;
    }
    
    /**
     * map -> queryString 변환
     * @param params
     * @return String
     */
    public static String mapToQuery(Map<String, String> params) {
        StringBuilder result = new StringBuilder();
        boolean first = true;
        for (Map.Entry<String, String> entry : params.entrySet()) {
            if (first) {
            	first = false;            	
            } else {
            	result.append("&");            	
            }
            try {
            	result.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
            	result.append("=");
            	result.append(URLEncoder.encode(entry.getValue(), "UTF-8"));				
			} catch (Exception e) {
				logException(e);
			}
        }
        return result.toString();
    }

    /**
     * json -> map 변환
     * @param json
     * @return
     * @throws Exception
     */
    public static Map<String, String> jsonToMap(String json) {
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            TypeReference<Map<String, String>> typeReference = new TypeReference<Map<String, String>>() {};       
            return objectMapper.readValue(json, typeReference);
        } catch (Exception e) {
        	logException(e);
        }
        return null;
        
    }

}

 

 

그리고 컨트롤러에서는 요청을 받고 요청을 보냈다가 응답을받아 적절한 응답형식으로 감싸서 리턴한다.

이때

@RequestBody,
@RequestParam

 

에 따라 쿼리스트링으로 받을지 JSON으로 받을지 content-type을 구분하고

각각에 대해 전략 패턴으로 구현해주었다.

 

 

package egovframework.covision.groupware.admin.api.web;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import ....Enums.Return;
import ....api.util.ProxyAPIUtils;

@Controller
@RequestMapping("/proxy/api")
public class ProxyAPICon {
	
    private Logger LOGGER = LogManager.getLogger(ProxyAPICon.class);

    /**
     * content-type json인 경우
     * 
     * 필수 파라미터
     * apiURL : 요청 url
     * method : 요청 method
     * 
     * 헤더에 apiToken 포함할 경우
     * apiTokenKey : token key
     * apiTokenValue : token value
     * @param bodyParams body 데이터 요청 처리
     * @return api 응답 리턴
     * @throws Exception
     */
    @RequestMapping(value = "/v1/type/json.do", method = {RequestMethod.POST})
    @ResponseBody
	public ResponseEntity<String> getOtherApiForJson(
			@RequestBody(required = false) Map<String, String> bodyParams) throws Exception {
    	LOGGER.info("request JSON: " + bodyParams.toString());
        return connProcess(ProxyAPIUtils::mapToJson, "application/json", bodyParams);
    }

    /**
     * content-type default인 경우
     * 
     * 필수 파라미터
     * apiURL : 요청 url
     * method : 요청 method
     * 
     * 헤더에 apiToken 포함할 경우
     * apiTokenKey : token key
     * apiTokenValue : token value
     * @param queryParams 쿼리스트링 요청 처리
     * @return api 응답 리턴
     * @throws Exception
     */
    @RequestMapping(value = "/v1/type/urlencoded.do", method = {RequestMethod.POST})
    @ResponseBody
	public ResponseEntity<String> getOtherApiForForm(
			@RequestParam(required = false) Map<String, String> queryParams) throws Exception {
    	LOGGER.info("request Form: " + queryParams.toString());
        return connProcess(ProxyAPIUtils::mapToQuery, "application/x-www-form-urlencoded", queryParams);
    }

    /**
     * server to server api 요청 처리 공통 함수
     * @param function content-type에 따른 전략패턴 적용
     * @param contentType
     * @param queryParams
     * @param bodyParams
     * @return api 요청후 받은 응답 리턴
     */
    private ResponseEntity connProcess(
    		Function<Map, String> function, String contentType, Map<String, String> params) {

    	Map<String, String> returnMap = new HashMap<>();
        Map<String, String> filterParams = ProxyAPIUtils.convertNotNullParams(params);
        String apiURL = Optional.ofNullable(filterParams.remove("apiURL")).orElse("");
        String method = Optional.ofNullable(filterParams.remove("method")).orElse("").toUpperCase();
        String apiTokenKey = null;
        String apiTokenValue = null;
        boolean tokenFlag = false;
        int responseCode = HttpStatus.INTERNAL_SERVER_ERROR.value();
        
        //apiKey를 헤더로 포함시킬 경우
        if (filterParams.containsKey("apiTokenKey") && filterParams.containsKey("apiTokenValue")) {
            apiTokenKey = Optional.ofNullable(filterParams.remove("apiTokenKey")).orElse("");
            apiTokenValue = Optional.ofNullable(filterParams.remove("apiTokenValue")).orElse("");
            tokenFlag = true;
        }

        try {
            String requestData = function.apply(filterParams);
        	
        	URL url = new URL(apiURL);
        	HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        	
        	conn.setRequestMethod(method);
        	conn.setRequestProperty("Content-Type", contentType);
        	conn.setRequestProperty("Accept", "application/json");
        	conn.setReadTimeout(3000);

            if (tokenFlag) {
            	conn.setRequestProperty(apiTokenKey, apiTokenValue);
            }
            if (!method.equals("GET")) {
            	conn.setDoOutput(true);
            	try(OutputStream os = conn.getOutputStream()) {
            		byte[] input = requestData.getBytes("utf-8");
            		os.write(input, 0, input.length);			
            	}            	
            }
        	
        	responseCode = conn.getResponseCode();
        	
        	try(BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))) {
        		StringBuilder response = new StringBuilder();
        		String responseLine = null;
        		while ((responseLine = br.readLine()) != null) {
        			response.append(responseLine.trim());
        		}
        		returnMap.put("status", Return.SUCCESS.toString());
        		returnMap.put("message", "성공");
        		returnMap.put("data", response.toString());
        	} finally {
        		conn.disconnect();
        	}
        } catch (Exception e) {
        	returnMap.put("status", Return.FAIL.toString());
    		returnMap.put("message", e.getMessage());
        }
        return ResponseEntity.status(responseCode).body(ProxyAPIUtils.mapToJson(returnMap));
    }
}

 

 

아직 더 다듬을 여지가 보이긴 하지만, 

일단은 타서비스에 우회해서 현재 브라우저에서 요청을 보낼수 있고 그 응답을 우리 서버로 받아와

적절히 가공만 해주면 사용할수 있게 되었다.

 

즉 화면에서 타서비스로 요청보내는것이 가능하므로 화면 개발만 하면되는 셈이다.

서비스의 개발이 없으니 당연 서버 재기동역시 필요없이 화면에 대한 리소스만 교체해주면 바로 운영중인 서버에서도

반영이 가능할것이다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
글 보관함