[Java] InputStream

2020. 4. 3. 23:48·JVM/Java

 

 

In/OutputStream 이란

입/출력 데이터 흐름의 통로

입력 스트림은 소스 데이터로부터 데이터를 읽어들이고

출력 스트림은 목적 대상까지 데이터를 흘려보내 쓴다.

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() 의 구현 메소드에 따라 동작이 달라짐을 알 수 있다.

 

 

InputStream
OutputStream

 

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);
  }
}

 

 

 

보조 스트림

InputStreamReader

바이트(byte)가 아닌 문자(char)를 읽기 위한 클래스다.

Java 의 char는 유니코드를 지원해야 하기 때문에 16bit (2 bytes)를 사용한다.

또, 문자의 값을 반환할 때는 반드시 인코딩(CharSet)이 필요하다.

    /**
     * Creates an InputStreamReader that uses the named charset.
     *
     * @param  in
     *         An InputStream
     *
     * @param  charsetName
     *         The name of a supported {@link Charset charset}
     *
     * @throws     UnsupportedEncodingException
     *             If the named charset is not supported
     */
    public InputStreamReader(InputStream in, String charsetName)
        throws UnsupportedEncodingException
    {
        super(in);
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        sd = StreamDecoder.forInputStreamReader(in, lockFor(this), charsetName);
    }

 

 

Reader 추상 클래스의 read() 반환 타입도 int 임을 알 수 있다.

public abstract class Reader implements Readable, Closeable {

	@Override
    public int read() throws IOException {
        ensureOpen();
        return -1;
    }

}

 

어? 그냥 char 반환하면 안되나?

EOL(-1)을 표현하기 위해서 int 형으로 반환한다.

실제 반환 값의 범위는 -1, 0 ~ 65535 (char, 16bit) 가 된다.

 

InputStream vs InputStreamReader 요약

  InputStream InputStreamReader
읽기 단위 byte char
read() 반환 타입 int int
read() 반환 값 범위 -1, 0 ~ 255 (byte, 8bit) -1, 0 ~ 65535 (char, 16bit)
인코딩 사용 여부 X O

 

 

BufferedReader

 

실제 데이터를 주고받는 역할은 없고 그저 버퍼를 제공한다.

File I/O 횟수를 저하시켜 성능 향상을 도모한다.

 

Buffered Input/OutputStream (Byte 기반) 

Buffered Reader/Writer (Character 기반) -> 문자열을 대상으로 읽고쓸 때 추천.

 

특징: 보조 스트림 버퍼가 꽉 찰 때까지 직접적인 입/출력을 수행하지 않고 기다린다.

버퍼가 꽉 차는 시점에 입력/출력을 수행하는데 이 연산을 'flush' 라고한다.

 

※ Tip: Stream을 close하지 않으면 FILE 객체의 이름을 변경하는 renameTo같은 메소드가 정상적으로 동작하지 않는다.
(평상시 파일을 열어놓은 상태로 파일삭제를 시도하거나 압축을 시도하면 실패하는 원리와 같다.)

 


Input Stream은 어떻게 생겼을까??

복잡한 부분을 생략하고 내부를 살펴보면 대략 이런식이다.

추상메소드를 갖는 추상클래스로서 read() 메소드는 반드시 정의되어야한다.

특이한 점은 int read(byte[] b, int offset, int length) 에서 추상 메소드인 read()를 호출한다는 점이다.

이 말인 즉슨 read에서 파생된 다른 일반 메소드 또한 반드시 추상메소드의 원형 read() 메소드가 정의(override)되어야 한다는 것을 의미한다.

 


오라클 사의 JAVA API 공식 문서에는 추상 메소드 read 에 대해 다음과 같이 기술되어있다.

생성된 스트림으로부터 한 '바이트'(0~255) 단위로 읽어들이는 메소드이다.

stream의 끝에 다르면 -1을 반환한다고 명세되어있다.

바이트는 8bit 를 갖는 데이터 타입이기 때문에

