13회차. 입출력(I/O)

KIMA·2023년 2월 16일
0
post-thumbnail

목표

자바의 Input과 Output에 대해 학습한다.

학습할 것

입출력(I/O)이란?

컴퓨터 내부 또는 외부 장치와 프로그램간의 데이터를 주고 받는 것
예를 들면 키보드로부터 데이터를 입력받거나, System.out.println()로 콘솔에 데이터를 출력하는 것

스트림(Stream)

자바에서 어느 한쪽에서 다른 쪽으로 데이터를 전달하기 위해, 두 대상을 연결하고 데이터를 전송할 수 있게 하는 것이다.
즉, 데이터를 전달하는데 사용되는 연결 통로이며, 연속적인 데이터의 흐름을 물에 비유하여 Stream이라는 명칭이 붙여졌다.

  • 특징
    • 단방향 통신만 가능하여, 입출력을 동시에 처리할 수 없다.
    • 큐(Queue)와 같은 FIFO(First In First Out)구조로 되어 있다.
      • 먼저 보낸 데이터를 먼저 받는다.
      • 중간에 건너뜀없이 연속적으로 데이터를 주고 받는다.

💡 Java 8 버전부터 추가된 스트림 API는 앞서 설명한 스트림과는 전혀 다른 개념이다.

바이트 기반 스트림 VS 문자 기반 스트림

입출력하는 데이터 단위에 따라 바이트 기반 스트림(1byte)과 문자 기반 스트림(2byte)으로 나뉜다.

  • 바이트 기반 스트림
    입출력하는 데이터 단위가 1byte이다.

    • 영문자(아스키코드)로 구성된 파일, 동영상 파일, 음악 파일의 입출력에 사용한다.
  • 문자 기반 스트림
    입출력하는 데이터 단위가 2byte이다.

    • 세계 모든 나라의 언어(유니코드)로 구성된 파일의 입출력에 사용한다.

바이트 기반 스트림 - InputStream과 OutputStream

바이트 기반 스트림은 바이트 단위로 데이터를 전송한다.

모든 바이트 기반 입력 스트림의 조상은 InputStream이고,
모든 바이트 기반 출력 스트림의 조상은 OutputStream이다.

  • 바이트 기반 입출력 스트림은 입출력 대상에 따라 다음과 같이 종류가 나뉜다.

    입출력 대상입력 스트림출력 스트림
    파일FileInputStreamFileOutputStream
    메모리(byte 배열)ByteArrayInputStreamByteArrayOutputStream
    프로세스(프로세스간 통신)PipedInputStreamPipedOutputStream
    오디오 장치AudioInputStreamAudioOutputStream
  • InputStreamOutputStream의 메소드

    • InputStream의 메소드

      InputStream정의
      abstract int read()1byte를 읽어서 반환한다.(0 ~ 255사이의 값)
      - 더이상 읽어올 데이터가 없으면 -1을 반환한다.
      - 추상 메소드이므로 InputStream의 각 자손들은 자신에 맞게 해당 메소드를 구현한다.
      int read(byte[] b)최대 바이트 배열 b의 크기 만큼 데이터를 읽어서 b에 저장한다.
      - 읽어 온 데이터의 수를 반환한다.
      - 내부적으로 read(byte[] b, int off, int len)를 사용한다.
      int read(byte[] b, int off, int len)최대 len개의 byte를 읽어서 바이트 배열 b의 지정된 위치(off)에 저장한다.
      - 읽어온 데이터의 개수를 반환한다.
      - 내부적으로 read()를 사용한다.
      void close()스트림을 닫아 사용하고 있던 자원을 반환한다.
      int available()스트림으로부터 읽어올 수 있는 데이터의 크기를 반환한다.
      void mark(int readlimit)현재위치를 표시해 놓는다.
      - 후에 reset()에 의해서 표시해 놓은 위치로 다시 돌아갈 수 있다.
      - readlimit은 되돌아갈 수 있는 byte의 수이다.
      - mark() 기능을 지원하는 것은 선택적이다.
      void reset()스트림에서의 위치를 마지막으로 mark()가 호출된 위치로 되돌린다.
      - reset() 기능을 지원하는 것은 선택적이다.
      boolean markSupported()mark()reset()을 지원하는지를 알려 준다.
      long skip(long n)스트림에서 주어진 길이(n)만큼 건너뛴다.
    • OutputStream의 메소드

      OutputStream정의
      abstract void write(int b)1byte를 출력한다.
      void write(byte[] b)주어진 바이트 배열 b에 저장된 모든 데이터를 출력한다.
      - 내부적으로 write(byte[] b, int off, int len)를 사용한다.
      void write(byte[] b, int off, int len)주어진 바이트 배열 b의 off위치부터 len개의 byte를 출력한다.

      - 내부적으로 write()를 사용한다.
      void close()스트림을 닫아 사용하고 있던 자원을 반환한다.
      void flush()스트림의 버퍼에 있는 모든 내용을 출력한다.
      - 버퍼가 있는 출력 스트림에만 해당 함수를 사용할 수 있으며, OutputStreamflush()는 아무 일도 하지않는다.

