자바의 입출력(I/O)
입출력은 컴퓨터 내부 또는 외부의 장치와 프로그램간의 데이터 교환을 말한다.
스트림은 데이터 전달을 위해 두 대상을 연결하고 데이터를 전송할 수 있는 연결통로를 의미한다. (이 스트림은 Collection과 Arrays에 포함되어 있는 스트림 메서드와 다르다.)
스트림은 단방향 통신만 가능하다. 그래서 입출력을 동시에 하려면 입력스트림과 출력스트림이 필요하다.
스트림의 구조는 queue과 같은 FIFO(first in first out)구조다.
바이트 기반의 스트림(InputStream, OutputStream)
스트림은 바이트단위로 데이터를 전송하며 입출력 대상에 따라 입출력 스트림이 있다.
예를 들어, 어떤 파일의 내용을 읽으려면 FileInputStream을 사용하면되고, byte배열의 메모리를 쓰려면 ByteArrayOutputStream을 사용하면 된다.
위의 모든 스트림은 InputStream과 OutputStream의 자손이다.
java.io 패키지에 많은 종류의 입출력 클래스들이 포함되어있다고 한다.
byte[]배열 b의 크기만큼 읽어서 0의 위치부터 데이터를 저장한다.
입력스트림으로부터 len개의 입력을 받아서 byte[]배열 b의 off위치부터 데이터를 저장한다.
read(byte[] b)와 read(byte[] b, int off, int len)의 메서드는 내부에서 추상메서드인 read()를 호출한다.
추상클래스를 상속받아서 추상메서드를 구현한 클래스의 인스턴스에 대해서 추상메서드가 호출될 것이기 때문에 추상메서드를 호출하는 코드를 작성해도 괜찮다고 한다. 따라서 추상 메서드인read()를 구현하지 않으면 다른 두 메서드의 존재 의미가 없는 것이다.
보조스트림은 말그대로 스트림의 기능을 보완하기 위한 스트림이다.
실제 데이터를 주고받는 스트림은 아니다. 그래서 데이터 입출력의 기능은 없다. 하지만 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있다.
그래서 보조스트림은 스트림이 생성되어야 생성 할 수 있다.
예시 코드:
BufferedInputStream bI = new BufferedInputStream(new FileInputStream());
bI.read();//보조스트림으로 데이터를 읽어온다.
코드상으로는 보조스트림이 입력기능을 수행하는 것처럼 보여진다. 하지만 실제로는 FileInputStream
이 입력기능을 실행하고, 보조스트림은 buffer만 제공한다. buffer의 사용여부에 따라 성능차이가 상당하기 때문에 대부분 buffer를 이용한 보조스트림을 사용한다.
FilterInputStream의 자손들인 BufferedInputStream, DataInputStream, DigestInputStream, LineNumberInputStream, PushbackInputStream은 FilterInputStream이 InputStream의 자손이기 때문에 입출력방법이 같다.
결국 모든 보조 스트림은 InputStream과 OutputStream의 자손이기에 입출력 방법이 다 같다.
문자기반 스트림(Reader, Writer)
위의 스트림들은 모두 바이트 기반의 스트림이었다. 바이트 기반은 입출력의 단위가 1byte다.
java에서는 c와 달리 char형이 1byte가 아닌 2byte다.
따라서 바이트 기반으로는 문자처리가 어렵다.
이를 보완하기 위해서 java에서는 문자기반 스트림을 제공한다.(InputStream->Reader, OutputStream->Writer)
바이트기반 스트림이 read()를 추상메서드로 하였는데, 문자기반 스트림은 프로그래밍적인 관점에서 볼때 read(char[] cbuf, int off, int len)을 추상메서드로 하는 것이 바람직하다고 한다.
문자기반 스트림도 바이트기반 스트림과 마찬가지로 다른 메서드들은 추상메서드를 호출하여서 추상메서드의 구현이 되어있지 않다면 의미없는 것이다.
이제 전체적으로 보았으니 세부적으로 봐보자.
바이트 기반 스트림을 살펴보면, InputStream과 OutputStream이 존재하고, 이 두 스트림은 모든 바이트기반 스트림의 조상이다.
여기서 flush는 buffer가 있는 스트림에서만 의미가 있다.
JVM이 프로그램이 종료될 때, 사용하지 않은 스트림은 자동으로 닫아주기는 하지만 close를 사용하여 반드시 닫아주자.
물론 ByteArrayInputStream과 같이 메모리를 사용하는 스트림이나 System.in, System.out같은 표준 입출력 스트림은 닫지 말아도 된다.
ByteArrayInputStream과 ByteArrayOutputStream은 메모리인 바이트배열에 데이터를 입출력 하는데 사용하는 스트림이다.
보통 다른 곳에 입출력하기 전에 데이터를 임시로 바이트배열에 담아서 변환하는 작업에 사용된다고한다.
자주 사용되지는 않지만 입출력 방법 예제를 보여주는 것에는 적합하다고 한다.
위의 예제에서 바이트배열은 사용하는 자원이 메모리 밖에 없으므로 가비지컬렉터에 의해 자동적으로 자원을 반환하여 close를 이용해서 스트림을 닫지 않아도 된다고 한다.
한번에 1byte씩만 읽고 쓰기 때문에 효율이 떨어진다.
그래서 다음 예제는 효율을 조금 더 올린 예제이다.
import java.io.*;
import java.util.*;
class IOEx2{
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[10];
ByteArrayInputStream input = new ByteArrayInputStream(inSrc);
ByteArrayOutputStream output = new BytearrayOutputStream();
input.read(temp, 0, temp.length);//temp배열에 0의 위치부터 temp개를 채운다.
optput.write(temp, 5, 5);//temp배열의 5의 위치 데이터부터 5개를 가져와서 채운다.
outSrc = output.toByteArray();//outSrc배열을 output스트림의 바이트배열로 채운다.
System.out.println("Input Source :"+Arrays.toString(inSrc));
System.out.println("temp :"+Arrays.toString(temp));
System.out.println("Output Source :"+Arrays.toString(outSrc));
}
}
실행결과
Input Source :[0,1,2,3,4,5,6,7,8,9]
temp :[0,1,2,3,4,5,6,7,8,9]
Output Source :[5,6,7,8,9]
input스트림을 이용해서 10개를 temp에 넣고, output스트림을 통해 temp배열에서 5개를 가져온다.
위의 식에서 input.read(temp); output.write(temp);를 사용하면 4개, 4개, 2개를 읽어서
temp배열에 [0,1,2,3]->[4,5,6,7]->[8,9,6,7]로 옮겨진다.
temp에 [4,5,6,7]이 남아있고 input스트림에서 읽어온 것(8,9)은 temp배열의 index 0부터 4칸을 채우기 때문에 기존 배열[4,5,6,7]에서 앞의 index 0,1만을 8,9로 바꾸어서 [8,9,6,7]로 만든다.
그래서 output스트림의 결과가 [0,1,2,3,4,5,6,7,8,9,6,7]이 된다.
이렇게 나오는 이유는 java가 보다 나은 성능을 위해서 temp에 담긴 내용을 지우고 쓰는 것이 아니라 그냥 기존의 내용 위에 덮어 쓰기 때문에 그렇다.
이를 수정하기 위해서 int len=input.read(temp); output.write(temp,0,len);으로 바꾼다.
이렇게 하면 input스트림에서 읽어올때 2개(8,9)가 남으면 len의 길이가 2이기 때문에 output스트림에서 temp배열의 index 0부터 2칸만을 채우기 때문에 결과값이 [0,1,2,3,4,5,6,7,8,9]로 나오게 된다.
다음은 FileInputStream 과 FileOutputStream이다.
이 두 스트림은 파일의 입출력을 위한 스트림이다. 실제 프로그래밍을 하는 과정에서 많이 사용되는 스트림이라고 한다.
import java.io.*;
class FileViewer{
public static void main(String[] args) throws IOException{
FileInputStream fis = new FileInputStream(args[0]);
int data=0;
while((data=fis.read)!=-1){
char c = (char)data;
System.out.print(c);
}
}
}
위의 결과는 위 코드 그대로 출력된다.
커맨드라인으로부터 입력받은 파일의 내용을 읽어서 그대로 화면에 출력하는 간단한 예제다.
(read()의 반환값은 int형(4byte)이지만, 입력값 없음을 알리는 -1을 제외하고는 0~255(1byte)범위의 정수값이기 때문에 char형(2byte)로 변환해도 데이터 손실은 없다.)
FileCopy.java의 내용을 FileInputStream으로 읽어와서 읽어 온 내용을 FileCopy.bak에 FileOutputStream으로 출력한다.
위의 예처럼 텍스트 파일을 다루는 경우에는 문자기반스트림을 사용하는 것이 더 효율적이다.
그 다음으로 이제는 바이트기반의 스트림들을 보조해주는 보조스트림을 살펴 볼 것이다.
FilterInputStream과 FilterOutputStream은 InputStream과 OutputStream의 자손이면서 모든 보조스트림의 조상이다.
위에서 다룬 FileInputStream과 FileOutputStream과 spelling이 비슷하여 헷갈릴 수 있으니 주의하자.
보조스트림은 윗 부분에서 얘기 했듯이 자체적으로 입출력을 수행 할 수 없어서 기반스트림이 필요하다.
보조스트림의 모든 메서드는 단순히 기반스트림의 메서드를 그대로 호출할 뿐이다.
보조스트림 자체로는 아무런 일도 하지 않음을 의미한다.
보조스트림은 상속을 통해 원하는 작업을 수행하도록 읽고 쓰는 메서드를 오버라이딩 해야한다.
FileterInputStream은 접근 제어자가 protected이기 때문에 인스턴스를 생성해서 사용할 수 없다. 그래서 상속을 통해서 Overriding 되어야 한다.
BufferedInputStream 과 BufferedOutputStream은 입출력 효율을 높이기 위해 버퍼를 사용하는 보조 스트림이다.
한 바이트씩 출력하는 것은 비효율적이기 때문에 한 번에 여러 바이트를 입출력하는 버퍼(바이트배열)를 사용한다.
대부분의 입출력에서 사용된다.
버퍼크기는 입력소스로부터 한번에 가져올 수 있는 데이터 크기로 지정하는 것이 좋다. 보통 입력소스가 파일인 경우 4096 정도의 크기로 하는 것이 좋다고 한다. 버퍼의 크기를 변경해보면서 최적 버퍼크기를 찾을 수 있다.
버퍼크기를 입력소스 파일크기만큼으로 지정하면, 처음 프로그램에서 입력소스로부터 파일 크기만큼 데이터를 읽어다 자신의 내부 버퍼에 저장하고나면, 그 이후부터는 내부 버퍼에 저장된 데이터를 읽으면 되기 때문에 외부의 입력소스로부터 읽는 것보다 빠르게 내부 버퍼에서 데이터를 읽어 작업 효율을 높여준다.
BufferedOutputStream을 사용할때는 마지막 출력부분이 출력소스에 쓰이지 못하고 버퍼에 남아있는 채로 프로그램이 종료될 수 있다는 점을 주의해야한다.
그래서 프로그램에서 모든 출력작업을 마친 후에는 close()나 flush()를 호출해서 마지막에 버퍼에 있는 모든 내용이 출력소스에 출력되도록 해야 한다.
기반스트림인 fos를 close()호출해서 닫아줄 뿐아니라 보조 스트림인 bos 또한 같이 닫아줘야 버퍼에 남아있는 6789가 호출된다.
여기서 그럼 궁금한 것이 생길 것이다. 꼭 기반 스트림을 닫고, 보조스트림을 닫아야 하는가? 이건 아니다.
보조스트림의 close는 기반 스트림의 close를 호출하기 때문에 보조 스트림만을 close해주면 된다.
DataInputStream과 DataOutputStream
이 보조스트림들 또한 FilterInputStream과 FilterOutputStream의 자손이다.
DataInputStream은 DataInput인터페이스를 구현하고, DataOutputStream은 DataOutput인터페이스를 구현하기 때문에
데이터를 읽고 쓰는데 있어서 byte단위가 아닌, 8가지 기본 자료형의 단위로 읽고 쓸 수 있다는 장점이 있다.
DataOutputStream이 출력하는 형식은 각 기본 자료형 값을 16진수로 표현하여 저장한다.
ex)int값을 출력한다면, 4byte 16진수로 출력
각 자료형의 크기가 다르기 때문에, 출력한 데이터를 다시 읽어 올 때는 출력 순서를 염두에 두어야 한다.
위의 예제에서 출력한 값들은 이진 데이터로 저장 된다. 문자 데이터가 아니므로 문서 편집기로 sample.dat을 열어 봐도 알 수 없는 글자들로 이루어져 있다.