본문 바로가기
Java

[Java] HTTP API 통신

by Alohawaii 2022. 2. 24.

최근 프로젝트를 진행하면서 외부 API를 호출해서 사용해야하는 일이 생겨 해당 내용을 정리한다.

1. HttpURLConnection 클래스를 사용

HttpURLConnection
 :HttpURLConnection 클래스는 HTTP 프로토콜 통신을 위한 클래스이다.

URL 클래스
 :URL 클래스는 자원을 요청할 주소를 나타내는 클래스
 
 사용 순서
 1) URL 객체 생성하여 연결하고자 하는 외부 api의 url 저장 
 2) HttpURLConnection 객체 생성
 3) request method, requestProperty 추가 설정
 4) 응답값 및 응답데이터 얻기
 
*POST 요청시 데이터 넘기기
 post 요청을 할때는 outputstream 객체로 데이터를 전송함. setDoOutput() 메소드를 사용하여 outputstream 객체로 전송할 데이터가 있다는 옵션을 설정해야 함.
 
 전송할 데이터가 문자열인 경우 outputstream 클래스를 확장하는 dataoutputstream 클래스의 writebytes() 메소드를 사용하여 데이터를 설정할 수 있음.
 dataoutputstream클래스는 생성자에 outputstream 객체를 전달하여 생성할 수 있음.

*응답데이터 얻기
 getInputStream() 메소드를 통해 응답 데이터를 읽을 수 있는 InputStream 객체를 얻을 수 있음.
 응답을 문자열 타입으로 얻기 위해 BufferedReader 객체를 사용할 수 있음. 

 

문제점
 응답코드가 4xx 또는 5xx면 IOException이 발생한다.
 타임아웃을 설정할 수 없다.
 쿠키 제어가 불가능하다.

 

// GET으로 쿠키값 담아 요청해서 응답값로 JSON값 가져오기 (HttpHRLConnection 사용)
    public static void httpConnectionGet() throws MalformedURLException {
        try {
            StringBuilder urlBuilder = new StringBuilder("요청할 url");
            urlBuilder.append("?" + URLEncoder.encode("key", "value"));

            URL url = new URL(urlBuilder.toString());
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();

            conn.setRequestMethod("GET");
            conn.setRequestProperty("Content-type", "application/json");
            conn.setRequestProperty("Accept", "application/json");
            conn.setRequestProperty("Cookie", "쿠키값");
            
            BufferedReader rd;
            if(conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
                rd = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
            } else {
                rd = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "UTF-8"));
            }

            StringBuilder sb = new StringBuilder();
            String line;

            while ((line = rd.readLine()) != null) {
                sb.append(line);
            } 
            
            rd.close();
            conn.disconnect();

            ObjectMapper mapper = new ObjectMapper();
            Map<String, String> result = mapper.readValue(rd.toString(), Map.class);
            
        } catch(Exception e) {
            
        }
    }

    // POST로 요청시 요청Body에 JSON객체 담아 요청하여 응답값으로 JSON객체 가져오기 (HttpURLConnection 사용)
    public static void httpConnectionPost() throws MalformedURLException {
        URL url = new URL("요청할 url");

        try {
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();

            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-type", "application/json");
            conn.setRequestProperty("Accept", "application/json");
            // json값을 outputStream으로 전달하기 위한 설정
            conn.setDoOutput(true);

            JSONObject jsonObject = new JSONObject();
            jsonObject.put("key", "1");
            jsonObject.put("content", "content1");
            jsonObject.put("name", "name");

            String jsonInputString = jsonObject.toString();

            OutputStream os = conn.getOutputStream();
            byte[] input = jsonInputString.getBytes("utf-8");
            os.write(input, 0, input.length);

            BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String inputLine;
            StringBuffer response = new StringBuffer();

            while ((inputLine = in.readLine()) != null) {
                response.append(inputLine);
            }

            ObjectMapper mapper = new ObjectMapper();
            Map<String, String> result = mapper.readValue(String.valueOf(response), Map.class);
            List<String> cookies = conn.getHeaderFields().get("Set-Cookie");

            if(cookies != null && cookies.size() > 0) {
                result.put("cookie", cookies.get(0));
            }

            in.close();
            conn.disconnect();

        } catch(Exception e) {
            e.printStackTrace();
        }
    }



 
 
2. RestTemplate를 사용하여 연결

RestTemplate란?
 spring 3.0부터 지원하고, 스프링에서 제공하는 http 통신에 유용하게 쓸수 있는 템플릿이다. HTTP 서버와의 통신을 단순화하고 restful원칙을 지킨다. 반복적인 코드들을 줄여주고 코드도 매우 간단하다.