🤔 read()의 반환 타입이 int인 이유
바이트 기반의 스트림이므로 데이터를 1byte(0 ~ 255) 읽어오고 읽어올 데이터가 없다면 -1을 리턴하므로, 총 256개의 값을 저장할 타입이 필요하다.
따라서, byte타입을 리턴 타입으로 설정할 수 없다.

'그러면 char 타입으로 하면 되지 않나?'라는 의문이 들 수 있지만, 정수형 중에서 연산이 가장 효율적이고 빠른 int 타입을 사용한다.

이제 바이트 기반 스트림의 각 종류를 살펴본다.

ByteArrayInputStream과 ByteArrayOutputStream

메모리 즉, 바이트 배열에 데이터를 입출력 하는데 사용되는 스트림이다.
주로 다른 곳에 입출력하기 전에 데이터를 임시로 바이트배열에 담아서 변환 등의 작업을 하는데 사용된다.

그리고 바이트 배열은 사용하는 자원이 메모리 밖에 없으므로 가비지컬렉터에 의해 자동으로 자원을 반환하므로, close()로 스트림을 닫지 않아도 된다.

FileInputStream과 FileOutputStream

파일에 데이터를 입출력 하는데 사용되는 스트림이다.

실제 프로그래밍에서 많이 사용되는 스트림 중의 하나이다.
하지만 텍스트파일을 다루는 경우에는 FileReaderFileWriter를 사용하는 것이 더 좋다.

다음은 FileInptuStreamFileOutputStream의 생성자들이다.

생성자설명
FileInputStream(String name)지정된 파일명(name)을 가진 실제 파일과 연결된 FileInputStream 생성
FileInputStream(File file)지정된 파일 인스턴스(file)로 FileInputStream 생성
FileInputStream(FileDescriptor fdObj)지정된 파일 디스크립터(fdObj)로 FileInputStream 생성
FileOutputStream(String name)지정된 파일명(name)을 가진 실제 파일과 연결된 FileOutputStream 생성
FileOutputStream(String name, boolean append)지정된 파일명(name)을 가진 실제 파일과 연결된 FileOutputStream 생성
- appendtrue이면 기존 파일 내용의 마지막에 덧붙이고, false이면 기존 파일 내용을 덮어쓴다.
FileOutputStream(File file)지정된 파일 인스턴스(file)로 FileOutputStream 생성
FileOutputStream(File file, boolean append)지정된 파일 인스턴스(file)로 FileOutputStream 생성
- appendtrue이면 기존 파일 내용의 마지막에 덧붙이고, false이면 기존 파일 내용을 덮어쓴다.
FileOutputStream(FileDescriptor fdObj)지정된 파일 디스크립터(fdObj)로 FileOutputStream 생성

스트림 입출력 예제

