스트림에 대해서 알아보기전에 아래 그림으로 먼저 구조를 이해하자
출력 스트림
: 자바가 가진 데이터를 hello.dat 파일 저장소에 저장하려면 출력 스트림
으로 내보낸다
입력 스트림
: hello.dat 파일 저장소를 읽어서 자바로 가져오려면 입력 스트림
을 사용한다
각 스트림은 단방향
방식으로 출력스트림
은 보내기만 할 수있고 입력 스트림
은 읽을 수만 있다
다음과 같은 예제로 파일에 값을 넣고 파일을 읽는 메소드를 알아보자
new FileOutputStream("경로", append 옵션)
false
로 되어있음true
: 기존 파일의 끝에서 이어서 적음false
: 기존 파일의 데이터를 지우고 처음부터 다시 적음write()
new FileInputStream("경로")
read()
-1
을 반환한다 -> EOF : End of Fileclose()
✅ 외부파일 데이터 부분적 읽기
이번 예제에서는 한번에 10개 값을 넣고 한번에 10개 값을 읽어보자
read(byte[], offset, length)
public class StreamMain {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("temp/hello.dat");
byte[] input = {65,66,67}; // write(byte[]) : 배열형식으로 만들면 한번에 넣을 수 있음
fos.write(input);
fos.close();
FileInputStream fis = new FileInputStream("temp/hello.dat");
byte[] buffer = new byte[10];
int readCount = fis.read(buffer,0,10);
// byte[] : 데이터가 읽혀지는 버퍼, offset : 데이터 기록되는 byte[] 인덱스 시작위치, length : 읽어올 byte 최대길이
System.out.println(readCount); // 3
System.out.println(Arrays.toString(buffer)); // [65, 66, 67, 0, 0, 0, 0, 0, 0, 0]
fis.close();
}
✅ 외부파일 데이터 한번에 읽기
이번에는 한번에 모든 데이터를 읽는방식을 알아보자
readAllBytes()
OutOfMemoryError
가 발생 FileInputStream fis = new FileInputStream("temp/hello.dat");
byte[] readBytes = fis.readAllBytes();
System.out.println(Arrays.toString(readBytes));
fis.close();
✅ InputStream
OutputStream
의 유래
현재의 컴퓨터는 대부분 byte
단위로 데이터를 주고 받음 (bit는 단위가 작음)
이렇게 데이터를 주고 받는것을 Input Output (I/O)
라고 한다
자바에서는 데이터를
외부에 있는 파일
에 저장하거나
네트워크
를 통해 전송하거나
콘솔
을 통해 출력할 때 모두 byte
단위로 주고 받는다
만약 파일
네트워크
콘솔
각각 데이터를 주고 받는 방식이 다르다면 상당히 불편할 것이다
이 불편함을 해결하기 위해서 자바는 InputStream
OutputStream
이라는 기본 추상 클래스
를 제공
-> 일부 동작하는 코드도 들어있기 때문에 추상클래스임
InputStream
를 상속받는 클래스는 대표적 FileInputStream
ByteArrayStream
SocketInputStream
read()
read(byte[])
readAllBytes()
를 사용할 수 있다OutputStream
을 상속받는 클래스 FileOutputStream
ByteArrayOutputStream
SocketOutputStream
write(int)
write(byte[])
를 사용할 수 있다✅ 메모리에 데이터를 저장, 읽기
!
: 참고로 메모리에 데이터를 저장하고 읽을 때는 대부분 컬렉션이나 배열을 사용한다
아래 기능을 잘 사용 하지 않음
public class ByteArrayMain {
public static void main(String[] args) throws IOException {
byte[] input = {1,2,3};
// 메모리에 쓰기
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(input);
// 메모리에서 읽기
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
byte[] bytes = bais.readAllBytes();
System.out.println(Arrays.toString(bytes));
}
}
✅ System.out
유래
사용자들이 출력하는 명령어인 System.out
은 사실 PrintStream
이다
PrintStream
은 OutputStream
을 상속받는다 -> write(byte[])
사용 가능
PrintStream
은 자바가 시작될 때 자동으로 만들어지며, 직접 생성하지 않는다
Println
은 자식클래스인 PrintStream
의 자체 부가 기능이다
자바에서 10MB 파일 하나를 쓰는데 대략 10~20초, 읽는데 5초이상이 걸린다
왜냐하면 자바는 1byte
씩 디스크에 데이터를 전달하기 때문에 느리다
10MB = 1byte * 10 * 1024 * 1024
= 무려 천만번 이상을 반복만하는 것
더 자세하게 설명하면 write()
와 read()
작업은 자바에서 OS의 시스템 콜
을 통해서 명령어를 전달함
시스템 콜
은 상대적으로 무거운 작업임
HDD 의 경우 SSD 보다 더 느림
해결법은 자바에서 read() write()
호출 횟수를 줄여서 시스템 콜 횟수를 줄여야 한다
해결방법으로 우선 버퍼를 사용하는 방법에 대해서 알아보자
✅ 버퍼
를 사용하는 경우
우선 버퍼는 모아서 목적지에 보내주는 역할을 한다
버퍼
를 사용하면 더 나은 성능을 보여준다 = 많은 데이터를 한번에 보내줌
버퍼의 크기를 크게하면 속도가 줄어드는 것을 확인할 수 있지만
일정 크기부터는 변화가 없는것을 알 수 있다 그 이유는
디스크나 파일 시스템에서 데이터를 읽고 쓰는 기본 단위가 4KB or 8KB
이기 때문이다
버퍼를 사용하는 경우 보통 4KB 8KB
정도로 잡는 것이 효율적이다
이렇게 버퍼를 사용하면 큰 성능향상이 있지만, 직접 버퍼를 만들고 관리해야한다는 단점이 있다
해결방법으로 Buffered 스트림
을 알아보자
✅ Buffered 스트림
을 사용하는 경우
버퍼기능을 내부에서 대신 처리해준다.
단순한 코드를 유지하면서 버퍼를 사용할 수 있다는 장점이 있다
Buffered 스트림
은 내부에서 단순히 버퍼 기능만 제공하며 반드시 대상이 있어야함
BufferedOutputStream 원리
(1)
BufferedOutputStream
에write(byte)
를 버퍼가 가득찰 때까지 호출
(2)BufferedOutputStream
의 버퍼가 가득참
(3)FileOutputStream
의write(byte[])
를 호출
(4) 전달된 모든byte[]
를 시스템 콜로 OS에 전달
(5) 버퍼의 데이터를 모두 전달했기 때문에BufferedOutputStream
의 버퍼의 내용을 비운다
(6) 다시 (1)번부터 반복한다
만약 버퍼가 다 안찼을 경우 데이터를 전달하고 싶으면 flush()
메소드를 호출한다
다 차지않은 버퍼안에 있는 데이터를 전달하고 난 후 -> 버퍼를 비운다
만약 버퍼에 데이터가 남아 있는 상태에서 close()
메소드를 호출하면
먼저 내부에서 flush()
를 호출하여 버퍼에 남아 있는 데이터를 모두 전달하고 비운다
즉 close()
를 호출해도 남은 데이터를 안전하게 저장할 수 있다
아래 코드처럼 마지막에 연결한 BufferedOutputStream
스트림만 닫아주면
연쇄적으로 FileOutputStream
스트림의 close()
호출된다
FileOutputStream fos = new FileOutputStream("파일이름"); // EX) hello.dat
BufferedOutputStream bos = new BufferedOutputStream(fos, "버퍼사이즈"); // EX) 4KB or 8KB
for(int i=0; i< "파일사이즈"; i++){ // EX) 10000
bos.write(1); // 버퍼가 가득찰때까지 write 함
}
bos.close(); // 마지막에 연결된 스트림을 닫아주면 다 닫아짐
정리하자면
FileOutputStream
과 같이 단독으로 사용할 수 있는 스트림을 기본스트림
이라고 부른다
BufferedOutputStream
과 같이 단독으로 사용할 수 없고, 보조기능을 제공하는 스트림을 보조 스트림
BufferedOutputStream
은 FileOutputStream
에 버퍼라는 보조 기능을 제공함
BufferedOutputStream
의 생성자에는 반드시 FileOutputStream
와 같은 대상 OutputStream
이 필수
BufferedIutputStream 원리
(1) :
BufferedIutputStream
에서 먼저 버퍼를 확인한다.
(2) : 버퍼에 데이터가 없으면 데이터를 불러온다.
->BufferedIutputStream
은FileIutputStream
에서read(byte[])
를 사용해서 불러옴
(3) 버퍼에 데이터가 있으면BufferedIutputStream
은read()
를 호출해서 데이터 읽기
(4) 다시 버퍼가 비어있으면FileIutputStream
에서 버퍼 크기만큼 조회해서 버퍼에 불러옴
FileInputStream fis = new FileInputStream("파일이름");
BufferedInputStream bis = new BufferedInputStream(fis, "버퍼크기"); // 버퍼에 크기만큼 데이터 넣기
int fileSize = 0;
int data;
while ((data = bis.read()) != -1) { // 버퍼에담아둔 것을 읽기
fileSize++;
}
bis.close(); // 마지막에 연결한 스트림을 닫아주면 된다
한마디로 버퍼의 크기만큼 데이터를 미리 읽어서 버퍼에 보관해두고, read()
를 통해 조회함
하지만 직접 버퍼를 만들어서 처리하는 것보다 BufferedIutputStream
BufferedOutputStream
를 사용하는 것이 더 성능이 떨어지는 것을 알 수 있다
버퍼스트림 클래스
는 자바 초창기에 만들어진 클래스로 멀티스레드
를 고려해서 만든 클래스이다
그래서 클래스 안에 락을 걸고 푸는 동기화
코드로 인해 성능이 저하되는 것이다
일반 싱글스레드 상황에서는 버퍼스트림 클래스
를 사용하고
큰 데이터를 다루거나, 성능최적화가 중요하다면 직접 버퍼를 만드는 방법을 고려하자
✅ 한번에 쓰고 읽기
추가로 파일의 크기가 크지 않다면 간단하게 한 번에 쓰고 읽는 것도 좋은 방법
이 방법은 성능은 가장 빠르지만, 메모리를 한 번에 많이 사용하기 때문에 파일의 크기가 작아야함
public class CreateFileV4 { // 쓰기
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("파일경로");
byte[] buffer = new byte[FILE_SIZE];
for (int i = 0; i < FILE_SIZE; i++) {
buffer[i] = 1;
}
fos.write(buffer); // 버퍼자체를 쓰기
fos.close();
}
}
public class ReadFileV4 { // 읽기
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("파일경로");
byte[] bytes = fis.readAllBytes(); // readAllBytes : 한번에 데이터를 다 읽음
fis.close();
System.out.println("File size: " + bytes.length / 1024 / 1024 + "MB");
}
}