[Java] 13주차 과제: I/O
by Roel Downey
스터디 링크 : 링크
I/O란?
I/O란? Input과 Output의 약자를 합쳐놓은 것이다. 입출력은 컴퓨터 내부 또는 외부 장치와 프로그램간의 데이터를 주고 받는 것을 말한다.
데이터를 입력 받는 것(읽는것)을 Input이고, 입력받은 데이터를 내보내는 것(파일로 쓰거나 외부로 전송할 때)이 Output이다.
스트림(Stream)이란
스트림이란 실제의 입력이나 출력이 표현된 데이터의 이상화된 흐름을 말하며, 자바에서는 파일이나 콘솔에서의 입출력을 스트림을 통해 다룬다. 스트림은 한 방향으로만 통신이 가능하기 때문에 입력과 출력을 동시에 처리할 수는 없다.
💡 람다와 스트림에서 얘기하는 스트림과 같은 용어를 사용하지만, 다른 개념이다.
InputStream과 OutputStream
스트림은 단방향 통신만 지원하기 때문에 사용 목적에 따라 입력 스트림과 출력 스트림으로 구분한다. 자바에서는 이를 구분하기 위해 java.io 패키지를 이용해 InputStream과 OutputStream을 지원한다.
InputStream
InputStream은 입력 스트림들 중 최상위 클래스로 추상 클래스이다. Byte 기반으로 입력되는 스트림은 InputStream 클래스를 상속받아 생성된다.
Method
- read() : 입력 스트림으로부터 1byte를 읽고 읽은 바이트를 리턴하는 메소드.
- read(byte[] b) : 입력 스트림으로부터 읽은 바이트들을 byte[] b에 저장하고 실제로 읽은 바이트 수를 리턴하는 메소드.
- read(byte[] b, int off, int len) : 입력 스트림으로부터 len byte만큼 읽어 byte[] b의 b[off] 부터 len개까지 저장한 후 읽은 byte 수인 len개를 리턴한다. 만약 len개보다 적은 byte를 읽는 경우 실제 읽은 byte수를 리턴하는 메소드.
- close() : 사용한 시스템 리소스를 반납 후 입력 스트림을 닫는 메소드.
OutputStream
OutputStream은 InputStream과 마찬가지로 출력 스트림들 중 최상위 클래스로 추상 클래스이다. Byte 기반으로 출력되는 스트림은 OutputStream 클래스를 상속받아 생성된다.
Method
- write(byte[] b) : 출력 스트림으로부터 주어진 byte[] b 의 모든 byte를 보내는 메소드.
- write(byte[] b, int off, int len) : 출력 스트림으로부터 byte[] b의 b[off]부터 len개 까지의 byte를 보내는 메소드.
- flush() : 버퍼에 남아있는 모든 byte를 출력하는 메소드.
- close() : 사용한 시스템 리소스를 반납 후 출력 스트림을 닫는 메소드.
바이트 스트림 (Byte Stream)
자바의 스트림은 기본적으로 Byte 단위로 스트림을 전송하며 입출력 대상에 따라 제공하는 클래스가 다르다. 또한, 그림, 멀티미디어, 문자 등 모든 종류의 데이터를 주고받을 수 있다는 특징을 가지고 있다.
캐릭터 스트림 (Character Stream)
자바에서 가장 작은 타입인 char 형이 2바이트이므로, 1바이트씩 전송되는 바이트 기반 스트림으로는 원활한 처리가 힘든 경우가 있다. 이러한 경우를 해결하기 위해 자바는 문자 기반 스트림을 지원한다. 문자 기반 스트림은 오직 문자 데이터를 주고받기 위해 존재하는 스트림으로 문자 데이터를 입출력 할 때 사용하는 스트림이다. Reader와 Writer 클래스를 상속받아 사용한다.
입출력 스트림 클래스
- FileReader / FileWriter : 파일 입출력 대상
- CharArrayReader / CharArrayWriter : 메모리 입출력 대상
- PipedReader / PipedWriter : 프로세스 입출력 대상
- StringReader / StringWriter : 문자열 입출력 대상
보조 스트림
스트림의 기능을 보완하기 위해 사용되는 스트림이다. 실제로 데이터를 주고받는 역할은 하지 않기 때문에 먼저 스트림을 생성한 후 보조 스트림을 사용해야 한다.
보조 스트림 클래스
- FilterInputStream / FilterOuputStream : 필터를 이용한 입출력
- BufferedInputStream / BufferedOutputStream : 버퍼를 이용한 입출력
- ObjectInputStream / ObjectOutputStream : 데이터를 객체 단위로 읽거나 읽어진 객체를 역직렬화
- DataInputStream / DataOuputStream : 입출력 스트림으로부터 자바의 기본 타입으로 데이터를 읽음
- SequenceInputStream : 두 개의 입력 스트림을 논리적으로 연결
- PushbackInputStream : 다른 입력 스트림에 버퍼를 이용하여 push back이나 unread와 같은 기능 추가
- PrintStream : 다른 출력 스트림에 버퍼를 이용해 다양한 데이터를 출력하기 위한 기능 추가
예제 코드
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
DataInputStream dis = new DataInputStream(new FileInputStream("sample.txt"));
표준 스트림
자바에서는 콘솔과 같은 표준 입출력 장치를 위해 System이라는 표준 입출력 클래스를 정의한다.
System 클래스는 java.lang 패키지에 포함되어 있다.
표준 입출력 스트림은 자바에서 기본적으로 생성하기 때문에 별도로 생성할 필요가 없다.
클래스 변수
- System.in : 콘솔로부터 데이터를 입력받음
- System.out : 콘솔로 데이터를 출력함
- System.err : 콘솔로 데이터를 출력함
자바 NIO(new IO)
자바 NIO는 기존의 자바 IO API를 대체하기 위해 자바 4부터 도입되었다. IO와 다르게 NIO가 가지는 대표적인 특징은 채널과 버퍼, Non-Blocking IO, Selectors이다.
IO와 NIO의 차이점
IO와 NIO는 데이터를 입출력한다는 목적은 동일하지만, 방식에서 큰 차이가 나타난다.
구분 | IO | NIO |
입출력 방식 | 스트림 방식 | 채널 방식 |
버퍼 방식 | non-Buffer | Buffer |
비동기 방식 | 지원 안함 | 지원 |
블로킹 / 넌블로킹 방식 | 블로킹 방식만 지원 (동기) |
블로킹 / 넌블로킹 방식 모두 지원 (동기/비동기 모두 지원) |
채널(Channel)
IO에서 바이트 스트림과 문자 스트림으로 데이터를 읽은 것과는 다르게 NIO에서는 채널을 통해 데이터를 읽고 쓴다.
채널과 스트림의 차이점
- 채널은 읽고 쓰는 것이 모두 가능한 양방향이 가능하지만, 스트림은 읽는 것이나 쓰는 것 중 하나만 가능한 단방향성을 가지고있다.
- 채널은 비동기적으로 읽고 쓸 수 있다.
- 채널은 항상 버퍼에서 부터 읽거나 버퍼로 쓴다.
스트림과 채널 (Stream vs Channel)
IO는 스트림(Stream) 기반이다.
스트림은 입력 스트림과 출력 스트림으로 구분되어 있기 때문에 데이터를 읽기 위해서는 입력 스트림을 생성해야 하고, 데이터를 출력하기 위해서는 출력 스트림을 생성해야 한다.
NIO는 채널(Channel) 기반이다.
채널은 스트림과 달리 양방향으로 입력과 출력이 가능하다.
그렇기 때문에 입력과 출력을 위한 별도의 채널을 만들 필요가 없다.
Buffer
NIO의 버퍼는 채널과 상호작용할 때 사용되는 것으로 데이터는 채널에서 버퍼로 읽어지거나 버퍼에서 읽혀 채널로 쓰여진다.
버퍼를 통해 데이터를 읽고 쓰는 것은 4단계를 거친다.
- 버퍼에 데이터를 쓰기
- buffer.flip() 메소드를 호출 (flip() 메소드는 버퍼의 읽기/쓰기 모드를 전환하는 메소드)
- 버퍼에서 데이터를 읽기
- buffer.clear() 혹은 buffer.compact() 호출 (clear() : 버퍼를 비우는 메소드, compact() : 이미 읽은 버퍼를 지우는 메소드)
non-buffer vs buffer
IO에서는 출력 스트림이 1바이트를 쓰면 입력 스트림이 1바이트를 읽는다.
이러한 시스템은 대체로 느리다.
이것보다는 버퍼(Buffer : 메모리 저장소)를 사용해서 복수 개의 바이트를 한꺼번에 입력받고 출력하는 것이 성능에 이점을 가지게 된다.
그래서 IO는 버퍼를 제공해주는 보조 스트림인 BufferedInputStream, BufferedOutputStream을 연결해 사용하기도 한다.
NIO는 기본적으로 버퍼를 사용해서 입출력을 하기 때문에 IO보다 높은 성능을 가진다.
IO는 스트림에서 읽은 데이터를 즉시 처리한다.
- 스트림으로부터 입력된 전체 데이터를 별도로 저장하지 않으면, 입력된 데이터의 위치를 이동해 가면서 자유롭게 이용할 수 없다.
NIO는 읽은 데이터를 무조건 버퍼에 저장한다.
- 버퍼 내에서 데이터의 위치 이동을 해가면서 필요한 부분만 읽고 쓸 수 있다.
non-direct buffer VS direct buffer
버퍼가 사용하는 메모리 위치에 따라서 non-direct buffer 와 direct buffer 로 분류한다.
non-direct buffer | direct buffer | |
사용하는 메모리 공간 | JVM이 관리하는 힙 메모리 공간 | OS가 관리하는 메모리 공간 |
버퍼 생성 시간 | 버퍼 생성이 빠르다 | 버퍼 생성이 느리다 |
버퍼의 크기 | 버퍼크기가 작다 | 버퍼크기가 크다 |
입출력 성능 | 성능이 낮다 | 성능이 높다.( 입출력이 빈번할 때 유리하다.) |
non-direct buffer 는 JVM 힙 메모리를 사용하므로 생성 시간이 빠르지만,
direct buffer 는 운영체제의 메모리를 할당받기 위해 운영체제의 네이티브(native) C함수를 호출해야 하고 여러가지 잡다한 처리를 해야하므로 상대적으로 생성이 느리다.
그렇기 때문에 direct buffer는 자주 생성하기보단 한 번 생성해놓고 재사용하는 것이 유리하다.
non-direct buffer는 JVM의 제한된 힙 메모리를 사용하므로 버퍼의 크기를 크게 잡을 수 없고,
direct buffer는 운영체제가 관리하는 메모리를 사용하므로 운영체제가 허용하는 범위 내에서 대용량 버퍼를 생성시킬 수 있다.
Buffer 를 사용하면 좋은이유에 대한 근본적인 이유를 고민해야 한다.
Buffer 를 사용하면 좋은 이유 차이점과 성능상의 장점이 있는지에 대한 이유가 중요
속도가 왜 빨라질까?
- 모아서 보내면 왜 빨라질까?
- 한 바이트씩 바로바로 보내는 것이 아니라 버퍼에 담았다가 한번에 모아서 보내는 방법인데 왜 이렇게 하는 것이
- 입출력 횟수가 포인트 이다.
- 단순히 모아서 보낸다고 이점이 있는 것이 아니다 → 시스템 콜의 횟수가 줄어들었기 때문에 성능상 이점이 생기는 것이다
- OS 레벨에 있는 시스템 콜의 횟수 자체를 줄이기 때문에 성능이 빨라지는 것이다.
블로킹과 넌블로킹 (Blocking vs non-blocking)
IO는 블로킹(Blocking) 된다.
입력 스트림의 read() 메소드를 호출하면 데이터가 입력되기 전까지 Thread는 블로킹(대기상태)가 된다.
마찬가지로 출력 스트림의 write() 메소드를 호출하면 데이터가 출력되기 전까지 Thread는 블로킹된다.
IO Thread가 블로킹되면 다른 일을 할 수 없고 블로킹을 빠져나오기 위해 인터럽트(interrupt)도 할 수 없다.
→ 블로킹을 빠져나오는 유일한 방법은 스트림을 닫는것이다.
NIO는 블로킹과 넌블로킹(non-blocking) 특징을 모두 가진다.
IO 블로킹과 NIO 블로킹과의 차이점은 NIO 블로킹은 Thread를 인터럽트(interrupt) 함으로써 빠져나올 수 있다.
블로킹의 반대개념이 넌블로킹인데, 입출력 작업 시 Thread가 블로킹되지 않는 것을 말한다.
NIO의 넌블로킹은 입출력 작업 준비가 완료된 채널만 선택해서 작업 Thread가 처리하기 때문에 작업 Thread가 블로킹되지 않는다.
→ 작업준비가 완료되었다는 뜻은 지금 바로 읽고 쓸수 있는 상태를 말한다.
NIO 넌블로킹의 핵심 객체는 멀티플렉서(multiplexor)인 셀렉터(Selector) 이다.
셀렉터는 복수 개의 채널 중에서 준비 완료된 채널을 선택하는 방법을 제공해준다.
Non-Blocking IO
NIO에서는 Non-Blocking IO를 사용할 수 있다. 하나의 스레드가 데이터를 버퍼로 읽어 들이거나 쓰는 동안 해당 스레드는 다른 작업을 진행할 수 있다는 특징을 가지고 있다.
Selector
셀렉터를 사용해서 하나의 스레드를 통해 여러개의 채널을 관리할 수 있다. 즉 하나의 스레드로 여러 네트워크의 연결을 관리할 수 있다.
파일 읽고 쓰기
Reader와 Writer
바이트기반 스트림의 조상이 InputStream과 OutputStream인 것과 같이
문자기반의 스트림에서는 Reader 와 Writer 가 그 역할을 수행한다.
Reader 와 Writer의 메서드에서는 바이트기반 스트림과 비교하여 byte 배열 대신 char 배열을 사용한다.
PipedReader와 PipedWriter
쓰레드간 데이터를 주고 받을 때 사용된다.
Piped는 다른 스트림과 달리 입력과 출력스트림을 하나의 스트림으로 연결(connect)해서 데이터를 주고 받는다는 특징이 있다.
스트림을 생성한 다음 어느 한 쪽 쓰레드에서 connect()를 호출해서 입력 스트림과 출력 스트림을 연결한다.
입출력을 마친 후에는 어느 한쪽 스트림만 닫아도 나머지 스트림은 자동으로 닫힌다.
StringReader와 StringWriter
CharArrayReader / CharArrayWriter와 같이 입출력 대상이 메모리인 스트림이다.
StringWriter에 출력되는 데이터는 내부 StringBuffer에 저장되며, StringWriter의 다음 과 같은 메서드를 이용해 저장된 데이터를 얻을 수 있다.
문자기반의 보조스트림
BufferedReader 와 BufferedWriter
버퍼를 이용해 입출력의 효율을 높일 수 있도록 해주는 보조 역할을 수행한다.
버퍼를 이용하면 입출력의 효율이 비교할 수 없을 정도로 좋아지기 때문에 사용하도록 하자!
- BufferedReader의 readLine()을 사용하면 데이터를 라인 단위로 읽을 수 있고
- BufferedWriter는 newLine() 이라는 줄바꿈을 해주는 메서드를 가지고 있다.
InputStreamReader와 OutputStreamWriter
바이트 기반 스트림을 문자 기반 스트림으로 연결시켜주는 역할을 수행한다.
추가적으로 바이트기반 스트림의 데이터를 지정된 인코딩의 문자데이터로 변환하는 작업을 수행한다.
'Java' 카테고리의 다른 글
[Java] mysql 데이터 타입 java 데이터 타입과 매칭 (0) | 2023.04.17 |
---|---|
[Java] AtomicLong 사용 방법 (0) | 2021.12.15 |
[Java] 12주차 과제: 애노테이션 (0) | 2021.02.16 |
[Java] 11주차 과제: Enum (0) | 2021.02.04 |
[Java] 10주차 과제: 멀티쓰레드 프로그래밍 (0) | 2021.01.23 |
블로그의 정보
What doing?
Roel Downey