[Java 8] try-with-resource 구조로 Http 데이터 송수신하기

2019. 9. 30. 17:16Java/Basic

반응형

Java에서 url을 호출하여 json 형태의 response를 받는 방법을 알아보자.

  1. Http 통신을 이용하여 데이터를 송수신 하기 위해 HttpURLConnection 을 이용한다.
  2. GET 방식의 경우, 기본 url 뒤에 ? 기호를 붙이고, 키=값 형태로 나열한다.(각 키&값 쌍은 & 기호로 구분한다.)
  3. Accept 헤더를 application/json으로 설정, 그 밖에 필요한 헤더들 함께 설정
  4. HttpURLConnection conn 객체로부터 responseCode를 가져와서 200이 아닐경우 json으로 파싱하는 과정 생략

1. HttpURLConnection 를 이용하여 Http 데이터 송수신하기

private static JSONParser jsonParser = new JSONParser();

public static Map<String, Object> sendGet(String targetUrl, Map<String, String> headers, Map<String, String> params) {
	Map<String, Object> response = new HashMap<>();
	HttpURLConnection conn = null;
	BufferedReader in = null;

	StringBuilder s = new StringBuilder(targetUrl);
	if(params != null && !params.isEmpty()) {
		s.append("?");
		params.forEach((key, value) -> {
			try {
				s.append(key +"=" + URLEncoder.encode(value, "UTF-8") +"&");
			} catch (UnsupportedEncodingException e) {
				logger.error("파라미터({})의 값({}) 인코딩 발생!", key, value);
			}
		});
	}
	try {
		URL url = new URL(s.toString());
		conn = (HttpURLConnection) url.openConnection();
		conn.setUseCaches(false);			
		conn.setRequestMethod("GET");

		headers.put("Accept", "application/json");		// response type
		for(Map.Entry<String, String> header : headers.entrySet()) {
			conn.setRequestProperty(header.getKey(), header.getValue());
		}

		response.put("status", conn.getResponseCode());
		if(conn.getResponseCode() != 200)
			return response;

		in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));			
		String inputLine = null;
		StringBuffer sb = new StringBuffer();
		while ((inputLine = in.readLine()) != null) {
			sb.append(inputLine);
		}
		Object data = sb.toString();
		Object obj = jsonParser.parse(sb.toString());
		if(obj instanceof JSONObject) {
			data = (JSONObject) obj;
		} else if(obj instanceof JSONArray) {
			data = (JSONArray) obj;
		}
		response.put("data", data);
	} catch(ParseException e) {
		response.put("status", "415");
		response.put("reason", "response data parsing 오류(response가 json형식이 아닙니다.)");
	} catch(Exception e) {
		e.printStackTrace();
	} finally {
		try {
			if(in != null) in.close();
		} catch (IOException e) {
			logger.error("BufferReader close 중 에러 발생");
		}
		if(conn != null) conn.disconnect();
	}
	return response;
}

ln 13: 파라미터의 값이 한글일 경우, get방식으로 요청을 보낼시 인코딩이 깨져서 원하는 response를 받지 못할 수 있다. 따라서, value값은 UTF-8로 인코딩해서 보내자
ln 25: response 형태는 json으로 받을 예정이니 Accept 헤더를 "application/json"으로 설정해줍니다.
ln 53~60: try 블럭에서 이용한 BufferedReader, HttpURLConnection 닫아주기


여기서 11번째 줄과 26번째 줄을 비교해보자.
11번째 줄에서는 params 맵을 람다식을 이용하여 key, value 로 분리하여 StringBuilder에 append 했습니다.
26번째 줄에서는 람다식이 아닌 for문으로 conn 인스턴스에 header를 추가했습니다.

26번째 줄을 람다식으로 구성하면 어떻게 될까요?

01

그랬더니 위와 같은 에러가 뜹니다.
Local variable conn defined in an enclosing scope must be final or effectively final

람다식 내에서 람다식 블럭 밖의 변수(인스턴스 포함)에 접근하려 할 때, 그 변수는 반드시 final 화 되어있어야만 합니다.
다시 말하자면, conn 인스턴스에 final을 붙여야 위의 블럭을 오류없이 이용할 수 있다는 것이다.


람다식 내에서 사용할 수 있는 블럭 밖 변수 종류
  1. 불변 변수 (상수로써 이용. 람다식 내에서 이 변수를 update할 수 없습니다.)
  2. 불변 인스턴스 (인스턴스 재정의는 불가능. 하지만 인스턴스의 필드값을 setter 등으로 변경은 가능합니다.)

문제는 여기서 끝나지 않습니다.
우리는 위의 코드를 단순히 final을 붙여서 final HttpURLConnection conn = null; 라고 작성한다 해서 문제를 해결할 수 없습니다.

바로, try 문 내에서 에러가 발생했을 시, finally 블럭에서 conn 인스턴스를 disconnect() 하기 위해 try문 밖에서 conn 객체를 선언하고, finally 블럭에서 그 객체를 닫아줬기 때문입니다.

불변객체는 재정의가 불가능합니다.
다시 말하자면, null로 초기화 한 후 try 블럭에서 conn = (HttpURLConnection) url.openConnection() 로 재설정하는 것이 불가능하다.

그렇다면 방법은 try 블럭 내에서 final HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 라고 conn을 초기화하고, conn의 사용을 마친 후 try 블럭 내에서 conn.disconnect() 하는 방법밖에 없을까??


2. try-with-resource 구조로 Http 데이터 송수신하기

두가지를 모두 해결할 수 있는 구조로 바꿔봅시다.
바로, try/catch/finally 블럭 구조에서 try-with-resource 구조로 바꾸면 됩니다!

