입출력 스트림

라이언양 위키
둘러보기로 가기 검색하러 가기

1 소개

자바에서 데이터는 스트림(Stream)을 통해 입출력됩니다. 스트림은 단일 방향으로 연속적으로 흘러가는 것을 말하는데, 물이 높은 곳에서 낮은 곳으로 흐르듯이 데이터는 출발지에서 도착지로 흘러갑니다.

프로그램이 출발지냐 또는 도착지냐에 따라서 사용하는 스트림의 종류가 결정됩니다. 프로그램이 도착지이면 흘러온 데이터를 입력받아야 하므로 입력 스트림을 사용합니다. 반대로 프로그램이 출발지면 데이터를 출력해야 하므로 출력 스트림을 사용합니다.

2 입출력 스트림의 종류

java.io 패키지에는 여러 가지 종류의 스트림(Stream) 클래스를 제공하고 있습니다. 이들 스트림 클래스는 다음과 같이 크게 두 종류로 구분됩니다.

  • 바이트(byte) 기반 스트림: 그림, 멀티미디어 등의 바이너리 데이터를 읽고 출력할 때 사용
  • 문자(character) 기반 스트림: 문자 데이터를 읽고 출력할 때 사용

스트림 클래스가 바이트 기반인지, 문자 기반인지를 구별하려면 최상위 클래스를 보면 됩니다. 바이트 기반 스트림의 최상위 입력 스트림은 InputStream, 최상위 출력 스트림은 OutputStream 이고, 하위 클래스의 Suffix에는 항상 -InputStream 또는 -OutputStream 으로 끝는다. 문자 기반 스트림의 최상의 입력 스트림은 Reader, 최상위 출력 스트림은 Writer 이고, 하위 클래스의 Suffix에는 항상 -Reader 또는 -Writer 로 끝난다.

3 보조 스트림

보조 스트림이란 다른 스트림과 연결이 되어 여러 가지 편리한 기능을 제공해주는 스트림을 말합니다. 보조 스트림은 자체적으로 입출력을 수행할 수 없기 때문에 입출력 소스와 바로 연결되는 InputStream, OutputStream, Reader, Writer 등에 연결해서 입출력을 수행합니다. 보조 스트림은 문자 변환, 입출력 성능 향상, 기본 타입 입출력 등의 기능을 제공합니다.

프로그램은 입력 스트림으로부터 직접 데이터를 읽지 않고, 보조 스트림에서 제공하는 기능을 이용해서 데이터를 읽습니다. 반대로 출력 스트림으로 직접 데이터를 보내지 않고 보조 스트림에서 제공하는 기능을 이용해서 데이터를 보냅니다.

3.1 보조 스트림 연결하기

보조 스트림을 연결하려면 보조 스트림을 생성할 때 자신이 연결될 스트림을 다음과 같이 생성자의 매개값으로 제공하면 됩니다.

보조스트림 변수 = new 보조스트림(연결스트림)

예를 들어 InputStream을 문자 변환 보조 스트림인 InputStreamReader에 연결하는 코드는 다음과 같습니다.

InputStream is = ...;
InputStreamReader isr = new InputStreamReader(is);

보조 스트림의 생성자 매개값은 InputStream, OutputStream, Reader, Writer 이외에 또 다른 보조 스트림이 될 수 있습니다. 이 말은 보조 스트림을 연속적으로 연결할 수 있다는 뜻입니다. 예를 들어 다음과 같이 문자 변환 보조 스트림인 InputStreamReader를 다시 성능 향상 보조 스트림인 BufferedReader에 연결할 수 있습니다.

InputStream is = System.in;
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);

3.2 문자 변환 보조 스트림

OutputStreamWriter는 바이트 기반 출력 스트림에 연결되어 문자 출력 스트림인 Writer로 변환하는 보조 스트림입니다.

<nowiki>
Writer writer = new OutputStreamWriter(바이트 기반 출력 스트림);
<nowiki>

예를 들어 파일 출력을 위한 바이트 기반 FileOutputStream을 다음과 같이 Writer 타입으로 변환할 수 있습니다.

FileOutputStream fos = new FileOutputStream("/home/ryanyang/tmp/test");
Writer w = new Writer(fos);

InputStreamReader는 바이트 기반 입력 스트림에 연결되어 문자 입력 스트림인 Reader로 변환하는 보조 스트림입니다.

Reader r = new InputStreamReader(바이트 기반 입력 스트림);

예를 들어, 파일 입력을 위한 바이트 기반 FileInputStream을 다음과 같이 Reader 타입을 변환할 수 있습니다.

FileInputStream fis = new FileInputStream("/home/ryanyang/tmp/test");
Reader r = new InputStreamReader(fis);
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;

public class CharacterConvertStream {
  public static void main(String[] args) {
    try {
      write("문자 변환 스트림을 사용합니다.");
      String data = read();
      System.out.println(data);
    } catch(Exception e) {}
  }

  public static void write(String str) throws Exception {
    FileOutputStream fos = new FileOutputStream("./CharacterConvertStream.txt");
    Writer writer = new OutputStreamWriter(fos);
    writer.write(str);
    writer.flush();
    writer.close();
  }

  public static String read() throws Exception {
    FileInputStream fis = new FileInputStream("./CharacterConvertStream.txt");
    Reader r = new InputStreamReader(fis);

    char[] buffer = new char[128];
    int readCharNum = r.read(buffer);
    r.close();
    String data = new String(buffer, 0, readCharNum);

    return data;
  }
}

3.3 성능 향상 보조 스트림