RestTemplate의 동작원리
 1. 어플리케이션이 RestTemplate를 생성하고, URL, HTTP 메소드 등의 헤더를 담아 요청한다.
 2. RestTemplate는 HttpMessageConverter를 사용하여 requestEntity를 요청메시지로 변환한다.
 3. RestTemplate는 ClientHttpRequestFactory로부터 ClientHttpRequest를 가져와서 요청을 보낸다.
 4. ClientHttpRequest는 요청메시지를 만들어 HTTP 프로토콜을 통해 서버와 통신한다.
 5. RestTemplate는 ResponseErrorHandler로 오류를 확인하고 있다면 처리로직을 태운다.
 6. ResponseErrorHandler는 오류가 있다면 ClientHttpResponse에서 응답데이터를 가져와서 처리한다.
 7. RestTemplate는 HttpMessageConverter를 이용해서 응답메시지를 java object(Class responseType)로 변환한다.
 8. 어플리케이션에 반환된다.
 
*connection pool의 적용
 resttemplate는 기본적으로 connection pool를 사용하지 않고 연결할때마다 로컬 포트를 열고 tcp connection을 맺는다. 문제는 close() 된 이후 사용된 소켓은 time_wait상태가 되는데 요청량이 많다면 이런 소켓들을 재사용하지 못하고 소켓이 오링나서 응답이 지연될 것이다.
 이런 경우 connection pool을 사용해서 해결할수있는데, dbcp처럼 소켓의 개수를 정해서 재사용하는것이다.
 
 RestTemplate에서 connection pool을 적용하려면, 위와 같이 HttpClient를 만들고 setHttpClient()를 해야한다.
   - setMaxConnPerRoute : IP,포트 1쌍에 대해 수행할 연결수를 제한한다.
   - setMaxConnTotal : 최대 오픈되는 커넥션 수를 제한한다.

    // GET으로 쿠키값 담아 요청해서 응답값로 JSON값 가져오기 (RestTemplate 사용)
    public static void httpConnectionRestTemplateGet() throws URISyntaxException {
        String url = "요청할 url";

        URI uri = new URIBuilder(url)
                .addParameter("parameter1", "value1")
                .addParameter("parameter2", "value2")
                .build();

        try {

            // 읽기시간, 연결시간 설정
            HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
            factory.setConnectTimeout(1000);
            factory.setReadTimeout(1000);

            // connection Pool 설정
            HttpClient httpClient = HttpClientBuilder.create()
                    .setMaxConnTotal(100)
                    .setMaxConnPerRoute(5)
                    .build();

            factory.setHttpClient(httpClient);

            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_JSON);
            // 쿠키값 set
            httpHeaders.add("Cookie", "value");

            HttpEntity<String> request = new HttpEntity<String>(httpHeaders);

            RestTemplate restTemplate = new RestTemplate(factory);
            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, request, String.class);

            HttpStatus httpStatus = response.getStatusCode();
            HttpHeaders responseHttpHeaders = response.getHeaders();
            String result = response.getBody();

        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    // POST로 요청시 요청Body에 JSON객체 담아 요청하여 응답값으로 JSON객체 가져오기 (RestTemplate 사용)
    public static void httpConnectionRestTemplatePost() {
        final String url = "요청할 url";

        try {
            // 읽기시간, 연결시간 설정
            HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
            factory.setConnectTimeout(1000);
            factory.setReadTimeout(1000);

            // connection Pool 설정
            HttpClient httpClient = HttpClientBuilder.create()
                    .setMaxConnTotal(100)
                    .setMaxConnPerRoute(5)
                    .build();

            factory.setHttpClient(httpClient);

            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_JSON);

            JSONObject jsonObject = new JSONObject();
            jsonObject.put("key", "1");
            jsonObject.put("content", "content1");
            jsonObject.put("name", "name");

            HttpEntity<String> request = new HttpEntity<String>(jsonObject.toString(), httpHeaders);

            RestTemplate restTemplate = new RestTemplate(factory);
            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, request, String.class);

            HttpStatus httpStatus = response.getStatusCode();

            // 서버로부터 전달받은 쿠키에 담긴 세션값 가져오기
            HttpHeaders headers = response.getHeaders();
            String cookies = headers.getFirst("Set-Cookie");
            String result = response.getBody();

        } catch(Exception e) {
            e.printStackTrace();
        }
    }


----

HttpURLConnection를 이용한 쿠키(Cookie), 세션(Session) 사용
프로젝트 진행중 외부 API 통신 과정에서 로그인 세션 정보를 유지해야 하는 일이 생겨 해당 내용을 정리한다.
로그인 시 세션값을 반환 받아, 정보 조회 시 요청할때 외부 api로 서버로부터 전달받은 세션아이디를 쿠키값에 담아 요청하는 경우이다.

보통은 서버에서 세션을 사용하고, 클라이언트에서 쿠키를 사용하는 경우에는 서버에서는 세션을 사용하고 클라이언트에서는 쿠키를 사용하도록 설정한다. 만약, 클라이언트에서도 쿠키를 사용한다면, 클라이언트가 두 번 이상 요청을 할때는 서버로부터 전달받은 세션 아이디를 쿠키에 담아서 전송하게 된다.

