JAVA

바이트 기반 스트림 : InputStream, OutPutStream

mukom 2022. 10. 2. 21:36

1. 바이트 기반 스트림

 

자바 외부 세계에서는 모든 데이터를 byte 단위로 저장하고 사용하며,

이러한 데이터를 읽고 쓰기 위하여 자바에서는 바이트 기반 스트림 'InputStream' 과 'OutputStream'을 제공하고 있다.

 

2. InputStream, OutPutStream

 

'InputStream' 과 'OutputStream' 은 모든 바이트 기반 스트림의 조상이며,

다음과 같은 메서드를 정의하고 있다.

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() 메서드에서 조금 이상한 점을 찾을 수 있다.

1 byte 씩 읽어오기 때문에 반환 값의 타입 또한 byte 여야 할 것 같지만

위의 표를 보면 int 타입이 반환되는 것을 확인할 수 있다.

 

그 이유는 읽어 올 대상 파일에서 더이상 읽어올 데이터가 없으면 EOF(End of File) 이라고 하는데,

이 때 read() 는 -1 을 반환한다.

 

바로 이 -1 에 대한 처리를 하기 위해서 읽어 온 byte 값에 음수가 있다면 

내부적으로 +256 을 해줘서 '0 부터 255 사이의 값'으로 표현하게 되는 것이다.

하지만 byte 가 나타낼 수 있는 양수 최대값은 127 이기 때문에 이를 int 로 처리한 것이다.

 

read() 와 write() 메서드 중 byte 배열과, int 타입 변수 2개를 쓰는 메서드가 있다.

이 때의 byte 배열은 파일에 대한 데이터를 읽거나 쓸 때 사용할 바구니라고 생각하면 되고, 

int 타입의 off(offset) 는 데이터를 배열에 담을 때의 시작 위치를 나타낸다.

마지막 int 타입의 len(length) 는  몇 개의 데이터를 읽어 올 것인지 지정하는 것이다.

 

이러한 이유로 len 에 지정할 수 있는 크기는 앞서 지정한 byte 배열의 길이보다 커서는 안 되며,

나머지 데이터의 양에 따라 지정한 크기보다 적을 수 있다.

 

또한 0 부터 255 로 표현된 값이 아닌 그 이전의 byte 값을 확인해보고 싶다면,

바구니에 담긴 데이터, 즉 byte 배열의 데이터를 출력해보면 된다.

 

다음은 외부의 파일을 읽어 오는 예시를 read() 메서드를 활용하여 작성한 것이다.

// read() 활용

FileInputStream in = new FileInputStream("C:\\Users\\User\\Desktop\\test.txt");

// 읽어 온 데이터의 반환 값을 담을 변수
int i = 0;

// 읽어 온 파일의 총 byte 계산하기 위한 변수
int cnt = 0;

