자바에서 입출력을 수행하려면, 두 대상을 연결하고 데이터를 전송할 수 있는 스트림이 필요하다. 이 전글의 모든 스트림과 같은 용어를 쓰지만 다른 개념이다.
※ 스트림은 Tv와 DVD를 연결하는 입력선, 출력선등과 같은 역할을 한다.
스트림이란 데이터를 운반하는데 사용되는 연결통로이다.
스트림은 물이 한쪽 방향으로만 흐르는 것과 같이 단방향 통신만 가능하다. 따라서 하나의 스트림으로 입,출력을 동시에 처리할 수 없다.
그래서 입력을 위한 입력스트림과 출력을 위한 출력스트림, 모두 2개의 스트림이 필요하다.
스트림은 먼저 보낸 데이터를 먼저 받게 돼 있으며 중간에 건너뜀 없이 연속적으로 데이터를 주고 받는다. Queue와 같은 FIFO(First In First Out)구조로 되어 있다고 생각하면 이해하기 쉬울 것이다.
스트림은 바이트 단위로 데이터를 전송하며 입출력 대상에 따라 다음과 같은 입출력 스트림이 존재한다.
어떠한 대상에 대해 작업을 할 것인지, 입력을 할 것인지, 출력을 할 것인지에 따라 맞는 스트림을 선택해서 사용하면 된다.
이들은 모두 InputStream 또는 OutputStream의 자손이며 각각에 필요한 추상메서드를 자신에 맞게 구현해 놓았다.
자바에서는 java.io패키지를 통해 많은 종류의 입출력관련 클래스들을 제공하며, 이를 처리할 수 있는 표준화된 방법을 제공함으로써 입출력의 대상이 달라져도 동일한 방법으로 입출력이 가능하게끔 만들었다.
※ read()의 반환타입이 byte가 아닌 int인 이유는, read()의 반환값 범위가 0~255와 -1이기 때문.
InputStream의 read()와 OutputStream의 write(int b)는 입출력의 대상에 따라 읽고 쓰는 방법이 다르기에 추상메서드로 정의돼있다.
스트림의 기능을 보완하기 위한 보조 스트림이 제공된다. 보조 스트림은 실제 데이터를 주고받지는 않기 때문에 데이터를 입출력할 수는 없지만, 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있다.
예를 들어 test.txt라는 파일을 읽기위한 FileInputStream을 사용할 때, 성능 향상을 위해 버퍼를 이용하는 BufferedInputStream이라는 보조 스트림을 아래와 같이 사용한다.
// 먼저 기반 스트림생성
FileInputStream fis = new FileInputStream("test.txt");
// 보조 스트림 생성
BufferedInputStream bis = new BufferedInputStream(fis);
bis.read(); // 보조 스트림은 BufferedInputStream으로부터 데이터를 읽는다.
코드 상으로는 보조 스트림이 입력 기능을 수행하는 것 처럼 보이지만 실제 입력기능은 매개변수로 넣은 FileInputStream이 수행하고 보조 스트림은 버퍼만을 제공한다.
버퍼를 사용한 입출력은 그렇지 않은 입출력과 성능차이가 상당하기에 대부분의 경우에 버퍼를 이용해 보조 스트림을 사용한다.
BufferedInputStream, DataInputStream, DigestInputStream, LineNumberInputStream, PushbackInputStream은 FilterInputStream의 자손이고 이는 InputStream의 자손이다.
보조 스트림의 종류
지금까지 알아본 스트림은 모두 바이트 기반의 스트림이었다. Java에서는 한 문자, 즉 char형이 c언어와(1byte임)는 다르게 2byte이다. 따라서 바이트 기반의 스트림으로는 2byte를 처리하기 힘들기에 문자기반의 스트림이 따로 제공된다.
InputStream → Reader
OutputStream → Writer
문자기반 스트림에도 따로 보조스트림이 있다.
모든 바이트 기반 스트림의 조상인 Input/OuputStream은 다음과 같은 메서드가 선언돼 있다.
스트림의 종류에 따라 mark()오 reset()을 사용해 이미 읽은 데이터를 되돌려 다시 읽을 수 있다. 사용하기 전에 markSupported()를 통해 스트림이 해당 기능을 지원하는지 확인한다.
flush()는 버퍼가 있는 출력스트림의 경우에만 의미가 있다. OutputStream에 정의된 flush()는 아무런 기능을 하지 않는다.
프로그램이 종료될 때 사용하고 닫지 않은 스트림을 JVM이 자동으로 닫아주나, 스트림을 사용해 모든 작업을 마치고 난 후에는 close()를 호출해 반드시 닫아주자.
그러나 ByteArrayInputStream과 같이 메모리를 사용하는 스트림과, System.in, System.out과 같은 표준 입출력 스트림은 닫지 않아도 된다.
실제 프로그래밍에서 많이 사용되는 스트림 중의 하나이다.
모든 보조 스트림의 조상이다. 보조 스트림은 자체적인 입출력을 수행할 수 없기에 기반 스트림을 필요로 한다.
// 생성자
protected FilterInputStream(InputStream in)
public FilterOutputStream(OutputStream out)
FilterInputStream/FilterOutputStream의 모든 메서드는 단순히 기반 스트림의 메서드를 그대로 호출할 뿐이다. FilterInputStream/FilterOutputStream자체로는 아무런 일도 하지 않음을 의미한다. FilterInput/OutputStream은 상속을 통해 원하는 작업을 수행하도록 읽고 쓰는 메서드를 오버라이딩해야 한다.
public class FilterInputStream extends InputStrema {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.read();
}
...
}
FilterInputStream(InputStream in)은 접근 제어자가 protected이기 때문에 인FilterInputStream의 인스턴스 생성은 불가능하고 오직 상속을 통해서 오버라이딩되어야 한다.
FilterInputStream/FilterOutputStream을 상속받아서 기반 스트림에 보조 기능을 추가한 보조 스트림 클래스는 아래와 같다.
FilterInputStream의 자손
BufferedInputStream, DataInputStream, PushbackInputStream등
FilterOutputStream의 자손
BufferedOutputStream, DataOutputStream, PrintStream 등
BufferedInputStream/BufferedOutputStream은 입출력의 효율을 높이기 위해 버퍼를 사용하는 보조스트림이다. 한 바이트씩 입출력하는 것 보다는 버퍼(바이트배열)를 이용해서 한 번에 여러 바이트를 입출력하는 것이 빠르기 때문에 대부분의 입출력 작업에 사용한다.
BufferedInputStream의 버퍼 크기는 입력소스로부터 한 번에 가져올 수 있는 데이터 크기로 지정하는 것이 좋다. 입력소스가 파일인 경우 보통 8192로 하며, 버퍼의 크기를 변경해가면서 테스트하면 최적의 버퍼크기를 알아낼 수 있다.
BufferedOutputStream 역시 버퍼를 이용해서 출력소스와 작업을 하게 되는데, 프로그램에서 write메서드를 이용한 출력이 BufferedOutputStream의 버퍼에 저장된다. 버퍼가 가득 차면, 그 때 버퍼의 모든 내용을 출력소스에 출력하고 다시 프로그램으로부터의 출력을 저장할 준비를 한다.
버퍼가 가득 찼을 때만 출력소스에 출력을 하기 때문에, 마지막 출력부분이 출력 소스에 쓰이지 못하고 프로그램이 종료될 수 있다는 점을 주의해야한다.
프로그램에서 모든 출력작업을 마친 후 BufferedOutputStream에 close()나 flush()를 호출하여 마지막에 버퍼에 있는 모든 내용이 출력소스에 출력되게 해야 한다.
SequenceInputStream은 여러 개의 입력스트림을 연속적으로 연결하여 하나의 스트림으로부터 데이터를 읽는 것과 같도록 처리하게 도와준다. 생성자를 제외하고 나머지 작업은 다른 입력스트림과 다르지 않다. 큰 파일을 작게 나누었다가 하난의 파일로 다시 합치는 작업등을 수행할 떄 좋다.
※ 다른 보조스트림과는 다르게 FIlterInputStream의 자손이 아닌 InputStream을 바로 상속 받아 구현 함.
위 생성자들을 사용하는 방법은 아래와 같다.
// 예시 1
Vector files = new Vector();
files.add(new FileInputStream("file.001"));
files.add(new FileInputStream("file.002"));
SequenceInputStream in = new SequenceInputStream(files.elements());
// 예시 2
FileInputStream file1 = new FileInputStream("file.001");
FileInputStream file2 = new FileInputStream("file.002");
SequenceInputStream in = new SequenceInputStream(file1, file2);
PrintStream은 데이터를 기반스트림에 다양한 형태로 출력할 수 있는 print, println, printf와 같은 메서드를 오버로딩하여 제공한다.
PrintStream과 PrintWriter는 거의 같은 기능을 하나, Writer가 더욱 다양한 언어의 문자를 처리하는데 적합하기 때문에 가능하면 PrintWriter를 사용하자.
바이트기반 스트림의 조상이 InputStream/OutputStream인 것과 같이 문자기반의 스트림에서는 Reader/Writer가 그와 같은 역할을 한다.
이 두 Reader/Writer를 상속받아 추상 메서드를 구현 해 놓은 클래스들이 다수 있다.
FileReader와 FileWriter는 파일로부터 텍스트 데이터를 읽고, 파일에 쓰는데 사용된다. FileInputStream/FileOutputStream과 사용법이 다르지 않다.
public class FileReaderWriterExample {
public static void main(String[] args) {
try {
String fileName = "test.txt";
FileInputStream fis = new FileInputStream(fileName);
FileReader fr = new FileReader(fileName);
int data = 0;
// FileInputStream을 이용하여 파일 내용을 읽고 화면에 출력함.
while ((data=fis.read()) != -1) {
System.out.print((char)data);
}
System.out.println();
fis.close();
// FileReader를 이용해서 파일 내용을 읽어 화면에 출력함.
while((data = fr.read()) != -1) {
System.out.print((char)data);
}
System.out.println();
fr.close();
} catch (IOException e) {}
}
}
위 예제에서 test.txt에 "Hello / 안녕하세요" 이렇게 두 단어만 적힌 파일이었다면, FileInputStream으로 입력을 한 결과에서는 한글이 깨져있을 것이다.
반면 FileReader로 입력을 한 결과에서는 한글이 정상적으로 출력된다.
StringReader/Writer는 CharArrayReader/Writer와 같이 입출력 대상이 메모리인 스트림이다. StringWriter에 출력되는 데이터는 내부 StringBuffer에 저장되며 StringWriter의 다음과 같은 메서드를 사용해 저장된 데이터를 얻는다.
StringBuffer getBuffer() // StringWriter에 출력한 데이터가 저장된 StringBuffer를 반환
String toString() // StringWriter에 출력된 (StringBuffer에 저장된) 문자열을 반환
근복적으로 String도 char배열이지만, 아무래도 char배열보다는 String으로 처리하는 것이 여러모로 편리한 경우가 더 많다.
--
BufferedReader/Writer는 버퍼를 이용해 입출력의 효율을 높인다. 버퍼 유/무의 입출력 성능차이는 비교도 할 수 없을 정도이기에 무조건 사용하자.
BufferedReader의 readLine()을 사용하면 데이터를 라인단위로 읽을 수 있고,
BufferedWriter는 newLine()이라는 줄바꿈 메서드를 가지고 있다.
public class BufferedReaderWriterExample {
public static void main(String[] args) {
try {
FileReader fr = new FileReader("BufferedReaderWriterExample.java");
BufferedReader br = new BufferedReader(fr);
String line = "";
for(int i=1;(line = br.readLine()) != null; i++) {
// ";"를 포함한 라인을 출력한다.
if(line.indexOf(";") != -1) {
System.out.println(i + ":" + line);
}
}
br.close();
} catch (IOException e) {}
}
}
InputStreamReader/OutputStreamWriter는 이름에서 알 수 있듯 바이트기반 스트림을 문자기반 스트림으로 연결시켜주는 역할을 한다. 그리고 바이트기반 스트림의 데이터를 지정된 인코딩의 문자데이터로 변환하는 작업을 수행한다.
InputStreamReader
OutputStreamReader
한글 윈도우에서 중국어로 작성된 파일을 읽을 때 InputStreamReader(InputStream in, String encoding)을 이용하여 인코딩이 중국어로 되어 있다는 것을 지정해야 파일 내용이 깨지지 않는다.
Properties prop = System.getProperties();
// getProperties()로 시스템의 정보들을 가져온다.
System.out.println(prop.get("sun.jnu.encoding"));
// 시스템의 encoding정보를 가져온다.