0. Overview
알고리즘 관련 문제를 풀다가, 기본적인 입력을 받는 부분에서 조차 Java의 입출력 시스템에 대한 이해도가 부족하다고 생각하여 해당 글을 정리하게 되었습니다.
1. I/O 란 ?
- Input과 Output의 약자로 입출력을 의미합니다.
- 입출력의 간단한 예로 키보드로 텍스트를 입력하고, 모니터로 입력한 텍스트를 출력하는 것이 있습니다.
- 자바의 입출력은 Stream을 통해 이루어집니다.
1 - 1. Node
자바에서 입출력을 수행하는 대상
- 입력 노드 : 키보드, 마우스, 파일, 네트워크, 데이터베이스 등
- 출력 노드 : 모니터, 스피커, 파일, 네트워크, 데이터베이스 등
2. Stream
입력 또는 출력 데이터가 한 방향으로 끊임없이 전송되는 것 (출발지 노드 → 도착 노드) , 데이터를 운반하는데 사용되는 연결 통로
- 입력 스트림 : 자바에서 데이터가 입력될 때 처리하는 스트림
- 출력 스트림 : 자바에서 데이터가 출력될 때 처리하는 스트림
- Input → Output 에서 어느 한쪽에서 다른 쪽으로 데이터를 보내려면 일종의 다리 역할(→)을 하는 것이 스트림이라고 생각합니다.
- 단방향 통신만 가능합니다. 즉, 하나의 스트림으로 입출력을 동시에 처리할 수 없어요 !
- 만약 입출력을 동시에 처리하고 싶다면 ? 입력 스트림 1개, 출력 스트림 1개로 총 2개의 스트림을 생성하면 됩니다.
3. 버퍼 (Buffer)
버퍼란 임시 공간을 의미합니다. 두 대상이 데이터를 주고 받는데 중간에 버퍼가 존재하며 이는 통신의 속도를 줄일 수 있는 장점이 존재해요.
예를 들어, A로부터 물건을 B까지 옮겨야 하는데 물건을 하나하나 왔다갔다 하면서 옮기는 것이 과연 효율적일까요 ? 전혀 아닙니다. 중간에 트럭이나, 수레와 같은 운반책을 두고 물건을 꽉 차게 담아서 그때 옮기는 것이 가장 효율적이고 빠른 방법이겠죠 ? 이때 수레나 트럭이 바로 버퍼(Buffer)의 역할이 되는거에요 !
키보드를 통해 입력이 발생하게 되면, 한 문자씩 버퍼에 쌓이게 되고 만약 버퍼가 가득 차거나 개행 문자(\n)를 만나게 되면 버퍼에 쌓여있던 내용을 한번에 전송하게 됩니다. (알고리즘 문제를 해결할 때 Scanner를 사용하지 말고 BufferReader를 사용하라는 이유가 이 때문입니다.)
4. 채널 (Channel)
채널이란 데이터가 통과하는 쌍방향 통로이며, 채널에서 데이터를 주고 받을 때 사용되는 것이 바로 버퍼(Buffer)입니다.
채널에는 소켓과 연결된 SocketChannel, 파일과 연결된 FileChannel, 파이프와 연결된 Pipe.SinkChannel & Pipe.SourceChannel 등이 존재하고 서버소켓과 연결된 ServerSocketChannel 또한 존재합니다.
4 - 1. +IO와 NIO
NIO란 ? Java 4 부터 새로운 입출력(New Input Output)이라는 뜻에서 java.nio 패키지에 포함이 되었습니다. IO는 스트림 기반이며 NIO는 채널 기반입니다. 위에서 살펴보았듯이 채널은 스트림과 다르게 양방향 입출력이 가능합니다.
IO 방식에서는 각각의 스트림에서 read()와 write()가 호출되면 데이터가 입력이 되고, 쓰레드는 블로킹(멈춤) 상태가 되는데요, 이때 작업이 끝날때까지 무작정 기다리는 현상이 발생합니다. 따라서 IO 쓰레드는 사용할 수 없게 되고 인터럽트 또한 할 수 없게 됩니다. 블로킹을 해제 하려면 스트림을 닫는 방법밖에 존재하지 않았죠.
그러나, NIO에서는 블로킹 상태일 때 인터럽트를 사용해서 빠져나올 수 있습니다.
표로 한번 살펴보겠습니다.
구분 | IO | NIO |
입출력 방식 | 스트림 | 채널 |
버퍼 방식 | Non-Buffer | Buffer |
비동기 방식 지원 | X | O |
Blocking/Non-Blocking 방식 | Blocking Only | Both |
사용 케이스 | 연결 클라이언트 작고, IO가 큰 경우(대용량) | 연결 클라이언트 많고, IO 처리가 적은 경우(저용량) |
4 - 2. InputStream과 OutputStream
스트림은 바이트(Byte) 단위로 데이터를 전송하며 입출력 대상에 따라 다음과 같이 입출력 스트림이 존재합니다.
입출력 대상의 종류 | 입력 스트림 | 출력 스트림 |
파일 | FileInputStream | FileOutputStream |
메모리 (byte[]) | ByteArrayInputStream | ByteArrayOutputStream |
프로세스 | PipedInputStream | PipedOutputStream |
주요 메서드로는 값을 읽고 쓰는 read와 write가 있습니다.
InputSream | OutputStream |
abstract int read() | abstract int write() |
int read(byte[] b) | int write(byte[]) |
int read(byte[] b, int off, int len) | int write(byte[] b, int off, int len) |
추가적으로 OuptutStream에는 close()와 flush()라는 메서드가 존재하는데 둘 다 리소스를 해제해주는 역할을 하지만 close()는 Stream을 영구적으로 닫아 재사용할 수 없는 상태로 만듭니다.
5. Java I/O
자바의 IO는 크게 Byte 단위 입출력과 문자 단위 입출력 클래스로 나뉩니다.
- Byte 단위 입출력 클래스들은 모두 InputStream과 OutputStream이라는 추상 클래스를 상속받아 만들어집니다.
- Char 단위 입출력 클래스들은 모두 Reader와 Writer라는 추상 클래스를 상속 받아 만들어집니다.
5 - 1. 데코레이터 패턴
자바의 IO 패키지는 데코레이터 패턴이 적용되어 있습니다.
데코레이터 패턴 : 하나의 클래스를 장식하는 것처럼 생성자에서 감싸서 새로운 기능을 계속 추가할 수 있도록 클래스를 만드는 방식
자바의 IO는 조립되어 사용되도록 데코레이터 패턴이 적용되었다고 했습니다. 결국 데코레이터를 번역하면 "장식"인데요 !
장식할 대상을 "주인공"이라고 생각해볼게요. 주인공에 계속해서 장식을 하는 상황을 예를 들어보겠습니다.
아무런 장식이 되지 않은 빵을 케이크를 만들기 위해 장식을 한다고 해보면, 빵에 생크림을 바르는 것이 장식이고 생크림을 바른 빵에 과일 토핑을 얹는 것도 장식입니다.
장식 -> 장식 -> 또 장식 ... 이런식으로 사용되는 것이 데코레이터 패턴입니다.
주인공과 주인공을 장식하는게 있다라고 이해합시다. 자바 IO에서 장식은 InputStream, OutputStream, Reader , Writer를 생성자에서 받아들인다는 특징이 있어요.
- 주인공은 어떤 대상에게서 읽어들일지, 쓸지를 결정합니다.
- 주인공은 1byte or byte[] 단위로 읽고 쓰는 메소드를 가집니다.
- 주인공은 1char or char[] 단위로 읽고 쓰는 메소드를 가집니다.
- 장식은 다양한 방식으로 읽고 쓰는 메소드를 가집니다.
📌 꼭 기억합시다 ! 자바 IO에서 장식은 InputStream, OutputStream, Reader , Writer를 생성자에서 받아들여요.
공식 문서를 통해 위의 추상 클래스들을 생성자로 받아들이는 IO 클래스가 어떤 것들이 있는지 꼭 확인해보시기 바랍니다.
그렇다면, 이해를 돕기 위해 간단한 예제를 하나 살펴볼게요.
[ 키보드로부터 한줄씩 입력받고, 한줄씩 화면에 출력하시오. 라는 요구사항이 있습니다. ]
1. 키보드로부터 입력받는다 : System.in (InputStream 의 주인공)
2. 화면에 출력한다 : System.out. (PrintStream의 주인공)
3. 키보드로 입력받는다는 것은 문자(char)를 입력받는 것 : char 단위 입출력
4. char 단위 입출력 클래스는 Reader, Writer 에 해당하겠죠 ?
5. 한줄 읽기 : BufferedReader 라는 클래스는 readLine() 이라는 한 줄 읽기 메소드를 갖습니다.
6. 한줄 쓰기 : PrintStream, PrintWriter
BufferedReader 클래스를 살펴보면, Reader를 생성자로 받는 바로 "장식" 입니다. 장식은 주인공이 없으면 사용할 수 없기 때문에 요구사항에 맞는 주인공을 또 찾아야겠죠 ? 여기까지 대략적인 코드는 다음과 같이 작성할 수 있어요.
public class KeyBoardExam {
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader();
String line = null;
while((line = br.readLine()) != null) {
System.out.println("읽은 값 : " + line);
}
}
}
여기서 우리는 BufferedReader 생성자 내에 주인공을 넣어줘야 합니다. 그렇다면 Reader를 상속받는 것중 하나를 찾아야겠죠 ?
Reader를 상속받는 클래스는 BufferedReader , CharArrayReader ... ~ URL Reader 가 있습니다.
- BufferedReader : 현재 장식으로 사용하고 있기 때문에 후보군에서 제외하겠습니다.
- CharArrayReader : char[] 로부터 읽어들입니다. (우리는 키보드로부터 읽어들일것이기 때문에 제외하겠습니다.)
- FilterReader : 마찬가지로 장식이기 때문에 Reader를 넣어줘야 하므로 제외하겠습니다.
- InputStreamReader : 이것이 바로 우리가 찾던.... 그것
InputStreamReader 같은 경우에도 장식입니다. 그런데 생성자의 인자로 InputStream을 받아요 ! 우리의 요구사항이 뭐였죠 ? 키보드로부터 입력을 받는 것이었습니다. 키보드는 System.in 바로 InputStream의 주인공이였죠. 따라서 InputStreamReader를 사용해주면 됩니다.
[ 최종 코드 ]
public class KeyBoardExam {
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = null;
while((line = br.readLine()) != null) {
System.out.println("읽은 값 : " + line);
}
}
}
이로써 키보드로부터 한줄씩 입력받고, 화면에 출력하는 요구사항을 만족할 수 있게 되었습니다.
5 - 2. 예제
[ FileInputStream & FileOutputStream ]
FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt");
- 파일에서 바이트 단위로 데이터를 읽고 쓸 때 사용됩니다. 주로 텍스트 파일이 아닌 이진 파일을 다룰 때 사용해요.
[ FileReader & FileWriter ]
FileReader reader = new FileReader("input.txt");
FileWriter writer = new FileWriter("output.txt");
- 파일에서 문자 단위로 데이터를 읽고 쓸 때 사용됩니다. 주로 텍스트 파일을 다룰 때 사용해요.
[ BufferedReader & BufferedWriter ]
BufferedReader br = new BufferedReader(new FileReader("input.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"));
- 해당 클래스는 버퍼링을 지원하여 입출력 성능을 향상시킵니다. 주로 문자 기반의 데이터를 라인 단위로 읽고 쓸 때 사용합니다.
[ InputStream & OutputStream ]
InputStream is = new FileInputStream("input.txt");
OutputStream os = new FileOutputStream("output.txt");
- 바이트 기반의 입출력을 위한 추상 클래스로 주로 다른 입출력 스트림 클래스의 상위 클래스로 사용됩니다.
[ InputStreamReader & OutputStreamReader ]
InputStreamReader isr = new InputStreamReader(new FileInputStream("input.txt"), "UTF-8");
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8");
- 바이트 스트림을 문자 스트림으로 변환하는데 사용됩니다. 주로 문자 인코딩과 관련된 작업에 필요합니다.
[ DataInputStream & DataOutputStream ]
DataInputStream dis = new DataInputStream(new FileInputStream("data.dat"));
DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.dat"));
- 기본 데이터 유형 (int, double, boolean 등)을 이진 형식으로 읽고 쓸 때 사용됩니다.
[ ObjectInputStream & ObjectOutputStream ]
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.dat"));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.dat"));
- 객체를 직렬화하고, 역직렬화 할 때 사용합니다. 객체를 파일에 저장하고 다시 읽을 때 유용합니다.
[ Scanner ]
Scanner scanner = new Scanner(new File("input.txt"));
- 입력 스트림에서 텍스트를 토큰으로 읽을 때 유용합니다. 주로 키보드 입력 또는 파일에서 데이터를 읽을 때 사용됩니다.
[ PrintStream ]
PrintStream ps = new PrintStream(new FileOutputStream("output.txt"));
- 출력 스트림으로 데이터를 쉽게 출력할 때 사용합니다. 주로 텍스트 파일에 결과를 기록할 때 유용합니다.
<< 참고 자료 >