프로젝트 중 Naver CLOVA OCR API를 불러오면서 API를 호출하는 방법에 대해 공부하게 되었고 제공되는 예제를 통해 그 구조를 뜯어보고자한다.
public class OCRGeneralAPIDemo {
public static void main(String[] args) {
String apiURL = "YOUR_API_URL";
String secretKey = "YOUR_SECRET_KEY";
String imageFile = "YOUR_IMAGE_FILE";
try {
URL url = new URL(apiURL);
HttpURLConnection con = (HttpURLConnection)url.openConnection();
con.setUseCaches(false);
con.setDoInput(true);
con.setDoOutput(true);
con.setReadTimeout(30000);
con.setRequestMethod("POST");
URL(String)
URL(String protocol, String host, int port, String file)
네트워크 연결시 사용되는 클래스로 리소스의 위치를 파악해 url 객체를 생성한뒤 해당 주소를 가리키는 url 객체가 된다.
URL 클래스를 이용하여 연결된 상대편으로부터 데이터를 읽을때는 그 전에 먼저 openStream() 메서드를 이용해 입력 스트림을 열어야한다.
HTTP 요청, 응답 처리하는 연결을 설정
url.openConnection()은 HttpURLConnection 혹은 FtpURLConnection을 반환하는데 프로토콜이 http://인 경우 반환된 객체를 HttpURLConnection 객체로 캐스팅 할 수 있다.
openConnection() 메서드는 실제 네트워크 연결을 설정하지 않고, URLConnection 클래스의 인스턴스를 반환한다. 네트워크 연결은 connect() 메서드를 호출하거나 헤더 필드를 읽거나 입력스트림/출력스트림을 가져올때 암시적으로 이루어진다.(I/O 오류 발생 시 IOException을 일으킨다)
HTTP 연결 속성을 설정하는 메서드다.
String boundary = "--" + UUID.randomUUID().toString().replaceAll("-", "");
con.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
con.setRequestProperty("X-OCR-SECRET", secretKey);
요청 헤더에 전송할 데이터 형식을 나타낸다.
헤더에 정보를 넣고 싶은 경우 사용하는 메서드로 key: value 형태로 전달한다.
Multipart 데이터의 경우 --고유스트링(uuid)
형식으로 데이터를 구분한다 네트워크는 출력스트림에 쌓인 데이터를 out.flush를 통해서 계속해서 받고 --고유스트링(uuid)--
라는 데이터 구분자가 들어올 경우 최종 데이터의 끝임을 인식하여 네트워크를 서버로 전송하게 된다.
JSONObject json = new JSONObject();
json.put("version", "V2");
json.put("requestId", UUID.randomUUID().toString());
json.put("timestamp", System.currentTimeMillis());
JSONObject image = new JSONObject();
image.put("format", "jpg");
image.put("name", "demo");
JSONArray images = new JSONArray();
images.put(image);
json.put("images", images);
String postParams = json.toString();
Http request body에는 현재 요청 헤더에서 ContentType:Multipart/form-data로 설정되었기때문에 각 데이터가 boundary로 구분된 형태로 전달이 되고 json데이터가 추가될 수 있다.
최종 JSON 구조는
{
"version": "V2",
"requestId": uuid,
"timestamp": time,
"images":[{
"format": "jpg",
"name": "demo",
}
]
}
형태가 된다. & 만들어진 JSON은 Http body에 넣기위해는 문자열 형태여야하기 때문에 String형태로 변환되어 전달된다.
con.connect();
DataOutputStream wr = new DataOutputStream(con.getOutputStream());
long start = System.currentTimeMillis();
File file = new File(imageFile);
writeMultiPart(wr, postParams, file, boundary);
wr.close();
client와 server간 연결이 생성되어 HTTP 요청을 주고받을 준비가 된다.
con.getoutputStream()으로 HTTP 요청의 body에 데이터를 전송할때 사용하는 출력 스트림을 반환받는다.
이후, DataOutputStream과 같은 보조 스트림(Wrapper stream)을 통해 데이터를 더 쉽게 다룰수있는 클래스를 사용하여 데이터를 처리한다.(wr.write()와 같은 메서드 사용 가능)
imageFile에 대한 파일 객체를 얻은 뒤 파라미터로 전달하게 된다.
File 객체를 생성할 경우 해당 객체를 통해 파일을 읽고, 쓰고, 위치를 파악할수있게 된다.
private static void writeMultiPart(OutputStream out, String jsonMessage, File file, String boundary) throws
IOException {
StringBuilder sb = new StringBuilder();
sb.append("--").append(boundary).append("\r\n");
sb.append("Content-Disposition:form-data; name=\"message\"\r\n\r\n");
sb.append(jsonMessage);
sb.append("\r\n");
out.write(sb.toString().getBytes("UTF-8"));
out.flush();
본격적으로 서버에 보낼 데이터를 생성하기 위해 데이터가 쓰여지는 메서드다
중요!!
StringBuilder의 경우 가변 객체로 문자열을 계속 추가하거나 수정할 수 있다. StringBuilder는 내부에서 버퍼를 계속 사용해서 문자열을 추가하는 작업을 효율적으로 처리하기때문에 불변객체 String에 비해 성능면에서 우수하다.
Multipart/form-data의 경우 위에서 언급했다시피 --boundary를 통해 데이터를 시작하고 구분하게 되는데 데이터 시작을 알리기위해 --boundary 가 가장 먼저 append된다.
이 부분에서 줄 바꿈 + 한줄 띄우기가 만들어진걸 볼 수 있는데 이것은 헤더와 바디를 구분하기위해서 한줄을 띄우는것이다. 이후, 위에서 생성한 jsonMessage가 body로 들어가는것을 볼 수 있다.
Http 헤더와 body가 작성된뒤, 문자열로 변환된뒤, byte형태로 변환되어 출력스트림에 전달된다.
out.flush를 통해 출력스트림에서 네트워크에 들어가게된다.
out은 이미 DataOutputStream 이기때문에 writeBytes를 통해 한번에 처리할수있지만 StringBuilder를 하나의 문자열로 합쳐서 한번에 bytes로 변환하는것이 네트워크 전송 효율을 높일 수 있는 방법이다!
if (file != null && file.isFile()) {
out.write(("--" + boundary + "\r\n").getBytes("UTF-8"));
StringBuilder fileString = new StringBuilder();
fileString
.append("Content-Disposition:form-data; name=\"file\"; filename=");
fileString.append("\"" + file.getName() + "\"\r\n");
fileString.append("Content-Type: application/pdf\r\n\r\n");
out.write(fileString.toString().getBytes("UTF-8"));
out.flush();
json 데이터를 전송한뒤, file이 있다면 file도 byte단위로 변환해서 request body에 추가하게 된다.
여기서 중요한 부분은 http 요청 body에 들어갈 json에서 파일 데이터로 변환이 되었기때문에 boundary가 필요하다!
boundary로 파일 데이터의 시작임을 알린다. http 요청 헤더에 boundary="????"라고 선언을 했기때문에 boundary 데이터가 달라지면 http 요청중 에러가 발생한다. 이후, fileString이라는 StringBuilder를 통해 추가할 데이터를 쌓을 공간을 마련한다.
fileString.append("Content-Disposition:form-data; name=\"file\"; filename=");
fileString.append("\"" + file.getName() + "\"\r\n");
fileString.append("Content-Type: application/pdf\r\n\r\n");
Content-Disposition:form-data
에서 파일이나 폼 데이터를 어덯게 전송할지 지정하는 역할을 한다. 파일 전송 시 파일이름을 명시해 서버가 어떤 파일이 전송되었는지 알수있게 한다.
Content-Type: application/pdf
다음 mulipart data로 어떤 형식의 파일이 올지 명시한다.
헤더나 바디가 완성될때마다 네트워크로 데이터를 전송해준다.
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[8192];
int count;
while ((count = fis.read(buffer)) != -1) {
out.write(buffer, 0, count);
}
out.write("\r\n".getBytes());
}
out.write(("--" + boundary + "--\r\n").getBytes("UTF-8")); // 최종 boundary 설정
}
out.flush();
}
파일이 존재할 경우 전달받은 File 객체를 통해 FileInputStream을 사용하여 데이터를 전달받을 통로를 마련한다.
원래 파일의 경우 한바이트씩 읽고 작성하는 형식으로 읽어야하지만 버퍼가 있다면 여러바이트를 한번에 읽고 한번에 작성하는게 가능해져 효율적이다.
fis.read(buffer)는 읽어들인 바이틀 수를 반환하게 된다.
FileInputStream.read를 통해 file 객체를 읽어들일 경우 파일의 끝에 도달하면 -1을 반환한다.
이것은 파일을 다 읽었음을 의미한다.
fis.read(buffer)를 통해 fis를 통해 파일을 읽어서 파라미터로 전달된 buffer로 저장한다. 이경우, 한 바이트씩 읽어 buffer에 저장되는것이 아니라 buffer의 크기만큼 한번에 file을 읽게 된다.
이후 buffer의 0부터 count까지 데이터를 출력스트림에 전달하게 된다.
while ((count = fis.read(buffer)) != -1) {
out.write(buffer, 0, count);
}
최대 버퍼가 8192로 설정됏기때문에 처음에 8192를 읽고 계속 8192만큼 읽다가 8192만큼 남지 않았을때 나머지 데이터를 읽고 다시 while문을 도는데 그때 fis.read(buffer)는 -1을 반환하며 파일의 끝까지 읽었다는 표시를 한다
이후, \r\n을 통해 개행을 하고 --boundary--를 통해 네트워크에 데이터가 끝났음을 알린다.
최종 http 요청은
--boundary
HTTP 헤더 (Content-Disposition 등)
HTTP 바디 (JSON 데이터 등)
--boundary
Multipart 헤더 (Content-Disposition 등)
Multipart 바디 (파일 데이터 등)
--boundary
Multipart 헤더 (Content-Disposition 등)
Multipart 바디 (다른 파일 데이터나 폼 필드 등)
--boundary--
이런 형식이 된다.. 어렵다..
응답은 (2)편에 계속..