참고: Item 9. close 처리해야하는 resource는 try-with-resource를 이용하자

try-with-resource 구조에서는 에러가 발생했을 시 자동으로 닫아줘야할(AutoCloseable) 객체를 try에 정의하고, try 블럭 내에서 그 객체를 사용하는 구조입니다.


1) GET

public static Map<String, Object> sendGet(String targetUrl,
		Map<String, String> headers, Map<String, String> params) throws Exception  {
	Map<String, Object> response = new HashMap<>();

	StringBuilder s = new StringBuilder(targetUrl);
	if(params != null && !params.isEmpty()) {
		s.append("?");
		params.forEach((key, value) -> {
			try {
				s.append(key +"=" + URLEncoder.encode(value, "UTF-8") +"&");
			} catch (UnsupportedEncodingException e) {
				logger.error("파라미터({})의 값({}) 인코딩 발생!", key, value);
			}
		});
	}

	URL url = new URL(s.toString());
	final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
	try(AutoCloseable a = () -> conn.disconnect()) {			
		conn.setUseCaches(false);			
		conn.setRequestMethod("GET");

		headers.put("Accept", "application/json");		// response type
		headers.forEach((key, value) -> {
			conn.setRequestProperty(key, value);
		});

		response.put("status", conn.getResponseCode());
		if(conn.getResponseCode() != 200)
			return response;

		try(final BufferedReader in = new BufferedReader(
				new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
			String inputLine = null;
			StringBuffer sb = new StringBuffer();
			while ((inputLine = in.readLine()) != null) {
				sb.append(inputLine);
			}
			Object obj;
			try {
				obj = jsonParser.parse(sb.toString());
			} catch (ParseException e) {
				response.put("status", "415");
				response.put("reason", "response data parsing 오류(response가 json형식이 아닙니다.)");
				return response;
			}
			Object data = null;
			if(obj instanceof JSONObject) {
				 data = (JSONObject) obj;
			} else if(obj instanceof JSONArray) {
				data = (JSONArray) obj;
			} else {
				data = sb.toString();
			}
			response.put("data", data);
		}
	}
	return response;
}

즉, 위의 코드의 ln 32에 정의한 final BufferedReader는 try 블럭 실행을 완료한 후 자동으로 닫아줍니다.
BufferedReader 클래스는 AutoCloseable 인터페이스를 구현하는 클래스이므로 try안에서 정의할 수 있으며 자동으로 닫는것도 가능합니다.

만일 에러가 발생했을시, 객체를 닫아주는 기능이 필요하지만 AutoCloseable 인터페이스가 구현되어있지 않을 경우에는 try안에 정의할 수 없습니다.
그럴 경우에는 직접 AutoCloseable 인터페이스를 구현하거나, ln 19 처럼 이용하면 됩니다.


2) POST

POST method도 GET method 와 다를게 없다.
차이점이라면, requestBody를 보내야한다는 차이점이 있겠다.
GET 방식에서는 url을 연결하고, 응답값을 읽어오기만 하면 되었다면, (InputStreamReader)
이번에는 reqeustBody를 먼저 보내고(OutputStreamWriter)
그 다음에 응답값을 읽어오면 된다.(InputStreamReader)

그렇다면, try-with-resource 블럭을 하나 더 추가하기만 하면 되겠네요?
코드로 봐봅시다.

public static Map<String, Object> sendPost(String targetUrl, Map<String, String> headers,
		Map<String, String> values) throws Exception  {
	Map<String, Object> response = new HashMap<>();

	URL url = new URL("http://demo.jiniworld.me/users");
	final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
	try(AutoCloseable a = () -> conn.disconnect()) {			
		conn.setRequestMethod("POST");
		conn.setUseCaches(false);			
		conn.setDoInput(true);
		conn.setDoOutput(true);
		conn.setConnectTimeout(100000);
		conn.setReadTimeout(100000);

		headers.put("Accept", "application/json");		// response type
		headers.put("Content-Type", "application/json");	// request type
		headers.forEach((key, value) -> {
			conn.setRequestProperty(key, value);
		});

		try(final OutputStreamWriter osw = new OutputStreamWriter(conn.getOutputStream(), "UTF-8")) {
			JSONObject json = new JSONObject();
			values.forEach((key, value) -> {
				json.put(key, value);
			});
			osw.write(json.toString());
			osw.flush();
		}

		response.put("status", conn.getResponseCode());
		if(conn.getResponseCode() != 200)
			return response;

		try(final BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {

			String inputLine = null;
			StringBuffer sb = new StringBuffer();
			while ((inputLine = in.readLine()) != null) {
				sb.append(inputLine);
			}
			Object obj;
			try {
				obj = jsonParser.parse(sb.toString());
			} catch (ParseException e) {
				response.put("status", "415");
				response.put("reason", "response data parsing 오류(response가 json형식이 아닙니다.)");
				return response;
			}
			Object data = null;
			if(obj instanceof JSONObject) {
				 data = (JSONObject) obj;
			} else if(obj instanceof JSONArray) {
				data = (JSONArray) obj;
			} else {
				data = sb.toString();
			}
			response.put("data", data);
		}
	}
	return response;
}

먼저 url을 연결하고, requestBody를 write한 후 flush 해야 request를 정상적으로 보낼 수 있다.
이때 주의할 점이 하나 있는데,
conn.getResponseCode() 는 응답코드를 가져오는 메서드 이기 때문에, OutputStreamWriter 으로 requestBody를 모두 작성한 후에 읽어들여야 한다는 점이다.
(그렇지 않을 경우, 우리가 ln 21~28 에서 설정한 requestBody값이 서버로 전달이 되지 않습니다.)


++ swagger 를 이용한 GET, POST 테스트 화면

03

02

728x90
반응형