Chapter 15 입출력(I/O)

1. 자바에서의 입출력

1.1 입출력이란?

컴퓨터 내부 또는 외부의 장치와 프로그램간의 데이터를 주고받는 것

1.2 스트림(stream)

자바에서 입출력을 수행하기 위해 두 대상을 연결하고 데이터를 운반하는데 사용되는 연결통로

먼저 보낸 데이터를 먼저 받게 되어 있으며 중간에 건너뜀 없이 연속적으로 데이터를 주고 받는다.

1.3 바이트기반 스트림 - InputStream, OutputStream

스트림은 바이트단위로 데이터를 전송하며 입출력 대상에 따라 다음과 같은 입출력스트림이 있다.

입력스트림 출력스트림 입출력 대상의 종류
FileInputStream FileOutputStream 파일
ByteArrayInputStream ByteArrayOutputStream 메모리(byte배열)
PipedInputStream PipedOutputStream 프로세스(프로세스간의 통신)
AudioInputStream AudioOutputStream 오디오장치

[표15-1] 입력스트림과 출력 스트림

이들은 모두 InputStream 또는 OutputStream의 자손들이며, 각각 읽고 쓰는데 필요한 추상메서드를 자신에 맞게 구현해 놓았다. 자바에서는 java.io패키지를 통해서 많은 종류의 입출력관련 클래스들을 제공한다.

InputStream OutputStream
abstract int read() abstract void write(int b)
int read(byte[] b) void write(byte[] b)
int read(byte[] b, int off, int len) void write(byte[] b, int off, int len)

[표15-2] InputStream과 OutputStream에 정의된 읽기와 쓰기를 수행하는 메서드

InputStream의 read()와 OutputStream의 write(int b)는 입출력의 대상에 따라 읽고 쓰는 방법이 다를 것이기 때문에 각 상황에 알맞게 구현하라는 의미에서 추상메서드로 정의되어 있다.

read()와 write(int b)를 제외한 나머지 메서드들은 추상메서드가 아니니까 굳이 추상메서드인 read()와 write(int b)를 구현하지 않아도 이들을 사용하면 될 것이라고 생각할 수도 있겠지만 사실 추상메서드인 read()와 write(int b)를 이용해서 구현한 것들이기 때문에 read()와 write(int b)가 구현되어 있지 않으면 이들은 아무런 의미가 없다.

public abstract class InputStream {
  ...
  // 입력스트림으로부터 1byte를 읽어서 반환한다. 읽을 수 없으면 -1을 반환한다.
  abstract int read();

