Overview
자바에서 HTTP 연결을 할 때, 일반적으로 HttpURLConnection을 사용한다. python의 경우 .request method에 여러 가지가 override되어 있어 HTTP 연결 방식(GET, POST, ...), parameter, 파일 입력 여부 등등을 함수의 인자로 넣으면 알아서 처리를 해 준다. 그러나 자바는 그런 게 없다. 따라서 연결 방식, parameter, 파일 입력 등을 직접 header와 body에 작성해 주어야 하고, 그 결과도 response code에 따라 error stream인지, 정상적인 stream인지 판단을 해야 한다.
따라서 해당 글에서는 Java에서 HttpURLConnection을 사용하는 방법을 설명하고자 한다.
HttpConnectionVO.java
backend에서 필요한 http 통신 결과는 http status와 http response body일 것이다. 이 외에도 cookie나, 그런 정보들이 필요할 수도 있다. 그렇지만 나의 경우(https://github.com/hyelie/Naver2Tistory) 그 정보들은 필요없기 때문에 아래와 같이 간단히 VO를 구성했다.
package utils;
/**
* consists of http status code 'code', http response body 'body'.
*/
public class HttpConnectionVO {
private Integer code;
private String body;
public HttpConnectionVO(Integer code, String body){
this.code = code;
this.body = body;
}
public void setCode(Integer code){
this.code = code;
}
public Integer getCode(){
return code;
}
public void setBody(String body){
this.body = body;
}
public String getBody(){
return body;
}
}
GET/POST method with parameter
https://tistory.github.io/document-tistory-apis/에 따르면 GET이든 POST든 url parameter에 값을 넣는다.
GET https://www.tistory.com/apis/post/read?
access_token={access-token}
&blogName={blog-name}
&postId={post-id}
POST https://www.tistory.com/apis/post/write?
access_token={access-token}
&output={output-type}
&blogName={blog-name}
&title={title}
&content={content}
&visibility={visibility}
&category={category-id}
&published={published}
&slogan={slogan}
&tag={tag}
&acceptComment={acceptComment}
&password={password}
따라서 아래와 같이 request 함수를 구성한다. 자세한 주석은 코드 내에 적어두었다. 중요한 점은 연결이 끝나거나 비정상적으로 종료되었다고 하더라도 connection을 종료시켜줘야 한다는 것이다.
/**
* Connect to parameter 'targetURL' with type 'type', paramter 'parameter'.
*
* @param targetURL - connecting url
* @param type - HTTP type. (GET, POST, ...)
* @param parameter - HTTP connection paramter
* @return HTTPConnectionVO
* @see HttpConnectionVO
* @see HttpConnection#getResponse(HttpURLConnection)
* @throws Exception when connection timeout, or refused, otherwise.
*/
public static HttpConnectionVO request(String targetURL, String type, String parameter) throws Exception {
HttpURLConnection con = null;
try {
URL url = new URL(targetURL); // URL 생성
con = (HttpURLConnection) url.openConnection(); // connection 생성
con.setRequestMethod(type); // http connection type 지정
if (type != "GET") { // GET인 경우 url에 정보가 들어가고, POST인 경우에는 값을 넣어 주어야 함.
con.setDoOutput(true); // connection에 작성 가능하게 설정
con.getOutputStream().write(parameter.getBytes()); // parameter 작성
}
return HttpConnection.getResponse(con);
}
catch (Exception e) {
if (e instanceof SocketTimeoutException) {
throw new Exception("연결 시간 초과입니다.");
} else if (e instanceof ConnectException) {
throw new Exception("연결이 거부되었습니다.");
} else {
throw new Exception("연결 중 알 수 없는 오류가 발생했습니다.");
}
}
finally{
if(con != null) con.disconnect();
}
}
HttpConnection.getResponse(HttpURLConnection con)
request 함수에서 HttpConnection.getResponse(con)은 내가 만든 함수이다. Java의 HttpURLConnection은 http status code에 따라 정상적인 stream으로 값을 받아올지, 아니면 error stream으로 값을 받아올지 결정한다. 그리고 이 부분은 모든 HttpURLConnection을 사용하는 부분에서 중복되기 때문에 따로 함수를 작성했다.
/**
* @return response of HttpURLConnectoin 'con' to HttpConnectionVO.
* @throws Exception to caller while running this function.
*/
private static HttpConnectionVO getResponse(HttpURLConnection con) throws Exception{
// response code에 따라 input stream 지정
Integer responseCode = con.getResponseCode();
InputStream responseStream;
if(responseCode < HttpURLConnection.HTTP_BAD_REQUEST){
responseStream = con.getInputStream();
}
else{
responseStream = con.getErrorStream();
}
// 결과 출력
BufferedReader in = new BufferedReader(new InputStreamReader(responseStream));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
return new HttpConnectionVO(responseCode, response.toString());
}
multipart/form-data로 파일 전송하기
HTTP multipart/form-data의 형식
파일 업로드를 할 때 http에서 정해진 protocol이 있다. 아래 사진을 보자. 출처 : https://lng1982.tistory.com/209
이런 식으로 content type에 boundary를 명시하고, 각 파일들은 그 boundary를 먼저 명시하고, 제일 마지막에 추가로 boundary를 작성하게 되어 있다. 마지막에 작성하는 boundary의 끝에는 추가로 --가 붙어 있는데 이는 http body의 끝을 알리는 문자이기 때문에 추가를 해 주어야 한다. 추가적으로 java의 경우 줄바꿈은 /r/n으로 입력한다.
좀 더 정확하게는 "--" + BOUNDARY + "\r\n"으로 구분한다.
정리하자면 body는 아래와 같은 형식을 가진다.
--BOUNDARY
파일 1 설명
--BOUNDARY
파일 2 설명
...
--BOUNDARY
파일 n 설명
--BOUNDARY--
그렇다면, boundary로 각 파일들을 구분하기 때문에 파일 내에 BOUNDARY에 해당하는 문자가 나온다면 거기서부터 다른 파일로 인식할 것이다. 따라서 boundary는 파일 내에 존재하지 않을 법한 문자열(UUID 등)으로 작성해야 할 것이다.
이렇게 작성한 소스코드는 아래와 같다.
/**
* Connect to parameter 'targetURL' with type 'type', paramter 'parameter' with file in parameter 'filePath'.
*
* @param targetURL - connecting url
* @param type - HTTP type. (GET, POST, ...)
* @param parameter - HTTP connection paramter
* @param filePath - file path send to targetURL.
* @return HTTPConnectionVO
* @see HttpConnectionVO
* @see HttpConnection#getResponse(HttpURLConnection)
* @throws Exception when connection timeout, or refused, otherwise.
*/
public static HttpConnectionVO request(String targetURL, String type, String parameter, String filePath) throws Exception {
HttpURLConnection con = null;
try {
URL url = new URL(targetURL + "?" + parameter);
con = (HttpURLConnection) url.openConnection();
con.setRequestMethod(type); // connection type 지정
con.setDoInput(true); // connection 입/출력 작성 설정
con.setDoOutput(true);
// input file
String BOUNDARY = "ibubyftcy000999------nitn67"; // BOUNDARY 지정
con.setRequestProperty("Connection", "Keep-Alive"); // header 지정
con.setRequestProperty("Content-Type", "multipart/form-data;boundary="+BOUNDARY);
DataOutputStream dos = new DataOutputStream(con.getOutputStream());
// 파일 입력
dos.writeBytes("--" + BOUNDARY + "\r\n"); // BOUNDARY로 감쌈
dos.writeBytes("Content-Disposition: form-data;name=\"uploadedfile\";filename=\"1.jpg\"\r\n");
dos.writeBytes("\r\n");
// 일반적인 경우 이 부분에 파일이 어떤 타입인지를 명시하는 Content-Type을 작성한다.
// 나의 경우 image만 올릴 수 있기 때문에 명시하지 않아도 된다.
dos.write(Utils.fileToBytes(filePath));
dos.writeBytes("\r\n");
dos.writeBytes("--" + BOUNDARY + "--\r\n"); // BOUNDARY로 감쌈
dos.flush(); // 전부 stream에 올림
return HttpConnection.getResponse(con);
}
catch (Exception e) {
System.out.println("오류오류 ");
if (e instanceof SocketTimeoutException) {
throw new Exception("연결 시간 초과입니다.");
} else if (e instanceof ConnectException) {
throw new Exception("연결이 거부되었습니다.");
} else {
throw new Exception("연결 중 알 수 없는 오류가 발생했습니다.");
}
}
finally{
if(con != null) con.disconnect();
}
}
요약
- header에 Content-Type=multipart/form-data; boundary=BOUNDARY를 작성한다.
- 각 파일들에 대해 다음과 같이 작성한다.
- "--" + BOUNDARY + "\r\n"
- Content-Type + "\r\n"
- 파일 내용을 작성한다. 이미지의 경우 bytes[]로 변환되어 있어야 한다.
- "\r\n"
- 모든 파일을 다 작성했다면 "--" + BOUNDARY + "--" + "\r\n"을 작성한다.
'Development > Java' 카테고리의 다른 글
[Java] Polymorphism - static polymorphism, dynamic polymorphism, casting (0) | 2023.02.23 |
---|---|
[Java] Inheritance - access modifier, super, overriding (0) | 2023.02.23 |
[Java] static, final (0) | 2023.02.23 |
[Java] Class - constructor, access modifier, identity & equality & hashCode (2) | 2023.02.22 |
[Java] Data types, 예외적인 String, 그리고 call by value (0) | 2023.02.21 |