[자바 스터디] 13주차 : IO

2023. 2. 16. 12:40·스터디/자바

스트림 (Stream) / 버퍼 (Buffer) / 채널 (Channel) 기반의 I/O

스트림

자바에서는 어느 한 쪽에서 다른 쪽으로 데이터를 전달하기 위해서는 두 대상을 연결할 무언가가 필요한데, 이를 스트림이라 한다. 즉, 스트림은 데이터를 전송하는데 사용되는 연결 통로 의 역할을 하고 이를 통해 I/O가 이루어진다.

스트림은 단방향으로만 데이터를 전송할 수 있다. 따라서 입력과 출력을 동시에 처리하기 위해서는 아래와 같이 입력 스트림과 출력 스트림, 총 두 개의 스트림이 필요하다.

스트림은 먼저 보낸 데이터를 먼저 받게 되고, 중간에 건너뜀 없이 데이터를 전송한다. 즉, 큐와 같은 FIFO 구조로 이루어져있다.

NIO (New I/O)

자바 1.4버전부터 추가된 API로 기존 스트림을 사용하는 I/O 방식과는 아래와 같은 차이점들이 있다.

1. 스트림과 채널

기존 IO는 스트림을 사용해 입출력을 처리하는데, NIO는 채널을 사용해 입출력을 처리한다. 채널은 스트림과 달리 양방향으로 입력과 출력이 가능하므로 각각 별도의 채널을 만들 필요 없이 하나의 채널로 처리가 가능하다.

2. non-buffer vs buffer

버퍼는 byte, char, int 등 기본 데이터 타입을 저장할 수 있는 저장소이다. 입출력 시 버퍼를 사용하지 않는 경우 입력 / 출력 데이터를 무조건 즉시 처리해야 하기 때문에 데이터를 별도로 처리하기도 어렵고 버퍼를 사용할 때보다 많은 시스템 콜이 발생하기 떄문에 성능이 저하될 수 있다. (버퍼 사용 시 복수 개의 바이트를 한꺼번에 전달하는 것이 가능하다)

NIO에서는 기본적으로 입출력 시 버퍼를 사용하기 때문에 기본적으로는 버퍼를 사용하지 않는 스트림 기반의 IO보다 높은 성능을 가진다.

버퍼를 사용하면 왜 성능이 좋아질까?
IO 수행 시 자바는 OS 레벨의 API에 대해 시스템 콜을 한다. 버퍼를 사용하면 OS 레벨의 시스템 콜 횟수가 줄어들기 때문에 성능이 좋아지는 것이다. 즉, 물컵을 채워야 하는 상황에서 주방에 왔다갔다 하는 것을 시스템 콜이라고 했을 때 물을 주방에 왔다갔다하며 한 모금씩 떠오는 것과 컵을 직접 들고 가서 컵 전체를 한번에 떠오는 것의 효율 차이를 생각할 수 있다. 버퍼를 이용해 바이트들을 모아뒀다가 한 번에 시스템 콜을 수행할 수 있으므로 전체적으로 성능이 좋아지게 된다. (단순히 버퍼를 사용했기 때문에 성능이 좋아지는게 아니다.)

3. blocking vs non-blocking

IO는 read(), write() 메서드 호출 시 해당 작업이 끝나기 전까지 Thread가 blocking(대기 상태)된다. IO Thread가 블러킹되면 스트림을 닫는 것만을 통해 blocking을 빠져나올 수 있다. 즉, IO 수행 중에는 다른 작업의 수행이 불가능하다.

반면, NIO는 인터럽트를 통해 블로킹 상태에서 빠져나와 다른 작업을 수행할 수 있다. 또한, 입출력 시 작업을 수행할 준비(지금 당장 읽고 쓸 준비)가 된 채널만 선택해서 처리하기 때문에 작업 Thread가 blocking 되지 않는다.

채널

NIO에서 사용되는 데이터 전송 통로, 쌍방향으로 데이터가 이동할 수 있다. 채널에서 데이터를 주고받을 때 버퍼가 사용된다.SocketChannel, FileChannel 등이 존재한다.