// read() 메서드의 반환 값이 
// -1 이 아닐 때까지(EoF)
while(-1 != (i = in.read()) {
	// 읽어 온 byte가 있을 때마다 증가시킨다.
    cnt++;
	// 데이터 출력
    System.out.printf("%d(%c)%n",i,i);
}
System.out.println(cnt + "bytes");


// test.txt 내용
abcd
1234
가나다라

// 출력 결과
97(a)
98(b)
99(c)
100(d)
13(
)
10(
)
49(1)
50(2)
51(3)
52(4)
13(
)
10(
)
234(?)
176(°)
128(?)
235(?)
130(?)
152(?)
235(?)
139(?)
164(¤)
235(?)
157(?)
188(¼)
24bytes

위의 출력 결과를 원본 파일과 비교해보면 이상한 점을 찾을 수 있다.

영어와 숫자는 제대로 출력되었으나, 한글을 이상한 기호와 함께 출력되었다.

 

한글만 이상하게 출력된 이유는, 

ms949, utf-8 등의 대부분 인코딩 타입은 한글을 최소 2byte를 사용하여 표현한다.

때문에 1byte 씩 읽어 오는 바이트 기반 스트림으로는 한글을 제대로 표현할 수 없게 되는 것이다.

 

다음은 read(byte[] b, int off, int len) 을 활용하여 작성해 보았다.

// read(byte[] b, int off, int len) 활용

FileInputStream in = new FileInputStream("C:\\Users\\User\\Desktop\\test.txt");

// 읽어 온 데이터의 수
int i = 0;

// 최대 다섯 개의 byte를 담을 수 있는 바구니
byte[] bytes = new byte[5];

// 읽어 온 값이 -1(EoF) 이 아닐 때까지
while((i = in.read(bytes, 0, bytes.length)) != -1) {
	// 	바구니 내부를 확인하는 코드
	System.out.println(Arrays.toString(bytes) + ", " + i);
}

// 출력 결과
[97, 98, 99, 100, 13], 5
[10, 49, 50, 51, 52], 5
[13, 10, -22, -80, -128], 5
[-21, -126, -104, -21, -117], 5
[-92, -21, -99, -68, -117], 4

위의 코드를 보면 int len 의 크기 지정을 바구니 길이에 맞춰 지정하였다.

즉, 5 개씩 담아 오겠다는 것을 의미한다.

 

하지만 5번째 줄의 i  값을 보면 5가 아닌 4가 보인다.

앞서 len에 대해서 설명했듯이 읽어 올 수 있는 데이터가 5개보다 적게 남았기 때문에 가능한 반환 값이다.

 

또한 4번째 줄의 5번째 요소와 5번째 줄의 5번째 요소가 동일한 값(-117)인 것을 확인할 수 있다. 

위의 원본 파일에는 동일한 내용이 없었는데 동일하게 나타난 이유가 무엇일까?

바로 4번째 줄의 데이터를 담고 있는 상태인 바구니에 새로운 데이터 4개를 담았기 때문이다.

새로운 데이터가 4개 뿐이기 때문에 index 0의 자리부터 차곡차곡 넣은 데이터가 index 3까지만 채워진 것이다.

4번째 바구니 : [-21, -126, -104, -21, -117]
새로 가져온 데이터 : [-92, -21, -99, -68]
5번째 바구니 : [-92, -21, -99, -68, -117]

이렇게 출력되면 마지막 -117 값으로 인하여 원본 파일과 다른 결과가 출력될 수 있다.

새로 읽어 온 데이터 만큼만 딱 맞게 가져온다면 이러한 문제는 쉽게 해결할 수 있게 된다.

즉 i 값을 활용하면 된다는 것이다.

FileInputStream in = new FileInputStream("C:\\Users\\User\\Desktop\\test.txt");
FileOutputStream out = new FileOutputStream("C:\\Users\\User\\Desktop\\test1.txt");

// 읽어 온 데이터의 수
int i = 0;

// 최대 다섯 개의 byte를 담을 수 있는 바구니
byte[] bytes = new byte[5];

// available() : 스트림에서 읽어 올 수 있는 데이터의 크기 반환(int)
while((i = in.read(bytes, 0, bytes.length)) != -1) {
    // 	읽어 온 만큼만 출력
	out.write(bytes, 0, i);
}

// test1 파일
abcd
1234
가나다라

만일 출력할 때 i 값을 사용하지 않고, bytes.length 를 그대로 사용하면

abcd
1234
媛€?섎떎?펻

'-117' 이 한 번 더 저장되면서 '가나다라' 가 제대로 저장되지 않는 것을 확인할 수 있다. 

 

 

3. 바이트 기반 보조 스트림

 

보조 스트림은 보조 스트림 자체로는 입출력을 수행할 수 없기 때문에 주 스트림이 필요하다.

1. 주스트림 주스트림_참조변수 = new 주스트림();
    보조스트림 보조스트림_참조변수 = new 보조스트림(주스트림_참조변수);

2. 보조스트림 보조스트림_참조변수 = new 보조스트림(new 주스트림());

모든 보조 스트림의 조상은 FilterInputStream / FilterOutputStream 이 있는데,

이 자체로는 사용하지 못하고 상속을 통한 오버라이딩을 통해 자손을 활용한다.

 

BufferedInputStream / BufferdOutputStream 은 Buffer 라는 접두사로 알 수 있듯이

입출력의 효율을 높이는 데에 사용하는 보조 스트림이다.

Buffer 사용으로 효율이 높아지는 이유
: 외부의 데이터를 그대로 읽으려는 것보다 내부 데이터로 전환하여 읽는 것이 더 빠르다.

버퍼 또한 일종의 바구니라고 생각하면 이해가 쉽다.

커다란 항아리의 물을 손으로 조금씩 옮기는 것보다

커다란 바구니에 담아 옮기는 것이 더 빠르게 옮길 수 있는 것을 생각해보면 된다.

(옮기려는 데이터)        (한 번에 옮길 수단) 
   커다란 항아리      +           손바닥                =    시간이 오래 걸린다 
   커다란 항아리      +           바구니                =    빠르게 옮길 수 있다.

다만 버퍼를 사용할 때에는 주의할 점이 있는데,

버퍼는 효율을 위하여 가득 찼을 때만 작동한다는 특징이 있기 때문에

조금이라도 덜 차면 버퍼가 작동하지 않고 데이터가 남아 있는 상태로 종료될 수 있는 가능성이 있다.

이러한 이유로 스트림이 종료되기 전에 버퍼에 혹시라도 남아 있는 데이터를 마저 반환해줘야 한다.

 

데이터를 강제로 내보내는 기능을 flush() 메서드를 통해 할 수 있는데,

close() 메서드의 내부에서도 작동하기 때문에 close()로 처리해주면 된다.

(close() 메서드를 사용할 수 있다는 것은 자동 자원 반환 처리를 할 수 있다는 것이다.)

 

다음은 여러 가지 경우로 파일을 읽어 오고자 할 때 걸리는 시간을 측정하기 위한 예시이다.

// 얼마나 시간이 걸리는지 확인하기 위한 변수
long start = System.currentTimeMillis();

// 1,310,832 바이트 크기의 파일
FileInputStream fis = new FileInputStream("C:\\Users\\User\\Downloads\\pycharm-professional-2021.1.1.exe");
// 보조 스트림 인스턴스 생성
BufferedInputStream bis = new BufferedInputStream(fis);

// 반환 값을 담을 변수
int i = 0;

// 파일을 읽어 오는 행위를 몇 번 반복했는지 확인할 수 있는 변수
int cnt = 0;




// 1. 버퍼 없이 파일을 읽어 올 때
while((i = fis.read()) != -1) {                                // cnt : 1310832
	cnt++;                                                     // 5782ms
}

// 2. 버퍼로 파일을 읽어 올 때
while((i = bis.read()) != -1) {                                // cnt : 1310832
	cnt++;                                                     // 34ms
}

// 3. 버퍼 + byte 배열 추가해서 읽어 올 때 (바구니 + 바구니)
byte[] cs = new byte[1024 * 8];                                // cnt : 161
while((i = bis.read(cs)) != -1) {                              // 3ms
	cnt++;
}

예시를 보면 알 수 있듯이 

버퍼만 사용하더라도 파일을 읽어내는 데에 걸리는 시간이 상당히 줄어든 것을 확인할 수 있다.

또한 3번 예시처럼 byte 배열을 추가로 사용하면 더 빠르게 읽어내는 것이 가능하다.

 

DataInputStream / DataOutputStream 또한 FilterInputStream / FilterOutputStream 를 상속 받은 자손으로

보조 스트림이며, 별도로 DataInput, DataOutput 인터페이스를 각각 구현하여 바이트 기반 보조 스트림이지만,

8가지 기본 자료형의 단위로 읽고 쓸 수 있는 스트림이라는 특징이 있다.

데이터 타입 read 메서드 write 메서드
boolean boolean readBoolean() writeBoolean(boolean b)
byte byte readByte() writeByte(int b)
char char readChar() writeChar(int c)
String char readChar()
String readUTF()
writeChars(String s)
writeUTF(String s)
short short readShort() writeShort(int s)
int int readInt() writeInt(int i)
long long readLong() writeLong(long l)
float float readFloat() writeFloat(float f)
double double readDouble() writeDouble(double d)

작성은 다음과 같이 할 수 있다.

// 파일로 만든다.
DataOutputStream dos = new DataOutputStream(new FileOutputStream("test.txt"));
// 파일을 읽는다.
DataInputStream dis = new DataInputStream(new FileInputStream("test.txt"));

// int 타입 4
dos.writeInt(10);
// double 타입 8
dos.writeDouble(Math.PI);
// char 타입 2
dos.writeChar(65);
dos.writeChar(44032);
// String 타입 4
dos.writeChars("a가");
// UTF 형식으로 문자열 출력 6
dos.writeUTF("a가");
// ms949 형식으로 문자열 출력 3
dos.write("a가".getBytes("ms949"));

// int 읽기
System.out.println(dis.readInt());
// double 읽기
System.out.println(dis.readDouble());
// char 읽기 
System.out.println(dis.readChar());
System.out.println(dis.readChar());
// String 읽기
System.out.println(dis.readChar());
System.out.println(dis.readChar());
// UTF 형식 읽기
System.out.println(dis.readUTF());
// ms949 형식 읽기
byte[] bs = new byte[3];
dis.read(bs);
System.out.println(new String(bs, "ms949"));

int 타입은 short 로도 출력할 수 있는데

System.out.println(dis.readInt());              // 10

System.out.println(dis.readShort());         // 0
System.out.println(dis.readShort());         // 10

int 타입은 4 byte, short 타입은 2 byte 이기 때문에 두 번 작성하게 된다.

첫 출력에서 0이 나오는 이유는 데이터가 출력될 때 이진 데이터로 저장되기 때문에 

int 타입은 4 byte 크기 만큼의 저장 공간을 할당 받게 되고 값의 크기 만큼 채워진다.

이로 인하여 2 byte 크기로 출력하게 되면 앞 부분은 0 으로 출력되는 것이다.

16 진수 : 00 00 00 0a
16 진수는 두 자리가 1 byte 이다.

String 의 경우 readString() 메서드가 없기 때문에 문자의 개수 만큼 readChar() 를 통해 읽어 올 수 있다.

(읽어 올 때마다 값이 변할 가능성이 있기 때문에 String 은 read 관련 메서드가 없다.