자바 프로세스가 가지고 있는 데이터를 밖으로 내보내려면 '출력 스트림'을 사용하면 되고, 반대로 외부 데이터를 자바 프로세스 안으로 가져오려면 '입력 스트림'을 사용하면 된다.
InputStream / OutputStream
현대의 컴퓨터는 대부분 byte 단위로 데이터를 주고받는다. 이렇게 데이터를 주고 받는 거승ㄹ Input/Output(I/O)라고 한다. 자바 내부에 있는 데이터를 외부에 있는 파일에 저장하거나, 네트워크를 통해 전송하거나 콘솔에 출력할 때 모두 byte 단위로 데이터를 주고받는다. 만약, 파일, 네트워크, 콘솔 각각 데이터를 주고받는 방식이 다를다면 상당히 불편할 것이다..
이런 문제를 해결하기 위해 자바는 InputStream
, OutputStream
이라는 기본 추상 클래스를 제공한다!
스트림을 사용하면 파일을 사용하든, 소켓을 통해 네트워크를 사용하든 모두 일관된 방식으로 데이터를 주고받을 수 있다! 각 구현 클래스들은 자신에게 맞는 추가 기능도 함께 제공한다. 이러한 추상화는 일관성, 유연성, 확장성, 재사용성, 일관된 에러 처리 등의 장점이 있다.
- InputStream
read()
: 데이터를 읽고 읽은 데이터의 수를int
형으로 반환한다. 스트림의 끝에 도달하면 -1을 반환한다.read(byte[], offset, length)
:byte[]
타입의 버퍼의 크기만큼 데이터를 한 번에 읽어온다. 마찬가지로 스트림의 끝에 도달하면 -1을 반환한다.byte[]
: 데이터가 읽혀지는 버퍼offset
: 데이터 기록되는 byte[]의 인덱스 시작 위치, 생략 시 0length
: 읽어올 byte의 최대 길이, 생략 시byte[].length
readAllBytes()
: 스트림이 끝날 때까지 모든 데이터를 한 번에 읽어온다.- 한 번의 호출로 모든 데이터를 읽을 수 있어 편하지만, 메모리 사용량을 제어할 수 없기에 큰 파일의 경우
OutOfMemoryError
가 발생할 수 있다.
- 한 번의 호출로 모든 데이터를 읽을 수 있어 편하지만, 메모리 사용량을 제어할 수 없기에 큰 파일의 경우
- OutputStream
write(int)
: byte 단위로 값을 출력한다write(byte[], offset, length)
: 버퍼의 크기만큼 데이터를 한 번에 출력한다.- 많은 데이터를 한 번에 전달하여 OS 시스템 콜 호출 수를 줄이고, HDD나 SDD 같은 장치들의 작동 횟수도 줄여 성능을 최적화할 수 있다.
- 디스크나 파일 시스템에서 데이터를 읽고 쓰는 기본 단위가 보통 4KB(4096 byte) 또는 8KB(8192 byte)이기 때문에, 이 잉상 버퍼의 크기를 늘려도 속도가 줄어들지 않는다.
- 따라서 버퍼의 크기는 보통 4KB, 8KB 정도롤 잡는 것이 효율적이다.
- 파일의 크기가 크지 않아서, 메모리 사용에 큰 영향을 주지 않는다면 버퍼의 크기를 파일 크기만큼 잡아서 쉽고 빠르게
write(byte[])
를 활용하여 한 번에 처리하자.
스트림을 사용한다는 것은 자바 입장에서 외부 자원을 사용하는 것이다. 자바에서 내부 객체는 자동으로 GC가 되지만 외부 자우너은 사용 후 close()를 호출하여 반드시 닫아주어야 한다
메모리 스트림
ByteArrayInputStream
: 메모리로부터 스트림을 읽는다.ByteArrayOutputStream
: 메모리에 스트림을 쓴다.
메모리에 어떤 데이터를 저장하고 읽을 때는 컬렉션이나 배열을 사용하면 되기에, 이 기능은 잘 사용하지 않는다. 주로 테스트 용도이다.
콘솔 스트림
PrintStream
(=System.out
): 이 스트림은 OutputStream을 상속받으며, 자바가 시작될 때 자동으로 만들어진다.- println(content): PrintStream이 자체적으로 제공하는 추가 기능으로, 편하게 데이터를 쓸 수 있다.
파일 스트림
FileInputStream
: 파일로부터 스트림을 읽는다.- ex)
FileInputStream fis = new FileInputStream("temp/hello.dat");
- ex)
FileOutputStream
: 파일에 스트림을 쓴다.- ex)
FileOutputStream fos = new FileOutputStream("temp/hello.dat");
- ex)
보조 스트림
기존 InputStream과 OutputStream에서도 byte[] 타입의 버퍼를 직접 생성하여 인자로 전달하면 성능을 크게 최적화할 수 있었다. 하지만, 직접 버퍼를 만들고 관리해야 하는 번거로운 단점이 있다. 버퍼를 사용하지 않는 단순한 코드를 유지하면서, 버퍼를 사용할 때와 같은 성능의 이점을 누리기 위해 사용할 수 있는 것이 바로 BufferedInputStream
과 BufferedOutputStream
이다.
이렇듯 단독으로 사용할 수 없고, 보조 기능을 제공하는 스트림을 '보조 스트림'이라고 한다. 반면, FileOutputStream과 같이 단독으로 사용할 수 잇는 스트림을 '기본 스트림'이라고 한다.
BufferedInputStream / BufferedOutputStream
이들은 기존 스트림에 단순히 버퍼 기능만 추가로 제공하는 것이다. 따라서 반드시 대상 스트림을 인자로 넘겨주어야 한다.
FileOutputStream fos = new FileOutputStream(FILE_NAME);
BufferedOutputStream bos = new BufferedOutputStream(fos, BUFFER_SIZE);
FileInputStream fis = new FileInputStream(FILE_NAME);
BufferedInputStream bis = new BufferedInputStream(fis, BUFFER_SIZE);
BufferedInputStream
- 내부에
byte[] buf
라는 버퍼를 가지고 있다. read()
를 호출하면BufferedInputStream
은read(byte)
를 통해 버퍼의 크기만큼 데이터를 불러오고, 이를byte[] buf
에 보관한다.read()
호출 시 버퍼에 있는 데이터를 반환하게 된다. 만약, 버퍼가 비어있다면 다시 버퍼 크기만큼 조회하고 버퍼에 담아두어 이를 반환한다.
- 내부에
BufferedOuptputStream
- 내부에
byte[] buf
라는 버퍼를 가지고 있다. write(byte)
를 통해 byte를 전달하면, 바로 스트림으로 내보내지 않고,byte[] buf
에 보관한다.- 버퍼가 가득차면 생성 시 인자로 전달되었던
OutputStream
에 있는write(byte[])
를 호출하여 비로소 스트림을 쓴다. (시스템 콜) -> 이후 버퍼의 내용은 비운다. flush()
메서드를 호출하여 버퍼가 다 차지 않아도 버퍼의 데이터를 전달하고 비울 수 있다.close()
로 닫으면 먼저 내부에서flush()
를 자동으로 호출하여 버퍼에 남아있는 데이터를 모두 전달하고 비운다. 그러고 나서 다음 연결된 스트림의close()
까지 호출해준다. (close()
연쇄 호출)
- 내부에
하지만, 이는 직접 byte[]
버퍼를 생성하여 다루는 방식보다 성능이 느린데, 이유는 동시성 제어를 위해 synchronized
키워드가 모두 포함되어 있기 때문이다. 만약 멀티스레드 환경이 아닌 상태에서 버퍼를 통해 성능을 극한으로 최적화하고 싶다면 직접 버퍼를 다루는 방법밖에 없다..
PrintStream
우리가 자주 사용해왔던 System.out에서 사욛외는 스트림으로, PrintStream과 다른 기본 스트림을 조합하면 마치 콘솔에 출력하는 것 처럼 파일이나 다른 스트림에 문자를 출력할 수 있다. (byte가 아니라 문자를 다룰 수 있다)
FileOutputStream fos = new FileOutputStream("temp/print.txt");
PrintStream printStream = new PrintStream(fos);
printStream.println("hello java!");
printStream.println(10);
printStream.println(true);
printStream.printf("hello %s", "world");
printStream.close();
DataInputStream / DataOutputStream
이들을 사용하면 자바의 String
, int
, double
, boolean
같은 데이터 형을 편리하게 저장하고 불러올 수 있다. 매번 byte 형태롤 저장하고 읽다보니 자바의 타입을 사용하기 위해서는 캐스팅해주는 작업이 필수적이었다.
FileOutputStream fos = new FileOutputStream("temp/data.dat");
DataOutputStream dos = new DataOutputStream(fos);
dos.writeUTF("회원A");
dos.writeInt(20);
dos.writeDouble(10.5);
dos.writeBoolean(true);
dos.close();
FileInputStream fis = new FileInputStream("temp/data.dat");
DataInputStream dis = new DataInputStream(fis);
System.out.println(dis.readUTF());
System.out.println(dis.readInt());
System.out.println(dis.readDouble());
System.out.println(dis.readBoolean());
dis.close();
저장한 data.dat
파일을 직접 열어보면 제대로 보이지 않는다. 왜냐하면 writeUTF()
의 경우 UTF-8 형식으로 저
장하지만, 나머지의 경우 문자가 아니라 각 타입에 맞는 byte 단위로 저장하기 때문이다.
예를 들어서 자바에서 int는 4byte를 묶어서 사용한다. 해당 byte가 그대로 저장되는 것이다.
여기서, 주의할 점은 꼭 저장한 순서대로 읽어야 한다. 그렇지 않으면 잘못된 데이터가 조회될 수 있다.
DataStream의 원리
DataStream
은 어떤 원리로 구분자나 한 줄 라인 없이 데이터를 저장하고 조회할 수 있는 것일까?
writeUTF()
은 UTF-8 형식으로 문자를 저장하는데, 저장할 때 2byte를 추가로 사용해서 앞에 글자의 길이를 저장해둔다. (65535 길이까지만 사용 가능)- 따라서
readUTF()
로 읽어들일 때 먼저 앞의 2byte로 글자의 길이를 확인하고 해당 길이 만큼 글자를 읽어들인다. - 자바의
Int(Integer)
는 4byte를 사용하기 때문에 4byte를 사용해서 파일을 저장하고, 읽을 때도 4byte를 읽어서 복원한다.
ObjectStream
- 객체 직렬화
자바 객체 직렬화(Serialization)는 메모리에 있는 객체 인스턴스를 바이트 스트림으로 변환하여 파일에 저장하거나 네트워크를 통해 전송할 수 있도록 하는 기능이다. 이 과정에서 객체의 상태를 유지하여 나중에 역직렬화(Deserialization)를 통해 원래의 객체로 복원할 수 있다.
객체 직렬화를 사용하려면 직렬화하려는 클래스는 반드시 Serializable
인터페이스를 구현해야 한다.
- ObjectStream 직렬화
public List<Member> findAll() { try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH))) { Object findObject = ois.readObject(); return (List<Member>) findObject; } catch (FileNotFoundException e) { return new ArrayList<>(); } catch (IOException | ClassNotFoundException e) { throw new RuntimeException(e); } }
ObjectOutputStream
를 사용하면 객체 인스턴스를 직렬화해서 byte로 변경할 수 있다.- 우리는 회원 객체 하나가 아니라 회원 목록 전체를 파일에 저장해야 하므로
members
컬렉션을 직렬화 해야한다. oos.writeObject(members)
를 호출하면members
컬렉션과 그 안에 포함된Member
를 모두 직렬화해서 byte로 변경한다. 그리고oos
와 연결되어 있는FileOutputStream
에 결과를 출력한다.
객체 직렬화를 사용하면 객체를 바이트로 변환할 수 있어, 모든 종류의 스트림에 전달할 수 있다. 이는 파일에 저장하는 것은 물론, 네트워크를 통해 객체를 전송하는 것도 가능하게 한다. 이러한 특성 때문에 초기에는 분산 시스템에서 활용되었다.
그러나 객체 직렬화는 1990년대에 등장한 기술로, 초창기에는 인기가 있었지만 시간이 지나면서 여러 단점이 드러났다. 또한 대안 기술(XML, JSON, 데이터베이스 등)이 등장하면서 점점 그 사용이 줄어들게 되었다. 현재는 객체 직렬화를 거의 사용하지 않는다.
- 객체 직렬화를 사용하지 않는 이유
- 버전 관리의 어려움
- 클래스 구조가 변경되면 이전에 직렬화된 객체와의 호환성 문제가 발생한다.
- serialVersionUID 관리가 복잡하다.
- 플랫폼 종속성
- 자바 직렬화는 자바 플랫폼에 종속적이어서 다른 언어나 시스템과의 상호 운용성이 떨어진다.
- 성능 이슈
- 직렬화/역직렬화 과정이 상대적으로 느리고 리소스를 많이 사용한다.
- 유연성 부족
- 직렬화된 형식을 커스터마이즈하기 어렵다.
- 크기 효율성
- 직렬화된 데이터의 크기가 상대적으로 크다.
- 버전 관리의 어려움
Reader / Writer
스트림의 모든 데이터는 byte 단위를 사용한다. 따라서 byte가 아닌 문자를 스트림에 '직접' 전달할 수는 없다. 예를 들어서 String 문자를 스트림을 통해 파일을 저장하려면 String을 getBytes(Charset)
을 통해 byte로 인코딩한 다음 저장해야 한다. 반대로 읽을 때도 byte로 전달받기 때문에 new String(byte[], Charset)
을 통해 읽어들인 byte[]
를 디코딩해야 한다.
자바는 byte를 다루는 I/O 클래스와 문자를 다루는 I/O 클래스를 둘로 나누어두었다.
- byte를 다루는 클래스 =>
InputStream
,OutputStream
의 자식- 부모 클래스의 기본 기능도 byte 단위를 다룬다.
- 종류
- FileInputStream / FileOutputStream
- ByteArrayInputStream / ByteArrayOutputStream
- ObjectInputStream / ObjectOutputStream
- PipedInputStream / PipedOutputStream
- FilterInputStream / FilterOutputStream
- 문자를 다루는 클래스 =>
Reader
,Wrtier
의 자식- 부모 클래스의 기본 기능은 String, char 같은 문자를 다룬다.
- 종류
- InputStreamReader / OutputStreamWriter
- 기본 스트림과 인코딩 할 문자 집합을 함께 전달해주어야 한다
- FileReader / FileWriter
- 파일 경로와 인코딩 할 문자 집합을 함께 전달해주어야 한다.
- CharrArrayReader / CharrArrayWriter
- FilterReader / FilterWriter
- PipedReader / PipedWriter
- StringReader / StringWriter
- BufferedReader / BufferedWriter
- 버퍼 보조 기능을 제공하는 클래스로, 반드시 기본 Reader, Writer를 전달해주어야 한다.
- 문자를 한 줄(라인) 단위로 읽는 메소드(
br.readLine()
)를 제공해서 이를 위해 주로 사용
- InputStreamReader / OutputStreamWriter
- Reader 계열의 멤버 메소드
int read()
int read(char buff[])
int read(char buff[], int offset, int length)
- Writer 계열의 멤버 메소드
void write(int c)
void write(char buff[])
void write(char buff[], int offset, int length)
중요한건, 모든 데이터는 byte 단위(숫자)로 저장된다. 따라서 Writer가 아무리 문자를 다룬다고 해도, 문자를 바로 저장하는 것이 아니라, 내부적으로 지정된 문자 집합을 사용해서 문자를 byte로 인코딩해서 저장한다.
정리
- 기본(기반, 메인) 스트림
- File, 메모리, 콘솔등에 직접 접근하는 스트림
- 단독으로 사용할 수 있음
- 예)
FileInputStream
,FileOutputStream
,FileReader
,FileWriter
,ByteArrayInputStream
,ByteArrayOutputStream
- 예)
- 보조 스트림
- 기본 스트림을 도와주는 스트림
- 단독으로 사용할 수 없음, 반드시 대상 스트림이 있어야함
- 예)
BufferedInputStream
,BufferedOutputStream
,InputStreamReader
,OutputStreamWriter
,DataOutputStream
,DataInputStream
,PrintStream
- 예)
'Java' 카테고리의 다른 글
[Java] 자바 기본 개념 정리 (기본 문법 제외) (2) | 2025.01.02 |
---|---|
[Java] Garbage Collection (3) | 2025.01.02 |
[Java] JVM / JRE / JDK (3) | 2025.01.01 |
[Java] Annotation (0) | 2024.10.10 |
[Java] Reflection (0) | 2024.10.10 |