모듈(자바 어플리케이션)을 여러개를 사용할 때 모듈간의 통신을 redis 등을 통하여 다양한 방법으로 통신할 수 있지만 URL 로 통신할 수도 있다.
이때, URLConnection
및 HttpURLConnection
클래스를 사용한다. 또한 단순 모듈간의 통신 뿐ㅁ나 아니라 예를들어 파일, 웹 페이지를 업로드 및 다운로드, HTTP 요청 및 응답 전송 및 검색 등을 위한 코드를 작성할 수 있다.
그럼 이제 URL을 만드는 순서를 알아보자.
다음과 같이 주어진 URL 주소에 대해 새 URL 객체를 생성한다.
URL url = new URL("http://www.google.com");
이 생성자는 URL 형식이 잘못된 경우 MalformedURLException
을 throw한다.
참고로 이 예외는 IOException
의 하위 클래스다.
필자는 application.yml
에 값을 지정해서 @Value
로 불러오거나 DB 에 저장해놓고 가져오기도 한다.
URLConnection
인스턴스는 URL 객체의 openConnection()
메소드 호출에 의하여 얻어진다.
URLConnection conn = url.openConnection();
프로토콜이 http://
인 경우 반환된 객체를 HttpURLConnection
객체 로 캐스팅할 수 있다.
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
openConnection() 메서드는 실제 네트워크 연결을 설정하지 않고, 단지 URLConnection 클래스의 인스턴스를 반환한다. 네트워크 연결은 connect() 메서드가 호출 될 때 명시적으로 이루어지거나, 헤더 필드를 읽거나 입력스트림/출력스트림을 가져올 때 암시적으로 이루어진다. URL의 openConnection() 메서드는 I/O 오류가 발생하면 IOException을 발생시킨다.
실제로 연결을 설정하기 전에 타임아웃, 캐시, HTTP 요청 방법 등과 같이 클라이언트와 서버 간의 다양한 옵션을 설정할 수 있다.
URLConnection
클래스는 연결을 구성하기위해 굉장히 다양한 메서드를 제공한다.
setConnectTimeout (int timeout)
연결 타임아웃 값을 설정한다(단위 : 밀리초).
java.net.SocketTimeoutException는 연결이 설정되기 전에 제한 시간이 만료되면 발생한다. 시간 초과가 0이면, 무한대 타임아웃(기본값)을 의미한다.
setReadTimeout (int timeout)
읽기 타임아웃 값을 설정한다(단위 : 밀리초). 제한 시간이 만료되고 연결의 입력 스트림에서 읽을 수 있는 데이터가 없으면 SocketTimeoutException이 발생한다. 시간 초과가 0이면, 무한대 타임아웃(기본값)을 의미한다.
setDefaultUseCaches (boolean default)
URLConnection이 기본적으로 캐시를 사용하는지 여부를 설정한다(기본값은 true). 이 메서드는 URLConnection 클래스의 다음 인스턴스에 영향을 준다.
setUseCaches (boolean useCaches)
연결이 캐시를 사용하는지 여부를 설정한다(기본값은 true).
setDoInput (boolean doInput)
URLConnection을 서버에서 콘텐츠를 읽는 데 사용할 수있는지 여부를 설정한다(기본값은 true).
setDoOutput (boolean doOutput)
URLConnection이 서버에 데이터를 보내는 데 사용할 수있는지 여부를 설정한다(기본값은 false).
setIfModifiedSince (long time)
주로 HTTP 프로토콜에 대해 클라이언트가 검색한 콘텐츠의 마지막 수정 시간을 새로 설정한다. 예를 들어, 서버가 지정된 시간 이후에 정적콘텐츠(이미지, HTML 등)가 변경되지 않았으면 콘텐츠를 가져오지 않고 상태 코드 304(수정되지 않음)를 반환한다. 클라이언트는 지정된 시간보다 최근에 수정된 경우 새로운 콘텐츠를 받게 된다.
setAllowUserInteraction (boolean allow)
사용자 상호 작용을 활성화 또는 비활성화한다. 예를 들어 필요한 경우 인증 대화 상자를 표시한다(기본값은 false).
setDefaultAllowUserInteraction (boolean default)
이후의 모든 URLConnection객체에 대한 사용자 상호 작용의 기본값을 설정한다.
setRequestProperty (String key, String value)
key=value 쌍으로 지정된 일반 요청 속성을 설정한다. 키가 있는 속성이 이미 있는 경우 이전 값을 새 값으로 적용한다.
위 메서드들은 연결을 설정하기 전에 호출해야 한다. 일부 메서드는 연결이 이미 설정된 경우
IllegalStateException
을 발생시킨다. 또한 하위 클래스ttpURLConnection
은 HTTP 관련 기능을 사용하여 연결을 구성하기 위한 다음 메서드를 제공한다.
setRequestMethod (String method)
HTTP 메소드 GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE 중 하나인 URL 요청에 대한 메소드를 설정합니다. (기본값은 GET).
setChunkedStreamingMode (int chunkLength)
콘텐츠 길이를 미리 알 수 없는 경우 내부 버퍼링 없이 HTTP 요청 본문을 스트리밍할 수 있다.
setFixedLengthStreamingMode (long contentLength)
콘텐츠 길이를 미리 알고 있는 경우 내부 버퍼링 없이 HTTP 요청 본문을 스트리밍할 수 있다.
setFollowRedirects (boolean follow)
이 정적 메서드는 HTTP 리다이렉션 뒤에 이 클래스의 미래 개체가 자동으로 따라야 하는지 여부를 설정한다(기본값은 true).
setInstanceFollowRedirects (boolean follow)
HTTP 리다이렉션 뒤에 이 HttpURLConnection 클래스의 인스턴스가 자동으로 따라와야 하는지 여부를 설정한다(기본값은 true).
연결이 이루어지면 서버는 URL 요청을 처리하고 메타데이터와 실제 콘텐츠로 구성된 응답을 다시 보낸다. 메타데이터는 헤더 필드라고 하는 키=값 쌍의 모음이다. 헤더 필드는 서버에 대한 정보, 상태 코드, 프로토콜 정보 등을 나타낸다. 실제 내용은 문서의 유형에 따라 텍스트, HTML, 이미지 등이 될 수 있다.
따라서 URLConnection
클래스는 헤더 필드를 읽기 위한 다음과 같은 방법을 제공한다.
getHeaderFields ()
모든 헤더 필드를 포함하는 맵을 반환한다. 키는 필드 이름이고 값은 해당 필드 값을 나타내는 문자열 목록이다.
getHeaderField (int n)
n 번째 헤더 필드의 값을 읽는다.
getHeaderField (String name)
명명된 헤더 필드의 값을 읽는다.
getHeaderFieldKey (int n)
n 번째 헤더 필드의 키를 읽는다.
getHeaderFieldDate (String name, long default)
Date로 구문 분석된 명명된 필드의 값을 읽는다. 필드가 없거나 값 형식이 잘못된 경우 기본값이 대신 반환된다.
getHeaderFieldInt (String name, int default)
정수로 구문 분석된 명명된 필드의 값을 읽는다. 필드가 없거나 값 형식이 잘못된 경우 기본값이 대신 반환된다.
getHeaderFieldLong (String name, long default)
긴 숫자로 구문 분석된 명명된 필드의 값을 읽는다. 필드가 없거나 값 형식이 잘못된 경우 기본값이 대신 반환된다.
위는 헤더 필드를 읽는 일반적인 메서드 이다. 자주 액세스하는 일부 헤더 필드의 경우
URLConnection
클래스는 보다 구체적인 메서드를 제공한다.
getContentEncoding ()
콘텐츠의 인코딩 유형을 나타내는 콘텐츠 인코딩 헤더 필드의 값을 읽는다.
getContentLength ()
콘텐츠의 크기(바이트)를 나타내는 콘텐츠 길이 헤더 필드의 값을 읽는다.
getContentType ()
컨텐츠의 유형을 나타내는 컨텐츠 유형 헤더 필드의 값을 읽는다.
getDate ()
서버의 날짜 시간을 나타내는 날짜 헤더 필드의 값을 읽는다.
getExpiration ()
만료 헤더 필드의 값을 읽고 응답이 오래된 것으로 간주되는 시간을 나타낸다. 이는 캐시 제어를 위한 것이다.
getLastModified ()
컨텐츠의 마지막 수정 시간을 나타내는 last-modified 헤더 필드의 값을 읽는다.
그리고 서브클래스
HttpURLConnection
은 추가 메소드를 제공한다:
connect()
를 호출하지 않고 암시적으로 연결이 설정된다. 실제 내용을 읽으려면 연결에서 InputStream
인스턴스 를 얻은 다음 InputStream
의 read()
메서드를 사용하여 데이터를 읽어야 힌다.
InputStream inputStream = urlCon.getInputStream();
byte[] data = new byte[1024];
inputStream.read(data);
InputStream
의 read()
는 데이터를 바이트의 배열로 읽는 로우-레빌(low-level)의 메서드이다. 문자 데이터를 읽기 위해서는 InputStream
을 InputStreamReader
에서 보다 편하게 조작하기 위해 래핑 한다 :
InputStream inputStream = urlCon.getInputStream();
InputStreamReader reader = new InputStreamReader(inputStream);
int character = reader.read(); // reads a single character
char[] buffer = new char[4096];
reader.read(buffer); // reads to an array of characters
또는 데이터를 문자열로 읽기 위해 InputStream
을 BufferedReader
로 래핑한다.
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String line = reader.readLine(); // reads a line
getInputStream()
메소드는 다음과 같은 예외를 던질 수 있다
서버에 데이터를 보내려면 먼저 연결에서 출력을 활성화해야 한다.
urlCon.setDoOutput(true);
연결과 관련된 OutputStream
객체를 가져온 다음 OutputStream
의 write()
메서드를 사용하여 데이터를 쓴다.
OutputStream outputStream = urlCon.getOutputStream();
byte[] data = new byte[1024];
outputStream.write(data);
OutputStream
의 write()
메서드는 바이트의 배열을 기록하는 낮은 수준의 방법이다. 문자 데이터를 쓰기 위해서는 OutputStream
을 OutputStreamWriter
에서 보다 편하게 조작하기 위해 래핑한다.
OutputStreamWriter writer = new OutputStreamWriter(outputStream);
int character = 'a';
writer.write(character); // writes a single character
char[] buffer = new char[4096];
writer.write(buffer); // writes an array of characters
또는 문자열을 작성하기 위해 PrintWriter
에서 OutputStream
을 래핑한다.
PrintWriter writer = new PrintWriter(outputStream);
String line = "This is String";
writer.print(line);
참고로 getOutputStream()
메소드는 IOException
또는 UnknownServiceException
를 throw 할수있다.
연결을 닫으려면 InputStream
또는 OutputStream
객체 에서 close()
메서드를 호출한다. 이렇게 하면 URLConnection
인스턴스와 연결된 네트워크 리소스가 해제된다. URLConnection
과 HttpURLConnection
의 API를 사용하는 방법이다.
rd.close();
con.disconnect();
public String sendReqPost(String url, Object reqMap) throws IOException {
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("POST");
con.setRequestProperty("Content-type", "application/json");
con.setDoOutput(true);
con.connect();
ObjectMapper om = new ObjectMapper();
String json = om.writeValueAsString(reqMap);
OutputStream outputStream = con.getOutputStream();
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
StringBuilder sb = new StringBuilder();
BufferedReader rd;
if (con.getResponseCode() >= 200 && con.getResponseCode() <= 300) {
rd = new BufferedReader(new InputStreamReader(con.getInputStream()));
} else {
rd = new BufferedReader(new InputStreamReader(con.getErrorStream()));
}
String line;
while ((line = rd.readLine()) != null) {
sb.append(line);
}
rd.close();
con.disconnect();
return sb.toString();
}
그럼 궁금한게 생길 수 있다.
outputStream.flush()
과 conn.getResponseCode()
는 둘 다 서버에 요청하는 코드인데 무슨 차이이고 뭘 써야할까?
conn.getResponseCode()
를 바로 호출하는 코드와, outputStream.flush()
메서드를 사용하는 코드의 차이는 다음과 같다.
flush()
메서드는 outputStream
에 기록된 데이터를 강제로 전송하는 역할을 한다.
즉, 데이터가 버퍼에 남아 있는 경우 이를 실제로 네트워크를 통해 서버로 전송하고 싹 비운다.
conn.getResponseCode()
메서드는 서버로부터의 HTTP 응답 코드를 반환한다.
이 메서드를 호출하면 내부적으로 다음과 같은 일이 발생한다
outputStream.flush()
를 호출하는 코드는 명시적으로 데이터를 전송한다.
즉, 내가 데이터 전송 시점을 명확히 제어할 수 있다.
conn.getResponseCode()
를 바로 호출하는 코드는 암묵적으로 데이터를 전송한다.
즉, 이 메서드가 호출될 때 내부적으로 데이터가 전송된다.
flush()
를 사용하면 코드가 보다 명확해진다.
데이터가 언제 전송되는지 명확히 알 수 있기 때문에 디버깅이나 유지보수 시 유리할 수 있다.
conn.getResponseCode()
를 바로 호출하는 코드는 간결하지만, 데이터 전송 시점을 명확히 알기 어렵다.
flush()
를 사용하면 데이터가 버퍼에 남아 있는 상태에서 추가 작업을 수행할 수 있다.
예를 들어, 데이터 전송 전에 로그를 기록하거나 다른 작업을 수행할 수 있다.
conn.getResponseCode()
를 호출하면 데이터 전송과 응답 수신이 연속적으로 일어나기 때문에 중간 작업을 수행하기 어렵다.
결론
두 방식 모두 올바르게 작동할 수 있지만, flush()를 사용하는 방식은 데이터 전송의 시점을 명확히 제어할 수 있다는 장점이 있다. 반면, conn.getResponseCode()를 바로 호출하는 방식은 코드가 간결해지지만, 데이터 전송 시점을 명확히 알기 어렵다는 단점이 있다. 따라서 상황에 맞게 적절한 방식을 선택하는 것이 중요하다.
// 만약 jsonStr 이 String 타입으로 존재한다고 가정할 때
JsonNode mapping = new ObjectMapper().readTree(jsonStr);
JsonNode resultCode = mapping.get("response").get("header").get("resultCode");
JsonNode resultMsg = mapping.get("response").get("header").get("resultMsg");
try{
mapping.get("response").get("body").get("items").get("item").forEach(jsonNode -> {
logger.info("obsrValue 값 {}", jsonNode.get("obsrValue").asText());
});
} catch(Exception e) {
logger.warn(e);
}