I/O
목표
- 자바의 Input과 Ontput에 대해 학습하세요.
학습할 것
- 스트림 (Stream) / 버퍼 (Buffer) / 채널 (Channel) 기반의 I/O
- InputStream과 OutputStream
- Byte와 Character 스트림
- 표준 스트림 (System.in, System.out, System.err)
- 파일 읽고 쓰기
스트림 (Stream) / 버퍼 (Buffer) / 채널 (Channel) 기반의 I/O
스트림(stream)
쉽게 이야기해서 데이터를 운반하는데 필요한 통로입니다.
스트림은 단방향만을 지원하기 때문에 읽기(read)와 쓰기(write)을 동시에 작업하기 위해서는 입력 스트림(input stream)과 출력 스트림(output stream) 2개의 스트림이 필요합니다.
스트림은 바이트 단위로 데이터를 전송하고 여러 스트림들을 지원합니다.
입력스트림 | 출력스트림 | 입출력 대상의 종류 |
---|---|---|
FileInputStream | FileOutputStream | 파일 |
ByteArrayInputStream | ByteArrayOutputStream | 메모리 - byte배열 |
PipedInputStream | PipedOutputStream | 프로세스(프로세스간 통신) |
AudioInputStream | AudioOutputStream | 오디오 장치 |
작업할 대상에 맞추어 알맞는 스트림을 사용할 수 있게끔 자바에서 java.io패키지를 통해 지원하고 있습니다.
버퍼 (Buffer)
위에 언급한 스트림 외에도 스트림의 기능을 보완하기 위해 보조스트림이 제공됩니다.
- 보조스트림은 실제 데이터를 주고받는 스트림이 아니기 때문에 데이터를 입출력할 수 있는 기능이 없습니다.
- 그러나 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있습니다.
- 그래서 스트림을 먼저 생성한 다음에 이를 이용해 보조스트림을 생성해야 합니다.
// 먼저 기반스트림 생성
FileInputStream fis = new FileInputStream("test.txt");
// 기반스트림을 이용해 보조스트림을 생성
BufferedInputStream bis = new BufferedInputStream(fis);
// 보조스트림인 BufferedInputStream으로부터 데이터를 읽음
bis.read();
위에 예제는 text.txt
라는 파일을 읽기 위해 FileInputStream을 사용할 때,
입력 성능을 향상시키기 위해 버퍼를 사용하는 보조스트림인 BufferedInputStream을 사용하는 코드입니다.
- 실제 입력기능은 BufferedInputStream과 연결된 FileInputStream이 수행
- 보조스트림인 BufferedInputStream은 버퍼만을 제공
- 버퍼를 사용한 입출력과 사용하지 않은 입출력간의 성능차이가 크므로, 일반적으로 버퍼를 이용한 보조스트림을 사용
입력 | 출력 | 설명 |
---|---|---|
FilterinputStream | FilterOutputStream | 필터를 이용한 입출력 처리 |
BufferedInputStream | BufferedOutputStream | 버퍼를 이용한 입출력 성능향상 |
DataInputStream | DataOutputStrea | int,float와 같은 기본형 단위(primitive type)로 데이터를 처리하는 기능 |
SequenceInputStream | 없음 | 두 개의 스트림을 하나로 연결 |
LineNumberInputStream | 없음 | 읽어 온 데이터의 라인 번호를 카운트 (JDK1.1부터 LineNumberReader로 대체) |
ObjectInputStream | ObjectOutputStream | 데이터를 객체단위로 읽고 쓰는데 사용. 주로 파일을 이용하며 객체 직렬화와 관련있음 |
없음 | PrintStream | 버퍼를 이요하며, 추가적인 print관련 기능 (print, printf, println메서드) |
PushbackInputStream | 없음 | 버퍼를 이용해서 읽어온 데이터를 다시 되돌리는 기능 |
모든 보조스트림 역시 InputStream과 OutputStream의 자손들이므로 입출력방법이 같습니다.
InputStream과 OutputStream
InputStream과 OutputStream은 모든 바이트기반의 스트림의 조상입니다.
선언된 메서드
InputStream의 메서드
메서드명 | 설명 |
---|---|
int vailable() | 스트림으로부터 읽어 올 수 있는 데이터의 크기를 반환한다. |
void close() | 스트림을 닫음으로써 사용하고 있던 자원을 반환한다. |
void mark(int readlimit) | 현재위치를 표시해 놓는다. 후에 reset()에 의해서 표시해 놓은 위치로 다시 돌아갈 수 있다. |
boolean markSupported() | mark()와 reset()을 지원하는지 알려준다. |
abstract int read() | 1byte를 읽어온다.(0~255사이의 값) 더 이상 읽어올 데이터가 없으면 -1을 반환한다. abstract 이므로 InputStream의 자손들이 상황에 맞게 구현해야 한다. |
int read(byte[] b) | 배열 b의 크기만큼 읽어서 배열을 채우고 읽어 온 데이터의 수를 반환한다. 반환하는 값은 항상 배열의 크기보다 작거나 같다. |
int read(byte[] b, int off, int len) | 최대 len개의 byte를 읽어서, 배열 b의 지정된 위치(off)부터 저장한다. 실제로 읽어 올 수 있는 데이터가 len개보다 적을 수 있다. |
void reset() | 스트림에서의 위치를 마지막으로 mark()이 호출되었던 위치로 되돌린다. |
long skip(long n) | 스트림에서 주어진 길이(n)만큼을 건너뛴다. |
프로그램이 종료될 때, 사용하고 닫지 않은 스트림을 JVM이 자동적으로 닫아주기는 하지만, 스트림을 사용하고 작업을 마친 경우에는 close()를 호출해서 반드시 닫아 주어야 합니다. 다만, ByteArrayInputSream과 같이 메모리를 사용하는 스트림과 System.in, System.out과 같은 표준 입출력 스트림은 닫지 않아도 됩니다.
Byte와 Character 스트림
ByteArrayInputStream 과 ByteArrayOutputStream 예제
스트림의 종류가 달라도 읽고 쓰는 방법은 동일하므로, 예제들을 통해 스트림에 읽고 쓰는 방법을 익히자!
byte[] inSrc = {0,1,2,3,4,5,6,7,8,9};
byte[] outSrc = null;
ByteArrayInputStream input = null;
ByteArrayOutputStream output = null;
input = new ByteArrayInputStream(inSrc);
output = new ByteArrayOutputStream();
int data = 0;
while ((data = input.read()) != -1) {
output.write(data);
}
outSrc = output.toByteArray();
System.out.println("Input Source : " + Arrays.toString(inSrc));
System.out.println("Ouput Source : " + Arrays.toString(outSrc));
결과 출력물을 보면 읽고 쓰기가 성공적으로 되었음을 볼 수 있습니다.
Input Source : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Ouput Source : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
바이트 스트림을 출력해보면 사람이 읽을 수 없는 바이트 값들을 볼 수 있습니다.
System.out.println(outSrc);
// [B@6e8dacdf
바이트배열은 사용하는 자원이 메모리 밖에 없으므로 가비지 컬렉터에 의해 자동적으로 자원을 반환하므로 close()를 이용해서 스트림을 닫지 않아도 됩니다. 그러나 read()와 write(int b)를 사용하기 때문에 한 번에 1byte만 읽고 쓰므로 작업효율이 떨어집니다.
BufferedInputStream과 BufferedoutputStream
스트림의 입출력 효율을 높이기 위해 버퍼를 쓰는 보조스트림입니다.
- 한 바이트씩 입출력하는 것보다 버퍼(바이트 배열)을 이용해서 한 번에 여러 바이트의 입출력을 수행하는 것이 빠르고 효율적입니다.
byte배열을 이용해 버퍼를 직접 구현할 수도 있지만, 버퍼 위에 계속 내용을 덮어쓸 경우, 읽어온 만큼만 출력해야 합니다.
생성자
- BufferedInputStream(InputStream in, int size)
- 주어진 InputStream 인스턴스를 입력소스(input source)로 함
- 지정된 크기(byte 단위)의 버퍼를 갖는 BufferedInputStream 인스턴스를 생성
- BufferedInputStream(InputStream in)
- 주어진 InputStream 인스턴스를 입력소스(input source)로 함
- 버퍼의 크기를 지정해주지 않으므로 기본적으로 8192byte(8k) 크기의 버퍼를 가짐
- 참고 : 버퍼의 크기를 변경해가면서 테스트하면 최적의 버퍼크기를 알아 낼 수 있습니다.
버퍼 동작 순서
- 프로그램에서 입력소스로부터 데이터를 읽기 위해 처음으로 read메서드를 호출
- BufferedInputStream이 입력소스로부터 버퍼 크기만큼의 데이터를 읽어 내부 버퍼에 저장
- 프로그램에서 BuffteredInputStream의 버퍼에 저장된 데이터를 읽으며 수행
- 프로그램에서 버퍼에 저장된 데이터를 모두 읽으면, 그 다음 데이터를 읽기 위해 read메서드가 호출
- BuffteredInputStream은 입력소스로부터 다시 버퍼크기 만큼의 데이터를 읽어다 버퍼에 저장
BufferedOutputStream가 버퍼를 이용할 경우에는 입력소스로부터 데이터를 읽을 때와는 반대로 작동합니다.
- 프로그램에서 write메서드를 이용하나 출력이 BufferedOutputStream의 버퍼에 저장
- 버퍼가 가득차면, 그 때 버퍼의 모든 내용을 출력소스에 출력
- 버퍼를 비우고 다시 프로그램으로부터의 출력을 저장할 준비
- 유의사항
- 버퍼가 가득 찼을 때에만 출력소스에 출력을 하기 때문에, 마지막 출력부분이 쓰이지 못하고 BufferedOutputStream의 버퍼에 남아있는 채로 프로그램이 종료될 수 있다는 점을 주의해야 합니다.
- 그래서 프로그램에서 모든 작업을 마친 후 BufferedOutputStream에 close()나 flush()를 호출해서 마지막에 버퍼에 있는 모든 내용이 출력소스에 출력되도록 해야 합니다.
close()
try {
FileOutputStream fos = new FileOutputStream("123.txt");
BufferedOutputStream bos = new BufferedOutputStream(fos, 5);
for (int i='1'; i<='9'; i++) {
bos.write(i);
}
fos.close(); // ?
} catch (Exception e) {
e.printStackTrace();
}
여기서 간단 Quiz 입니다.
fos.close();
케이스 출력물 : ?bos.close();
케이스 출력물 : ?bos.close();fos.close();
케이스 출력물 : ?fos.close();bos.close();
케이스 출력물 : ?
Answer
fos.close();
케이스 출력물 : 12345bos.close();
케이스 출력물 : 123456789bos.close();fos.close();
케이스 출력물 : 123456789fos.close();bos.close();
케이스 출력물 : 12345 with Stream Closed exception
BufferedOutputStream의 close()는 기반 스트림인 FileOutputStream의 close()를 호출하기 때문에 FileOutputStream의 close()는 따로 호출해주지 않아도 됩니다.
표준 스트림 (System.in, System.out, System.err)
먼저 표준 입출력이란 콘솔을 통한 데이터 입력과 콘솔로의 데이터의 출력을 의미합니다.
자바에서는 표준입출력(standard I/O)를 위해 3가지 입출력 스트림, System.in, System.out, System.error를 제공합니다.
System.in : 콘솔로부터 데이터를 입력받는데 사용 (InputStream) System.out : 콘솔로 데이터를 출력하는데 사용 (PrintStream) System.err : 콘솔로 데이터를 출력하는데 사용 (PrintStream)
파일 읽고 쓰기
파일을 읽고 쓰는데, 바이트 기반 스트림인 FileInputStream/FileOutputStream 과 문자 기반 스트림인 FileReader/FileWriter가 있습니다.
// test.txt
// Hello, 안녕하세요?
try {
String fileName = "test.txt";
FileInputStream fis = new FileInputStream(fileName);
FileReader fr = new FileReader(fileName);
int data = 0;
while((data=fis.read()) != -1) {
System.out.print((char) data);
}
System.out.println();
fis.close();
while((data=fr.read()) != -1) {
System.out.print((char)data);
}
System.out.println();
fr.close();
} catch (Exception e) {
e.printStackTrace();
}
사용방법은 서로 다르지 않지만, FileInputStream을 사용했을 때는 한글이 깨져서 출력되는 것을 알 수 있습니다.
- FileInputStream - abstract int read() 메서드
- 1byte를 읽어 온다(0~255사이의 값).
- 더 이상 읽어올 데이터가 없으면 -1을 반환한다. abstract 이므로 InputStream의 자손들이 상황에 맞게 구현해야 한다.
- Reader - int read() 메서드
- 입력소스로부터 하나의 문자를 읽어 온다. char의 범위인 0~65535범위(2byte)의 정수를 반환하며, 입력스트림의 마지막 데이터에 도달하면, -1을 반환한다.
- 단순히 2byte로 스트림을 처리하는 것만이 아니고, Reader/Writer과 자손들은 여러 종류의 인코딩과 자바에서 사용하는 유니코드(UTF-16)간의 변환을 자동적으로 처리해준다.
- Reader는 특정 인코딩을 읽어서 유니코드로 변환하고 Writer는 유니코드를 특정 인코딩으로 변환하여 저장한다.
출처
자바의 정석