InputStream과 OutputStream

InputStream

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

메서드 설명
int read() 입력 스트림으로부터 1바이트를 읽어서 int로 반환
int read(byte[] b) 입력 스트림에서 여러 바이트를 배열 b에 저장하고 바이트의 길이를 반환한다. 만약 스트림의 end에 도달하여 읽기가 불가능하면 -1 반환
close() 스트림과 시스템 자원들을 닫고 반환

이외에도 int readNBytes(byte[] b, int off, int len), byte[] readAllBytes() 등 여러 종류의 읽기 메서드가 제공된다.

read() 메소드의 반환 타입이 int인 이유는 바이트를 반환할 때 0~255 사이의 값으로 반환하고, 스트림의 마지막에 도달할 경우 -1 을 반환하도록 되어 있기 때문이다.

즉, 반환 값은 int 이지만 4바이트를 모두 쓰는 것이 아니고 읽어오는 값 자체는 1바이트이다.

OutputStream

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

메서드 설명
void write(int b) 1바이트 (b)를 출력 스트림에 쓴다
void write(byte[] b) b.length 길이의 바이트를 출력 스트림에 쓴다
flush() 버퍼에 잔류하는 모든 바이트를 출력 스트림에 쓴다
close() 스트림과 시스템 자원들을 닫고 반환

이떄, flush()는 버퍼를 사용하는 스트림에서만 의미있다. 기본적인 OutputStream에서는 아무런 동작을 하지 않는다. 기본적으로 스트림을 사용하는 IO는 버퍼를 통하지 않지만 보조 스트림을 통해 기능을 추가하면 가능하다.

Byte와 Character 스트림

Byte Stream

지금까지 살펴본 XxxStream 방식의 스트림들은 모두 Byte 기반 스트림이다. 이러한 스트림들은 데이터를 8비트(1바이트) 단위로 처리한다. 따라서 binary 데이터 즉, 이미지나 동영상 등을 입출력할 때 사용하면 좋다.

입출력 대상 입력 스트림 출력 스트림
파일 FileInputStream FileOutputStream
바이트 배열 ByteArrayInputStream ByteArrayOutputStream
Pipe (Thread간의 데이터 전달) PipedInputStream PipedOutputStream

Character Stream

반면, Character 스트림은 데이터를 16비트(2바이트) 단위로 처리한다. 또한, 데이터를 유니코드로 읽어들인다. 따라서 text 데이터를 입출력할 때 사용하면 좋다. JSON, HTML 등을 송수신할 때 주로 사용된다고 한다.

클래스들의 이름은 대체로 Byte 기반 스트림에서 Input/OutputStream이 Reader/Writer로 바뀌도록 명명되어있다. 또한, 제공하는 메소드들도 바이트 기반 스트림과 거의 유사하다.

입출력 대상 입력 스트림 출력 스트림
파일 FileReader FileWriter
바이트 배열 CharArrayReader CharArrayWriter
Pipe (Thread간의 데이터 전달) PipedReader PipedWriter

보조 스트림

스트림의 기능을 보완하기 위해 보조 스트림이 제공된다. 보조 스트림은 실제로 데이터를 주고받는 기능은 없지만 주 스트림을 감싸서 해당 스트림에 기능을 더하거나 향상시킬 수 있다.

Byte과 Character 기반 스트림 모두에 보조 스트림이 제공된다. 각각 FilterInput/OutputStream 과 FilterReader/Writer를 상속받아 추가 기능을 수행하는 보조 스트림을 구현한다. FilterXxx 클래스는 단순히 Input/OutputStream 또는 Reader/Writer를 상속받아 상위 클래스의 메서드를 호출하도록 구현되어 있다.

public abstract class FilterReader extends Reader {
    protected Reader in;

    protected FilterReader(Reader in) {
        super(in);
        this.in = in;
    }

    public int read() throws IOException {
        return in.read();
    }
    ...
}

picture 2

예시

아래의 예시는 버퍼를 사용하지 않는 FileInputStream을 BufferedInputStream으로 감싸서 버퍼를 사용하도록 하는 예제이다.