프로그램의 실행 성능은 입출력이 가장 늦은 장치를 따라갑니다. CPU와 메모리가 아무리 뛰어나도 하드 디스크의 입출력이 늦어지면 프로그램의 실행 성능은 하드 디스크의 처리 속도에 맞춰집니다. 네트워크로 데이터를 전송할 때도 마찬가지입니다. 느린 네트워크 환경이라면 컴퓨터 사양이 아무리 좋아도 메신저와 게임의 속도는 느릴 수밖에 없습니다.

이 문제에 대한 완전한 해결책은 될 수 없지만, 프로그램이 입출력 소스와 직접 작업하지 않고 중간에 메모리 버퍼와 작업함으로써 실행 성능을 향상시킬 수 있습니다. 예를 들어 프로그램은 직접 하드 디스크에 데이터를 보내지 않고 메모리 버퍼에 데이터를 보냄으로써 쓰기 속도가 향상됩니다. 버퍼는 데이터가 쌓이기를 기다렸다가 꽉 차게 되면 데이터를 한꺼번에 하드 디스크로 보냄으로써 출력 횟수를 줄여줍니다.

기본적으로 출력 스트림은 내부에 작은 버퍼를 가지고 있습니다. 하지만 이것만으로는 불충분합니다. 보조 스트림 중에서는 위와 같이 메모리 버퍼를 추가로 제공하여 프로그램의 실행 성능을 향상시키는 것들이 있습니다. 바이트 기반 스트림에서는 BufferedInputStream, BufferedOutputStream이 있고 문자 기반 스트림에는 BufferedReader, BufferedWriter가 있습니다.

3.4 기본 타입 입출력 보조 스트림

DataInputStream과 DataOutputStream 보조 스트림을 연결하면 기본 타입인 boolean, char, short, int, long, float, double을 입출력할 수 있습니다.

3.5 프린터 보조 스트림

PrintStream과 PrintWriter는 프린터와 유사하게 출력하는 print(), println() 메소드를 가지고 있는 보조 스트림입니다. 지금까지 매우 빈번히 사용했던 콘솔 출력 스트림인 System.out이 바로 PrintStream 타입이기 때문에 print(), println() 메소드를 사용할 수 있었습니다. PrintStream은 바이트 기반 출력 스트림과 연결되고, PrintWriter는 문자 기반 출력 스트림과 연결됩니다.

3.6 객체 입출력 보조 스트림

ObjectOutputStream과 ObjectInputStream 보조 스트림을 연결하면 메모리에 생성된 객체를 파일 또는 네트워크로 출력할 수 있습니다.

ObjectOutputStream은 객체를 직렬화하는 역할을 하고, ObjectInputStream은 객체로 역직렬화하는 역할을 합니다. 직렬화란 객체를 바이트 배열로 만드는 것을 말하고, 역직렬화란 바이트 배열을 다시 객체로 복원하는 것을 말합니다.

ObjectOutputStream의 writeObject() 메소드는 객체를 직렬화해서 출력 스트림으로 보냅니다.

oos.writeObject(객체);

반대로 ObjectInputStream의 readObject() 메소드는 입력 스트림에서 읽은 바이트를 역직렬화해서 객체로 다시 복원해서 리턴합니다. 리턴 타입은 Object 타입이기 때문에 원래 타입으로 다음과 같이 강제 변환해야 합니다.

객체타입 변수 = (객체타입) ois.readObject();

자바는 모든 객체를 직렬화하지 않습니다. java.io.Serializable 인터페이스를 구현한 객체만 직렬화합니다. Serializable 인터페이스는 메소드 선언이 없는 인터페이스입니다. 객체를 파일로 저장하거나, 네트워크로 전송할 목적이라면 개발자는 클래스를 선언할 때 implements Serializable 을 추가해야 합니다. 이것은 개발자가 JVM에게 직렬화해도 좋다고 승인하는 역할을 한다고 보면 됩니다.

Board.java 파일

import java.io.Serializable;
import java.util.Date;

public class Board implements Serializable {
  private int bno;
  private String title;
  private String content;
  private String writer;
  private Date date;

  public Board(int bno, String title, String content, String writer, Date date) {
    this.bno = bno;
    this.title = title;
    this.content = content;
    this.writer = writer;
    this.date = date;
  }

  public int getBno() { return bno; }
  public void setBno(int bno) { this.bno = bno; }
  public String getTitle() { return title; }
  public void setTitle(String title) { this.title = title; }
  public String getContent() { return content; }
  public void setContent(String content) { this.content = content; }
  public String getWriter() { return writer; }
  public void setWriter(String writer) { this.writer = writer; }
  public Date getDate() { return date; }
  public void setDate(Date date) { this.date = date; }
}
ObjectStreamBoard.java 파일

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;

public class ObjectStreamBoard {
  public static void main(String[] args) throws Exception {
    writeList();
    List<Board> list = readList();

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    for (Board board : list) {
      System.out.println(
        board.getBno() + "\t" + board.getTitle() + "\t" +
        board.getContent() + "\t" + board.getWriter() + "\t" +
        sdf.format(board.getDate())
      );
    }
  }

  public static void writeList() throws Exception {
    List<Board> list = new ArrayList();

    list.add(new Board(1, "제목1", "내용1", "글쓴이1", new Date()));
    list.add(new Board(1, "제목2", "내용2", "글쓴이2", new Date()));
    list.add(new Board(1, "제목3", "내용3", "글쓴이3", new Date()));

    FileOutputStream fos = new FileOutputStream("./board.db");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    oos.writeObject(list);
    oos.flush();
    oos.close();
  }

  public static List<Board> readList() throws Exception {
    FileInputStream fis = new FileInputStream("./board.db");
    ObjectInputStream ois = new ObjectInputStream(fis);
    List<Board> list = (List<Board>)ois.readObject();
    return list;
  }
}