[Java] 입출력 I/O - 바이트기반의 보조스트림 (2)Data
DataInputStream과 DataOutputStream
DataInputStream과 DataOutputStream도 각각 FilterInputStream과 FilterOutputStream의 자손이며 DataInputStream은 DataInput인터페이스를, DataOutputStream은 DataOutput인터페이스를 각각 구현하였기 때문에, 데이터를 읽고 쓰는데 있어서 byte 단위가 아닌, 8가지 기본 자료형의 단위로 읽고 쓸 수 있다는 장점이 있다.
DataOutputStream이 출력하는 형식은 각 기본 자료형 값을 16진수로 표현하여 저장한다. 예를 들어 int값을 출력한다면, 4 byte의 16진수로 출력된다.
각 자료형의 크기가 다르므로, 출력한 데이터를 다시 읽어 올 때는 출력했을 때의 순서를 염두에 두어야 한다.
메서드 / 생성자 | 설명 |
DataInputStream(InputStream in) | 주어진 InputStream인스턴스를 기반스트림으로 하는 DataInputStream인스턴스를 생성한다. |
boolean readBoolean() byte readByte() char readChar() short readShort() int readInt() long readLong() float readFloat() double readDouble() int readUnsignedByte() int readUnsignedShort() |
각 타입에 맞게 값을 읽어 온다. 더 이상 읽을 값이 없으면 EOFException을 발생시킨다. |
void readFully(byte[] b) void readFully(byte[] b, int off, int len) |
입력스트림에서 지정된 배열의 크기만큼 또는 지정된 위치에서 len만큼 데이터를 읽어온다. 파일의 끝에 도달하면 EOFException이 발생하고, I/O에러가 발생하면 IOException이 발생한다. |
String readUTF() | UTF-8 형식으로 쓰여진 문자를 읽는다. 더 이상 읽을 값이 없으면 EOFException이 발생한다. |
static String readUTF(DataInput in) | 입력스트림(in)에서 UTF-8 형식의 유니코드를 읽어온다. |
int skipBytes(int n) | 현재 읽고 있는 위치에서 지정된 숫자(n) 만큼을 건너뛴다. |
메서드 / 생성자 | 설명 |
DataOutputStream(OutputStream out) | 주어진 OutputStream인스턴스를 기반스트림으로 하는 DataOutputStream인스턴스를 생성한다. |
void writeBoolean(boolean b) void writeByte(int b) void writeChar(int c) void writeChars(String s) void writeShort(int s) void writeInt(int l) void writeLong(long l) void writeFloat(float f) void writeDouble(double d) |
각 자료형에 알맞은 값들을 출력한다. |
void writeUTF(String s) | UTF형식으로 문자를 출력한다. |
void writeChars(String s) | 주어진 문자열을 출력한다. wrtieChar(int c) 메서드를 여러번 호출한 결과와 같다. |
int size() | 지금까지 DataOutputStream에 쓰여진 byte의 수를 알려준다. |
import java.io.*;
class DataOutputStreamEx1 {
public static void main(String args[]) {
FileOutputStream fos = null;
DataOutputStream dos = null;
try {
fos = new FileOutputStream("sample.dat");
dos = new DataOutputSTream(fos);
dos.writeInt(10);
dos.writeFloat(20.0f);
dos.writeBoolean(true);
dos.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
FileOutputStream을 기반으로 하는 DataOutputStream을 생성한 후, DataOutputStream의 메서드들을 이용해서 sample.dat파일에 값들을 출력했다. 이 때 출력한 값들은 이진 데이터(binary data)로 저장된다. 문자 데이터(text data)가 아니므로 문서 편집기로 sample.dat를 열어봐도 알 수 없는 글자들로 이루어져 있을 것이다. 파일을 16진 코드로 볼 수 있는 UltraEdit과 같은 프로그램이나 ByteArrayOutputStream을 사용하면 이진데이터를 확인할 수 있다.
import java.io.*;
import java.util.Arrays;
class DataOutputStreamEx2 {
public static void main(String args[]) {
ByteArrayOutputStream bos = null;
DataOutputStream dos = null;
byte[] result = null;
try {
bos = new ByteArrayOutputStream();
dos = new DataOutputSTream(bos);
dos.writeInt(10);
dos.writeFloat(20.0f);
dos.writeBoolean(true);
result = bos.toByteArray();
String[] hex = new String[result.length];
for(int i=0; i<result.length; i++){
if(result[i] < 0) {
hex[i] = String.format("%02x", result[i] + 256);
} else {
hex[i] = String.format("02x", result[i]);
}
}
System.out.println("10진수 : " + Arrays.toString(result));
System.out.println("16진수 : " + Arrays.toString(hex));
dos.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
이전의 예제를 변경해서 FileOutputStream 대신 ByteArrayOutpuStream을 사용하였다. 결과를 보면 첫 번째 4 byte인 0, 0, 0, 10은 writeInt(10)에 의해서 출력된 값이고, 두 번째 4 byte인 65, -96, 0, 0은 wrtieFloat(20.0f)에 의해서 출력된 것이다. 그리고 마지막 1 byte인 1은 writeBoolean(true)에 의해서 출력된 것이다.
위와 같이 모든 bit의 값이 1인 1 byte의 데이터가 있다고 할 때, 왼쪽에서 첫 번째 비트를 부호로 인식하지 않으면 부호 없는 1 byte가 되어 범위는 0~255이므로 이 값은 최대값인 255가 되지만, 부호로 인식하는 경우 범위는 -128~127이 되고, 이 값은 0보다 1작은 값인 -1이 된다.
결국 같은 데이터이지만 자바의 자료형인 byte의 범위가 부호있는 1 byte 정수의 범위인 -128~127이기 때문에 -1로 인식한다는 것이다. 그래서 이 값을 0~255사이의 값으로 변환하려면 256을 더해주어야 한다.
예를 들어 -1의 경우 -1 + 256 = 255가 된다. 그리고 반대의 경우 256을 빼면된다. 그 다음에 String.format()을 사용해서 10진 정수를 16진 정수로 변환하여 출력했다.
이처럼 ByteArrayInputStream과 ByteArrayOutputStream을 사용하면 byte단위의 데이터 변환 및 조작이 가능하다는 것을 알려준다.
|참고| InputStream의 read()는 반환타입이 int이며 0~255의 값을 반환하므로 256을 더하거나 뺄 필요가 없다. 반면에 read(byte[] b)와 같이 byte배열을 사용하는 경우 상황에 따라 0~255 범위의 값으로 변환해야할 필요가 있다.
사실 DataOutputStream에 의해서 어떻게 저장되는지 몰라도 DataOutputStream의 write메서드들로 기록한 데이터는 DataInputStream의 read메서드들로 읽기만 하면 된다.
이 때 한 가지 주의해야 할 것은 이 예제와 같이 여러가지 종류의 자료형으로 출력한 경우, 읽을 때는 반드시 쓰인 순서대로 읽어야 한다는 것이다.
import java.io.*;
class DataInputStreamEx1 {
public static void main(String args[]) {
try {
FileInputStream fis = new FileInputStream("sample.dat");
DataInputStream dis = new DataInputStream(fis);
System.out.println(dis.readInt());
System.out.println(dis.readFloat());
System.out.println(dis.readBoolean());
dis.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
맨 처음 예제를 실행해서 만들어진 sample.dat을 읽어서 화면에 출력하는 예제이다. sample.dat 파일로부터 데이터를 읽어 올 때, 아무런 변환이나 자릭수를 셀 필요없이 단순히 readInt()와 같이 읽어 올 데이터의 타입에 맞는 메서드를 사용하기만 하면된다.
문자로 데이터를 저장하면, 다시 데이터를 읽어 올 때 문자들을 실제 값으로 변환하는, 예를 들면 문자열 "100"을 숫자 100으로 변환하는, 과정을 거쳐야 하고, 또 읽어야 할 데이터의 개수를 결정해야하는 번거로움이 있다.
하지만 이처럼 DataInputStream과 DataOutputStream을 사용하면, 데이터를 변환할 필요도 없고, 자릿수를 세어서 따지지 않아도 되므로 편리하고 빠르게 데이터를 저장하고 읽을 수 있게 된다.
import java.io.*;
class DataOutputStreamEx3 {
public static void main(String args[]) {
int[] score = { 100, 90, 95, 85, 50 };
try {
FileOutputStream fos = new FileOutputStream("sample.dat");
DataOutputStream dos = new DataOutputStream(fos);
for(int i=0; i<score.length; i++){
dos.writeInt(score[i]);
}
dis.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
int 형 배열 score의 값들을 DataOutputStream을 이용해서 score.dat파일에 출력하는 예제이다. type명령으로 score.day의 내용을 보면 숫자가 아니라 문자들이 나타나는데, 그 이유는 type명령이 파일의 내용을 문자로 변환해서 보여주기 때문이다. 파일에 실제 저장된 내용은 다음과 같다.
00 00 00 64 00 00 00 5A 00 00 00 5F 00 00 00 55 00 00 00 32
100 90 95 85 50
int의 크기가 4 byte이므로 모두 20 byte의 데이터가 저장되어 있다. 참고로 16진수 두 자리가 1 byte이다. 참고로 16진수 두 자리가 1 byte이다. 밑줄 아래의 숫자는 10진수로 변환한 결과이다.
다음 예제에서는 이 파일을 읽어서 데이터의 총합을 구할 것이다.
import java.io.*;
class DataOutputStreamEx3 {
public static void main(String args[]) {
int sum = 0;
int score = 0;
FileInputStream fis = null;
DataInputStream dis = null;
try {
fis = new FileInputStream("score.dat");
dis = new DataInputStream(fis);
while(true) {
score = dis.readInt();
System.out.println(score);
sum += score;
}
} catch(EOFException e) {
System.out.println("점수의 총합은 " + sum + " 입니다.");
} catch(IOException ie) {
ie.printStackTrace();
} finally {
try {
if(dis != null) dis.close();
} catch(IoException ie) {
iE.printStackTrace();
}
}
}
}
DataInputStream의 readInt() 와 같이 데이터를 읽는 메서드는 더 이상 읽을 데이터가 없으면 EOFException을 발생시킨다. 그래서 다른 입력스트림들과는 달리 무한반복문과 EOFException을 처리하는 catch문을 이용해서 데이터를 읽는다.
원래 while문으로 작업을 마친 후에 스트림을 닫아 줘야 하는데, while문이 무한 반복문이기 때문에 finally 블럭에서 스트림을 닫도록 처리하였다.
참조변수 dis가 null일 때 close()를 호출하면 NullPointerException이 발생하므로 if문을 사용해서 dis가 null인지 체크한 후에 'close()'를 호출해야 한다. 그리고 'close()'는 IOException을 발생시킬 수 있으므로 try-catch 블럭으로 감싸주었다.
지금까지는 try 블럭 내에서 스트림을 닫아주었지만, 작업도중에 예외가 발생하서 스트림을 닫지 못하고 try 블럭을 빠져나갈 수 있기 때문에 이처럼 finally블럭을 이용해서 스트림을 닫아주는 것이 더 확실한 방법이다.
그러나 여기선 예제가 복잡해지는 것을 막기 위해 간단히 try 블럭 내에서 스트림을 닫도록 코드를 작성하였다.
사실 프로그램이 종료될 때, 가비지 컬렉터가 사용하던 자원들을 모두 해제 해주기 때문에 이렇게 간단한 예제에서는 스트림을 닫지 않아도 별 문제가 되지는 않는다. 그래도 가능하면 스트림을 사용한 직후에 바로 닫아서 자원을 반환해 주는것이 좋다.
JDK1.7부터는 try-with-resources문을 이용해서 close()를 직접 호출하지 않아도 자동 호출되도록 할 수 있다. 아래의 예제는 위 예제를 try-with-resources문을 이용해서 변경한 것인데, 보다 간결해졌다.
import java.io.*;
class DataOutputStreamEx3 {
public static void main(String args[]) {
int sum = 0;
int score = 0;
FileInputStream fis = null;
DataInputStream dis = null;
try (FileInputStream fis = new FileInputStream("score.dat");
DataInputStream dis = new DataInputStream(fis)) {
while(true) {
score = dis.readInt();
System.out.println(score);
sum += score;
}
} catch(EOFException e) {
System.out.println("점수의 총합은 " + sum + " 입니다.");
} catch(IOException ie) {
ie.printStackTrace();
}
}
}