ByteArrayInputStreamByteArrayOutputStream은 자주 사용되지는 않지만 스트림을 이용한 입출력 방법을 보여 주는 예제를 작성하기에 적합해서, 이 스트림으로 여러 입출력 예제를 살펴보자.

  • 예제. 바이트 배열 inSrc의 데이터를 다른 바이트 배열 outSrc로 옮기기

    • 방법1. read()로 한 바이트씩 옮기기

      byte[] inSrc = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
      byte[] outSrc = null;
      
      ByteArrayInputStream input = null;
      ByteArrayOutputStream output = null;
      
      input = new ByteArrayInputStream(inSrc);
      output = new ByteArrayOutputStream();
      
      int data = 0;
      while ((data = input.read()) != -1) {
        output.write(data);
      }
      outSrc = output.toByteArray();
      
      System.out.println("Input Source : " + Arrays.toString(inSrc));
      System.out.println("Output Source : " + Arrays.toString(outSrc));
      Input Source : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
      Output Source : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    • 방법2. read(byte[] b)로 여러 byte씩 옮기기

      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();
      
          System.out.println("Input Source : " + Arrays.toString(inSrc));
      
          try {
              while(input.available() > 0) {
                  input.read(temp);
                  output.write(temp);
                  outSrc = output.toByteArray();
      
                  printArrays(temp, outSrc);
              }
          } catch (IOException e) {}
      }
      
      private static void printArrays(byte[] temp, byte[] outSrc) {
          System.out.printf("%-15s: %s%n", "temp", Arrays.toString(temp));
          System.out.printf("%-15s: %s%n", "Output Source", Arrays.toString(outSrc));
      }
      Input Source : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
      temp           : [0, 1, 2, 3]
      Output Source  : [0, 1, 2, 3]
      temp           : [4, 5, 6, 7]
      Output Source  : [0, 1, 2, 3, 4, 5, 6, 7]
      temp           : [8, 9, 6, 7]
      Output Source  : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 7]
      • 마지막 temp 배열에서 6과 7이 존재하는 이유는 read(byte[] b)는 보다 나은 성능을 위해 바이트 배열 b에 담긴 내용을 지우고 쓰는 것이 아니라 기존의 b위에 덮어 쓰기 때문이다.
        • 따라서 코드를 다음과 같이 수정해야한다.
          while(input.availavle() > 0) {
            int len = input.read(temp);
            output.write(temp, 0, len);
          }

문자 기반 스트림 - Reader, Writer

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

InputStream👉 Reader
OutputStream 👉 Writer

모든 문자 기반 입력 스트림의 조상은 Reader이고,
모든 문자 기반 출력 스트림의 조상은 Writer이다.

  • 문자 기반 입출력 스트림은 입출력 대상에 따라 다음과 같이 종류가 나뉜다.

    입출력 대상입력 스트림출력 스트림
    파일FileReaderFileWriter
    메모리(char 배열)CharArrayReaderCharArrayWirter
    프로세스(프로세스간 통신)PipedReaderPipedWriter
    문자열 버퍼StringBufferReaderStringBufferWriter
  • ReaderWriter의 입출력 메소드

    • Reader의 입력 메소드

      Reader정의
      int read()하나의 char(2byte)를 읽어서 반환한다.
      - 더이상 읽어올 데이터가 없으면 -1을 반환한다.
      - 내부적으로 read(char[] b, int off, int len)를 사용한다.
      int read(char[] cbuf)최대 char 배열 cbuf의 크기 만큼 데이터를 읽어서 cbuf에 저장한다.
      - 읽어 온 데이터의 수 또는 -1을 반환한다.
      - 내부적으로 read(char[] b, int off, int len)를 사용한다.
      abstract int read(char[] cbuf, int off, int len)최대 len개의 char를 읽어서 char 배열 cbuf의 off 위치에 저장한다.
      - 읽어 온 데이터의 수 또는 -1을 반환한다.
    • Writer의 출력 메소드

      Writer정의
      void write(int c)한 char(2byte)를 출력한다.
      - 내부적으로 write(char[] cbuf, int off, int len)를 사용한다.
      void write(char[] cbuf)주어진 문자 배열 cbuf에 저장된 모든 문자를 작성한다.
      - 내부적으로 write(char[] cbuf, int off, int len)를 사용한다.
      abstract void write(char[] cbuf, int off, int len)주어진 문자 배열 cbuf의 off위치부터 len개의 문자를 출력한다.
      void write(String str)주어진 문자열을 출력한다.
      void write(String str, int off, int len)주어진 문자열의 일부(off번째 문자부터 len개 만큼의 문자들)를 출력한다.

