자바(15) - 입출력
2021년08월10일자바 입출력과 스트림
입출력은 프로그램의 가장 기본 기능이지만, 외부 저장 장치나 네트워크와 연동해야 하기 때문에 장치에 따라 다르게 구현해야 한다. 자바는 장치에 따라 독립적이고 효율적인 입출력 기능을 제공한다. 자바에서 모든 입출력은 스트림(stream)을 통해 이루어진다. 스트림이란 네트워크에서 유래된 용어이다. 자료 흐름이 물의 흐름과 같다는 의미에서 사용한다. 입출력 장치는 매우 다양하기 때문에 장치에 따라 입출력 부분을 일일이 다르게 구현을 하면 프로그램 호환성이 떨어질 수 밖에 없다. 이런 문제를 해결하기 위해 자바는 입출력 장치와 무관하고 일관성 있게 프로그램을 구현할 수 있도록 일종의 가상 통로인 스트림을 제공하는 것이다. 자료를 읽어 들이려는 소스(source)와 자료를 쓰려는 대상(target)에 따라 각각 다른 스트림 클래스를 제공한다.
자바에서 입출력 기능을 사용하는 곳은 파일 디스크, 키보드, 모니터, 메모리 입출력, 네트워크 등이 있다. 이런 곳에서 일어나는 모든 입출력 기능을 스트림 클래스로 제공한다. 따라서 자바에서 자료를 입출력하려면 여러 스트림 클래스에 대해 알아야 하지만, 구현 방식이 서로 비슷하니 크게 걱정할 필요 없다. 스크림은 크게 세 가지 기준에 따라 분류될 수 있다.
13장에서 배운 스트림과 다른 용도이니 헷갈리지 말자.
- 입력 스트림과 출력 스트림
어떤 대상으로부터 자료를 읽어 들일 때 사용하는 스트림이 입력 스트림이다. 예를 들어 입력 스트림은 어떤 동영상을 재생하기 위해 동영상 파일에서 자료를 읽을 때 사용한다. 편집 화면에 사용자가 쓴 글을 파일에 저장할 때는 출력 스트림을 사용한다. 스트림은 단방향으로 자료가 이동하기 때문에 입력과 출력을 동시에 할 수 없다. 입력 자료의 이동이 출력 자료의 이동과 한 스트림에서 동시에 일어날 수 없기 때문이다. 일방 통행 외길에 차가 양방향으로 다닐 수 없는 것에 비유할 수 있다. 따라서 어떤 스트림이 있다고 하면 그 스트림은 입력 스트림이거나 출력 스트림이다. 스트림의 이름을 보면 입력용인지 출력용인지 알 수 있다. InputStream이나 Reader로 끝나는 이름의 클래스는 입력 스트림이다. 반면 OutputStream이나 Wrtier로 끝나는 이름의 클래스는 출력 스트림이다.
- 입력 스트림: FileInputStream, FileReader, BufferedInputStream, BufferedReader 등
- 출력 스트림: FileOutputStream, FileWriter, BufferedOutputStream, BufferedWriter 등
- 바이트 단위 스트림과 문자 단위 스트림
원래 자바의 스트림은 바이트(byte) 단위로 자료의 입출력이 이루어진다. 그러므로 그림, 동영상, 음악 파일 등 대부분 파일은 바이트 단위로 읽거나 쓰면 된다. 그런데 자바에서의 문자를 나타내는 char형은 2바이트이기 때문에 1바이트만 읽으면 한글 같은 문자는 깨진다. 따라서 입출력 중 가장 많이 사용하는 자료인 문자를 위해 문자 스트림을 별도로 제공한다. 즉 읽어 들이는 자료형에 따라 바이트용과 문자용 스트림이 있다. 스트림 클래스의 이름이 Stream으로 끝나는 경우는 바이트 단위를 처리하는 스트림이다. Reader나 Writer로 끝나는 이름은 문자를 위한 스트림 클래스이다.
- 바이트 스트림: FileInputStream, FileOutputStream, BufferedInputStream, BufferedOutPutStream 등
- 문자 스트림: FileReader, FileWriter, BufferedReader, BufferedWriter 등
- 기반 스트림과 보조 스트림
어떤 스트림이 자료를 직접 읽거나 쓰는 기능을 제공하는 스트림인가, 아니면 자료를 직접 읽거나 쓰는 기능은 없이 다른 스트림에 부가 기능을 제공하는가에 따라 기반 스트림과 보조 스트림으로 구분할 수 있다. 기반 스트림은 읽어 들일 곳(소스)이나 써야 할 곳(대상)에서 직접 읽고 쓸 수 있으며 입출력 대상에 직접 연결되어 생성되는 스트림이다. 반면에 보조 스트림은 직접 읽고 쓰는 기능은 없다. 따라서 항상 다른 스트림을 포함하여 생성된다. 즉 기반 스트림에 보조 스트림을 더하여 기능을 추가한다.
기반 스트림인가 보조 스트림인가 여부는 이름으로 판단하기 어려울 수 있다. 대부분 기반 스트림이 소스나 대상의 이름을 가지고 있지만, 보조 스트림 중에도 이름만 봐서 알 수없는 경우도 있기에 많이 사용하는 클래스 위주로 기억해 두자.
- 기반 스트림: FileInputStream, FileOutputStream, FileReader, FileWriter 등
- 보조 스트림: InputStreamReader, OutputStreamWriter, BufferedInputStream, BufferedOutputStream 등
표준 입출력
자바에서는 화면에 출력하고 입력받는 표준 입출력 클래스를 미리 정의해 두었다. 이 클래스는 프로그램이 시작될 때 생성되므로 따로 만들 필요가 없다. 우리가 지금까지 화면 출력을 위해 사용한 System.out은 표준 출력을 위한 객체이다. 표준 입출력은 콘솔 화면에 입출력된다고 해서 콘솔 입출력이라고도 한다. 표준 입출력을 위한 System 클래스는 다음과 같이 세 개의 변수를 가지고 있다.
자료형 | 변수 이름 | 설명 |
---|---|---|
static PrintStream | out | 표준 출력 스트림 |
static InputStream | in | 표준 입력 스트림 |
static OutputStream | err | 표준 오류 출력 스트림 |
System.out은 표준 출력용, System.in은 표준 입력용 스트림이다. 빨간색으로 오류 메시지를 출력할 때는 System.err를 사용한다. out, in, err 모두 정적 변수이다. 지금까지 우리가 System 클래스를 생성하지 않고도 System.out을 사용할 수 있었던 이유는 out 변수가 System 클래스의 정적 변수이기 때문이다.
System.in으로 화면에서 문자 입력 받기
입출력에 관련한 코드를 구현하면 예외 처리를 해주어야 한다.
import java.io.IOException;
public class SystemInTest1 {
public static void main(String[] args) throws IOException {
System.out.println("알파벳 입력: ");
int i;
try {
i = System.in.read(); // read() 메서드로 한 바이트 읽음
System.out.println(i);
System.out.println((char)i); // 문자로 변환하여 출력
} catch (IOException e) {
e.printStackTrace();
}
}
}
A라고 알파벳을 쓰고 엔터를 누르면 입력한 값이 변수 i에 들어간다. i는 4바이트지만 System.in은 바이트 단위로 읽어 들이는 InputStream이므로 1바이트만 읽는다. 읽어 들인 1바이트를 출력하면 문자에 대한 숫자 값, 즉 아스키 값을 출력한다.
다음은 알파벳 여러개 쓰고 엔터 누르는 예제이다.
import java.io.IOException;
public class SystemInTest2 {
public static void main(String[] args) throws IOException {
System.out.println("알파벳 여러 개 입력: ");
int i;
try {
while((i = System.in.read()) != -1) {
System.out.println((char)i);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
while 문에서 read() 메서드를 이용해 한 바이트씩 읽는다. \n
값이 입력될 때까지 반복 수행한다.
Scanner
Scanner 클래스는 java.util 패키지에 있는 입력 클래스이다. Scanner 클래스는 문자뿐 아니라 정수, 실수 등 다른 자료형도 읽을 수 있다. 또한 콘솔 화면뿐 아니라 파일이나 문자열을 생성자의 매개변수로 받아 자료를 읽어 올 수 있다.
생성자 | 설명 |
---|---|
Scanner(File source) | 파일을 매개변수로 받아 Scanner를 생성 |
Scanner(InputStream source) | 바이트 스트림을 매개변수로 받아 Scanner를 생성 |
Scanner(String source) | String을 매개변수로 받아 Scanner를 생성 |
우리가 이전까지 사용한 Scanner scanner = new Scanner(System.in)
이것이 표준 입력으로부터 자료를 읽어 들이는 기능이 되는 것이다.
Console 클래스
System.in을 사용하지 않고 간단히 콘솔 내용을 읽을 수 있는 Console 클래스도 있다. 직접 콘솔 창에서 자료를 입력받을 때 이 클래스를 사용한다.
메서드 | 설명 |
---|---|
String readLine() | 문자열을 읽는다. |
char[] readPassword() | 사용자에게 문자열을 보여 주지 않고 읽는다. |
Reader reader() | Reader 클래스를 반환한다. |
PrintWriter writer() | PrintWriter 클래스를 반환한다. |
import java.io.Console;
public class ConsoleTest {
public static void main(String[] args) {
Console console = System.console();
System.out.print("이름:");
String name = console.readLine();
System.out.print("직업:");
String name = console.readLine();
System.out.print("비밀번호:");
char[] pass = console.readPassword();
String strPass = new String(pass);
}
}
Console 클래스는 연동되지 않는 경우도 있어서 Scanner를 더 많이 사용한다.
바이트 단위 스트림
InputStream
바이트 단위로 읽는 스트림 중 최상위 스트림이다. InputStream은 추상 메서드를 포함한 추상 클래스로서 하위 스트림 클래스가 상속받아 각 클래스 역할에 맞게 추상 메서드 기능을 구현한다. 주로 사용하는 하위 클래스는 다음과 같다.
- FileInputStream: 파일에서 바이트 단위로 자료를 읽는다.
- ByteArrayInputStream: Byte 배열 메모리에서 바이트 단위로 자료를 읽는다.
- FilterInputStream: 기반 스트림에에서 자료를 읽을 때 추가 기능을 제공하는 보조 스트림의 상위 클래스
InputStream은 바이트 자료를 읽기 위해 다음 메서드를 제공한다.
- int read(): 입력 스트림으로부터 한 바이트의 자료를 읽는다. 읽은 자료의 바이트 수를 반환한다.
- int read(byte b[]): 입력 스트림으로부터 b[] 크기의 자료를 b[]에 읽는다. 읽은 자료의 바이트 수를 반환한다.
- int read(byte b[], int off, int len): 입력 스트림으로부터 b[] 크기의 자료를 b[]의 off 변수 위치부터 저장하여 len 만큼 읽는다. 읽은 자료의 바이트 수를 반환한다.
- void close(): 입력 스트림과 연결된 대상 리소스를 닫는다. (FileInputStream인 경우 파일 닫음)
read() 메서드의 반환형은 int이다. 한 바이트를 읽어서 int에 저장한다. 한 바이트만 읽는 데 반환형이 int인 이유는 더 이상 읽어 들일 자료가 없는 경우에 정수 -1이 반환되기 때문이다. 파일에서 자료를 읽는 경우 파일의 끝에 도달하면 -1이 반환된다.
FileInputStream
FileInputStream은 파일에서 바이트 단위로 자료를 읽어 들일 때 사용하는 스트림 클래스이다. 스트림을 사용하기 위해서는 먼저 스트림 클래스를 생성해야 한다. 생성자는 아래와 같다.
- FileInputStream(String name): 파일 이름 name(경로 포함)을 매개변수로 받아 입력 스트림을 생성한다.
- FileInputStream(File f): File 클래스 정보를 매개변수로 받아 입력 스트림을 생성한다.
// input.txt 만들고 ABC 입력하고 테스트
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamTest1 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("input.txt"); // input.txt 파일 입력 스트림 생성
System.out.println(fis.read());
System.out.println(fis.read());
System.out.println(fis.read());
} catch (IOException e) {
System.out.println(e);
} finally {
try {
fis.close();
} catch (IOException e) {
System.out.println(e);
} catch (NullPointerException e) {
System.out.println(e);
}
}
}
}
파일 끝까지 읽으려면 반복문을 써서 읽어 보자.
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class FileInputStreamTest2 {
public static void main(String[] args) {
try(FileInputStream fis = new FileInputStream("input.txt")) {
int i;
while((i = fis.read()) != -1) {
System.out.println((char)i);
}
System.out.println("end");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
read() 메서드로 파일을 읽는 경우 파일의 끝에 도달하면 -1을 반환한다. 그것을 이용해 끝까지 읽을 수 있다.
int read(byte[] b) 메서드로 읽기
자료를 read() 메서드로 한 바이트씩 읽는 것보다 배열을 사용하여 한꺼번에 많이 읽으면 처리 속도가 훨씬 빠르다.
// input2.txt는 A부터 Z까지 입력했다.
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamTest3 {
public static void main(String[] args) {
try(FileInputStream fis = new FileInputStream("input2.txt")) {
byte[] bs = new byte[10];
int i;
while((i = fis.read(bs)) != -1) {
for(byte b: bs) {
System.out.print((char)b);
}
System.out.print(": " + i + "바이트 읽음");
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("end");
}
}
ABCDEFGHIJ: 10 바이트 읽음 KLMNOPQRST: 10 바이트 읽음 UVWXYZQRST: 6 바이트 읽음
위처럼 하면 최대 10바이트 씩 읽고 처리해준다. 그런데 마지막에 QRST가 남아있다. 기존에 남아 있던 자료가 출력된 것이다. 그러면 어떻게 해야할까 for문을 수정해주면 된다.
for(int k=0; k<i; k++) {
System.out.print((char)bs[k])
}
OutputStream
바이트 단위로 쓰는 스트림 중 최상위 스트림이다. 자료의 출력 대상에 따라 다른 스트림을 제공한다.
- FileOutputStream: 바이트 단위로 파일에 자료를 쓴다.
- ByteArrayOutputStream: Byte 배열에 바이트 단위로 자료를 쓴다.
- FilterOutputStream: 기반 스트림에서 자료를 쓸 때 추가 기능을 제공하는 보조 스트림의 상위 클래스
제공하는 메서드는 다음과 같다.
- void write(int b): 한 바이트를 출력한다.
- void write(byte[] b): b[] 배열에 있는 자료를 출력한다.
- void write(byte b[], int off, int len): b[] 배열에 있는 자료의 off 위치부터 len 개수만큼 자료를 출력한다.
- void flush(): 출력을 위해 잠시 자료가 머무르는 출력 버퍼를 강제로 비워 자료를 출력한다.
- void close(): 출력 스트림과 연결된 대상 리소스를 닫는다. 출력 버퍼가 비워진다.
FileOutputStream
파일에 바이트 단위 자료를 출력하기 위해 사용하는 스트림이다. 생성자는 다음과 같다.
- FileOutputStream(String name): 파일 이름 name(경로 포함)을 매개 변수로 받아 출력 스트림을 생성
- FileOutputStream(String name, boolean append): append 값이 true이면 파일 스트림을 닫고 다시 생성할 때 파일의 끝에 이어서 쓴다. 디폴트 값은 false 이다.
- FileOutputStream(File f): File 클래스 정보를 매개변수로 받아 출력 스트림을 생성
- FileOutputStream(File f, boolean append): append 값이 true이면 파일 스트림을 닫고 다시 생성할 때 파일의 끝에 이어서 쓴다. 디폴트 값은 false 이다.
다음은 write() 메서드 사용하는 예제이다.
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamTest1 {
public static void main(String[] args) {
try(FileOutputStream fos = new FileOutputStream("output.txt")) {
fos.write(65); // FieOutStream은 파일에 숫자를 쓰면 해당하는 아스키 코드 값으로 반환됨.
fos.write(66);
fos.write(67);
} catch(IOException e) {
e.printStackTrace();
}
}
}
생성자에 매개변수로 true를 추가하고 다시 실행하면 이어서 써진다.
문자 단위 스트림
Reader클래스는 문자 단위로 읽는 스트림 중 최상위 스트림으로 다음 하위 클래스를 주로 사용한다.
- FileReader: 파일에서 문자 단위로 읽는 스트림 클래스이다.
- InputStreamReader: 바이트 단위로 읽은 자료를 문자로 변환해 주는 보조 스트림 클래스이다.
- BufferedReader: 문자로 읽을 때 배열을 제공하여 한꺼번에 읽을 수 있는 기능을 제공해주는 보조 스트림이다.
메서드는 다음과 같다.
- int read(): 파일로부터 한 문자를 읽는다. 읽은 값을 반환한다.
- int read(char[] buf): 파일로부터 buf 배열에 문자를 읽는다.
- int read(char[] buf, int off, int len): 파일로부터 buf 배열의 off 위치에서부터 len 개수만큼 문자를 읽는다.
- void close(): 스트림과 연결된 파일 리소스를 다는다.
FileReader
FileReader의 생성자는 아래와 같고 FileInputStream과 마찬가지로 읽을 파일이 안보이면 FileNotFoundException이 발생한다.
- FileReader(String name): 파일 이름 name(경로 포함)을 매개변수로 받아 입력 스트림을 생성한다.
- FileReader(File f): File 클래스 정보를 매개변수로 받아 입력 스트림을 생성한다.
FileReader대신 FileInputStream을 쓰게 되면 바이트 단위로 갖고오기 때문에 한글이 깨질 수도 있다. 따라서 FileReader로 읽는 것이 좋다.
Writer
Writer는 문자 단위로 출력하는 스트림 중 최상위 스트림으로 다음 하위 클래스를 주로 사용한다.
- FileWriter: 파일에 문자 단위로 출력하는 스트림 클래스
- OutputStreamWriter: 파일에 바이트 단위로 출력한 자료를 문자로 변환해 주는 보조 스트림
- BufferedWriter: 문자로 쓸 때 배열을 제공하여 한꺼번에 쓸 수 있는 기능을 제공해 주는 보조 스트림
메서드는 다음과 같이 제공한다.
- void write(int c): 한 문자를 파일에 출력.
- void write(char[] buf): 문자 배열 buf의 내용을 파일에 출력.
- void write(char[] buf, int off, int len): 문자 배열 buf의 off 위치에서부터 len 개수의 문자를 파일에 출력
- void write(String str): 문자열 str를 파일에 출력
- void write(String str, int off, int len): 문자열 str의 off번째 문자부터 len 개수만큼 파일에 출력
- void flush(): 파일에 출력하기 전에 자료가 있는 공간(출력 버퍼)을 비워 출력
- void close(): 파일과 연결된 스트림을 닫는다. 출력 버퍼도 지워짐.
FileWriter
다른 스트림 클래스와 마찬가지로 생성자를 사용해서 스트림을 생성한다. FileOutputStream과 마찬가지로 출력 파일이 존재하지 않으면 파일을 생성한다.
- FileWriter(String name): 파일 이름을 매개변수로 받아 출력 스트림 생성
- FileWriter(String name, boolean append): true이면 파일 스트림을 닫고 다시 생성할 때 파일 끝에 이어서 씀
- FileWriter(File f): File 클래스 정보를 매개변수로 받아 출력 스트림을 생성
- FileWriter(File f, boolean append): true이면 파일 스트림을 닫고 다시 생성할 때 파일 끝에 이어서 씀
보조 스트림
보조 스트림은 입출력 대상이 되는 파일이나 네트워크에 직접 쓰거나 읽는 기능은 없다. 말 그대로 보조 기능을 추가하는 스트림이다. 이 보조 기능은 여러 스트림에 적용할 수 있다. 우리가 흔히 먹는 커피를 봐보자. 우유를 넣으면 라떼가 되고 모카 시럽을 넣으면 모카 커피가 된다. 커피라는 메인에 서브로 올리면 맛이 변하는 것이다. 보조 스트림도 이렇게 기반 스트림을 보조해주며 기능을 추가해준다. 보조 스트림은 감싸고 있다는 의미로 해서 Wrapper 스트림이라고도 한다. 스스로는 입출력 기능이 없기 때문에 생성자의 매개변수로 다른 스트림을 받게 되면 자신이 감싸고 있는 스트림이 읽거나 쓰는 기능을 수행할 때 보조 기능을 추가한다.
보조 스트림처럼 다양한 기능을 제공하는 클래스를 디자인 페턴에서 ‘데코레이터’라고 한다.
직렬화
인스턴스를 네트워크를 통해 전송해야 할 일이 있다. 이때 필요한 것이 직렬화이다. 그리고 저장된 내용이나 전송받은 내용을 다시 복원하는 것을 역직렬화라고 한다. 다시 말해 직렬화란 인스턴스 내용을 연속 스트림으로 만드는 것이다. 스트림으로 만들어야 파일에 쓸 수도 있고 네트워크로 전송할 수도 있다. 따라서 직렬화 과정에서 하는 일은 인스턴스 변수 값을 스트림으로 만드는 것이다. 이를 보조 스트림인 ObjectInputStream과 ObjectOutputStream을 사용하여 구현한다.