  // 입력스트림으로부터 len개의 byte를 읽어서 byte배열 b의 off위치부터 저장한다.
  int read(byte[] b, int off, int len) {
    ...
    for(int i=offl i < off + len; i++) {
      // read()를 호출해서 데이터를 읽어서 배열을 채운다.
      b[i] = (byte)read();
    }
    ...
}
// 입력스트림으로부터 byte배열 b의 크기만큼 데이터를 읽어서 배열 b에 저장한다.
int read(byte[] b) {
  return read(b, 0, b.length);
}
...

이 코드는 InputStream의 실제 소스코드의 일부를 이해하기 쉽게 약간 변경한 것인데, 여기서 read(byte[] b, int off, int len)의 코드를 보면 read()를 호출하는 것을 알 수 있다. 메서드는 선언부만 알고 있어도 호출이 가능하기 때문에, 추상메서드를 호출하는 코드를 작성할 수 있다.

결론적으로 read()는 반드시 구현되어야하는 핵심적인 메서드이고, read()없이는 read(byte[] b, int off, int len)와 read(byte[] b)는 의미가 없다는 것을 확인할 수 있다.

1.4 보조스트림

실제 데이터를 주고받는 스트림이 아니기 때문에 데이터를 입출력할 수 있는 기능은 없지만, 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있다.

예를 들어 test.txt라는 파일을 읽기위해 FileInputStream을 사용할 때, 입력 성능을 향상시키기 위해 버퍼를 사용하는 보조스트림인 BufferedInputStream을 사용하는 코드는 다음과 같다.

// 먼저 기반스트림을 생성한다.
FileInputStream fis = new FileInpustStream("test.txt");

// 기반스트림을 이용해서 보조스트림을 생성한다.
BufferedInputStream bis = new BufferedInputStream(fis);
bis.read(); // 보조스트림인 BufferedInputStream으로부터 데이터를 읽는다.

코드 상으로는 보조스트림인 BufferedInputStream이 입력 기능을 수행하는 것처럼 보이지만, 실제 입력기능은 BufferedInputStream과 연결된 FileInputStream이 수행하고, 보조 스트립인 BufferedInputStream은 버퍼만을 제공한다. 버퍼를 사용한 입출력과 사용하지 않은 입출력간의 성능차이는 상당하기 때문에 대부분의 경우에 버퍼를 이용한 보조스트림을 사용한다.

입력 출력 설명
FilterInputStream FilterOutputStream 필터를 이용한 입출력 처리
BufferedInputStream BufferedOutputStream 버퍼를 이용한 입출력 성능향상
DataInputStream DataOutputStream int, float와 같은 기본형 단위(primitive type)로 데이터를 처리하는 기능
SequenceInputStream 없음 두 개의 스트림을 하나로 연결
LineNumberInputStream 없음 읽어 온 데이터의 라인 번호를 카운트(JDK1.1부터 LineNumberReader로 대체)
ObjectInputStream ObjectOutputStream 데이터를 객체 단위로 읽고 쓰는데 사용. 주로 파일을 이용하며 객체 직렬화와 관련 있음
없음 PrintStream 버퍼를 이용하며, 추가적인 print관련 기능(print, printf, println메서드)
PushbackInputStream 없음 버퍼를 이용해서 읽어 온 데이터를 다시 되돌리는 기능(unread, push back to buffer)

[표15-3] 보조스트림의 종류

1.5 문자기반 스트림 - Reader, Writer

java에서는 한 문자를 의미하는 char형이 1byte가 아니라 2byte이기 때문에 바이트기반의 스트림으로 2byte인 문자를 처리하는 데는 어려움이 있다. 이 점을 보완하기 위해서 문자기반의 스트림이 제공된다. 문자데이터를 입출력할 때는 바이트기반 스트림 대신 문자기반 스트림을 사용하자.

InputStream --> Reader
OutputStream --> Writer

바이트기반 스트림과 문자기반 스트림의 읽기 쓰기에 사용되는 메서드를 비교하면 byte배열 대신 char배열을 사용한다는 것과 추상메서드로 선언된 메서드의 종류가 다르다.

보조스트림 역시 문자기반 보조스트림이 존재하며 사용목적과 방식은 바이트 기반과 다르지 않다.

2. 바이트기반 스트림

2.1 InputStream과 OutputStream

모든 바이트기반의 스트림의 조상이며 다양한 메소드가 선언되어 있다.

스트림의 종류에 따라서 mark()와 reset()을 사용하여 이미 읽은 데이터를 되돌려서 다시 읽을 수 있다. 이 기능을 지원하는 스트림인지 확인하는 markSupported()를 통해서 알 수 있다.

flush()는 버퍼가 있는 출력스트림의 경우에만 의미가 있으며, OutputSteream에 정의된 flush()는 아무런 일도 하지 않는다.

프로그램이 종료될 때, 사용하고 닫지 않은 스트림을 JVM이 자동적으로 닫아 주기는 하지만, 스트림을 사용해서 모든 작업을 마치고 난 후에는 close()를 호출해서 반드시 닫아주어야 한다. 그러나 ByteArrayInputStream과 같이 메모리를 사용하는 스트림과 System.in, System.out과 같은 표준 입출력 스트림은 닫아 주지 않아도 된다.

2.2 ByteArrayInputStream과 ByteArrayOutputStream

메모리, 즉 바이트배열에 데이터를 입출력 하는데 사용되는 스트림이다.

스트림의 종류가 달라도 읽고 쓰는 방법은 동일하므로 아래 예제를 통해서 스트림에 읽고 쓰는 방법을 잘 익혀보자.

[예제15-4]

import java.io.*;
import java.util.Arrays;

class IOEx4 {
  public static void main(String[] args) {
    byte[] inSrc = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    byte[] outSrc = null;

    byte[] temp = new byte[4];

    ByteArrayInputStream input = null;
    ByteArrayOutputStream output = null;

    input = new ByteArrayInputStream(inSrc);
    output = new ByteArrayOutputStream();

    try {
      while(input.available() > 0) { // 읽어 올 수 있는 데이터의 크기 반환
        int len = input.read(temp); // 읽어 온 데이터의 개수를 반환
        output.write(temp, 0, len); // 읽어 온 만큼만 write
    } catch(IOException e) {}

    outSrc = output.toByteArray();

    System.out.println("Input Source :" + Arrays.toString(inSrc));
    // Input Source :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    System.out.println("temp :" + Arrays.toString(temp));
    // temp :[8, 9, 6, 7]
    System.out.println("Output Source :" + Arrays.toString(outSrc));
    // Output Source :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  }
}

2.3 FileInputStream과 FileOutputStream

파일에 입출력을 하기 위한 스트림

[예제15-5] 커맨드라인으로부터 입력받은 파일의 내용을 읽어서 그대로 화면에 출력하는 예제

import javaj.io.*;
class FileViewer {
  public static void main(String args[]) throws IOException {
    FileInputStream fis = new FileInputStream(args[0]);
    int data = 0;

    while((data=fis.read()) != -1) {
      char c = (char)data;
      System.out.print(c);
    }
  }
}

3. 바이트기반의 보조스트림

3.1 FilterInputStream과 FilterOutputStream

InputStream/OutputStream의 자손이면서 모든 보조스트림의 조상으로 자체적으로 입출력을 수행할 수 없기 때문에 기반 스트림이 필요하여 FilterInputStream/FilterOutputStream은 상속을 통해 원하는 작업을 수행하도록 읽고 쓰는 메서드를 오버라이딩해야 한다.

public class FilterInputStream extends InputStream {
  protected volatile InputStream in;
  protected FilterInputStream(InputStream in) {
    this.in = in;
  }
  public int read() throws IOException {
    return in.read();
  }
  ...
}

생성자 FilterInputStream(InputStream in)는 접근 제어자가 protected이기 때문에 FilterInputStream의 인스턴스를 생성해서 사용할 수 없고 상속을 통해서 오버라이딩되어야 한다. FilterInputStrem/FilterOutputStream을 상속받아서 기반스트림에 보조기능을 추가한 보조스트림 클래스는 다음과 같다.

FilterInputStream의 자손 - BufferedInputStream, DataInputStream, PushbackInputStream 등
FilterOutputStream의 자손 - BufferedInputStream, DataOutputStream, PrintStream 등

3.2 BufferedInputStream과 BufferedOutputStream

스트림의 입출력 효율을 높이기 위해 버퍼를 사용하는 보조스트림으로 한 바이트씩 입출력하는 것보다는 버퍼(바이트 배열)를 이용해서 한 번에 여러 바이트를 입출력하는 것이 빠르기 때문에 대부분의 입출력 작업에 사용된다.

BufferedInputStream은 입력소스로 부터 버퍼 크기만큼의 데이터를 읽어다 자신의 내부 버퍼에 저장하고 프로그램에서는 버퍼에 저장된 데이터를 읽으며, 다 읽고 그 다음 데이터를 읽기 위해 read메서드가 호출되면, BufferedInputStream은 입력소스로부터 다시 버퍼크기 만큼의 데이터를 읽어다 버퍼에 저장하는 작업을 반복한다.

BufferedOutputStream 역시 버퍼를 이용해서 출력소스와 작업을 하게 되는데 프로그램에서 write메서드를 이용한 출력이 BufferedOutputStream의 버퍼에 저장된다. 버퍼가 가득 차면, 그 때 버퍼의 모든 내용을 출력소스에 출력한다. 버퍼가 가득 찼을 때만 출력소스에 출력을 하기 때문에, 마지막 출력부분이 출력소스에 쓰이지 못하고 BufferedOutputStream의 버퍼에 남아있는 채로 프로그램이 종료될 수 있다는 점을 주의해야 한다. 그래서 프로그램에서 모든 출력작업을 마친 후 BufferedOutputStream에 close()나 flush()를 호출해서 마지막에 버퍼에 있는 모든 내용이 출력소스에 출력되도록 해야 한다.

3.3 DataInputStream과 DataOutputStream

데이터를 읽고 쓰는데 있어서 byte단위가 아닌, 8가지 기본 자료형의 단위로 읽고 쓰기 가능

[예제15-10]

import java.io.*;
class DataInputStreamEx1 {
  public static void main(String args[]) {
    try {
      FileInputStream fis = new FileInputStream("sample.dat");
      DataInputStream dis = new DataInputStream(fis);

      System.out.println(dis.readInt());
      System.out.println(dis.readFloat());
      System.out.println(dis.readBoolean());
      dis.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

예제 15-8을 실행해서 만들어진 sample.dat를 읽어서 화면에 출력하는 예제이다. sample.dat파일로부터 데이터를 읽어올 때 아무런 변환이나 자릿수를 셀 필요없이 단순히 readInt()와 같이 읽어올 데이터의 타입에 맞는 메서드를 사용하기만 하면 된다.

3.4 SequenceInputStream

여러 개의 입력스트림을 연속적으로 연결해서 하나의 스트림으로부터 데이터를 읽는 것과 같이 처리할 수 있도록 도와준다. 큰 파일을 여러개의 작은 파일로 나누었다가 하나의 파일로 합치는 것과 같은 작업을 수행할 때 사용하면 좋을 것이다.

메서드 / 생성자 설명
SequenceInputStream(Enumeration e) Enumeration에 저장된 순서대로 입력스트림을 하나의 스트림으로 연결한다.
SequenceInputStream(InputStream s1, InputStream s2) 두 개의 입력스트림을 하나로 연결한다.
[사용예1]
Vector files = new Vector();
files.add(new FileInputStream("file.001"));
files.add(new FileInputStream("file.002"));
SequenceInputStream in = new SequenceInputStream(files.elements());

[사용예2]
FileInputStream file1 = new FileInputStream("file.001");
FileInputStream file2 = new FileInputStream("file.002");
SequenceInputStream in = new SequenceInputStream(file1, file2);

3.5 PrintStream

데이터를 기반스트림에 다양한 형태로 출력할 수 있는 print, println, printf와 같은 메서드를 오버로딩하여 제공

PrintStream과 거의 같은 기능을 갖는 PrintWrite가 JDK1.1에서 부터 추가되었는데 보다 다양한 문자를 처리하는데 적합하기 때문에 가능하면 PrintWriter를 사용하는 것이 좋다.

4. 문자기반 스트림

4.1 Reader와 Writer

char 배열을 사용한 문자기반의 스트림 제공. Text의 읽기/쓰기에 특화되어있다.

InputStream/OutputStream Reader/Writer
Byte 기반 Char 기반
0 and 255 0 and 65535

Reader/Writer의 인코딩에 따라 한번에 읽는 크기가 달라질 수 있다.

4.2 FileReader와 FileWriter

File로부터 Text 데이터를 읽고 쓰는데 사용된다.

//Reader 사용법
Reader reader = new FileReader("myfile.txt");

int data = reader.read();
while(data != -1){
    char dataChar = (char) data;
    data = reader.read();
}
//Writer 사용법
Writer writer = new FileWriter("file-output.txt");

writer.write("Hello World Writer");
writer.close();

4.3 PipedReader와 PipedWriter

주로 쓰레드 간에 데이터를 주고 받을 때 사용된다.

PipedReader/PipedWriter 는 문자기반의 스트림 형태의 Pipe의 내용을 읽거나 쓰는 용도이다. PipedInput/OutputStream 는 Byte기반이다.

PipedReader는 반드시 PipedWriter에 연결되어야 한다.

//사용법

PipedWriter pipedWriter = new PipedWriter();

//반드시 연결
PipedReader pipedReader = new PipedReader(pipedWriter);

int data = pipedReader.read();
while(data != -1) {
  //do something with data...
  doSomethingWithData(data);

  data = pipedReader.read();
}
pipedReader.close();

4.4 StringReader와 StringWriter

StringReader/Writer는 입출력 대상이 메모리인 스트림이다. 내부의 StringBuffer에 저장된다.

StringReader는 문자열 데이터를 Reader 형태로 변환해준다. 문자열 데이터를 오직 Reader를 기반으로 접근하게 변환이 필요할 경우 유용한다.

//사용법
String input = "Input String... ";
StringReader stringReader = new StringReader(input);

int data = stringReader.read();
while(data != -1) {
  //do something with data...
  doSomethingWithData(data);

  data = stringReader.read();
}
stringReader.close();

5. 문자기반의 보조스트림

5.1 BufferedReader와 BufferedWriter

BufferedReader/BufferedWriter 버퍼를 이용해서 입출력의 효율을 높일 수 있게 해주는 역할을 한다.

//사용법
BufferedReader bufferedReader = new BufferedReader(new FileReader("input-file.txt"));

The BufferedReader will read a block of characters from the FileReader (typically into a char array). Each character returned from read() is thus returned from this internal array. When the array is fully read the BufferedReader reads a new block of data into the array etc.

Buffer Size

bufferedReader의 버퍼사이즈를 정할 수 있다.

It is best to use buffer sizes that are multiples of 1024 bytes.

//BufferedReader 사용법
int bufferSize = 8 * 1024;

BufferedReader bufferedReader = new BufferedReader(
                      new FileReader("input-file.txt"), bufferSize);

String line = bufferedReader.readLine();
//BufferedWriter 사용법
int bufferSize = 8 * 1024;

BufferedWriter bufferedWriter = new BufferedWriter(
            new FileWriter("output-file.txt"), bufferSize);

5.2 InputStreamReader와 OutputStreamWriter

Byte 기반 스트림을 문자기반 스트림으로 연결시켜주는 역할을 한다.

주로 파일이나 네트워트 연결로부터 Byte로 제공되는 Text를 문자기반(char)로 읽고 쓰기를 할 경우에 많이 쓰인다.

//FileInputStream을 InputStreamReader로 Wrapping.
Reader inputStreamReader = new InputStreamReader(new FileInputStream("input.txt"), "UTF-8");

int data = inputStreamReader.read();
while(data != -1){
    char theChar = (char) data;
    data = inputStreamReader.read();
}

inputStreamReader.close();

Read()

주의) char 값의 정수를 리턴한다.

int data = inputStreamReader.read();

char aChar = (char) data;

정리

자바 IO패키지는 source로 부터 raw한 데이터를 읽고 raw한 데이터를 destination에 쓰는 것.

Source&Destination

  • File
  • Pipes
  • Network Connection
  • In-memory Buffers (e.g. arrays)
  • System.in, System.out, System.error

Source --> Program --> Destination

주요 io 클래스 관계

6. 표준입출력과 File

6.1 표준입출력 - System.in, System.out, System.err

위의 세 스트림은 JVM이 시작할때 초기화가 되므로 직접 초기화를 할 필요가 없다.

System.in

System.in 은 콘솔의 키보드 인풋에 연결된 inputStream이다.

System.out

System.out은 PrintStream이다. 콘솔로 데이터를 내보는 역할을 한다.

System.err

System.err PrintStream이다. System.out과 비슷하지만 일반적으로 에러메세지를 내보낼때 사용된다.

System.out과 System.err 사용비교

try {
  InputStream input = new FileInputStream("test.txt");
  System.out.println("File opened...");

} catch (IOException e){
  System.err.println("File opening failed:");
  e.printStackTrace();
}

6.2 표준입출력의 대상변경 - setOut(), setErr(), setIn()

InputStream으로 System.in, OutputStream으로 System.out, System.err의 읽기/쓰기를 새로우 스트림으로 변경할 수 있다.

System Stream 설정

OutputStream output = new FileOutputStream("system.out.txt");
PrintStream printOut = new PrintStream(output);

System.setOut(printOut);

6.3 RandomAccessFile

입력과 출력을 하나의 클래스로 파일에 대한 입력/출력을 모두 할 수 있게 설계된 클래스. 가장 큰 장점은 파일의 어느 위치에나 읽기/쓰기가 가능하다는 점이다.

RandomAccessFile 생성

RandomAccessFile file = new RandomAccessFile("file.txt", "rw");

생성자의 두번쨰 'rw'는 RandomAccessFile의 mode 값이다. rw는 Read/Write를 뜻한다.

RandomAccessFile 이동

RandomAccessFile file = new RandomAccessFile("file.txt", "rw");

//단위 : Byte
file.seek(200);

long pointer = file.getFilePointer();

file.close();

RandomAccessFile 읽기/쓰기

//읽기 사용법
RandomAccessFile file = new RandomAccessFile("file.txt", "rw");

int aByte = file.read();

file.close();
//쓰기 사용법
RandomAccessFile file = new RandomAccessFile("file.txt", "rw");

file.write("Hello World".getBytes());

file.close();

6.4 File

File 클래스로 통해 파일과 디렉토리를 다룰 수 있도록 제공한다

File클래스는 파일과 파일의 메타데이터의 접근하는 기능만 제공한다. 만약 파일의 내용을 읽기/쓰기 기능을 원한다면 file Stream을 이용해야한다.

만약 NIO를 사용한다면 파일에 대한 접근은 java.nio.FileChannel를 사용해야한다.

File 초기화

File file = new File("input-file.txt");

존재여부 확인

File 객체 생성시 실제 파일이 없어도 예외가 나지 않고 정상적으로 생성된다.

boolean fileExists = file.exists();

디렉토리 생성

mkdir() / mkdirs()

File file = new File("c:\\users\\javastudy\\newdir");

boolean dirCreated = file.mkdir();

파일 길이

long length = file.length();

Rename

boolean success = file.renameTo(new File("c:\\data\\new-file.txt"));

Move로 사용할 수 있음.

파일 삭제

boolean success = file.delete();

디렉토리확인

boolean isDirectory = file.isDirectory();

파일 리스트

list() / listFiles()

File file = new File("c:\\data");

String[] fileNames = file.list();

File[]   files = file.listFiles();

7. 직렬화(Serialization)

7.1 직렬화란?

직렬화란 객체를 데이터 스트림으로 만드는 것을 뜻한다. 객체에 저장된 데이터를 스트림에 쓰기위해 연속적인(serial) 데이터로 변환하는 것을 말한다.

자바 오브젝트를 쓰고(Serialization) 읽기(Deserializetion) 하기 위해선 Serializable 인터페이스를 구현(선언)해야한다.

7.2 ObjectInputStream과 ObjectOutputStream

ObjectInputStream/ObjectOutputStream은 raw 한 byte를 읽는거 대신, InputStream/OutputStream을 통해 자바 오브젝트를 읽을 수 있도록 해준다.

//ObjectInputStream 사용법

ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("object.data"));

MyClass object = (MyClass) objectInputStream.readObject();
//etc.

objectInputStream.close();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("object.data"));

MyClass object = new MyClass();

output.writeObject(object);

output.close();

7.3 직렬화가 가능한 클래스 만들기 - Serializable, transient

직렬화가 가능한 클래스를 만드는 방법은 Serializable 인터페이스를 원하는 클래스에 구현(선언)하면 된다.

class UserInfo implements Serializable {
  String name;
  String password;
  int age;
}

password와 같은 직렬화하지 않을 멤버변수 앞에 transient 를 붙이면 된다.

class UserInfo implements Serializable {
  String name;
  transient String password;
  int age;
}

7.4 직렬화가능한 클래스의 버전관리

serialVersionUID

직렬화된 객체를 역직렬화할 떄는 직렬화 했을 떄와 같은 클래스를 사용해야한다. 하지만 클래스의 내용이 변경된 경우 역직렬화에 실패한다.

public static class Person implements Serializable {

    private static final long serialVersionUID = 1234L;

    public String name = null;
    public int    age  =   0;
    //public String address = "";
}

static 변수나 transient가 붙은 인스턴스변수는 직렬화에 영향을 미치지 않는다.

Object Serialization Today

In today's world (2015 and forward) many Java projects serialize Java objects using different mechanisms than the Java serialization mechanism. For instance, Java objects are serialized into JSON, BSON or other more optimized binary formats. This has the advantage of the objects also being readable by non-Java applications. For instance, JavaScript running in a web browser can natively serialize and deserialize objects to and from JSON.