[STUDY HALLE] 13주차 과제: I/O

3 minute read

학습 목표

자바의 Input과 Ontput에 대해 학습하세요.

  • 스트림 (Stream) / 버퍼 (Buffer) / 채널 (Channel) 기반의 I/O
  • InputStream과 OutputStream
  • Byte와 Character 스트림
  • 표준 스트림 (System.in, System.out, System.err)
  • 파일 읽고 쓰기

Deadline: ~2021/02/20 13:00

I/O

I/O는 입력(input)과 출력(out)을 의미하는 것으로, 컴퓨터 내부 또는 외부 장치(키보드, 마우스)와 데이터를 주고 받는다.

스트림(Stream)
스트림(stream)이란 실제의 입력이나 출력이 표현된 데이터의 순서가 있는 흐름을 의미한다. 즉, 스트림은 운영체제에 의해 생성되는 가상의 통로이다. ‘흐름’이기 때문에 하나의 방향성을 가지고 있어 단방향 통신만 가능하다. 입력과 출력을 처리하고 싶다면, 입력 스트림과 출력스트림을 각각 따로 생성하여 사용해야한다는 것이다.

버퍼(Buffer)
버퍼는 데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리의 영역이다. I/O 과정에서 버퍼를 사용하게 되면 입력된(혹은 출력하려는) 데이터가 바로 전달되지 않고 중간에 버퍼를 거쳐 간접적으로 전달되는데, 이를 통해 입출력 속도와 처리 속도의 차이로 발생하는 비효율성을 줄일 수 있다.

채널(Channel)
채널은 서버와 클라이언트간의 통신수단을 나타낸다. ‘데이터가 다니는 통로’라는 점에서 스트림과 비슷하지만, 채널은 양방향이기 때문에 읽기와 쓰기를 동시에 하는 것이 가능하다.

InputStream과 OutputStream

java.io 패키지에 속해있으며, 여러가지 모든 stream클래스의 부모 클래스이다.

InputStream
InputStream은 바이트 기반 입력 스트림의 최상위 클래스로 추상 클래스이다. 모든 바이트 기반 입력 스트림은 이 클래스를 상속받아서 만들어진다.

메소드 설명
abstract int read() 입력 스트림으로 부터 1바이트를 읽고 읽은 바이트를 리턴한다.
int read(byte[] b) 입력 스트림으로부터 읽은 바이트들을 매개값으로 주어진 바이트 배열 b에 저장하고 실제로 읽은 바이트 수를 리턴한다
int read(byte[] b, int off, int len) 스트림으로 부터 len개의 바이트만큼 읽고 매개값으로 주어진 바이트 배열 b[off] 부터 len개까지 저장한다.
void close() 사용한 시스템 자원을 반납하고 입력 스트림을 닫는다.

outputStream
OutputStream은 바이트 기반 출력 스트림의 최상위 클래스로 추상 클래스이다. 모든 바이크 기반 출력 클래스는 이 클래스를 상속받아 만들어 진다.

메소드 설명
abstract void write(int b) 출력 스트림으로 1바이트를 보낸다
void write(byte[] b) 출력 스트림으로 주어진 바이트 배열 b의 모든 바이트를 보낸다
void write(byte[] b, int off, int len) 스트림으로 주어진 바이트 배열 b[off]부터 len개까지의 바이트를 보낸다
void flush() 버퍼에 잔류하는 모든 바이트를 출력한다
void close() 사용한 시스템 자원을 반납하고 출력스트림을 닫는다.

Byte와 Character 스트림

위처럼 스트림은 기본적으로 바이트(byte)단위로 데이터를 송수신한다. 일반적으로 바이트로 구성된 동영상, 이미지 파일등을 처리하는데 사용될 수 있지만, java에서 문자형(char)의 크기는 2byte 이므로 처리하는데 문제가 발생할 수 있다. 때문에 문자 기반의 스트림을 별도로 제공한다. 문자 스트림은 바이트 스트림 클래스에 대응되어 존재하므로, 바이트 스트림 클래스 이름에 InputStream을 Reader로, OutputStream을 Writer로 변경하여 사용할 수 있다.
보조 스트림
자바에서 제공하는 보조 스트림은 실제로 데이터를 주고받을 수는 없지만, 다른 스트림의 기능을 향상시키거나 새로운 기능을 추가해 주는 스트림이다. 모든 보조 스트림은 FilterInputStream/FilterOutputStream 을 상속받는다.

