Java Stream 이란
입/출력 데이터 흐름의 통로
입력 스트림은 소스 데이터로부터 데이터를 읽어들이고
출력 스트림은 목적 대상까지 데이터를 흘려보내 쓴다.
Java Stream 특징
Queue , FIFO(First In First Out) 방식
Byte 단위로 흐른다.
입출력 대상에 따라 나뉜다.
- FileStream -> 파일
- ByteArrayStream -> 메모리 (byte 배열)
- PipeStream -> 프로세스
Input/OutputStream
InputStream 은 read() , OutputStream 은 write() 추상 메소드를 기본적으로 구현하게 되어있다.
FileInput/Ouput Stream은 모두 In/Output Stream의 자식
abstract method read / write 를 오버라이딩 한형태로 구현해놓은 것임.
InputStream.java
public abstract class InputStream implements Closeable {
// 추상 메소드 read()
// 1 byte 씩 스트림 데이터를 읽어들인다.
public abstract int read() throws IOException;
// 반환 값은 실제로 읽어들인 byte 수
// 더 이상 스트림을 읽을 수 없는 경우 -1 을 리턴한다.
// b 바이트 배열에 읽은 데이터를 쓴다.
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
// b 바이트 배열에 읽은 데이터를 쓴다.
// offset 으로부터 최대 len 개의 byte 를 읽는다.
// 읽어들인 바이트 수가 반환 값으로 반환 값은 len 과 같거나 작다.
public int read(byte[] b, int off, int len) throws IOException {
Objects.checkFromIndexSize(off, len, b.length);
if (len == 0) {
return 0;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
}
OutputStream.java
public abstract class OutputStream implements Closeable, Flushable {
// b 값을 스트림에 쓴다.
// 이 때 b는 바이트 단위다. (0-255 값)
public abstract void write(int b) throws IOException;
// b 베열의 데이터를 OutputStream 에 쓴다.
// OutputStream 구현체가 버퍼를 가진 경우, 버퍼에 쓴다.
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
// b 바이트 배열 offset 으로부터 len 개의 데이터를 OutputStream 에 쓴다.
public void write(byte[] b, int off, int len) throws IOException {
Objects.checkFromIndexSize(off, len, b.length);
// len == 0 condition implicitly handled by loop bounds
for (int i = 0 ; i < len ; i++) {
write(b[off + i]);
}
}
}
위 abstract method 인 read(), write() 를 구현체에서 입출력 대상 종류에 따라 달라진다는 점을 알 수 있고
인자를 1개, 3개로 받는 메소드는 결국 추상 메소드인 read(), write() 의 구현 메소드에 따라 동작이 달라짐을 알 수 있다.
ByteArrayStream 예제
ByteArrayStream 의 입출력 대상은 메모리 (byte[])다.
입출력 스트림 동작을 이해하는데 도움이 되는 구현체므로 예제를 통해 알아보자.
ByteArrayInputStream.java
public class ByteArrayInputStream extends InputStream {
// 애초에 버퍼로 초기화 가능하다.
public ByteArrayInputStream(byte[] buf) {
this.buf = buf;
this.pos = 0;
this.count = buf.length;
}
@Override
public synchronized int read() {
return (pos < count) ? (buf[pos++] & 0xff) : -1;
}
}
ByteArrayStreamTest.java
public class ByteArrayStreamTest {
@DisplayName("read 는 1 바이트씩 읽어서 데이터를 반환한다.")
@Test
void byteInputStreamByOne() {
// given
byte[] inputSource = {1, 2, 3, 4, 5, 6, 7, 8, 9};
byte[] outputDestination;
// inputSource 를 내부 버퍼로 사용한다.
// 자체적으로 offset 관리를 하는데 초기값은 당연 0이다.
var byteArrayInputStream = new ByteArrayInputStream(inputSource);
var byteArrayOutputStream = new ByteArrayOutputStream();
int data = 0;
// when
while ((data = byteArrayInputStream.read()) != -1) {
// inputSource 버퍼로부터 1 바이트씩 읽어온다.
// 읽어 올 때마다 offset++
byteArrayOutputStream.write(data);
}
outputDestination = byteArrayOutputStream.toByteArray();
// then
assertThat(outputDestination).isEqualTo(inputSource);
}
@DisplayName("read 에 byte[] 를 넘기면 해당 임시 저장소에 자기가 가진 버퍼의 값을 복사한다.")
@Test
void byteInputStreamTempByteArray() {
// given
byte[] inputSource = {1, 2, 3 , 4, 5, 6, 7, 8, 9};
byte[] outputDestination;
byte[] temp = new byte[9];
var byteArrayInputStream = new ByteArrayInputStream(inputSource);
var byteArrayOutputStream = new ByteArrayOutputStream();
// when
int readCount = byteArrayInputStream.read(temp, 0, temp.length);
byteArrayOutputStream.write(temp, 4, 5);
outputDestination = byteArrayOutputStream.toByteArray();
// then
assertThat(readCount).isEqualTo(9);
assertThat(outputDestination).isEqualTo(new byte[]{5,6,7,8,9});
}
@DisplayName("임시 버퍼를 매개로 입출력 스트림을 동기화한다.")
@Test
void byteStreamWithBuffer() {
byte[] inputSource = {1, 2, 3, 4, 5, 6, 7, 8, 9};
byte[] outputDestination;
byte[] buf = new byte[4];
var byteArrayInputStream = new ByteArrayInputStream(inputSource);
var byteArrayOutputStream = new ByteArrayOutputStream();
try {
while(byteArrayInputStream.available() > 0) {
// bufferArrayInputStream 버퍼에 있는 모든 데이터를 읽어서 buf 에 입력한다.
// 이미 읽은 데이터는 bufferArrayInputStream 내부 버퍼의 pos (offset) 이 변해 다시 읽지 않게된다.
int len = byteArrayInputStream.read(buf);
// buf 시작 offset 항상 0, 읽어들인 수 만큼 (len) buf 에 있는 값을 ByteArrayOutputStream 에 쓴다.
byteArrayOutputStream.write(buf, 0, len);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
outputDestination = byteArrayOutputStream.toByteArray();
assertThat(outputDestination).isEqualTo(inputSource);
}
}
보조 스트림
실제 데이터를 주고받는 역할은 없고 그저 '버퍼' 를 제공함.
기능: File I/O 횟수를 저하시켜 성능(속도) 향상
Buffered Input/Output Stream (Byte 기반)
Buffered Reader/Writer (Character 기반) -> 문자열을 대상으로 읽고쓸 때 추천.
※ java 기준 Byte 1byte Character 2byte
특징: 보조 스트림 버퍼가 꽉 찰 때까지 직접적인 입/출력을 수행하지 않음.
-> 꽉 차는 시점에 입력/출력이 수행됨.
임의의 타이밍에 개발자가 버퍼를 비우며 쓰고싶다면? -> flush()
버퍼를 닫고싶다면 ? -> close() (자동으로 flush를 수행 후 보조 스트림 버퍼를 닫음)
※ Tip: Stream을 close하지 않으면 FILE 객체의 이름을 변경하는 renameTo같은 메소드가 정상적으로 동작하지 않는다.
(평상시 파일을 열어놓은 상태로 파일삭제를 시도하거나 압축을 시도하면 실패하는 원리와 같다.)
Input Stream은 어떻게 생겼을까??
추상메소드를 갖는 추상클래스로서 read() 메소드는 반드시 정의되어야한다.
특이한 점은 int read(byte[] b, int offset, int length) 에서 추상 메소드인 read()를 호출한다는 점이다.
이 말인 즉슨 read에서 파생된 다른 일반 메소드 또한 반드시 추상메소드의 원형 read() 메소드가 정의(override)되어야 한다는 것을 의미한다.
오라클 사의 JAVA API 공식 문서에는 추상 메소드 read 에 대해 다음과 같이 기술되어있다.
stream의 끝에 다르면 -1을 반환한다고 명세되어있다.
바이트 타입은 자바에서 1byte == 8bit 를 갖는 데이터 타입이기 때문에
실제로 읽어들이는 데이터의 단위를 나타내면
16진수 2자리 ex. 0xXX
2진수 8자리 ex. 01001001
[예제 코드]
-1에 도달하는 시점까지라는 것은 input_array에 대한 input_stream의 끝 지점 (즉 원소 '8' 을 읽은 다음 step)에 도달한 시점이라는 의미이다.
-> 모든 input stream 의 바이트 값을 다 읽은 경우.
[실행 결과]
실제 while문 한 싸이클 당 내부 동작은 (~ 여기부터 포스팅 계속!)
의문점: FILE이나 Stream의 IO 과정에서 EOF (End-Of-File) 파일의 끝 혹은 스트림의 끝이라는 것을 어떻게 아는 것일까?
배열같은 경우 배열의 끝원소의 다음 원소를 읽으려고 시도하면 segmentation Fault를 발생시키거나
outOfIndex Execption이 발생한다.
근데 우린 스트림이든 파일을 읽으면서든 Null Pointer Exception이나 outOfIndex Exception을 마주친 적이 없다.
그 단서는 C, C++에서 End Of File을 체크해주는 함수가 정의되어있다.
이 함수는 stream에 대해 파일의 끝 플래그가 설정되는지 여부를 표시한다.
음.. 추측 하건데 당연히 파일 IO는 사용자 모드가 아니라 커널 모드에서 이뤄질 것이고
관련 함수는 모두 시스템콜 함수로서 인터럽트를 발생시키고..
End-Of-File Flag 를 만나는 시점에에서 어떤 invocation이 일어나 더 이상 파일 읽기를 중단 시키는 것 같다.
그리고 EOF 값 자체는 스트림에 넣지 않는다. (추후 더 깊게.. 내부를 뜯어봐야겠다.)
[Reference]
https://stackoverflow.com/questions/20648611/input-output-stream-end-of-stream
http://www.cplusplus.com/reference/cstdio/feof/
https://docs.oracle.com/javase/7/docs/api/
'JVM > Java' 카테고리의 다른 글
[Java] Data Transfer Object (DTO) (0) | 2023.01.27 |
---|---|
[JAVA] JDK 11 'var' Type Inference (0) | 2020.11.03 |
자바 정규식을 활용한 패스워드 정책설정 예제 (0) | 2019.06.01 |
자바 정규식이란? (0) | 2019.06.01 |
Eclipse 자동완성 기능 등록. (0) | 2019.05.25 |