실제로 읽어들이는 데이터의 단위를 나타내면

16진수 2자리 ex. 0xXX 

2진수 8자리 ex. 01001001

 


[예제 코드]

input stream으로 부터 한 바이트씩 'input_array'로부터 읽어 output stream에 쓰는 코드다.

-1에 도달하는 시점까지라는 것은 input_array에 대한 input_stream의 끝 지점 (즉 원소 '8' 을 읽은 다음 step)에 도달한 시점이라는 의미이다.

-> 모든 input stream 의 바이트 값을 다 읽은 경우.

[실행 결과]

출력 결과다. ouputstream에 대한 별도의 형변환 없이는 이상한 문자열들이 출력된다.

 

실제 while문 한 싸이클 당 내부 동작은 (~ 여기부터 포스팅 계속!)

 


의문점: FILE이나 Stream의 IO 과정에서 EOF (End-Of-File) 파일의 끝 혹은 스트림의 끝이라는 것을 어떻게 아는 것일까?

배열같은 경우 배열의 끝원소의 다음 원소를 읽으려고 시도하면 segmentation Fault를 발생시키거나

OutOfIndex Execption이 발생한다.

근데 우린 스트림이든 파일을 읽으면서든 Null Pointer Exception이나 OutOfIndex  Exception을 마주친 적이 없다.

 

그 단서는 C, C++에서 End Of File을 체크해주는 함수가 정의되어있다.

스트림 내부의 포지션 지시자가 EOF를 다음 동작에 가리키게 될 것이다.

이 함수는 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
'JVM/Java' 카테고리의 다른 글
  • [Java] Data Transfer Object (DTO)
  • [JAVA] JDK 11 'var' Type Inference
  • 자바 정규식을 활용한 패스워드 정책설정 예제
  • 자바 정규식이란?
M_Falcon
M_Falcon
  • M_Falcon
    Falcon
    M_Falcon
  • 전체
    오늘
    어제
    • 분류 전체보기 (432)
      • Web (16)
        • Nodejs (14)
        • Javascript (23)
        • FrontEnd (4)
      • DataBase (39)
        • Fundamental (1)
        • Redis (4)
        • PostgreSQL (10)
        • NoSQL (4)
        • MySQL (9)
        • MSSQL (3)
        • Error (4)
      • Algorithm (79)
        • Algorithm (문제풀이) (56)
        • Algorithm (이론) (23)
      • JVM (65)
        • Spring (13)
        • JPA (5)
        • Kotlin (13)
        • Java (24)
        • Error (7)
      • 기타 (70)
        • Kafka (3)
        • Kubernetes (3)
        • Docker (13)
        • git (19)
        • 잡동사니 (27)
      • 재테크 (11)
        • 세무 (4)
        • 투자 (3)
        • 보험 (0)
      • BlockChain (2)
        • BitCoin (0)
      • C (32)
        • C (10)
        • C++ (17)
        • Error (3)
      • Low Level (8)
        • OS (3)
        • 시스템 보안 (5)
      • 네트워크 (3)
      • LINUX (30)
        • Linux (26)
        • Error (4)
      • 저작권과 스마트폰의 이해 (0)
      • 생각 뭉치 (6)
      • 궁금증 (2)
      • Private (4)
        • 이직 경험 (0)
        • 꿈을 찾아서 (1)
      • Android (21)
        • OS (4)
  • 블로그 메뉴

    • 홈
    • WEB
    • 알고리즘
    • DataBase
    • Linux
    • Mobile
    • C
    • 방명록
  • 링크

    • github
  • 공지사항

  • 인기 글

  • 태그

    백준
    C++
    kafka
    javascript
    Programmers
    docker
    android
    JPA
    Kotlin
    linux
    algorithm
    Bitcoin
    database
    알고리즘
    Git
    java
    프로그래머스
    PostgreSQL
    ubuntu
    Spring
  • hELLO· Designed By정상우.v4.10.3
M_Falcon
[Java] InputStream
상단으로

티스토리툴바