일반적인 서버에서 세션 사용, 클라이언트에서 쿠키를 사용하는 경우
1. 클라이언트의 첫번째 요청
 1) 클라이언트가 http request message를 서버에 요청
 2) 서버가 요청에 대한 session id가 없으면 새로 생성, 있으면 그대로 사용
 3) 서버가 (session id를 담은) http response message를 클라이언트에 응답

2. 클라이언트의 두번째 요청
 1) 클라이언트가(session id를 담은) http request message를 서버에 요청
 2) 서버가 요청에 대한 session id가 없으면 새로 생성, 있으면 그대로 사용 
 3) 서버가 (session id를 담은) http response message를 클라이언트에 응답
 
예)
String cookie = "쿠키값";
// connection 연결
HttpURLConnection conn = url.openConnection();

// cookie 값 set
conn.setRequestProperty("Cookie", cookie); 
// cookie 값 get
Map<String, List<String>> header = conn.getHeaderFields();
List<String> cookie = header.get("Set-Cookie");

---

ObjectMapper
 : json -> java object 또는 java object -> json 으로 변환할 때 사용하는 jackson 라이브러리의 클래스

 

프로젝트 중 HttpURLConnection 과정에서 json객체를 요청하고, 응답받아 Map 또는 도메인으로 파싱하는 경우에 사용하였다.
 
1. String -> DTO로 매핑
 Account account = mapper.readValue(jsonStr, Account.class);

2. 스트링에서 DTO List로 매핑
 List<Account> accounts = Arrays.asList(mapper.readValue(jsonStr, Account[].class); 

3. 객체를 json 스트링으로 변환
 String jsonStr = mapper.writeValueAsString(accounts);

* Json를 객체에 파싱할때 객체의 프로퍼티 정보를 알기 위해서는 객체 생성을 위한 기본 생성자(접근 제한자 무관)가 반드시 존재해야 하고, getter/setter/field 중에 접근 제한자가 위 조건을 만족하는 것이 하나라도 있어야 한다.

Getter/Setter 없이 Property를 읽도록 하는 방법

1. @JsonProperty
 필드에 해당 어노테이션만 추가해주어도 프로퍼티를 읽을 수 있다.
 다만, 모든 필드에 추가하기에는 부담이 있다.

2. @JsonAutoDetect
 클래스에 어노테이션을 추가하는 방법
 클래스 단위로 Visibility를 설정할 수 있음

 

*나의 경우에는 Entity내에 Entity 파싱시 컬럼을 찾지 못해서 unrecognized property exception이 발생하였고 Entity내의 Entity의 컬럼에 @JsonProperty 어노테이션을 붙여 해결하였다. 
*HTTPURLConnection 사용시 json 반환값으로 한글데이터가 포함되어 있었는데 한글이 깨져오는 바람에 ObjectMapper의 readValue시 JsonParseException: Unexpected character ('a' (code 97)) 관련에러가 발생하였고 InputStream시 인코딩타입을 UTF-8로 지정해주어 해결하였다.

 

---

 

[참고]
(RestTemplate HttpHeaders 패키지) https://stackoverflow.com/questions/50951583/how-to-resolve-httpheaders-has-private-access-in-org-apache-http-httpheaders

(HttpClient 사용시)
https://jekalmin.tistory.com/entry/Java-Http-%EC%9A%94%EC%B2%AD-%EB%B3%B4%EB%82%B4%EA%B8%B0-HttpClient

([Java]Java로 HTTP GET, POST 통신하기)
https://limdevbasic.tistory.com/14

(JAVA JSON POST 보내기)
https://velog.io/@noah_ark/JAVA-JSON-POST-%EB%B3%B4%EB%82%B4%EA%B8%B0

(HttpURLConnection를 이용한 쿠키(Cookie), 세션(Session) 사용
https://caileb.tistory.com/146

(HttpUrlConnection을 통한 서버와 세션 유지)
https://simsimjae.tistory.com/283

(json 형식의 string을 자바 객체(DTO)로 맵핑하기..)
https://suyou.tistory.com/250

(ObjectMapper는 Property를 어떻게 찾을까 ?)
https://bactoria.github.io/2019/08/16/ObjectMapper%EB%8A%94-Property%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%B0%BE%EC%9D%84%EA%B9%8C/

(RestTemplate (정의, 특징, URLConnection, HttpClient, 동작원리, 사용법, connection pool 적용)
https://sjh836.tistory.com/141

(Spring RestTemplate을 사용하여 쿠키 검색)
https://stackoverflow.com/questions/44493162/retrieve-a-cookie-using-spring-resttemplate

 

 

'Java' 카테고리의 다른 글

[Java] zip4j 라이브러리 사용하여 압축  (0) 2022.02.27
[Java] 람다식  (0) 2022.02.27