FileInputStream fileInputStream = new FileInputStream("documents.csv");
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

bufferedInputStream.read();

이때, 주 스트림을 감싸고 있는 보조 스트림을 통해 read() 메소드를 호출한다. 코드만 보면 보조 스트림이 읽기 작업을 수행할 것 처럼 보이지만 아래의 BufferedInputStream 코드를 통해 알 수 있듯 실제 읽기 기능은 주 스트림을 통해서 수행되고 보조 스트림은 일종의 Wrapper 클래스 처럼 기능을 추가/향상 하는 역할만 수행한다.

private void fill() throws IOException {
    ...
    int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
    ...
}

---

private InputStream getInIfOpen() throws IOException {
    InputStream input = in; // 생성자로 받은 InputStream
    if (input == null)
        throw new IOException("Stream closed");
    return input;
}

표준 스트림 (System.in, System.out, System.err)

표준 스트림은 콘솔을 통한 데이터 입력과 출력을 위한 스트림을 말한다. 자바에서는 아래와 같이 3가지 표준 스트림이 제공되며 java.lang.System에 static 멤버로 선언되어 있다.

public final class System {
    ...
    // 콘솔로 부터 데이터를 입력받는데 사용
    public static final InputStream in = null;
    // 콘솔에 데이터를 출력하는데 사용
    public static final PrintStream out = null;
    // 콘솔에 데이터를 출력하는데 사용
    public static final PrintStream err = null;
    ...
}

지금껏 콘솔에 출력을 위해 사용했던 System.out.println() 에서 System.out이 바로 위의 PrintStream 객체이다.

PrintSteram 객체는 FilterOutputStream을 상속받는 보조 스트림의 일부로 생성자를 통해 별도의 스트림을 주입받는다.

public class PrintStream extends FilterOutputStream implements Appendable, Closeable {
    ...
    private PrintStream(boolean autoFlush, OutputStream out) {
        super(out);
        ...
    }
    ...
}

그리고 다시 System의 newPrintStream() 메서드를 보면 실제로 System에서 사용하는 PrintStream은 BufferedOutputStream을 감싸고 있는 것을 알 수 있다.

private static PrintStream newPrintStream(FileOutputStream fos, String enc) {
    if (enc != null) {
        try {
            return new PrintStream(new BufferedOutputStream(fos, 128), true, enc);
        } catch (UnsupportedEncodingException uee) {}
    }
    return new PrintStream(new BufferedOutputStream(fos, 128), true);
}

out과 err 모두 PrintStream 이지만 out은 표준 출력, err는 표준 에러를 출력하는데 사용된다. 오류 내용은 보다 정확하고 빠르게 출력되어야 하기 때문에 버퍼링을 지원하지 않는다고 한다(버퍼링 도중 프로그램이 종료되면 버퍼의 내용이 출력되지 않음).

표준 입출력 대상 변경

기본적으로는 콘솔을 대상으로 표준 입출력이 이루어지지만, 아래 메서드들을 사용하면 임의로 생성한 Stream으로 그 대상을 변경할 수 있다. 즉, System.out.println()의 대상을 파일 등으로 바꿀 수 있다.

메서드 설명
static void setIn(InputStream in) System.in을 인자 in 으로 변경
static void setOut(PrintStream out) System.out을 인자 out 으로 변경
static void setErr(PrintStream err) System.err을 인자 err 으로 변경

파일 읽고 쓰기

텍스트 파일은 Character Stream을(성능상 이점), 바이너리 파일은 Byte Stream을 사용하면 된다. 추가적으로 성능을 높이기 위해 Buffered 계열의 보조 스트림으로 주 스트림을 감싸서 버퍼를 사용하도록 하는 것이 좋다.

텍스트 파일 예시

try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
     BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {

    String line;

    while ((line = reader.readLine()) != null) {
        writer.write(line + "\n");
    }

} catch (IOException e) {
    throw new RuntimeException(e);
}

좌측이 input.txt, 우측이 output.txt.
picture 3

