오늘은 파일 다운로드와 업로드에 대해 배우는 시간을 가졌다.
그리고 오후에는 다음주부터 시작할 세미 프로젝트를 같이 할 다른 수강생들과 함께 첫 인사를 나누고 어떤 프로젝트 주제를 할지에 대해 이야기 해보는 시간을 가졌다.
그러고 나니 정말 내가 프로젝트를 해야되는구나라는게 와닿기 시작했다.
1. InputStream / Reader
- HttpServletRequest의 객체
- 브라우저에서 웹 서버로 들여올 때 사용하는 객체다.
- 읽기전용 객체
* 브라우저가 서버로 보낸 모든 요청 메세지는 톰캣이 분석해서 요청객체에 저장하고, 요청객체를 서블릿/JSP에 전달하기 때문에 개발자가 직접 InputStream과 Reader를 사용할 일은 없다.
1-1. InputStream
- 1byte씩 읽어오는 객체다.
- 바이너리 데이터를 읽어오는 객체다. (영상 / 그림/ 압축파일 등)
- request.getInputStream()을 실행해서 획득한다.
- 브라우저가 웹 서버로 보낸 바이너리 데이터를 읽을 수 있다.
1-2. Reader- 1글자씩 읽어오는 객체다.
- 텍스트 데이터를 읽어오는 객체다.
- request.getReader()를 실행해서 획득한다.
- 브라우저가 웹서버로 보낸 텍스트 데이터를 읽을 수 있다.
2. OutputStream / Writer
- HttpServletResponse의 객체
- 웹 서버에서 브라우저로 내보낼 때 사용하는 객체다.+
- 출력전용 객체
2-1. OutputStream
- 1byte씩 출력하는 객체다.
- 텍스트 데이터를 출력하는 객체다.
- response.getOutputStream()을 실행해서 획득한다.
- 웹 서버가 브라우저로 바이너리 데이터를 출력할 때 사용한다.
- 주로 파일 다운로드를 구현할 때 사용한다.
2-2. Writer
- 1글자씩 출력하는 객체다.
- 텍스트 데이터를 출력하는 객체다.
- response.getWriter()을 실행해서 획득한다.
- 웹서버가 브라우저로 텍스트 데이터를 출력할 때 사용한다.
- 서블릿 / JSP에서 HTML 컨텐츠를 응답으로 보낼 때 사용한다.
- 서블릿 : response.getWriter()를 실행하면 PrintWriter가 획득된다.
- JSP : pageContext.getWriter()를 실행하면 JspWriter가 획득된다.
* JSP에서는 out 내장객체 변수에 JspWriter가 대입되어 있고,
out.write("<태그> 내용 </태그>")로 HTML 컨텐츠를 응답으로 보낸다.
* 서블릿에서는 response.getWriter를 실행해서 PrintWriter를 획득하고, printWriter의 println("<태그> 내용 </태그>")로 응답을 보낸다.
com.sample.util/FileDownloader.java
package com.sample.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import org.apache.tomcat.jakartaee.commons.io.IOUtils;
import com.sample.dao.BoardDao;
import com.sample.vo.Board;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
// 파일다운로드를 요청하는 URL : http://localhost/web-board/download?no=글번호
@WebServlet("/download")
public class FileDownloader extends HttpServlet {
private String directory = "C:\\app\\web-workspace\\temp";
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 쿼리스트링으로 전달된 글번호를 조회한다.
int boardNo = StringUtils.stringToInt(request.getParameter("no"));
// BoardDao객체를 생성하고, 글번호에 해당하는 게시글 정보를 조회한다.
BoardDao boardDao = new BoardDao();
Board board = boardDao.getBoardByNo(boardNo);
// 첨부파일의 파일명을 조회한다.
String fileName = board.getFileName();
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// 파일 다운로드
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// 1. 응답컨텐츠의 타입을 지정한다.
// application/octet-stream은 바이너리 데이터 타입의 기본값이다.
// 컨텐츠 타입을 알 수 없는 바이너리 데이터는 application/otctet-stream을 컨텐츠 타입으로 지정ㅇ한다.
response.setContentType("application/octet-stream");
// 2. 응답메세지의 헤더부에 다운로드되는 파일이름 정보를 추가한다.
// Content-Disposition은 응답 헤더이름이다.
// Content-Disposition을 이용해서 다운로드 되는 파일이름 정보를 응답메세지의 응답헤더부에 포함시킨다.
// Content-Disposition의 응답 헤더값은 "attachment; filename=파일명"이다.
// "attachment;"는 첨부파일을 브라우저창에서 오픈없이 저장시킨다.
// "filename=파일명"은 다운로드 되는 파일명의 지정한다.
// "filename=" + URLEncoder.encode(fileName, "utf-8")"는 파일명에 한글이 포함되어 있을 경우,
// 한글이 깨지지 않도록 utf-8방식을 한글을 변환해서 응답으로 보니다.
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "utf-8"));
// 3. 다운로드할 파일을 읽어오는 FileInputStream 객체를 생성한다.
// FileInputStream 객체는 파일을 읽어오는 객체다.
// new File(디렉토리, 파일명)은 파일의 정보를 표현하는 객체를 생성한다.
// new FileInputStream(new File(디렉토리, 파일명));은 파일객체가 표현하는 파일을 읽어오는 객체를 생성한다.
// 지정된 디렉토리와 파일면으로 저장된 파일을 읽어오는 FileInputStream 객체를 생성한다.
InputStream in = new FileInputStream(new File(directory, fileName));
// 4. 응답객체가 브라우저로 브라우저로 바이너리 데이터를 출력하는 OutPutStream 객체를 획득한다.
// OutputStream 객체는 출력을 담당하는 객체다.
// response.getOutputStream()을 실행하면 OutputStream 객체를 반환한다.
// response.getOutputStream()을 실행해서 획득한 OutputStream객체는 브라우저로 출력하는 객체다.
OutputStream out = response.getOutputStream();
// 5. IOUtils 클래스의 copy(InputStream in, OutputStream out)은 in으로 읽어온 바이너리 데이터를 out으로 보낸다.
IOUtils.copy(in, out);
// 6. InputStream과 OutputStream은 컴퓨터의 읽기/쓰기 자원을 사용하기 때문에 읽기/쓰기 작업이 완료되면 사용했던 자원을 반납한다.
in.close();
out.close();
}
}
io - input / output (입출력)
- java.io 패키지의 클래스
- 애플리케이션에서 리소스를 읽어오거나, 애플리케이션의 데이터를 목적지로 출력하는 작업을 수행하는 클래스
0. Stream
- 데이터의 흐름이다.
- App에서 Resource로 가는 것은 input / 오는 것은 output
1. ByteStream
- 한 번에 1byte씩 읽거나 쓰는 스트림
- 텍스트데이터와 바이너리데이터(그림, 영상, 파일)를 읽고 쓸 수 있다.
- 종류
1-1. InputStream
- 리소스로부터 1byte씩 읽어오는 스트림
- 주요 메소드
int read() : 1byte씩 읽어서 정수로 반환한다.
int read(byte[] buf) : 1byte씩 읽어서 byte[]배열에 순서대로 저장하고, 배열에 저장된 갯수를 반환한다.
void close() : 읽기 작업을 위해서 사용했던 컴퓨터의 자원을 반납한다.
* InputStream은 반드시 이 메소드들을 가지고 있다.
1-2. OutputStream
리스스로 1byte씩 출력하는 스트림
void write(int value) : 1byte씩 출력한다.
void write(byte[] buf, int offset, int length) : byte배열의 데이터를 offset 위치에서 length만큼 출력한다.
void flush() : 임시저장소(버퍼)에 저장된 모든 데이터를 내보낸다.
void close() : 쓰기 작업을 위해서 사용했던 컴퓨터의 자원을 반납한다.
* OutputStream은 반드시 이 메소드들을 가지고 있다.
- 대표적인 클래스
- FileInputStream - 파일로부터 1byte씩 데이터를 읽어온다.
- FileOutputStream - 파일로 1byte씩 데이터를 출력한다.
- BufferedInputStream - 다른 InputStream의 읽기 성능을 향상시킨다.
- BufferedOutputStream - 다른 OutputStream의 쓰기 성능을 향상시킨다.
- DataInputStream - 데이터 타입 그대로 읽어온다.
- DataOutputStream - 데이터 타입 그대로 출력한다.
- ObjectInputStream - 직렬화된 객체를 스트림으로 읽어서 객체로 복원한다.(역직렬화)
- ObjectOutputStream - 객체를 스트림으로 출력한다.(직렬화)
- PrintStream (이 클래스 혼자 쓰기만 존재한다.) - 대표적인 출력스트림이다.
2. CharacterStream
- 한 번에 1글자씩 읽거나 쓰는 스트림
- 텍스트데이터만 읽고 쓸 수 있다.
- 종류
- Reader
리소스로부터 1글자씩 읽어오는 스트림
- Writer
리소스로 1글자씩 출력하는 스트림
- 대표적인 클래스
- FileReader
- FileWriter
- BufferedReader
- BufferedWriter
- InputStreamReader
- OutputStreamWriter
- PrintWriter
InputStream / OutputStream 예시 (File에 텍스트 출력하기)
io/com.sample/Demo1.java (FileOutputStream 방식)
package com.sample;
import java.io.FileOutputStream;
import java.io.IOException;
public class Demo1 {
public static void main(String[] args) throws IOException {
// 파일에 출력하기
// 1. 파일에 1byte씩 출력하는 FileOutputStream 객체를 생성한다.
FileOutputStream out = new FileOutputStream("demo1.txt");
// 2. 파일에 1byte씩 쓰기
out.write(65);
out.write(66);
out.write(67);
out.write(68);
out.write(69);
// 3. 자원 반납하기
out.close();
}
}
를 적고 실행 후 새로고침 하면
![](https://velog.velcdn.com/images/hcw0709/post/826981fe-5625-4583-ac2e-bf142f0e0540/image.png)
- demo1.txt가 새로 생기고 ABCDE가 들어가 있다. 65 / 66 / 67 / 68 / 69가 ABCDE다.
그러나, 문장으로는 적을 수 없기 때문에 문장을 적기 위해서는
String text = "안녕하세요, 반갑습니다.";
byte[] bytes = text.getBytes();
out.write(bytes, 0, bytes.length); // byte 안에 들은 것을 배열의 0부터 끝까지 출력해라
를 추가하면
![](https://velog.velcdn.com/images/hcw0709/post/a247a062-8232-4845-b8fe-ff10adb55ab1/image.png)
demo1.txt에도 문장이 추가된다.
다른 방법으로는
io/com.sample/Demo2.java (println 방식)
package com.sample;
import java.io.IOException;
import java.io.PrintStream;
public class Demo2 {
public static void main(String[] args) throws IOException {
// 파일에 기록하기
// PrintStream 객체를 생성한다.
PrintStream out = new PrintStream("demo2.txt");
// PrintStream의 println(String text)를 이용해서 문자열을 파일로 출력한다.
out.println("안녕하세요, 반갑습니다.");
out.println("제 이름은 홍길동입니다.");
// 쓰기 작업에 사용했떤 컴퓨터의 자원을 반납한다.
out.close();
}
![](https://velog.velcdn.com/images/hcw0709/post/26fc0e97-4924-4c11-ba60-8eeab9a2bcf5/image.png)
demo2.txt가 생성된다.
io/com.sample/Demo3.java (FileWiter 방식)
package com.sample;
import java.io.FileWriter;
import java.io.IOException;
public class Demo3 {
public static void main(String[] args) throws IOException {
// 파일에 기록하기
FileWriter out = new FileWriter("demo3.txt");
// 텍스트 출력 전용의 FileWriter 객체를 생성한다.
out.write("안녕하세요");
out.write("반갑습니다.");
// 버퍼에 남아있는 모든 데이터를 출력시킨다.
out.flush();
// 쓰기 작업에 사용했던 컴퓨터의 자원을 반납한다.
out.close();
}
}
![](https://velog.velcdn.com/images/hcw0709/post/e2c5ca07-e6b9-41b2-8115-2bfd46fbadc2/image.png)
- 문자열을 쓸 수 있다.
- println과 다르게 줄바꿈이 없다.
io/com.sample/Demo4.java (PrintWiter 방식)
package com.sample;
import java.io.IOException;
import java.io.PrintWriter;
public class Demo4 {
public static void main(String[] args) throws IOException{
// 파일에 기록하기
PrintWriter out = new PrintWriter("demo4.txt");
out.println("안녕하세요");
out.println("반갑습니다.");
out.flush(); // auto flush 기능을 true로 설정하면 실행할 필요 없음
out.close();
}
}
![](https://velog.velcdn.com/images/hcw0709/post/268dea33-1a5b-4a84-8169-3b2a4bffccb0/image.png)
![](https://velog.velcdn.com/images/hcw0709/post/dbaba6fb-3c54-4a0a-8038-0be3bc57eb9f/image.png)
- 버퍼가 있는가 없는가의 차이는 속도차이가 크다.
- flush를 통해 버퍼가 다 차지 않아도 내려보내준다.
- 텍스트를 기록할 때는 printWriter를 제일 많이 사용한다.
![](https://velog.velcdn.com/images/hcw0709/post/a317a2ae-fcbe-4528-8b0b-3f21f7e5b8d0/image.png)
- 텍스트 이외의 파일은 그대로 읽어오는 복사의 경우가 많다.
읽어오는 작업
io/com.sample/Demo5.java (FileInputStream 방식)
package com.sample;
import java.io.FileInputStream;
import java.io.IOException;
public class Demo5 {
public static void main(String[] args) throws IOException{
// 파일 읽어오기
FileInputStream in = new FileInputStream("demo5.txt");
// 1byte씩 읽어오기
int value = 0;
while ((value = in.read()) != -1) {
System.out.print((char) value);
}
in.close();
}
}
> demo5.txt에 있는 문자열 데이터
![](https://velog.velcdn.com/images/hcw0709/post/fe783f45-e2d3-4c9d-96a7-85093efacae7/image.png)
> Demo5.java를 실행하였을 때 출력되는 모습
![](https://velog.velcdn.com/images/hcw0709/post/d9b6f2a8-563e-4606-92c3-732e87a25039/image.png)
그런데, 한글을 불러 올 때는 글자가 깨지는 현상이 발생한다.
그래서
io/com.sample/Demo6.java (BufferedReader 방식)
package com.sample;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Demo6 {
public static void main(String[] args) throws IOException {
// BufferedReader는 텍스트를 한 줄씩 읽어온다.
BufferedReader in = new BufferedReader(new FileReader("demo6.txt"));
String line1 = in.readLine();
String line2 = in.readLine();
String line3 = in.readLine();
String line4 = in.readLine();
String line5 = in.readLine();
System.out.println(line1);
System.out.println(line2);
System.out.println(line3);
System.out.println(line4);
System.out.println(line5);
in.close();
}
}
![](https://velog.velcdn.com/images/hcw0709/post/62daaa87-5a4b-4251-af04-ca1fc43efb6e/image.png)
- 한 줄씩 읽어온다.
- 값이 없다면 null을 반환한다.
- 그래서
package com.sample;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Demo6 {
public static void main(String[] args) throws IOException {
// BufferedReader는 텍스트를 한 줄씩 읽어온다.
BufferedReader in = new BufferedReader(new FileReader("demo6.txt"));
// String readLine() : 텍스트를 한 줄 씩 읽어서 반환한다. 스트림의 끝에 도달하면 null을 반환한다.
String text = null;
while ((text = in.readLine()) != null) {
System.out.println(text);
}
in.close();
}
}
를 하면
![](https://velog.velcdn.com/images/hcw0709/post/039fc938-1dbf-4778-b207-06bfd292a0df/image.png)
- null값이 사라진다.
> 텍스트 데이터의 유형
text/plain : 일반 텍스트 : 일반 텍스트, 데이터분석의 대상이 이니다. 프로그램 대상이 아니다.
세리에A에서 14경기에 선발 출전해 2골을 기록했고, UEFA 챔피언스리그에서도 조별리그 6경기에 모두 선발 출전했다. 나폴리는 김민재의 활약에 힘입어 세리에A 선두를 달리고 있으며 챔피언스리그 조별리그 A조 1위로 통과했다.
csv(comma-separated values) : CSV 전용 라이브러리가 필요하다.
이것이 자바다,신용권,한빛미디어,35000,31500.10
자바의 정석,남궁성,도우출판사,30000,27000
모두를 위한 클라우드 컴퓨팅,노서영,Jpub,39000,35500
text/xml : XML 전용 라이브러리가 필요하다.
<data>
<book>
<title>이것이 자바다</tilte>
<author>신용권</author>
<publisher>한빛미디어</publisher>
<price>35000</price>
<discount-price>31500</discount-price>
<stock>10</stock>
</book>
</data>
application/json : JSON 전용 라이브러리가 필요하다.
[
{"title":"이것이 자바다", "author":"신용권, "publisher":"한빛미디어", "price":35000, "discountPrice":31500, "stock":10},
{"title":"자바의 정석", "author":"남궁성, "publisher":"도우출판사", "price":30000, "discountPrice":27000, "stock":10},
]
* 대부분 읽어오는 파일의 형태는 csv, text/xml, application/json이다.
다른 형식의 파일 데이터도 가지고 오기 위해
io/com.sample/Demo7.java
package com.sample;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
public class Demo7 {
public static void main(String[] args) throws Exception {
// InputStream나 Reader 객체를 획득(생성)했다는 것은 소스(리소스)로부터 데이터를 읽어올 준비가 되었다는 것이다.
// OutputStream나 Writer 객체를 획득(생성)했다는 것은 대상(리소스)으로 데이터를 출력할 준비가 되었다는 것이다.
// URL은 웹에서 리소스(자원)의 위치를 나타내는 주소다.
URL url = new URL("https://t1.daumcdn.net/news/202211/21/tvreport/20221121125631535ihfm.jpg");
// url이 나타내는 자원을 읽어올 준비가 되었다.
InputStream in = url.openStream();
// 김고은.jpg를 출력할 준비가 되었다.
OutputStream out = new FileOutputStream("김고은.jpg");
int value = 0;
while ((value = in.read()) != -1) {
out.write(value);
}
in.close();
out.close();
}
}
를 실행하면 김고은.jpg가 생성된다.
![](https://velog.velcdn.com/images/hcw0709/post/8fc1b95a-96a7-43d9-a796-7c9e58a061e2/image.png)
그리고 김고은.jpg를 들어가면 사진이 출력된다.
### commons-io-2.11.0
그러나, 이 방식 역시 너무나 복잡하기에
apache.org에서 commons-io-2.11.0를 받아 그 안의 commons-io-2.11.0.jar파일을
io/lib폴더에 넣어주고 buildpath를 실행해주었다.
그리고 실행을 하게 되면,
// url이 나타내는 자원을 읽어올 준비가 되었다.
InputStream in = url.openStream();
// 김고은.jpg를 출력할 준비가 되었다.
OutputStream out = new FileOutputStream("김고은.jpg");
IOUtils.copy(in, out);
in.close();
out.close();
}
}
위의 사진과 같이 똑같이 김고은.jpg가 생성된다.
### 자원(텍스트 데이터, 바이너리 데이터)의 복사
![](https://velog.velcdn.com/images/hcw0709/post/7b7b24a8-b58a-4ec6-acb2-c437dc8cbfe9/image.png)
- 대부분의 입/출력 작업은 자원을 복사하는 작업이다.
- 원본 리소스를 읽어와서 대상 리소스로 출력하는 것이다.
Ex) 첨부파일 업로드 처리 : 서버로 업로드된 첨부파일을 읽어서 지정된 경로에 파일로 출력하는 것이다.
첨부파일 다운로드 처리 : 서버의 지정된 폴더에 저장된 파일을 읽어서 응답객체의 출력스트림으로 복사하는 것이다.
download.zip을 복사하는 코드
package com.sample;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class Demo8 {
public static void main(String[] args) throws IOException {
long startUnixTime = System.currentTimeMillis();
System.out.println("### 파일 복사 시작");
InputStream in = new FileInputStream("download.zip");
OutputStream out = new FileOutputStream("download-backup.zip");
int value = 0;
while ((value = in.read()) != -1) {
out.write(value);
}
in.close();
out.close();
System.out.println("### 파일 복사 종료");
long endUnixTime = System.currentTimeMillis();
System.out.println((endUnixTime - startUnixTime) + "밀리초 소요됨");
}
}
> InputStream, OutputStream을 사용하였을 때
![](https://velog.velcdn.com/images/hcw0709/post/cbeb2640-5515-4f01-8a56-795622276254/image.png)
복사가 되지만 오래 걸린다.
그래서 읽기 성능을 향상시켜주는 BufferedInputStream을 사용한다.
package com.sample;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class Demo8 {
public static void main(String[] args) throws IOException {
long startUnixTime = System.currentTimeMillis();
System.out.println("### 파일 복사 시작");
// BufferedInputㅁStream과 BufferedOutputStream은 InputStream과 OutputStream의 읽기/쓰기 성능을 향상시키는 클래스다.
BufferedInputStream in = new BufferedInputStream(new FileInputStream("download.zip"));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("download-backup.zip"));
int value = 0;
while ((value = in.read()) != -1) {
out.write(value);
}
in.close();
out.close();
System.out.println("### 파일 복사 종료");
long endUnixTime = System.currentTimeMillis();
System.out.println((endUnixTime - startUnixTime) + "밀리초 소요됨");
}
}
> BufferedStream을 사용하였을 때
![](https://velog.velcdn.com/images/hcw0709/post/aca77531-2660-4f00-82bd-b0a8a273ef7a/image.png)
훨씬 빠른 속도를 보여준다.
![](https://velog.velcdn.com/images/hcw0709/post/0c9d539f-4bd5-47fb-957f-a331e4c7ed43/image.png)
텍스트를 다룰 때는 두가지를 기억하자. 읽어온다 ? BufferedReader, 출력한다? PrintWriter