ReaderWriter는 여러 종류의 인코딩과 자바에서 사용하는 유니코드(UTF-16)간의 변환을 자동적으로 처리해준다.
Reader는 특정 인코딩을 읽어서 유니코드로 변환하고,
Writer는 유니코드를 특정 인코딩을 변환하여 출력한다.

이제 문자 기반 스트림의 각 종류를 살펴본다.

FileReader와 FileWriter

FileInputStream / FileOutputStream와 내용이 비슷하므로 생략한다.

PipedReader와 PipedWriter

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

StringReader와 StringWriter

CharArrayReaderCharArrayWriter와 같이 입출력 대상이 메모리인 스트림이다.

StringWriter에 출력되는 데이터는 내부의 StringBuffer에 저장되며, 다음의 두 메서드를 통해 저장된 데이터를 얻을 수 있다.

  • StringBuffer getBuffer() : StringBuffer 반환
  • String toString() : StringBuffer에 저장된 문자열 반환
String inputData = "HELLO";
StringReader input = new StringReader(inputData);
StringWriter output = new StringWriter();

int data = 0;
while((data = input.read()) != -1) {
  output.write(data);
}

System.out.println("Input Data : " + inputData);
System.out.println("Output Data : " + output.toString());
System.out.println("Output Data : " + output.getBuffer().toString());
Input Data : HELLO
Output Data : HELLO
Output Data : HELLO

보조 스트림

데이터를 입출력하는 스트림(이를 기반 스트림이라 칭한다.)에 보조 기능(입출력 성능 향상 등)을 추가한 것이다.

보조 스트림은 자체적으로 입출력을 수행할 수 없기 때문에, 입출력시 기반스트림을 활용한다.

보조 스트림도 마찬가지로 바이트 기반 스트림의 보조 스트림, 문자 기반 스트림의 보조 스트림으로 나뉜다.
먼저, 바이트 기반 스트림의 보조 스트림을 살펴보자.

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

바이트 기반 스트림의 모든 보조 스트림의 조상 - FilterInputStreamFilterOutputStream

public class FilterInputStream extends InputStream [
   protected volatile InputStream in;
   
   // 접근제어자가 protected이므로 FilterInputStream을 생성하여 사용할 수 없고 상속을 통해 오버라이딩되어야 한다.
   protected FilterInputStream(InputStream in) {
     this.in = in;
   }
   
   public int read() throws IOException { // 자식 클래스들은 해당 메소드를 오버라이딩하여 원하는 작업을 수행하도록 읽고 쓰게 한다.
     return in.read();
   }
}

FilterInputStreamFilterOutputStream을 상속받은 바이트 기반 스트림의 보조 스트림의 종류는 다음과 같다.

  • FilterInputStream의 자손 : BufferedInputStream, DataInputStream, PushbackInputStream
  • FilterOutputStream의 자손 : BufferedOutputStream,DataOutputStream, PrintStream

이제 바이트 기반 스트림의 보조 스트림을 하나씩 살펴보자.

버퍼를 이용하는 BufferedInputStreamBufferedOutputStream

스트림의 입출력 효율을 높이기 위해
데이터를 한 바이트 또는 문자바로 보내는 것이 아니라,
버퍼(바이트 배열)에 담아놓았다가 한번에 모아서 보내는 방식이다.

  • 생성자

    생성자설명
    BufferedInputStream(InputStream in, int size)주어진 InputStream 인스턴스를 입력소스로 하며, 지정된 크기(byte 단위)의 버퍼를 갖는 BufferedInputStream 인스턴스 생성
    BufferedInputStream(InputStream in)주어진 InputStream 인스턴스를 입력소스로 하며, 버퍼의 크기를 지정해주지 않으므로 기본적으로 8192byte 크기의 버퍼를 갖는 BufferedInputStream 인스턴스 생성
    BufferedOutputStream(OutputStream out, int size)주어진 OutputStream 인스턴스를 출력 소스로 하며, 지정된 크기(byte 단위)의 버퍼를 갖는 BuffferedOutputStream 인스턴스를 생성한다.
    BufferedOutputStream(OutputStream out)주어진 OutputStream 인스턴스를 출력 소스로 하며, 버퍼의 크기를 지정해주지 않으므로 기본적으로 8192byte 크기의 버퍼를 갖는 BuffferedOutputStream 인스턴스를 생성한다.
    • 버퍼의 크기는 입력 소스로부터 한 번에 가져올 수 있는 데이터의 크기로 지정하면 좋다.
      • 보통 입력소스가 파일인 경우, 8192byte(약 8kbyte) 정도의 크기로 하는 것이 보통
      • 버퍼의 크기를 변경해가면서 테스트하면 최적의 버퍼 크기를 알아낼 수 있다.
  • 메소드

    메소드설명
    flush()BufferedInputStream - 입력 소스로부터 버퍼 크기만큼 버퍼에 입력받는다.
    BufferedOutputStream - 버퍼의 모든 내용을 출력 소스에 출력한다.
    close()flush()를 호출하고, BufferedInputStream이나 BufferedOutputStream이 사용하던 모든 자원을 반환한다.

