[Java] I/O 스트림
I/O 스트림
데이터의 흐름을 스트림(Stream)이라고 한다. 자바에서 입출력 대상에 상관없이 동일한 방법으로 입출력을 할 수 있도록 I/O 스트림 모델이라는 것을 정의했다. 자바 I/O 모델의 스트림을 I/O 스트림이라고 한다. 스트림 관련 클래스들은 java.io 패키지에 정의되어 있다.
I/O 스트림은 크게 입력 스트림(Input Stream)과 출력 스트림(Output Stream)으로 나뉜다. 입력 스트림은 실행 중인 자바 프로그램으로 데이터를 읽어 들이는 스트림이고, 출력 스트림은 실행 중인 자바 프로그램으로부터 데이터를 내보내는 스트림이다.
/** try-catch-finally **/
OutputStream out = null;
try {
out = new FileOutputStream("test.txt"); // 생성
out.write(7); // 사용
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close(); // 종료
}
} catch (IOException e) {
e.printStackTrace();
}
}
/** try-with-resource **/
try (OutputStream out = new FileOutputStream("test.txt")) {
out.write(7);
} catch (IOException e) {
e.printStackTrace();
}
I/O 스트림은 스트림의 생성, 사용, 종료 및 소멸의 순서로 사용한다. try-with-resource 문을 사용하면 안정적인 close 메서드 호출이 보장되므로 코드가 훨씬 간결해진다.
바이트 스트림
가장 기본적인 데이터의 입출력 단위는 바이트이다. 바이트 단위로 데이터를 입출력하는 스트림을 바이트 스트림이라고 한다.
public abstract class InputStream implements Closeable {
...
public abstract int read() throws IOException;
public int read(byte b[]) throws IOException { ... }
public int read(byte b[], int off, int len) throws IOException { ... }
...
}
public abstract class OutputStream implements Closeable, Flushable {
...
public abstract void write(int b) throws IOException;
public void write(byte b[]) throws IOException { ... }
public void write(byte b[], int off, int len) throws IOException { ... }
...
}
InputStream 은 바이트 기반 입력 스트림의 최상위 클래스인 추상클래스이다. 주요 메서드로 데이터를 읽어들이는 read 가 있다. read() 메서드는 읽어 들인 1 바이트의 유효한 데이터에 3바이트의 0을 채워 4바이트형 int(0 ~ 255)를 반환하고, 스트림의 끝에 도착해 읽어들일 데이터가 없는 경우 -1을 반환한다. byte 배열을 매개변수로 받는 read 메서드를 사용하면, 데이터를 읽은 후 배열에 저장해 많은 양의 데이터를 읽을 수 있다.
OutputStream 은 바이트 기반 출력 스트림의 최상위 클래스인 추상클래스이다. 주요 메서드로 데이터를 저장하는 write 가 있다. write(int b) 메서드는 인자로 전달되는 int형 데이터의 첫 번째 바이트만을 저장한다. byte 배열을 매개변수로 받는 write 메서드를 사용하면, 배열의 데이터를 저장해 많은 양의 데이터를 쓸 수 있다.
InputStream 과 OutputStream을 상속하는 클래스는 반드시 추상메서드 read() 와 write(int b) 를 구현해야 한다.
InputStream 을 상속하는 대표적인 클래스로 FileInputStream 이 있고, OutputStream 을 상속하는 대표적인 클래스로 FileOutputStream 이 있다.
보조 스트림/필터 스트림
입출력 대상과 연결을 위한 클래스가 아닌 입출력 스트림에 덧붙여 데이터를 조합, 가공 및 분리하는 역할을 하는 클래스이다. 보조 스트림의 상위 클래스로 FilterInputStream 과 FilterOutputStream 이 있다. 보조 스트림 클래스의 인스턴스 생성 시 매개변수로 각각 InputStream 과 OutputStream 을 받는다.
try (DataInputStream in = new DataInputStream(new FileInputStream("test.txt"))) {
int num1 = in.readInt();
double num2 = in.readDouble();
} catch (IOException e) {
e.printStackTrace();
}
try (DataOutputStream out = new DataOutputStream(new FileOutputStream("test.txt"))) {
out.writeInt(100);
out.writeDouble(3.14);
} catch (IOException e) {
e.printStackTrace();
}
DataInputStream 과 DataOutputStream 은 기본 자료형 데이터의 입력과 출력을 위한 보조 스트림이다.
DataInputStream 의 주요 메서드들로 readXXX 메서드 들이 있다. XXX에는 자바의 기본 자료형인 char, int, double 등이 들어간다. 기본 자료형의 읽기를 위한 메서드들이다.
DataOutputStream 의 주요 메서드들로 writeXXX 메서드 들이 있다. XXX에는 자바의 기본 자료형인 char, int, double 등이 들어간다. 기본 자료형의 쓰기를 위한 메서드들이다.
자료형을 결정해서 데이터를 입출력하는 경우에는 반드시 그 순서를 지켜야 한다.
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("test.txt"))) {
int num = in.read();
} catch (IOException e) {
e.printStackTrace();
}
try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("test.txt"))) {
out.write(100);
} catch (IOException e) {
e.printStackTrace();
}
BufferedInputStream 과 BufferedOutputStream 은 버퍼링 기능을 제공하는 보조 스트림이다. 버퍼 스트림은 내부에 버퍼(메모리 공간)를 가진다. 입출력 대상으로부터 데이터를 읽고 쓸 때 보다 메모리에서 데이터를 읽고 쓸 때 속도가 더 빠르므로, 버퍼 스트림은 입출력 스트림보다 좋은 성능을 가진다.
버퍼 출력 스트림 사용 시 버퍼를 비워주어야 할 때, 즉 출력 대상으로 데이터를 보내야 할 때 메서드 flush 를 사용한다. flush를 자주 사용하면 버퍼링을 통한 성능 향상에 방해가 되므로 제한적으로 호출하는 것이 좋다. 그리고 스트림 종료 시 버퍼는 자동으로 비워진다.
FileOutputStream out = new FileOutputStream("test.txt");
BufferedOutputStream bfOut = new BufferedOutputStream(out);
DataOutputStream dBfOut = new DataOutputStream(bfOut);
필요에 따라 여러개의 필터를 연결할 수 있다. 위의 코드를 보면 BufferedOutputStream 과 DataOutputStrea 을 연결해 파일을 대상으로 버퍼링 기능을 갖는 스트림을 생성하여 기본 자료형 데이터를 저장할 수 있다.
참고로 이러한 기능이 가능한 것은 보조 스트림이 데코레이터 패턴(Decorator Pattern)을 구현하기 때문이다.
문자 스트림
문자 단위로 입출력하는 스트림을 문자 스트림이라고 한다. 자바는 모든 문자를 유니코드를 기준으로 표현하고 운영체제는 문자 표현방식(인코딩 방식)이 운영체제마다 다르다. 문자 스트림을 사용하면 입출력 시 인코딩 방식을 바꿔준다. 문자 입력 스트림은 데이터를 읽어들일 때 유니코드 기반으로 문자의 인코딩을 변경하고, 문자 출력 스트림은 데이터를 쓸 때 운영체제가 사용하는 인코딩 방식으로 문자의 인코딩을 변경한다.
public abstract class Reader {
...
public int read() throws IOException { ... }
public int read(char[] cbuf) throws IOException { ... }
public abstract int read(char[] cubf, int off, int len) throws IOException { ... }
public int read(CharBuffer target) throws IOException { ... }
...
}
public abstract class Writer {
...
public void write(int c) throws IOException { ... }
public void write(char[] cbuf) throws IOException { ... }
public void write(char[] cbuf, int off, int len) throws IOException { ... }
public void write(String str) throws IOException { ... }
public void write(String str, int off, int len) throws IOException { ... }
...
}
Reader 와 Writer 클래스는 문자 스트림의 최상위 추상 클래스이다. Reader 의 read 메서드는 문자를 읽어 반환한다. 반환형이 int 이고 오버로딩된 메서드마다 반환되는 값이 다르므로 주의해야 한다. Writer 의 write 메서드는 문자를 입력한다.
try (Reader in = new FileReader("test.txt")) {
int ch;
while(true) {
ch = in.read();
if(ch == -1)
break;
System.out.print((char)ch);
}
} catch (IOException e) {
e.printStackTrace();
}
try (Writer out = new FileWriter("test.txt")) {
for(int ch = (int)'A'; ch < (int)('Z' + 1); ch++)
out.write(ch);
} catch (IOException e) {
e.printStackTrace();
}
Reader 를 상속하는 대표적인 클래스로 FileReader 가 있고, Writer 를 상속하는 대표적인 클래스로 FileWriter 가 있다.
BufferedReader bfr = new BufferedReader(new FileReader("test.txt"));
BufferedWriter bfr = new BufferedWriter(new FileWriter("test.txt"));
public String readLine() throws IOException { ... }
public void newLine() thrwos IOException { ... }
BufferedReader 와 BufferedWriter 는 문자 스트림에 버퍼링 기능을 제공하는 보조 스트림이다. BufferedReader 클래스에는 read 메서드 이외에 한 줄을 읽어 반환하는 readLine 메서드가 있다. BufferedWriter 클래스에는 write 메서드 이외에 줄 바꿈 문자를 삽입하는 newLine 메서드가 있다.
직렬화와 역직렬화
바이트 스트림을 통해 인스턴스를 통째로 저장하고 꺼내는 것이 가능하다. 인스턴스를 통째로 저장하는 것을 객체 직렬화(Object Serialization) 라 하고, 저장된 인스턴스르 꺼내는 것을 객체 역 직렬화(Object Deserialization) 이라 한다. 자바 직렬화는 자바 시스템 간 데이터 교환을 위해 존재하며 인스턴스를 영속화(Persistence) 할 때 사용된다.
public class SBox implements java.io.Serializable {
String s;
public SBox(String s) { this.s = s; }
public String get() { return s; }
}
SBox box = new SBox("Robot");
try(ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("Object.bin"))) {
oo.writeObject(box);
} catch(IOException e) {
e.printStackTrace();
}
try(ObjectinputStream oi = new ObjectinputStream(new FileInputStream("Object.bin"))) {
SBox box = (SBox) oi.readaObject();
} catch(ClassNotFoundException e) {
e.printStackTrace();
} catch(IOException e) {
e.printStackTrace();
}
ObjectInputStream 을 사용하여 객체 직렬화를 할 수 있다. 직렬화 대상이되는 인스턴스의 클래스는 java.io.Serializable 을 구현해야 한다. 참고로 Serializable 은 마커 인터페이스로 구현해야할 메서드는 없다. writeObject 메서드를 사용하여 인스턴스를 직렬화 한다. 다양한 writeXXX 메서드가 있어 기본 자료형 데이터도 직렬화 할 수 있다.
ObjectOutputStream 을 사용하여 객체 역 직렬화를 할 수 있다. 역직렬화를 하려면 직렬화 대상이 된 객체의 클래스가 클래스 패스에 존재하며 import 되어 있어야 한다. 또, 자바 직렬화 대상 객체는 동일한 serialVersionUID 를 가지고 있어야 한다. readObject 메서드를 사용하여 인스턴스를 역 직렬화한다. 다양한 readXXX 메서드가 있어 기본 자료형 데이터도 역직렬화 할 수 있다.
public class SBox implements java.io.Serializable {
String s; // 함께 저장
public SBox(String s) { this.s = s; }
public String get() { return s; }
}
public class SBox implements java.io.Serializable {
transient String s; // 저장 안됨
public SBox(String s) { this.s = s; }
public String get() { return s; }
}
인스턴스를 저장하면 인스턴스 변수가 참조하는 인스턴스까지 함께 저장이 된다. 이때, 참조하는 인스턴스의 클래스도 Serializable을 구현해야 한다. transient 선언을 추가하면 참조하는 인스턴스는 저장하지 않는다. 그리고 복원 시 해당 참조변수는 null로 초기화된다. 기본 자료형 변수에도 transient 선언을 할 수 있으며, 복원 시 0으로 초기화된다.
참조
윤성우의 열혈 JAVA 프로그래밍
https://docs.oracle.com/javase/9/docs/api/index.html?overview-summary.html
https://techblog.woowahan.com/2550/