I/O

smj_716·2025년 3월 5일

Java-study / live-study

목록 보기
12/16

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

1) 스트림(Stream)

스트림은 데이터를 연속적으로 읽고 쓰는 개념이다. 자바에서 Input/Output은 스트림을 통해 이루어진다.

스트림은 단방향으로 동작하고 아래와 같이 두가지로 나뉜다.

  • 입력 스트림 : 데이터를 읽어오는 통로
  • 출력 스트림 : 데이터를 내보내는 통로

🖥️ 바이트 기반 스트림

import java.io.*;

public class ByteStreamExample {
    public static void main(String[] args) {
        String filePath = "byte_example.txt";

        // 파일에 데이터 쓰기 (바이트 기반)
        try (FileOutputStream fos = new FileOutputStream(filePath)) {
            fos.write("Hello, Java!".getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 파일에서 데이터 읽기 (바이트 기반)
        try (FileInputStream fis = new FileInputStream(filePath)) {
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data); // 바이트를 문자로 변환
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

🖥️ 문자 기반 스트림

import java.io.*;

public class CharStreamExample {
    public static void main(String[] args) {
        String filePath = "char_example.txt";

        // 파일에 데이터 쓰기 (문자 기반)
        try (FileWriter writer = new FileWriter(filePath)) {
            writer.write("Hello, Java!");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 파일에서 데이터 읽기 (문자 기반)
        try (FileReader reader = new FileReader(filePath)) {
            int data;
            while ((data = reader.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

🔴 바이트 기반 스트림 - InputStream, OutputStream

스트림은 바이트 단위로 데이터를 전송하고 입출력 대상에 따라 다음과 같은 입출력 스트림이 있다.

입력 스트림출력 스트림입출력 대상의 종류
FileInputStreamFileOutputStream파일
ByteArrayInputStreamByteArrayOutputStream메모리(byte 배열)
PipedInputStreamPipedOutputStream프로세스(프로세스간 통신)
AudioInputStreamAudioOutputStream오디오 장치

📢 위 입출력 스트림은 각각 InputStreamOutputStream의 자손들이며 각각 읽고 쓰는데 필요한 추상 메서드를 자신에 맞게 구현해놓은 구현체이다.

InputStreamOutputStream
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()write(int b)는 입출력 대상마다 구현 방식이 달라야 하기 때문에 InputStream과 OutputStream에서 추상 메서드로 정의되어 있다.
  • 즉, read(byte[] b), write(byte[] b, int off, int len) 같은 다른 메서드들은 내부적으로 read()와 write(int b)를 사용하기 때문에 기본 read()write(int b)구현하지 않으면 이들도 동작하지 않는다.

👉 read() 반환 타입이 int인 이유?
read()의 반환값 범위는 0~255(1바이트 데이터) 또는 -1(EOF, 즉 더 이상 읽을 데이터 없음) 이다. byte는 음수를 표현하는 특성 때문에 이를 구별하기 어려워 int를 사용하여 -1과 0~255를 명확히 구별한다.

🔴 보조 스트림

스트림의 기능을 보완하기 위해 보조스트림이라는 것이 제공된다.
보조스트림은 실제 데이터를 주고받는 스트림이 아니기 때문에 데이터를 입출력할 수 있는 기능은 없지만 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있다.

즉, 스트림을 먼저 생성한 다음에 이를 이용해 보조스트림을 생성해서 활용한다.

💡 Buffer를 사용하면 좋은 이유
한 바이트씩 바로바로 보내는 것이 아니라 버퍼에 담았다가 한번에 모아서 보내는 방법이다. 단순이 모아서 보낸다고 이점이 있는게 아니라 OS 레벨에 있는 시스템 콜 횟수를 줄이기 때문에 성능이 빨라지는 것이다.

ex) test.txt라는 파일을 읽기 위해 FileInputStream을 사용할 때, 입력 성능을 향상시키기 위해 버퍼를 사용하는 보조스트림인 BufferedInputStream을 사용할 수 있다.

// 먼저 기반 스트림을 생성한다.
FileInputStream fileInputStream = new FileInputStream("test.txt");

// 기반 스트림을 이용해 보조 스트림을 생성한다.
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

// Buffered**Stream 생성 시 사이즈도 정의하여 생성할 수 있다. (2번째 파라미터)
// default : 8192
BufferedInputStream bis = 
	new BufferedInputStream(fileInputStream, 8192);

// 보조스트림을 이용해 데이터를 읽는다.
bufferedInputStream.read();

🔴 문자 기반 스트림 - Reader, Write

위의 스트림은 모두 바이트 기반, 즉 입출력 단위가 1byte이다.
but, Java에서는 한 문자를 의미하는 char 형이 1byte가 아니라 2byte이기 때문에 바이트기반의 스트림으로 2byte인 문자를 처리하는데에 어려움이 있어 문자 기반 스트림을 제공한다.

  • InputStream → Reader
  • OutputStream → Writer

2. IO와 NIO의 차이점

IO와 NIO는 데이터를 입출력한다는 목적은 동일하지만, 방식에서 큰 차이가 나타난다.

구분IONIO
입출력 방식스트림 방식채널 방식
버퍼 방식넌버퍼(non-buffer)버퍼(buffer)
비동기 방식지원 안함지원
블로킹 / 넌블로킹 방식블로킹 방식만 지원(동기)블로킹 / 넌블로킹 방식 모두 지원 (동기/비동기 모두 지원)

➡️ Stream vs Channel

IO는 스트림(Stream) 기반이다.
스트림은 입력 스트림과 출력 스트림으로 구분되어 있기 때문에 데이터를 읽기 위해서는 입력 스트림을 생성해야 하고, 데이터를 출력하기 위해서는 출력 스트림을 생성해야 한다.

NIO는 채널(Channel) 기반이다.
채널은 스트림과 달리 양방향으로 입력과 출력이 가능하다. 그렇기 때문에 입력과 출력을 위한 별도의 채널을 만들 필요가 없다.

➡️ non-buffer vs buffer

IO에서는 출력 스트림이 1바이트를 쓰면 입력 스트림이 1바이트를 읽어 대체로 느리다.
버퍼(메모리 저장소)를 사용하여 복수 개의 바이트를 한꺼번에 입력, 출력하면 성능이 높아진다.
-> 그래서 IO는 버퍼를 제공해주는 보조 스트림인 BufferedInputStream, BufferedOutputStream을 연결해 사용하기도 한다.

NIO는 기본적으로 버퍼를 사용하여 입출력하여 IO보다 높은 성능을 가진다.

➡️ Blocking vs non-blocking

IO는 블로킹(Blocking) 된다.

  • 입력 스트림의 read() 메소드를 호출하면 데이터가 입력되기 전까지 Thread는 블로킹(대기상태)가 된다. 마찬가지로 출력 스트림의 write() 메소드를 호출하면 데이터가 출력되기 전까지 Thread는 블로킹된다.
  • IO Thread가 블로킹되면 다른 일을 할 수 없고 블로킹을 빠져나오기 위해 인터럽트(interrupt)도 할 수 없다.
  • 블로킹을 빠져나오는 유일한 방법스트림을 닫는것이다.

NIO는 블로킹과 넌블로킹(non-blocking) 특징을 모두 가진다.
IO블로킹과 NIO 블로킹과의 차이점은 NIO 블로킹은 Thread를 인터럽트(interrupt) 함으로써 빠져나올 수 있다.


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

표준입출력은 콘솔을 통한 데이터 입력과 콘솔로의 데이터 출력을 의미한다.
자바에서는 표준 입출력(standard I/O)를 위해 3가지 입출력 스트림을 제공한다.

System.in   // 콘솔로부터 데이터를 입력받는데 사용
System.out  // 콘솔로 데이터를 출력하는데 사용
System.err  // 콘솔로 데이터를 출력하는데 사용

자바 어플리케이션의 실행과 동시에 사용할 수 있게 자동적으로 생성되기 때문에 개발자가 별도로 스트림을 생성하는 코드를 작성하지 않아도 된다.

✏️ System.in

  • 우리가 키보드로 입력하는 데이터를 처리하는 객체이다.
  • System.in의 타입은 InputStream이며 바이트 단위로 입력을 받는다.
  • 한글 같은 문자들은 보통 2바이트 이상을 사용하기 때문에 문자 깨짐이 발생할 수 있어 보통 ScannerBufferedReader를 사용해서 문자를 제대로 읽는다.
  • 콘솔입력은 버퍼를 가지고 있기 때문에 한번에 버퍼의 크기만큼 입력이 가능하다.
  • 입력의 끝을 알리는 Enter 키나 '^z' 를 누르기 전까지는 데이터가 입력중인 것으로 간주하여 커서가 입력을 기다리는 상태(블락킹)에 머무르게 된다.

✏️ System.out

  • 우리가 System.out.println()으로 콘솔에 출력할 때 쓰는 객체이다.
  • System.out의 타입은 PrintStream이며 PrintStream은 OutputStream의 자식 클래스이지만 예외 처리(Try-Catch)를 강요하지 않는다.

✏️ System.err

  • System.out이 일반적인 메시지를 출력하는 거라면 System.err은 오류 메시지를 출력할 때 사용한다.
  • System.out과 마찬가지로 PrintStream 타입이지만 오류 메시지를 따로 구분해서 볼 수 있도록 사용한다.
  • 콘솔에서는 out과 err가 비슷해보이지만 파일로 저장할 때는 따로 분리가 가능하다.
  • System.out.println()의 출력은 output.txt에 저장 / System.err.println()의 출력은 error.txt에 저장

4. 파일 읽고 쓰기

자바에서 파일을 읽고 쓰는 방법은 자바의 내장 클래스인 FileWriter, BufferedWriter, FileReader, BufferedReader를 사용한다
여기서 파일 쓰기를 위한 BufferedWriter와 FileWriter의 객체를 사용할 때 try-catch의 마지막 finally block에서 null check 및 close()하는 코드를 삽입해 줘야 하는데, 여기서는 java 7에 도입된 try-resource-catch을 사용한다면 자원 해제 코드를 작성하지 않아도 된다.

🖥️ 파일 쓰기

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class FileWriteExample {
    public static void main(String[] args) {
        String filePath = "example.txt"; // 저장할 파일 경로

        // try-with-resources 사용 (자동 close)
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
            writer.write("Hello, Java!");
            writer.newLine(); // 개행 추가
            writer.write("This is a file writing example.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

🖥️ 파일 읽기

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class FileReadExample {
    public static void main(String[] args) {
        String filePath = "example.txt"; // 읽을 파일 경로

        // try-with-resources 사용 (자동 close)
        try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
            String line;
            while ((line = reader.readLine()) != null) { // 한 줄씩 읽기
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

0개의 댓글