데이터를 입력받는 과정은 다음과 같다.
1. 프로그램에서 입력소스로부터 데이터를 읽기 위해 처음으로 read()를 호출한다.
2. BufferedInputStream은 입력소스로부터 버퍼 크기만큼의 데이터를 읽어다 자신의 내부 버퍼에 저장한다.
3. 이제 프로그램에서는 read()호출시마다 BufferedInputStream 버퍼에 저장된 데이터를 읽는다.
4. 버퍼에 저장된 모든 데이터를 다 읽으면, 2번으로 다시 돌아간다.
외부의 입력소스로부터 읽는 것보다 내부의 버퍼로 읽는 것이 훨씬 빠르고, OS 레벨인 시스템 콜의 횟수를 감소시켜 성능상 이점이 있다.

데이터를 출력하는 과정은 다음과 같다.
1. 프로그램에서 출력소스로에 데이터를 출력하기위해 처음으로 write()를 호출한다.
2. BufferedOutputStream은 출력할 데이터를 자신의 내부 버퍼에 저장한다.
3. write() 계속 호출하다가 버퍼가 가득 차면, 그 때 버퍼의 모든 데이터를 출력소스로 출력하고 2번으로 돌아간다.
버퍼가 가득 찼을 때만 출력소스에 출력을 하기 때문에, 모든 출력작업을 마친 후에 마지막 출력부분이 출력소스에 출력되지 못하고 BufferedOutputStream의 버퍼에 남아있는 채로 프로그램이 종료될 수 있다.
따라서, 마지막에는 반드시 flush()close()를 호출하여 버퍼에 있는 모든 내용이 출력소스로 출력되도록 하자.

  • 예제 - BufferedOutputStream

    // 1. 먼저 기반스트림을 생성한다.
    FileOutputStream fos = new FileOutputStream("123.txt");
    
    // 2. 기반스트림을 이용해서 보조스트림을 생성한다.
    // 버퍼 크기를 5로 설정해 주었다.
    BufferedOutputStream bos = new BufferedOutputStream(fos, 5);
    
    // 3. 보조스트림으로부터 데이터를 출력한다.
    // 버퍼가 꽉 찰 때마다 123.txt에 출력된다.
    for (char i = '1'; i <= '9'; i++) {
      bos.write(i);
    }
    
    // 4. 보조스트림을 종료하여, 버퍼에 있는 모든 데이터를 출력한다.
    // 보조스트림이 종료될 때, 기반 스트림도 같이 종료된다.
    bos.close();
    • 만약 4번을 수행하지 않았을 경우의 123.txt
      12345
    • 만약 4번을 수행했을 경우의 123.txt
      123456789

DataInputStream과 DataOutputStream

DataInputStreamDataInput 인터페이스를 DataOutputStreamDataOutput 인터페이스를 구현했기 때문에, 데이터 입출력을 byte 단위가 아닌 8가지 기본 자료형의 단위로 읽고 쓸 수 있다.

DataOutputStream이 출력하는 형식은 각 기본 자료형 값을 16진수로 표현하여 저장한다. 즉, 이진 데이터로 저장한다.

DataInputStreamDataOutputStream의 메서드와 생성자는 다음과 같다.