// 예시
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

위와 같이 데코레이터 패턴을 통해 실제 입출력 기능을 가진 클래스와 다양한 기능을 제공하는 보조스트림을 이용하여 기능을 구현할 수 있다.

I/O 와 NI/O(New I/O)

구분 IO NIO
입출력 방식 스트림 방식 채널 방식
버퍼 방식 넌버퍼 버퍼
비동기 방식 지원 안 함 지원
blocking / non-blocking blocking 방식만 지원 모두 지원

기존의 I/O에서 입력 스트림의 read()/write() 메소드를 호출하면, 데이터를 읽을 때/쓸 때까지 쓰레드가 블로킹(대기 상태)가 된다. I/O 쓰레드가 대기 상태가 되면 다른 일을 하거나 빠져나올 수(인터럽트)없는데, 스트림을 닫아야만 블로킹을 빠져나올 수 있다. 반면 NI/O는 쓰레드를 인터럽트하여 빠져나올 수 있다. 작업 준비가 완료된 채널만 선택하여 작업 쓰레드가 처리하며, 이렇게 여러 채널 중 준비된 채널을 선택할 수 있도록 하는 것은 셀렉터(Selector)라는 객체의 기능이다. 다만 NI/O가 항상 논블로킹인 것은 아닌데, 파일에 데이터를 읽고 쓰는 FileChannel은 NI/O에서도 항상 블로킹이다. 셀렉터에 대한 예시와 설명이 잘 정리된 블로그 링크를 첨부한다.
N/IO 셀렉터 : https://hbase.tistory.com/39

표준 스트림

java에서는 콘솔 같은 표준 입출력 장치를 위해 System이라는 표준 입출력 클래스를 정의한다. java.lang클래스에 정의되어 있으며, 표준 입출력 스트림은 별로도 생성하지 않아도 자동으로 생성된다.

클래스 변수 입출력 스트림 설명
System.in InputStream 콘솔로부터 데이터를 입력받음.
System.out PrintStream 콘솔로 데이터를 출력함.
System.err PrintStream 콘솔로 데이터를 출력함.

파일 읽고 쓰기

// text.txt
hello world!
public class Main {
    public static void main(String[] args) throws IOException {

        try(
            BufferedReader br = new BufferedReader(new FileReader(("./test.txt")));
            BufferedWriter bw = new BufferedWriter(new FileWriter(("./test.txt"),true));
            )
        {
            String temp;
            while ((temp = br.readLine()) != null) {
                System.out.print(temp);
            }

            bw.write(System.lineSeparator());
            bw.write("hi");
            bw.flush();

            while ((temp = br.readLine()) != null) {
                System.out.println(temp);
            }
        }
        catch(IOException e){
            e.printStackTrace();
        }

    }
}
hello world!
hi

텍스트 파일을 읽어오고, 문자열을 추가하는 간단한 예제를 작성해 보았다. 코드를 보면 스트림을 close하는 과정이 생략되어 있는데, try-with-resource문은 객체의 close문을 자동으로 호출해주기 때문이다. 그리고 버퍼의 내용을 파일에 쓰기 위해 flush()를 호출하지 않아도 close()에 의해 자동으로 호출되어 동작함을 알 수 있는데, 그럼에도 flush()를 호출하도록하자. close()가 항상 flush()를 호출한다는 보장이 없기 때문이다. 예를 들어, java.io.PrintWriter에서 close()는 flush()를 호출하지 않는다.

참고문헌

https://www.notion.so/IO-58b47ed3d28249b6b9960d7866a6f03e
https://docs.oracle.com/javase/tutorial/essential/io/bytestreams.html
http://www.tcpschool.com/java/java_io_stream
https://stackoverflow.com/questions/9858495/using-flush-before-close

Leave a comment