자바의 Input과 Ontput에 대해 학습하세요.
I/O는 Input/Output의 약자로, 말 그대로 프로그램의 입력과 출력을 의미한다. 자바에서의 간단한 예시로는 Scanner를 통해 키보드로 텍스트를 쳐서 넣는 것이 입력, System.out.println을 통해 콘솔에 메시지를 띄우는 것이 출력이라고 할 수 있겠다.
자바에서 모든 입출력은 스트림을 통해 이루어진다. 스트림은 데이터가 통과하는 연결 통로로, 데이터의 흐름이 물이 흐르는것과 비슷하다고 하여 stream 이라는 이름이 붙여졌다. 물이 양뱡향으로 흐르지 않듯이, 스트림도 한쪽 방향으로만 데이터를 보낼 수 있다. 따라서 데이터의 입출력을 모두 하려면 입력 방향과 출력 방향의 두 가지의 스트림(InputStream과 OutputStream)이 필요하다.
스트림의 데이터는 순차적으로 흘러간다. 즉, 먼저 보낸 데이터가 먼저 도착한다. (큐와 같은 FIFO - First In First Out 방식) 데이터를 보내는 곳을 source, 도착하는 곳을 sink라고 하며, source - 입력 스트림 - 출력 스트림 - sink 의 순서로 데이터가 도착하게 된다.
버퍼는 자료의 임시 저장 공간을 의미한다. 기본적으로 입력 장치와 출력 장치의 입출력 속도에 차이가 있을 수 밖에 없는데, 이에 따라 입력과 출력간의 병목을 해결하기 위해 만들어진 개념이다.
우리가 30분짜리 동영상을 본다고 하자. 동영상의 출력 시간은 30분이 된다. 하지만, 30분짜리 동영상을 입력하는 시간은 30분보다 작다. 때문에 버퍼가 없고 입출력 속도가 같다면 30분짜리 동영상을 30분동안 입력하고, 출력하는 비효율적인 작업을 해야 한다. 하지만 현실에서는 입력하는 속도가 더 빠르기 때문에 동영상을 빠르게 내려받아 버퍼에 저장하고, 출력하는 속도에 맞춰서 동영상을 출력하게 된다.
동영상을 시청할 때 모두 한 번 씩은 경험했던, 동영상 재생이 멈추는 버퍼링(buffering)이 바로 입력 속도에 문제가 있는 등의 이유로 데이터 입력이 출력 속도를 따라잡지 못해서 데이터가 버퍼로 저장되는 동안 출력하지 않고 대기하는 현상이다.
입출력에서 사용되는 버퍼는 CPU와 보조기억장치 사이에 사용되는 임시 저장 공간을 의미하는데, CPU의 연산 처리 속도에 비해 HDD, SDD등의 보조기억장치의 쓰기 속도가 현저히 느리기 때문에 사용한다. 이 때 버퍼로 사용되는 장치는 CPU보다는 느리지만 HDD, SDD보다는 훨씬 빠른 RAM이다.
채널은 서버와 클라이언트 간의 통신 수단으로, 네트워크 소켓과 같이 입출력을 수행하는 개방 연결을 의미한다.
채널은 입력된 데이터를 버퍼에 저장하고 버퍼에 저장된 데이터를 출력하며, 스트림과 다르게 읽기 / 쓰기를 동시에 할 수 있기 때문에, 입출력 채널을 따로 만들지 않아도 된다. 또한 채널은 비동기적으로 중단되거나 닫힐 수 있다. 한 스레드가 어떤 채널을 통해 입출력 작업을 진행하고 있을 때 다른 스레드가 그 채널을 닫을 수 있기 때문에, 파일 입출력을 하는 스레드를 언제든지 중지 시킬 수 있고, 이를 이용해 네트워크 상에서 non-blocking IO가 가능하다.
자바에서 채널은 java.nio.channel 패키지에 인터페이스로 정의되어 있다. 여기서 nio는 New IO로, 자바 4부터 추가되었으며, 자바 7에서부터는 IO와 NIO 사이의 일관성 없는 클래스 설계를 바로 잡고 비동기 채널 등의 기능을 추가한 NIO.2가 추가되었다. 기존 IO와는 다르게 버퍼와 채널을 사용하며, blocking 방식과 non-blocking 방식을 모두 사용한다. 버퍼를 사용하기 때문에 기본적으로 IO보다 성능이 좋다.
스트림을 이용한 자바 입출력은 InputStream과 OutputStream을 사용하며, 프로그램을 기준으로 프로그램이 데이터의 출발지인지 도착지인지에 따라 데이터를 입력 받으면 InputStream, 출력하면 OutputStream이 된다. InputStream의 출발지는 키보드, 파일, 네트워크상의 프로그램이 될 수 있고, OutputStream의 도착지는 모니터, 파일, 네트워크상의 프로그램이 될수 있다.
자바에서 InputStream과 OutputStream은 java.io 패키지에 정의되어 있다. java.io 패키지에는 파일 시스템의 정보를 담는 File 클래스와 InputStream, OutputStream을 바탕으로 한 다양한 입출력 클래스가 들어있다. 주로 쓰이는 클래스들을 정리하면 다음과 같다.
클래스명 | 설명 |
---|---|
File | 파일 시스템의 정보를 담는 클래스 |
Console | 콘솔에 문자를 입출력 하기 위한 클래스 |
InputStream / OutputStream | 바이트 단위 입출력의 최상위 클래스 |
FileInputStream / FileOutputStream | 파일 입출력을 위한 InputStream / OutputStream의 하위 클래스 |
DataInputStream / DataOutputStream | 데이터 입출력을 위한 InputStream / OutputStream의 하위 클래스 |
ObjectInputStream / ObjectOutputStream | 객체 입출력을 위한 InputStream / OutputStream의 하위 클래스 |
PrintStream | 데이터를 다양한 형태로 입출력 할 수 있는 InputStream / OutputStream의 하위 클래스 |
BufferedInputStream / BufferedOutputStream | 성능 향상 보조 스트림. 버퍼 방식을 사용하는 InputStream / OutputStream의 하위 클래스 |
Reader / Writer | 문자 단위 입출력을 하는 최상위 클래스 |
FileReader / FileWriter | 파일 입출력을 문자 단위로 하는 Reader / Writer의 하위 클래스 |
InputStreamReader / OutputStreamWriter | 바이트 스트림에서 문자 스트림, 문자 스트림에서 바이트 스트림으로의 변환을 제공하는 문자 입출력 클래스. Reader / Writer의 하위 클래스 |
PrintWriter | Writer 클래스의 하위 클래스로 일반적으로 쓰는 print, println 등의 메소드가 정의되어 있는 클래스 |
BufferedReader / BufferedWriter | 버퍼 방식을 사용하는 Reader / Writer의 하위 클래스 |
그 외에 AutoInput/OutputStream, ByteArrayInput/OutputStream, PipedInput/OutputStream 등등의 하위 클래스들이 존재한다.
기본적으로 InputStream은 바이트 입출력을 하고, Reader와 Writer는 문자열 단위의 입출력을 한다. 입출력 스트림은 사용을 다 하고 난 뒤에는 반드시 close() 메소드를 통해 닫아주어야 한다.
InputStream은 모든 바이트 기반 입력 스트림들의 상위 클래스이며, 다음과 같은 메소드들을 가진다.
메소드명 | 설명 |
---|---|
int available() | 읽을 수 있는 바이트 수 반환 |
void close() | 열려있는 InputStream 종료 |
void mark(int readlimit) | 현재의 위치를 표시 |
boolean markSupported() | InputStream에 makrk 된 지점이 있는지 확인 |
abstract int read() | InputStream에서 한 비트를 읽어서 int로 반환 |
int read(byte[] b) | b 만큼의 데이터를 읽어서 저장하고 읽은 바이트 수 반환 |
int read(byte[] b, int off, int len) | len 만큼 읽어서 b의 off 위치에 저장하고 읽은 바이트 수 반환 |
void reset() | mark()를 마지막으로 호출한 위치로 이동 |
long skip(long n) | InputStream에서 n바이트만큼 데이터를 스킵하고 바이트 수를 반환 |
OutputStream은 모든 바이트 기반 출력 스트림들의 상위 클래스이며, 다음과 같은 메소드들을 가진다.
메소드명 | 설명 |
---|---|
void close() | 열려있는 OutputStream 종료 |
void flush() | 버퍼에 남아있는 출력 스트림을 강제로 출력 |
void write(byte[] b) | 바이트 배열 b(버퍼)의 내용을 출력 |
void write(byte[] b, int off, int len) | b의 off 위치에서 len길이 만큼 출력 |
abstract void write(int b) | 정수 b의 하위 1바이트를 출력 |
자바에서는 콘솔 입출력과 같이 표준 입출력을 위한 System 이라는 표준 입출력 클래스를 정의하고 있다. 우리가 가장 처음 "Hello World!"를 출력할 때 배우는 System.out.println()이 바로 이 System 클래스의 멤버 변수 out을 이용한 출력이다. System 클래스는 java.lang 패키지에 정의되어 있으며, 내부에 static 멤버 변수들을 가지고 있다.
public class System {
public static final InputStream in;
public static final PrintStream out;
public static final PrintStream err;
}
System.in은 콘솔로 데이터를 입력받고, System.out과 System.err는 콘솔에 데이터를 출력한다. 이 때 err는 오류를 출력할 때 사용한다. in, out, err는 static final 변수이므로 따로 인스턴스를 만들 필요 없이 System.out.println()과 같이 사용하면 된다. 입력을 사용해야 할 때는
Scanner scanner = new Scanner(System.in);
과 같이 사용한다.
그런데 흔히 쓰는 println()의 코드를 까보면 synchronized가 걸려있다. 때문에 동일 시간에 하나의 스레드만 실행되어 속도 및 리소스 사용 면에서 효율적이지 못하다. 때문에 디버깅 등의 용도로 System.out.println()을 남발하는 것은 좋지 못하다.
자바에서는 버퍼링 도중 프로그램이 멈추면 자동으로 버퍼 비우기를 하기 때문에, err를 보다 빠르고 정확하게 출력하기 위해 System.err은 버퍼링을 지원하지 않는다.
java.io 패키지에는 File이라는 클래스가 정의되어 있다. 이 클래스에는 파일의 정보를 담는다. 생성자에 파일 경로 String이나 uri를 입력해서 생성하며, String parent, String child 를 통해 parent 폴더 안의 child 파일에 대한 정보를 담을 수도 있다. 이렇게 파일 클래스를 만들면 해당 경로의 파일의 정보를 조작할 수 있다.
이때 파일 경로를 입력할 때, 유닉스 계열(Mac 등)과 윈도우의 디렉토리 구분자가 백슬래쉬(\)와 슬래쉬(/)로 다르기 때문에 서로 다른 운영체제에서는 작동하지 않을 수 있는데, File 클래스 내의 변수 seperator를 사용해서 경로를 입력할 수 있다.
이렇게 만든 File객체는 FileReader, BufferedReader를 통해 내용을 읽어올 수도 있고, FileWriter, BufferedWriter를 통해 데이터를 입력할 수도 있다.
public class FileTest {
private static final String FILE_NAME = "test.txt";
public static void main(String[] args) {
File file = new File(FILE_NAME);
try {
FileWriter fw = new FileWriter(file);
BufferedWriter bw = new BufferedWriter(fw);
bw.write("Hello World!"); // 버퍼에 내용 작성
bw.flush(); // 버퍼 비우기
bw.close(); // 스트림을 사용 후 반드시 close 해줄 것
fw.close();
} catch (IOException e) {
System.err.println(e);
}
String readLine = null;
try {
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
readLine = br.readLine();
br.close();
fr.close();
} catch (IOException e) {
System.err.println(e);
}
System.out.println(readLine);
}
}
위 예제는 File 클래스와 Reader, Writer를 통해 "Hello World!"라는 문자열을 파일에 쓰고 출력하는 예제다. 유의해야 할 상황은 스트림을 사용해서 파일을 작성 및 읽어올 때 입력 및 출력 상황에서 예외가 발생할 수 있기 때문에 반드시 IOException(또는 그냥 Exception)을 처리해주어야 한다는 점이다. (스터디 내용 중 예외 처리 참조)
위 예제 코드를 실행하면 같은 디렉토리에 test.txt 파일이 생기고 내용은 다음과 같다.
Hello World!
이 파일을 FileReader로 읽어와서 BufferedReader로 readLine()하게 되면 파일의 맨 첫줄인 "Hello World!"를 읽어와서 문자열에 입력할 수 있다. 만약 숫자 등 다양한 자료형을 입력하고 싶다면 BufferedWriter를 기반으로 DataOutputStream을 생성해서 파일을 작성하면 된다.