메소드 / 생성자설명
DataInputStream(InputStream in)주어진 InputStream 인스턴스를 기반스트림으로 하는 DataInputStream 인스턴스를 생성한다.
boolean readBoolean()
byte readByte()
char readChar()
short readShort()
int readInt()
long readLong()
float readFloat()
double readDouble()
int readUnsignedByte()
int readUnsignedShort()
각 타입에 맞게 값을 읽어온다.
- 더 이상 읽을 값이 없으면 EOFException을 발생시킨다.
void readFully(byte[] b)
void readFully(byte[] b, int off, int len)
입력스트림에서 지정된 배열의 크기만큼 또는 지정된 위치에서 len만큼 데이터를 읽어온다.
- 파일의 끝에 도달하면 EOFException이 발생한다.
- I/O 에러가 발생하면 IOException이 발생한다.
String readUTF()UTF-8 형식으로 쓰여진 문자를 읽는다.
- 더 이상 읽을 값이 없으면 EOFException을 발생시킨다.
static String readUTF(DataInput in)입력스트림(in)에서 UTF-8형식의 유니코드를 읽어온다.
int skipBytes(int n)현재 읽고 있는 위치에서 지정된 숫자(n)만큼 건너뛴다.

메소드 / 생성자설명
DataOutputStream(OutpuStream out)주어진 OutpuStream 인스턴스를 기반스트림으로 하는 DataOutputStream 인스턴스를 생성한다.
void writeBoolean(boolean b)
void writeByte(int b)
void writeChar(int c)
void writeShort(int s)
void writeInt(int i)
void writeLong(long i)
void writeFloat(float f)
void writeDouble(double d)
각 타입에 맞게 값을 출력한다.
void writeChars(String s)주어진 문자열을 출력한다.
writeChar(int c)메서드를 여러번 호출한 결과와 같다.
void writeUTF(String s)UTF 형식으로 문자를 출력한다.
int size()지금까지 DataOutputStream에 쓰여진 byte의 수를 알려준다.

각 자료형의 크기가 다르므로, 출력한 데이터를 다시 읽어올 때는 출력했을 때의 순서를 염두에 두어야 한다.

FileOutputStream fos = new FileOutputStream("sample.dat");
DataOutputStream dos = new DataOutputStream(fos);

dos.writeInt(10);
dos.writeFloat(21.2f);
dos.writeBoolean(true);
dos.close();
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();
10
21.2
true

SequenceInputStream

여러 개의 입력스트림을 연속적으로 연결해서 하나으 ㅣ스트림으로부터 데이터를 읽는 것과 같이 처리한다.

큰 파일을 여러 개의 작은 파일로 나누었다가 하나의 파일로 합치는 것과 같은 작업을 수행할 때 사용하면 좋다.

  • SequenceInputStream를 생성하는 두가지 방법
    • 첫째, SequenceInputStream(InputStream s1, InputStream s2) : 두 개의 입력스트림을 하나로 연결한다.
    • 둘째, SequecneInputStream(Enumeration e) : Enumeration에 저장된 순서대로 입력스트림을 하나의 스트림으로 연결한다.
      • Vector에 연결할 입력스트림들을 저장한 다음 VectorEnumeration elements()를 호출하여 생성자의 매개변수로 사용한다.

PrintStream

데이터를 기반스트림에 문자로 출력할 수 있는 print, println, printf와 같은 메소드를 데이터의 자료형에 맞게 오버로딩하여 제공한다.

데이터를 문자로 출력하는 것이기 때문에 문자기반 스트림의 역할을 수행한다. 그래서 PrintSteram보다 향상된 기능의 문자기반 스트림인 PrintWriter가 추가되었으나 그 동안 매우 빈번히 사용되던 System.outPrintStream이다 보니 둘 다 사용할 수 밖에 없게 되었다.

  • 메소드
    • void print : 출력소스에 인자로 주어진 값을 문자로 출력한다.
    • println : 출력소스에 인자로 주어진 값을 문자로 출력하고, 마지막에 줄바꿈 문자를 출력한다.
      • 인자가 주어지지 않으면 줄바꿈 문자만 출력한다.
    • printf(String format, Object... args) : 출력소스에 인자로 주어진 값을 주어진 형태의 문자로 출력한다.

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

문자 기반 스트림의 모든 보조 스트림 조상은 ReaderWriter 이다.

