스트림 (stream)
- 데이터를 전달하는 통로
- 단방향 통신만 가능
- 입력과 출력을 동시에 처리할 수 없음
- 입력과 출력을 동시에 처리하려면 input stream과 output stream 2개의 스트림이 필요
- FIFO(First In First Out) 구조
보조 스트림
- 스트림의 기능을 보완하기 위한 기능
- 실제 데이터를 주고받는 스트림이 아니기 때문에 데이터를 입출력할 수 있는 기능은 없음
- 보조스트림 만으로는 입출력을 처리할 수 없고, 스트림을 먼저 생성한 다음에 이를 이용해서 보조스트림을 생성
BufferedInputStream
- 실제 기능은 FileInputStream이 수행
- 보조스트임인 BufferedInputStream은 버퍼만을 제공함
- 버퍼를 사용한 입출력과 그렇지 않은 경우는 성능 차이가 상당하기 때문에 대부분의 경우에 버퍼를 이용한 보조스트림 사용
// 기반 스트림 생성
FileInputStream fis = new FileInputStream("test.txt");
// 기반 스트림을 이용해서 보조스트림을 생성
BufferedInputStream bis = new BufferedInputStream(fis);
bis.read(); // 보조스트림인 BufferedInputStream으로부터 데이터를 읽음
버퍼 (Buffer)
- 임시로 데이터를 담아둘 수 있는 일종의 큐
- 바이트 단위의 데이터가 입력될 때마다 Stream은 즉시 전송하게 되는데 이것은 디스크 접근이나 네트워크 접근 같은 오버헤드가 발생
- Buffer는 중간에서 입력을 모아서 한번에 출력함으로써 I/O 의 성능을 향상
문자 기반 스트림
- 바이트 기반 스트림은 입출력의 단위가 1byte
- java에서는 char형이 2byte임 → 문자 기반 스트림은 입출력의 단위가 2byte
표준입출력
- 콘솔을 통한 데이터 입력과 데이터 출력을 의미
- System.in
- System.out
- System.err
- java.lang.System 클래스의 static 필드
- JVM이 프로그램 시작 시점에 자동으로 초기화
실제 내부
public final class System {
public static final InputStream in;
public static final PrintStream out;
public static final PrintStream err;
static {
// 이 부분은 JVM이 초기화하면서 실행됨
registerNatives(); // 네이티브 메서드 등록
in = new BufferedInputStream(new FileInputStream(FileDescriptor.in));
out = new PrintStream(new BufferedOutputStream(new FileOutputStream(FileDescriptor.out)), true);
err = new PrintStream(new BufferedOutputStream(new FileOutputStream(FileDescriptor.err)), true);
}
}
- FileDescriptor.in, FileDescriptor.out, FileDescriptor.err
- OS 레벨의 표준 스트림 (0, 1, 2) 의미
표준 입출력 매서드가 느리다고 여겨지는 이유
- 추상화 레벨이 높음
- System.out.println() ➡️ PrintStream ➡️ OutputStream ➡️ FileOutputStream ➡️ FileDescriptor.out
- 중간에 객체 생성, 메서드 호출, 동기화(synchronized) 등이 포함
- PrintStream 의 대부분의 매소드는 synchronized 이기 때문에 오버헤드 발생
- 싱글스레드에서도 락 ➡️ 불필요한 비용
- 잦은 flush
- 기본적으로 버퍼는 존재해요 (BufferedOutputStream, BufferedInputStream 사용)
- PrintStream 생성자에서 autoFlush = true로 설정돼 있음
- 줄 바꿈이 일어나거나 println을 쓸 때마다 버퍼를 비움
- 비교 대상이 훨씬 빠르기 때문
- BufferedWriter + FileWriter
- BufferedReader + FileReader
- 버퍼 크기를 크게 설정할 수 있고,
- flush 제어가 가능하고,
- 동기화 제어도 직접 할 수 있어서 더 빠르다
개선안
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
- System.in은 byte stream
- System.in은 InputStream 타입이고, 기본적으로 바이트 단위로 읽음
- InputStream.read()는 한 번에 1바이트만 읽기 때문에 비효율적
- InputStreamReader는 byte ➡️ char 변환하는 보조 스트림
- InputStream이 byte 단위로 입력을 받은 뒤, InputStreamReader가 문자 단위 데이터로 변환
- 문자 단위 입력 ➡️ 출력
- BufferedReader는 버퍼링된 문자 입력 처리
- BufferedReader는 내부에 버퍼(기본 8KB)를 두고 여러 글자를 한 번에 읽어들임
입출력 방법에 따른 속도 비교
방법 속도 추천 용도
System.in.read() | 매우 느림 | 거의 안 씀 |
Scanner | 느림 | 간단한 테스트 |
BufferedReader | 빠름 | 일반적인 입력 |
BufferedReader + StringTokenizer | 더 빠름 | 숫자/토큰 빠르게 처리 |
FastReader 직접 구현 | 최적 | 알고리즘, 대량 입력 |
파일 입력 리디렉션 | 빠름 | 테스트 자동화, 성능 실험 |
Java NIO
- Stream은 blocking방식과 Non-Buffer의 특징으로 인해 입출력 속도가 느림
- 자바 4부터 새로운 입출력(New Input/Output)이라는 뜻에서 java.nio 패키지가 포함
- NIO는 Non-blocking방식으로 데이터를 처리 할 수 있어서 과도한 스레드 생성을 피하고 스레드를 효과적으로 재사용
- 자바 7에서 IO와 NIO 사이의 일관성 없는 클래스 설계를 바로 잡고 비동기 채널 등의 네트워크 지원을 대폭 강화한 NIO.2 API가 추가
- NIO.2는 java.nio의 하위 패키지(java.nio.channels, java.nio.charset, java.nio.file)에 통합
채널 (Channel)
- 데이터가 흘러다니는 양방향의 통로
- Stream과 다르게 양방향이기 때문에 input/output을 구분하지 않음
- 기본적으로 Buffer를 통해서만 read와 write를 할 수 있는 buffer방식
- blocking방식과 non-blocking방식 모두 가능
Java IO와 NIO의 차이점
구분 IO NIO
입출력 방식 | Stream | Channel |
버퍼 방식 | non-Buffer | Buffer |
비동기 방식 | 지원 x | 지원 o |
블로킹/논블로킹 방식 | 블로킹 | 블로킹/논블로킹 |
Blocking vs Non-Blocking
- java IO
- 입력 스트림의 read(), 출력 스트림의 write() 를 호출하면 데이터가 입력되기 전까지 쓰레드는 Blocking됨
- 스레드가 블로킹 되면 다른 일을 할 수 없음
- 블로킹을 빠져나오기 위해 인터럽트 하는 것도 불가능
- 블로킹을 빠져나오는 방법은 스트림을 닫는 것
- java NIO
- Blocking: 스레드를 인터럽트 함으로써 빠져나올 수가 있음
- Non-Bloking: 입출력 작업 준비가 완료된 채널만 선택해서 작업 쓰레드가 처리하므로 작업 쓰레드가 블로킹되지 않음