이진 파일 예시

try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream("inputFile.jpg"));
     BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream("outputFile.jpg"))) {

    byte[] buffer = new byte[16384];

    while (inputStream.read(buffer) != -1) {
        outputStream.write(buffer);
    }
} catch (IOException e) {
    throw new RuntimeException(e);
}

피드백

  • 데코레이터 패턴의 대표적인 예시가 java의 io 패키지의 클래스들이다.

    • 데코레이터 패턴 - 객체를 다른 객체로 감싸서 행동을 동적으로 추가하는 것을 말함
  • BufferedOutputStream의 경우 버퍼 사이즈가 가득찼을 때만 실제로 쓰기 작업을 수행하므로 마지막 쓰기 내용이 버퍼를 가득 채우지 않고 && close() 혹은 flush()가 수행되지 않으면 마지막 내용은 작성되지 않을 수 있으니 꼭 자원을 닫아주어야 한다.

    • try-with-resources 사용해도 해결가능
    // close 하지 않은 경우
    BufferedOutputStream writer = new BufferedOutputStream(new FileOutputStream("output.txt"), 4);
    for (char c = '0'; c < '6'; c++) {
        writer.write(c);
    }
    // writer.close();
    
    BufferedReader reader = new BufferedReader(new FileReader("output.txt"));
    System.out.println(reader.readLine()); // 0123
    // close 한 경우
    BufferedOutputStream writer = new BufferedOutputStream(new FileOutputStream("output.txt"), 4);
    for (char c = '0'; c < '6'; c++) {
        writer.write(c);
    }
    writer.close();
    
    BufferedReader reader = new BufferedReader(new FileReader("output.txt"));
    System.out.println(reader.readLine()); // 012345

참고 자료

  • https://bingbingpa.github.io/whiteship-live-study-week13/
  • https://www.notion.so/I-O-af9b3036338c43a8bf9fa6a521cda242
  • https://www.tutorialspoint.com/Character-Stream-vs-Byte-Stream-in-Java
  • https://jenkov.com/tutorials/java-io/filterinputstream.html

'스터디 > 자바' 카테고리의 다른 글

[자바 스터디] 15주차 : 람다식  (0) 2023.02.27
[자바 스터디] 14주차 : 제네릭  (0) 2023.02.18
[자바 스터디] 12주차 : 애노테이션  (0) 2023.02.12
[자바 스터디] 11주차 : Enum  (0) 2023.02.12
[자바 스터디] 10주차 : 멀티쓰레드 프로그래밍  (0) 2023.02.06
'스터디/자바' 카테고리의 다른 글
  • [자바 스터디] 15주차 : 람다식
  • [자바 스터디] 14주차 : 제네릭
  • [자바 스터디] 12주차 : 애노테이션
  • [자바 스터디] 11주차 : Enum
gmelon
gmelon
백엔드 개발을 공부하고 있습니다.
  • gmelon
    gmelon's greenhouse
    gmelon
  • 전체
    오늘
    어제
    • 분류 전체보기 (91)
      • 개발 공부 (28)
        • Java (6)
        • Spring (10)
        • 알고리즘 (11)
        • 기타 (1)
      • 프로젝트 (12)
        • [앱] 플랭고 (4)
        • 졸업 프로젝트 (8)
      • 스터디 (0)
        • 자바 (30)
      • 기록 (15)
        • 후기, 회고 (9)
        • SSAFYcial (5)
        • 이것저것 (1)
      • etc. (6)
        • 모각코 (6)
  • 블로그 메뉴

    • 홈
    • 방명록
    • github
    • 스크랩
  • 인기 글

  • 태그

    AWS 프리티어 종료
    groupingBy mapping
    자바 Collector
    Java Collector
    java
    groupingBy()
    싸피 회고
    한글프로그래밍언어
    자바
    2024 상반기 회고
    프리티어 종료
    졸업프로젝트
    태초마을이야
    Collector groupingBy()
    2024 회고
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
gmelon
[자바 스터디] 13주차 : IO
상단으로

티스토리툴바