이제 문자 기반 스트림의 보조 스트림을 하나씩 살펴보자.

BufferedReader와 BufferedWriter

BufferedInputStream / BufferedOutputStream와 비슷한 내용은 생략한다.

  • 메소드
    • BufferedReaderString readLine()을 통해 데이터를 라인단위로 읽을 수 있다.
    • BufferedWriternewLine()을 통해 줄바꿈을 수행할 수 있다.

InputStreamReader와 OutputStreamWriter

바이트 기반 스트림을 문자기반 스트림으로 연결시켜준다.
바이트 기반 스트림의 데이터를 지정된 인코딩의 문자데이터로 변환하는 작업을 수행한다.

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

System.out.print("문장을 입력하세요. : ");
line = br.readLine();
System.out.println("입력된 문장: " + line);
br.close();
문장을 입력하세요: hello
hello

표준 입출력

콘솔로부터의 데이터 입력과 콘솔로의 데이터 출력을 의미한다.

자바에서는 표준 입출력을 위해 다음의 3가지 입출력 스트림을 제공한다.
3가지 표준 입출력 스트림

  1. System.in - 콘솔로부터 데이터를 입력받는데 사용한다.
  2. System.out - 콘솔로 데이터를 출력하는데 사용한다.
  3. System.err - 콘솔로 데이터를 출력하는데 사용한다.
public final class System {
  public final static InputStream in = nullInputStream();
  public final static PrintStream out = nullPrintStream();
  public final static PrintStream err = nullPrintStream();
}

이 3가지는 System 클래스에 선언된 static 변수이며, 자바 어플리케이션의 실행과 동시에 사용할 수 있게 자동적으로 생성된다.
또한, 버퍼를 이용하는 BufferedInputStreamBufferedOutputStream의 인스턴스를 사용하기 때문에, 콘솔 입력시 Enter키나 입력의 끝(EOF)을 알리는 ^z(윈도우 기준)를 입력할 때까지(blocking 상태가 풀릴 때까지) 편집이 가능하며 한 번에 버퍼의 크기만큼 입력이 가능하다.

System.in

콘솔로부터 데이터를 입력받는데 사용한다.

  • 입력의 끝(EOF)을 알리는 ^z(윈도우 기준)을 입력하면 데이터 입력이 종료되고 read()는 -1을 리턴한다.
  • 엔터를 입력하면 두 개의 특수문자 \r(carrage return, 커서를 현재 라인의 첫번째 컬럼으로 이동시킨다.), \n(new line, 커서를 다음 줄로 이동시킨다.)가 입력된다.
    • 이와 같이 엔터도 사용자 입력으로 간주되므로 엔터 입력시 반환된 \r\n를 매번 제거해줘야는 불편함이 있다.
      • 이 경우에는 System.inBufferedReader를 이용해서 BufferedReaderreadLine()을 통해 라인단위로 데이터를 입력받으면, 반환 값에서 \r\n가 제거되어 위와 같은 불편함이 사라진다.
      • C언어가 탄생한 텍스트 기반의 사용자인터페이스(콘솔) 세대에서 자바가 탄생한 그래픽기반의 사용자인터페이스 세대로 변경되면서 자바는 콘솔을 통한 입력에 대한 지원이 미약했으나, ScannerConsole같은 클래스가 추가되면서 많이 보완되었다.

System.out

콘솔로 데이터를 출력하는데 사용한다.

System.err

콘솔로 데이터를 출력하는데 사용한다.

표준 입출력의 대상 변경

System.in, System.out, System.err의 입출력 대상은 기본적으로 콘솔 화면이지만 변경이 가능하다.

변경하는 메소드는 다음과 같다.

메소드설명
static void setIn(InputStream in)System.in 의 입력을 지정된 InputStream 으로 변경
static void setOut(PrintStream out)System.out 의 출력을 지정된 PrintStream으로 변경
static void setErr(PrintStream out)System.err 의 출력을 지정된 PrintStream으로 변경
class StandardIOExample {
  public static void main(String[] args) {
	PrintStream ps = null;
    FileOutputStream fos = null;

    fos = new FileOutputStream("test.txt");
    ps = new PrintStream(fos);
    System.setOut(ps);

    System.out.println("hello by system.out");
    System.err.println("hello by system.err");
  }
}  
C:\...> java StandardIOExample 
hello by system.err

