다시 하자 기초! 입출력 (I/O)
입출력이란?
I/O란 input과 output의 약자로 입력과 출력, 간단히 줄여서 입출력 또는 아이오라고 한다. 즉, 컴퓨터 내부 또는 외부와의 장치와 프로그램간의 데이터를 주고 받는 것을 말한다.
스트림(stream)
자바에서 입출력을 수행하려면, 어느 한쪽에서 다른 쪽으로 데이터를 전달하려면, 두 대상을 연결하고 데이터를 전송할 수 있는 무언가가 필요한데 이것을 스트림이라고 정의했다. 입출력에서의 스트림은 '람다와 스트림'에서 같은 용어를 쓰지만 다른 개념이다.
스트림이란 데이터를 운반하는데 사용되는 연결통로이다.
스트림이란 연속적인 데이터의 흐름을 물에 비유해서 붙여진 이름인데, 여러 가지로 유사한 점이 많다. 물이 한쪽 방향으로만 흐르는 것과 같이 스트림은 단방향통신만 가능하기 때문에 하나의 스트림으로 입력과 출력을 동시에 처리가 불가하다. 그래서 입력과 출력을 동시에 하려면 입력스트림과 출력스트림을 각각 만들어 총 2개의 스트림이 필요하다.
스트림은 먼저 보낸 데이터를 먼저 받게 되어 있으며 중간에 건너뜀이 없이 연속적으로 데이터를 주고 받는다. Queue와 같은 FIFO구조로 되어 있다고 생각하면 이해가 쉽다.
바이트기반 스트림
스트림은 바이트단위로 데이터를 전송하며 입출력 대상에 따라 다음과 같은 입출력스트림이 있다.
입력스트림 | 출력스트림 | 입출력 대상의 종류 |
FileInputStream | FileOutputStream | 파일 |
ByteArrayInputStream | ByteArrayOutputStream | 메모리(byte배열) |
PipedInputStream | PipedOutputStream | 프로세스(프로세스간의 통신) |
AudioInputStream | AudioOutputStream | 오디오장치 |
이들은 모두 InputStream, OutputStream의 자손들이며 각각 읽고 쓰는데 필요한 추상메서드를 자신에 맞게 구현해 놓음 자바에서는 java.io패키지를 통해서 많은 종류의 입출력관련 클래스들을 제공하고 있으며, 입출력을 처리할 수 있는 표준화된 방법을 제공함으로써 입출력의 대상이 달라져도 동일한 방법으로 입출력이 가능하기 때문에 프로그래밍을 하기에 편리하다.
InputStream | OutputStream |
abstract int read() | abstract void write(int b) |
int read(byte[] b) | void write(byte[] b) |
int read(byte[] b, int off, int len) | void write(byte[] b, int off, int len) |
위의 메서드의 사용법만 잘 알고 있어도 데이터를 읽고 쓰는 것은 입출력 대상의 종류에 상관없이 아주 간단한 일이 될 것이다. read()와 write(int b)는 입출력 대상에 따라 읽고 쓰는 방법이 다를 것이기 때문에 각 상황에 맞게 구현하라는 의미로 추상메서드로 정의되어 있다.
read()와 write(int b)를 제외한 나머지 메서드들은 추상메서드가 아니니까 굳이 추상메서드인 read()와 write(int b)를 구현하지 않아도 이들을 사용하면 될 것이라 생각할 수도 있겠지만 그 둘을 이용해서 만든 메서드들이라 추상메서드는 반드시 구현을 해줘야 나머지 메서드들도 사용이 가능하다.
보조스트림
보조스트림은 실제 데이터를 주고받는 스트림이 아니다. 때문에 데이터를 입출력할 수 있는 기능은 없지만, 스트림의 기능을 향상시키거나, 새로운 기능을 추가할 수 있다. 그래서 보조 스트림만으로는 입출력을 처리할 수 없고, 스트림을 먼저 생성한 다음에 이를 이용해서 보조스트림을 생성해야한다.
예를 들어 test.txt라는 파일을 읽기위해 FileInputStream을 사용할 때, 입력 성능을 향상시키기 위해 버퍼를 사용하는 보조스트림인 BufferedInputStream을 사용하는 코드는 다음과 같다.
//먼저 기반스트림을 생성한다.
FileInputStream fis = new FileInputStream("test.txt");
//기반스트림을 이용해서 보조스트림을 생성
BufferedInputStream bis = new BufferedInputStream(fis);
//보조스트림인 BufferedInputStream으로부터 데이터를 읽는다.
bis.read();
코드 상으론 보조스트림이 입력기능을 수행하는 것처럼 보이지만, 실제 입력기능은 FileInputStream이 수행하고, 보조스트림은 버퍼만 제공한다. 버퍼를 사용한 입출력과 사용하지 않은 입출력간의 성능차이는 상당하기 때문에 대부분 버퍼를 이용한 보조스트림을 사용한다.
입력 | 출력 | 설명 |
FilterInputStream | FilterOutputStream | 필터를 이용한 입출력 처리 |
BufferedInputStream | BufferedOutputStream | 버퍼를 이용한 입출력 성능향상 |
DataInputStream | DataOutputStream | int, float와 같은 기본형 단위로 데이터를 처리하는 기능 |
SequenceInputStream | 없음 | 두 개의 스트림을 하나로 연결 |
LineNumberInputStream | 없음 | 읽어 온 데이터의 라인 번호를 카운트 |
ObjectInputStream | ObjectOutputStream | 데이터를 객체단위로 읽고 쓰는데 사용 주로 파일을 이용하며 객체 직렬화와 관련있음 |
없음 | PrintStream | 버퍼를 이용하며, 추가적인 print관련 |
PushbackInputStream | 없음 | 버퍼를 이용해서 읽어 온 데이터를 다시 되돌리는 기능 |
바이트기반의 스트림
문자기반 스트림 - Reader, Writer
지금까지 알아본 스트림은 모두 바이트기반의 스트림이였다. 바이트기반이라 함은 입출력 단위가 1byte라는 뜻이다. C언어와 달리 Java에서는 한 문자를 의미하는 char형이 1byte가 아니라 2byte이기 때문에 바이트기반의 스트림으로 2byte인 문자를 처리하는데 어려움이 있었다. 이점을 보완하기 위해서 문자기반의 스트림이 제공된다. 문자데이터를 입출력 할 때는 바이트기반 스트림 대신 문자기반 스트림을 사용하자.
InputStream ---------> Reader
OutputStream ----------> Writer
바이트기반 스트림 | 문자기반 스트림 |
FileInputStream FileOutputStream |
FileReader FileWriter |
ByteArrayInputStream ByteArrayOutputStream |
CharArrayReader CharArrayWriter |
PipedInputStream PipedOutputStream |
PipedReader PipedWriter |
StringBufferInputStream StringBufferOutputStream |
StringReader StringWriter |
바이트기반 스트림과 문자기반 스트림은 이름만 조금 다를 뿐 활용법은 거의 같다.
보조스트림 역시 다음과 같은 문자기반 보조스트림이 존재하며 사용목적과 방식은 바이트기반 보조스트림과 같다.
바이트기반 보조스트림 | 문자기반 보조스트림 |
BufferedInputStream BufferedOutputStream |
BufferedReader BufferedWriter |
FilterInputStream FilterOutputStream |
FilterReader FilterWriter |
LinNumberInputStream | LineNumberReader |
PrintStream | PrintWriter |
PushbackInputStream | PushbackReader |
InputStream과 OutputStream
앞서 얘기한 바와 같이 InputStream과 OutputStream은 모든 바이트기반의 스트림의 조상이며 다음과 같은 메서드가 선언되어 있다.
스트림의 종류에 따라서 mark()와 reset()을 사용하며 이미 읽은 데이터를 되돌려서 다시 읽을 수 있다. 이 기능을 지원하는 스트림인지 확인하는 markSuppoprted()를 통해서 알 수 있다. flush()는 버퍼가 있는 출력스트림의 경우에만 의미가 있으며, OutputStream에 정의된 flush()는 아무런 일도 하지 않는다.
프로그램이 종료될 때, 사용하고 닫은 않은 스트림을 JVM이 자동적으로 닫아 주기는 하지만, 스트림을 사용해서 모든 작업을 마치고 난 후에는 close()를 호출해서 반드시 닫아주어야 한다.
class IOEx4 {
public static void main(String[] args) {
byte[] inSrc = {0,1,2,3,4,5,6,7,8,9};
byte[] outSrc = null;
byte[] temp = new byte[4];
ByteArrayInputStream input = null;
ByteArrayOutputStream output = null;
input = new ByteArrayInputStream(inSrc);
output = new ByteArrayOutputStream();
try{
while(input.available() > 0) {
int len = input.read(temp); //읽어 온 데이터의 개수를 반환한다.
output.write(temp, 0, len); //읽어 온 만큼만 writer한다.
}
} catch(IOException e) {}
outSrc = output.toByteArray();
System.out.println("Input Source :" + Arrays.toString(inSrc));
System.out.println("temp :" + Arrays.toString(temp));
System.out.println("Output Source :" + Arrays.toString(outSrc));
}
}
실행 결과
Input Stream : [0,1,2,3,4,5,6,7,8,9]
temp : [8,9,6,7]
Output Stream : [0,1,2,3,4,5,6,7,8,9]
FileInputStream과 FilOutputStream
생성자 | 설명 |
FileInputStream(String name) | 지정된 파일이름을 가진 실제 파일과 연결된 FileInputStream을 생성한다. |
FileInputStream(File file) | 파일의 이름이 String이 아닌 File인스턴스로 지정해주어야 하는 점을 제외하고 FileInputStream(String name)과 같다. |
FileInputStream(FileDescriptor fdObj) | 파일 디스크립터(fdObj)로 FileInputStream을 생성한다. |
FileOutputStream(String name) | 지정된 파일이름을 가진 실제 파일과의 연결된 FileOutputStream을 생성한다. |
FileOutputStream(String name, boolean append) | 지정된 파일이름을 가진 실제 파일과 연결된 FileOutputStream을 생성한다. 두번째 인자인 append를 true로 하면, 출력 시 기존의 파일내용의 마지막에 덧붙인다. false면, 기존의 파일내용을 덮어쓰게 된다. |
FileOutputStream(File file) | 파일의 이름을 String이 아닌 File인스턴스로 지정해주어야 하는 점을 제외하고 FileOutputStream(String name)과 같다. |
FileOutputStream(File file, boolean append) | 파일의 이름을 String이 아닌 File인스턴스로 지정해주어야하는 점을 제외하고 FileOutputStream(String name, boolean append)과 같다. |
FileOutputStream(FileDescriptor fdObj) | 파일 디스크립터로 FileOutputStream을 생성한다. |
FileCopy.java 파일의 내용을 그대로 FileCopy.bak로 복사하는 일을 할 때, 단순히 FileCopy.java의 내용을 read()로 읽어서 write(int b)로 FileCopy.bak에 출력한다. 이처럼 텍스트파일을 다루는 경우에는 문자기반이 더 좋다.
바이트기반의 보조스트림
FilterInputStream과 FilterOutputStream
protected FilterInputStream(InputStream in)
public FilterOutputStream(OutputStream out)
하지만 이 자체로는 아무런 일도 하지 않기에 상속을 통해 원하는 작업을 수행하도록 메소드를 오버라이딩해야 한다.
public class FilterInputStream extends InputStream {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.read();
}
...
}
생성자 FilterInputStream(InputStream in)는 접근제어자가 protected이기 때문에 인스턴스를 생성해서 사용할 수 없고 상속을 통해서 오버라이딩되어야 한다.
BufferedInputStream과 BufferedOutputStream
생성자 | 설명 |
BufferedInputStream(InputStream in, int size) | 주어진 InputStream인스턴스를 입력소스로 하며 지정된 크기의 버퍼를 갖는 BufferedInputStream인스턴스를 생성 |
BufferedInputStream(InputStream in) | 버퍼의 크기를 지정해주지 않으므로 기본적으로 8192byte크기의 버퍼를 갖게 된다. |
버퍼크기는 입력소스로부터 한 번에 가져올 수 있는 데이터의 크기로 지정하면 좋다. 보통 입력소스가 파일인 경우 4096정도의 크기로 하는 것이 보통이며, 버퍼의 크기를 변경해가면서 테스트하면 최적의 버퍼크기를 알아낼 수 있다. 프로그램에서 입력소스로부터 데이터를 읽기 위해 처음으로 read메서드를 호출하면, BufferedInputStream은 입력소스로 부터 버퍼 크기만큼의 데이터를 읽어다 자신의 내부 버퍼에 저장한다. 이제 프로그램에서는 BufferedInputStream의 버퍼에 저장된 데이터를 읽으면 되는 것이다. 외부의 입력소스로 부터 읽는 것보다 내부의 버퍼로 부터 읽는 것이 훨씬 빠르기 때문에 그만큼 작업 효율이 높아진다.
메서드/생성자 | 설명 |
BufferedOutputStream(OutputStream out, int size) | 주어진 OutputStream인스턴스를 출력소스로 하며 지정된 크기의 버퍼를 갖는 인스턴스를 생성한다. |
BufferedOutputStream(OutputStream out) | 버퍼 크기가 8192byte크기로 생성된다. |
flush() | 버퍼의 모든 내용을 출력소스에 출력한 다음, 버퍼를 비운다. |
close() | flush()를 호출해서 버퍼의 모든 내용을 출력소스에 출력하고 인스턴스가 사용하던 모든 자원을 반환한다. |
입력소스로부터 데이터를 읽을 때와는 반대로, 프로그램에서 write메서드를 이용한 출력이 BufferedOutputStream의 버퍼에 저장된다. 버퍼가 가득차면, 그 때 버퍼의 모든 내용을 출력한다. 그리고 버퍼를 비우고 다시 출력을 저장할 준비함.버퍼가 가득 찼을 때만 출력소스에 출력을 하기 때문에, 마지막 출력부분이 출력소스에 쓰이지 못하고 버퍼에 남아 있는채로 종료될 수 있기 때문에 close()나 flush()를 호출해서 마지막에 버퍼에 있는 모든 내용이 출력소스에 출력되도록 해야한다.
DataInputStream과 DataOutputStream
위 방식의 장점은 데이터를 읽고 쓰는데 있어서 byte단위가 아닌, 8가지 기본 자료형의 단위로 읽고 쓸 수 있다는 점이다. DataOutputStream이 출력하는 형식은 각 기본 자료형 값을 16진수로 표현하여 저장한다. 예를 들어 int값을 출력한다면, 4byte의 16진수로 출력된다. 각 자료형의 크기가 다르므로, 출력한 데이터를 다시 읽어올 때는 출력했을 때의 순서를 염두 해야한다.
SequenceInputStream
여러 개의 입력스트림을 연속적으로 연결해서 하나의 스트림으로부터 데이터를 읽는 것과 같이 처리할 수 있도록 도와준다. 생성자를 제외하고 나머지 작업은 다른 입력스트림과 다르지 않다. 큰 파일을 여러 개의 작은 파일로 나누었다가 하나의 파일로 합치는 것과 같은 작업을 수행할 때 사용하면 좋다.
메서드/생성자 | 설명 |
SequenceInputStream(Enumeration e) | Enumeration에 저장된 순서대로 입력스트림을 하나의 스트림으로 연결한다. |
SequenceInputStream(InputStream s1,InputStream s2) | 두 개의 입력스트림을 하나로 연결한다. |
PrintStream
데이터를 기반스트림에 다양한 형태로 출력할 수 있는 print, println, printf와 같은 메서드를 오버라이딩하여 제공한다. PrintStream은 데이터를 적절한 문자로 출력하는 것이기 때문에 문자기반 스트림의 역할을 수행한다. 그래서 JDK1.1부터 좀더 향상된 PrintWriter가 추가되었으나 그 동안 매우 빈번하게 쓰인 System.out이 PrintStream기반이라 둘 다 사용한다. 하지만 PrintWriter가 더 성능이 좋기 때문에 가능하면 이걸 쓰는게 좋다.
문자기반 스트림
Reader와 Writer
문자기반 스트림이라는 것은 단순히 2byte로 스트림을 처리하는 것만을 의미하지않고 문자 데이터를 다루는데 필요한 또 하나의 정보는 인코딩(encoding)이다. 문자기반 스트림은 여러 종류의 인코딩과 자바에서 사용하는 유니코드간의 변환을 자동적으로 처리해준다. Reader는 특정 인코딩을 읽어서 유니코드로 변환하고 Writer는 유니코드를 특정 인코딩으로 변환하여 저장한다.
PipedReader와 PipedWriter
쓰레드 간에 데이터를 주고 받을 때 사용된다. 다른 스트림과는 달리 입력과 출력스트림을 하나의 스트림으로 연결해서 데이터를 주고 받는다. 스트림을 생성한 다음에는 어느 한쪽 쓰레드에서 connet()를 호출해서 입력스트림과 출력스트림을 연결한다. 입출력을 마친 후에는 어느 한쪽 스트림만 닫아도 나머지 스트림은 자동으로 닫힌다. 이 점을 제외하면 일반 입출력과 똑같다.
StringReader와 StringWriter
근본적으로는 String도 char배열이지만, 아무래도 char배열보다는 String으로 처리하는게 여러모로 편리한 경우가 더 많을 것이다.
StringBuffer getBuffer() StringWriter에 출력한 데이터가 저장된 StringBuffer를 반환한다.
String toString() StringWriter에 출력한 (StringBuffer에 저장된) 문자열을 반환한다.
InputStreamReader와 OutputStreamWriter
이름에서 알 수 있듯이 바이트기반 스트림을 문자기반 스트림으로 연결시켜주는 역할을 한다. 그리고 바이트기반 스트림의 데이터를 지정된 인코딩의 문자데이터로 변환하는 작업을 수행한다.
생성자/메서드 | 설명 |
InputStreamReader(InputStream in) | OS에서 사용하는 기본 인코딩의 문자로 변환하는 InputStreamReader를 생성한다. |
InputStreamReader(InputStream in, String encoding) | 지정된 인코딩을 사용하는 인스턴스를 생성한다. |
String getEncoding() | 인코딩을 알려준다. |
생성자/메서드 | 설명 |
OutputStreamWriter(OutputStream in) | OS에서 사용하는 기본 인코딩의 문자로 변환하는 인스턴스를 생성한다. |
OutputStreamWriter(OutputStream in, String encoding) | 지정된 인코딩을 사용하는 인스턴스를 생성한다. |
String getEncoding() | OutputStreamWriter의 인코딩을 알려준다. |