솔루션 엔지니어로서 일을 하다보면 종종 외부 라이브러리의 도움 없이 HttpUrlConnection
을 이용하여 HTTP 통신 테스트를 하는 경우가 자주 있다. 필자는 이를 쌩자바
라고 부르는데, 이번 포스트에서는 쌩자바를 이용한 HTTP 파일 다운로드 📂 하는 방법을 정리해보려고 한다.
기본적인 흐름은 다음과 같이 크게 2가지로 나눌 수 있다.
HttpUrlConnection
연결Stream
을 통해 다운로드HttpUrlConnection
연결import java.net.HttpURLConnection;
import java.net.URL;
public class FileDownloadTest {
public static void main(String[] args) {
String spec = "https://file-examples-com.github.io/uploads/2017/02/file-sample_100kB.doc";
String outputDir = "D:/sample/output/download";
try{
URL url = new URL(spec);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
int responseCode = conn.getResponseCode();
System.out.println("responseCode " + responseCode);
} catch (Exception e){
System.out.println("An error occurred while trying to download a file.");
e.printStackTrace();
}
}
console : responseCode 200
통신에 성공했으면 남은 작업은 Stream 을 통해 파일을 가져오자.
Stream
을 통해 다운로드Content-Disposition
or URL
에서 파일명 가져오기InputStream
➡️ FileOutputStream
을 통해 파일 다운로드import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class FileDownloadTest {
public static void main(String[] args) {
String spec = "https://file-examples-com.github.io/uploads/2017/02/file-sample_100kB.doc";
String outputDir = "D:/sample/output/download";
InputStream is = null;
FileOutputStream os = null;
try{
URL url = new URL(spec);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
int responseCode = conn.getResponseCode();
System.out.println("responseCode " + responseCode);
// Status 가 200 일 때
if (responseCode == HttpURLConnection.HTTP_OK) {
String fileName = "";
String disposition = conn.getHeaderField("Content-Disposition");
String contentType = conn.getContentType();
// 일반적으로 Content-Disposition 헤더에 있지만
// 없을 경우 url 에서 추출해 내면 된다.
if (disposition != null) {
String target = "filename=";
int index = disposition.indexOf(target);
if (index != -1) {
fileName = disposition.substring(index + target.length() + 1);
}
} else {
fileName = spec.substring(spec.lastIndexOf("/") + 1);
}
System.out.println("Content-Type = " + contentType);
System.out.println("Content-Disposition = " + disposition);
System.out.println("fileName = " + fileName);
is = conn.getInputStream();
os = new FileOutputStream(new File(outputDir, fileName));
final int BUFFER_SIZE = 4096;
int bytesRead;
byte[] buffer = new byte[BUFFER_SIZE];
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
is.close();
System.out.println("File downloaded");
} else {
System.out.println("No file to download. Server replied HTTP code: " + responseCode);
}
conn.disconnect();
} catch (Exception e){
System.out.println("An error occurred while trying to download a file.");
e.printStackTrace();
try {
if (is != null){
is.close();
}
if (os != null){
os.close();
}
} catch (IOException e1){
e1.printStackTrace();
}
}
}
}
console :
responseCode 200
Content-Type = application/msword
Content-Disposition = null
fileName = file-sample_100kB.doc
File downloaded
Process finished with exit code 0
이와같이 크게 2가지 작업만 해주면 쌩자바를 이용해 쉽게 파일을 다운받을 수 있다.
이번 포스트는 기본적으로 HttpUrlConnection
객체를 통해 오픈된 파일을 다운받았지만, 나중에 기회가 된다면 setRequestProperty()
등의 메서드를 이용해 토큰값이 있어야 접근 가능한 url 등에 대해서도 정리해보겠다.
좋은 글 잘 읽고 갑니다.
코드를 테스트해보니
String spec = "https://file-examples-com.github.io/uploads/2017/02/file-sample_100kB.doc";
위 리소스는 Content-Disposition 헤더가 특별히 없는 경우이므로 에러가 없지만
Content-Disposition 헤더가 있는 리소스일 경우에는 코드 상에서 에러가 발생합니다.
확인해보니 Content-Disposition 헤더의 filename 문자열을 추출하는 과정에서
실수가 있어 에러가 발생하기 때문에 코드를 살짝 수정하면 좋을 것 같습니다.
테스트는 spec 변수를 아래 리소스로 변경해 확인해보시면 됩니다.
String spec = "https://i.picsum.photos/id/152/536/354.jpg?hmac=Vh-3tACtfo0tExdnZBiHdzcsxRIS0Q-a8GN1QSC0b3U&name=4";
위 리소스는 Content-Disposition = inline; filename="152-536x354.jpg" 로 헤더값이 옵니다.
여기서 filename에 해당하는 문자열인 152-536x354.jpg 을 추출하는 기존 코드를 확인하면
아래와 같이 되어 있습니다.
코드의 의도를 보니 +1 을 통해서 앞의 따옴표는 잘 제거하셨는데, -> 152-536x354.jpg"
뒤의 따옴표가 제거되지 않아서 저장 시점에 오류가 발생합니다.
뒤의 따옴표를 제거할 수 있게 endIndex 를 주셔서 처리해주시거나 아래처럼 수정하시면 될 것 같습니다.
한가지 궁금한 점이 있습니다.
바이트 배열 생성 시 BUFFER_SIZE 로 특정 상수 값을 할당하셨는데
해당 수치에 대해 명시적으로 상수 선언을 한 이유와
이 수치를 어떤 기준으로 판단해 크기를 정해야 하는지 배우고 싶습니다.