C:\...> type test.txt
hello by system.out

Channel 기반의 I/O

자바4부터 자바의 기본 입출력 방식이었던 Stream의 문제점을 해결하고자 NIO(New Input Output)가 java.nio 패키지에 포함되어 등장하였고, Channel이 NIO의 기본 입출력 방식이다.

이제부터 NIO가 기존 방식과 달라진 부분을 살펴보자.
IO VS NIO

출처 : 이것이 자바다

IONIO
입출력 방식스트림채널
버퍼 유무non-bufferbuffer
비동기 지원 유무지원 안함지원
블로킹 / 넌블로킹 방식블로킹 방식만 지원(동기)블로킹 / 넌블로킹 방식 모두 지원
(동기 / 비동기 모두 지원)

입출력 방식 - Stream VS Channel

Stream

스트림은 입력 스트림과 출력 스트림으로 구분되어 있어
데이터를 읽기 위해서는 입력 스트림을 출력하기 위해서는 출력 스트림을 생성해야한다.

Channel

데이터가 흘러다니는 양방향의 통로이다.
양방향이기 때문에 입력과 출력을 구분하지 않아 입력과 출력을 위한 별도의 채널을 만들 필요가 없다.

버퍼 유무 - non-buffer VS buffer

non-buffer

데이터를 버퍼가 없이 바로 입출력한다.
스트림으로부터 입력된 전체 데이터를 별도로 저장하지 않으면, 입력된 데이터의 위치를 이동해가면서 자유롭게 이용할 수 없다.
따라서 버퍼를 제공해주는 보조 스트림인 BufferedInputStream, BufferedOutputStream을 연결해 사용하기도 한다.

buffer

기본적으로 버퍼를 통해서만 입출력이 가능하다.
버퍼 내에서 데이터의 위치를 이동해가며 필요한 부분만 읽고 쓸 수 있다.

블로킹 / 넌블로킹 방식

블로킹(대기 상태)

입력 스트림의 read()를 호출하면 데이터가 입력될 때까지 Thread는 블로킹된다.
출력 스트림의 write()를 호출하면 데이터가 출력될 때까지 Thread 는 블로킹된다.
IO Thread가 블로킹되면 다른 일을 할 수 없고 블로킹을 빠져나오기 위해 interrupt도 할 수 없다.
블로킹을 빠져나오는 유일한 방법은 스트림을 닫는 것이다.

넌블로킹

Thread를 Interrupt함으로써 빠져나올 수 있다.
넌블로킹은 과도한 스레드 생성을 피하고 스레드를 효과적으로 재사용할 수 있다.

🤔 그렇다면 무조건 IO보다 NIO가 좋은 것일까?
아니다.
첫째, 입출력 처리가 오래걸리는 작업일 경우 스레드를 재사용하여 Non-blocking 방식으로 처리하는 NIO는 좋은 효율을 내지 못할 수 있다.
둘째, 대용량 데이터를 처리해야할 경우, NIO의 버퍼 할당 크기가 문제가 된다.
셋째, 모든 입출력 작업에 버퍼를 무조건 사용해야 하므로 즉시 처리하는 IO보다 복잡하다.

정리하자면,
NIO는 불특정 다수의 클라이언트를 연결하거나 하나의 입출력 처리작업이 오래걸리지 않는 경우에 사용하고,
IO는 연결 클라이언트 수가 적고 전송되는 데이터가 대용량이면서 순차적으로 처리될 필요성이 있는 경우에 사용한다.

직렬화(Serialization)

직렬화는 객체를 컴퓨터에 저장했다가 다음에 다시 꺼내쓰거나 네트워크를 통해 컴퓨터 간에 객체를 주고받을 수 있도록 한다.

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

반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화(deserialization)라고 한다.

객체를 직렬화하기 위해서는 변수 타입이 기본형 타입이거나 java.io.Serializable 인터페이스를 상속 받아야 한다.

🤔 질문

  • 자바에서의 File Descriptor가 무엇인가?

Reference

  • 자바의 정석 3rd Edition, 남궁성 지음
  • I/O
profile
안녕하세요.

0개의 댓글