입출력이란?

I/O 는 Input, Output의 약자로 입력과 출력을 의미한다.

입출력은 컴퓨터 내부 또는 외부의 장치와 프로그램간의 데이터를 주고 받는 것을 말한다.

 

스트림(stream)

원래 모니터랑 키보드라고 쓰려했는데 이제 보니까 모니터가 2개다. 그냥 듀얼 모니터를 쓴다고 생각하자.

 

자바에서 입출력을 수행하기 위해선, 두 대상을 연결하고 데이터를 전송할 수 있는 통로가 필요하다.

이를 스트림이라고 한다. 이 스트림은 전 장의 스트림 API와는 완전히 다른 개념이다.

 

스트림은 연속적인 데이터의 흐름을 흐르는 물에 비유해 붙여진 이름이라고 한다.

물이 한쪽으로만 흐르듯이, 스트림은 단방향 통신만 지원하며, 먼저 보낸 데이터를 먼저 받고,

중간에 건너뜀 없이 연속적으로 데이터를 주고받는다. (큐 형태)

 

따라서 한 방향으로만 데이터를 전송할 수 있으므로 입출력을 동시해 수행하려면 입력 스트림과, 출력 스트림, 총 2개의 스트림이 필요하다.

 

바이트 기반 스트림

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

  • FileInputStream , FileOutputStream : 파일 입출력
  • ByteArrayInputStream, ByteArrayOutputStream : 메모리(byte 배열)
  • PipedInputStream, PipedOutputStream : 프로세스간의 통신
  • AudioInputStream, AudioOutputStream : 오디오장치

그리고 이들은 모두 InputStream과 OutputStream의 자손 클래스들이다.

자바에서는 java.io 패키지를 통해 많은 종류의 입출력 클래스들을 제공하며, 입출력을 처리하는 표준화된 방법을 제공한다.

//InputStream
abstract int read()
int read(byte[] b)
int read(byte[] b, int off, int len)

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

InputStream의 read(), write(int b)는 입출력의 대상에 따라 읽고 쓰는 방법이 달라져야 하기 때문에 추상 메서드로 정의되어 있다.

 

그리고 나머지 read, write 메서드들은 추상 메서드인 read(), write(int b)를 이용해 구현한 것이기 때문에

read(), write(int b)가 구현되어 있지 않으면 의미가 없다.

 

https://docs.oracle.com/javase/tutorial/essential/io/bytestreams.html

 

Byte Streams (The Java™ Tutorials > Essential Java Classes > Basic I/O)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyBytes {
    public static void main(String[] args) throws IOException {

        FileInputStream in = null;
        FileOutputStream out = null;

        try {
            in = new FileInputStream("xanadu.txt");
            out = new FileOutputStream("outagain.txt");
            int c;

            while ((c = in.read()) != -1) {
                out.write(c);
            }
        } finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }
}

공식 문서의 사용 예시이다.

파일 입출력을 위해 FileInputStream, FileOutputStream을 사용했다.

 

read()의 반환값이 int인 이유는 읽기에 실패했을 경우 -1을 반환하기 때문이다.

 

스트림을 사용한 후에는 close() 메서드를 이용해 스트림을 닫아주어야한다.

JVM이 프로그램이 종료될때 닫지 않은 스트림을 닫아둔다고는 하나, 무조건 close()로 닫아주는 것이 좋다고 한다.

(System.in, System.out 같은 표준 입출력 스트림은 닫아주지 않아도 된다.)

 

공식문서에서는 close()를 호출하는 것이 매우 중요하다고 이야기하며,

예시에선 finally문을 이용해 예외가 발생하더라도 close()가 호출되도록 했다.

이렇게 close()를 반드시 호출해야 심각한 메모리 누수를 피할 수 있다고 이야기한다.

 

Memory Leak(메모리 누수)란?

어떤 객체가 더 이상 사용하지 않지만 GC에 의해 삭제되지 않아 계속해서 메모리에 남아있는 현상이다.

이러한 누수가 커질 경우 OutOfMemoryError의 원인이 되기도 한다.

 

스트림을 사용한 후 close()를 사용하지 않을 경우 메모리 누수의 원인이 될 수 있다.

 

문자기반 스트림

바이트기반 스트림이 바이트를 단위로 입출력했다면 문자기반 스트림은 char 단위로 입출력한다.

바이트기반 스트림의 조상이 InputStream/OutputStream이듯이 문자기반 스트림의 조상은 Reader/Writer 이다.

 

문자기반 스트림은 char 단위로 입출력하는 것 외에도

여러 종류의 인코딩과 자바에서 사용하는 유니코드(UTF-16)간의 변환을 자동으로 처리해준다.

Reader는 특정 인코딩을 읽어 유니코드로 변환하고, Writer는 유니코드 특정 인코딩으로 변환해 저장한다.

 

문자 데이터를 다루는데 사용된다는 것을 제외하면 바이트기반 스트림과 사용방법이 거의 같다.

import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.IOException;

public class CopyLines {
    public static void main(String[] args) throws IOException {

        BufferedReader inputStream = null;
        PrintWriter outputStream = null;

        try {
            inputStream = new BufferedReader(new FileReader("xanadu.txt"));
            outputStream = new PrintWriter(new FileWriter("characteroutput.txt"));

            String l;
            while ((l = inputStream.readLine()) != null) {
                outputStream.println(l);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

 

보조 스트림

이러한 스트림들 외에도 스트림의 기능을 보완하기 위한 보조 스트림이 제공된다.

 

보조 스트림은 실제 데이터를 주고받는 스트림이 아니기 때문에 보조 스트림 만으로 데이터를 입출력할 수는 없지만,

스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있다.

 

스트림을 먼저 생성한 후에 이를 매개변수로 넣어 보조 스트림을 생성할 수 있다.

FileInputStream fis = new FileInputStream("test.txt");

BufferedInputStream bis = new BufferedInputStream(fis);
bis.read();

 

보조스트림의 종류

  • BufferedInputStream/BufferedOutputStream : 입출력에 버퍼를 지원한다.
  • DataInputStream/DataOutputStream : 데이터를 읽고 쓰는데 byte가 아닌 원시 자료형의 단위를 지원한다.
  • SequenceInputStream : 여러개의 입력스트림을 연결해 하나의 스트림처럼 사용할 수 있게 해준다.
  • PrintStream : 데이터를 다양한 형태로 출력할 수 있는 print, println, printf같은 메서드를 오버라이딩해 제공한다.
  • InputStreamReader/OutputStreamWriter : 바이트스트림을 문자기반 스트림으로 연결해준다.

이러한 보조 스트림들의 조상은 FilterInputStream/FilterOutputStream이다.

보통은 스트림에 버퍼(Buffer)를 제공하기 위해 사용된다.

 

버퍼(Buffer)란?

데이터를 전송하는 저,고속 장치간의 속도차이를 줄여주는 역할을 하는 중간 저장소이다.

데이터를 한번에 전달하는 것이 아니라 묶어서 전달한다.

 

하드디스크, 키보드, 모니터 등의 외부장치의 속도는 매우 느리다.

키보드가 눌릴때마다 CPU로 정보를 이동시키는 것보다,

중간에 Buffer를 둬서 데이터를 묶어뒀다가 한번에 이동시키는 것이 효율적이고 빠르다.

 

표준입출력

표준입출력은 콘솔(console, 도스창)을 통한 데이터 입력과 콘솔로의 데이터 출력을 의미한다.

자바에서는 표준 입출력을 위해 System.in, System.out, System.err 3가지 입출력 스트림을 제공한다.

 

이 3가지 스트림은 자바 어플리케이션 실행과 동시에 자동으로 사용 가능하도록 생성되며,

종료시에 닫히므로 별도로 생성하는 코드를 작성하거나 close()를 호출해 닫아줄 필요가 없다.

 

이들은 System 클래스의 스태틱 변수이며, BufferedInputStream, BufferedOutputStream의 인스턴스를 사용해서 구현되어 있다.

setIn(), setOut(), setErr() 메서드를 통해 입출력 대상을 콘솔 화면에서 다른 곳으로 변경할 수 있다.

 

File

파일은 가장 기본적이면서 가장 많이 사용되는 입출력 대상이기 때문에,

자바에서는 File 클래스를 만들어 파일과 디렉토리를 다룰 수 있도록 만들었다.

File(String fileName) //주어진 문자열을 이름으로 갖는 파일을 위한 인스턴스를 생성한다.

static String pathSeparator // OS에서 사용하는 경로 구분자 Windows ";"  UNIX":"
static char pathSeparatorChar
static String separator // OS에서 사용하는 이름 구분자 Windows "\\" UNIX "/"
static char separatorcChar

File 인스턴스는 파일일 수도 있고, 디렉터리일 수도 있다.

File 클래스의 생성자에 들어가는 fileName은 주로 경로를 포함해서 지정해주며,

파일 이름만 사용할 시 프로그램의 실행 위치가 경로로 간주된다.

 

OS마다 파일의 경로 구분자가 다르기 때문에 직접 입력해서 사용할 시 특정 OS에선 오류를 일으킬 수 있다.

OS에 독립적으로 프로그램을 작성하기 위해선 File 클래스에서 제공하는 static 변수를 사용하자.

 

직렬화(Serialization)

직렬화란 객체를 데이터 스트림으로 만드는 것을 뜻한다.

즉 객체에 저장된 데이터를 스트림에 쓰기 위한 연속적인 데이터로 변환하는 것을 이야기한다.

반대로 스트림에서 데이터를 읽어 객체를 만드는 것을 역직렬화라고 이야기한다.

 

ObjectInputStream, ObjectOutputStream

각각 역직렬화, 직렬화에 사용하는 스트림이다.

기반 스트림을 필요로 하는 보조 스트림이므로, 객체 생성시에 입출력을 위한 스트림을 지정해줘야한다.

ObjectInputStream(InputStream in)
ObjectOutputStream(OutputStream out)

 

만약 파일에 객체를 저장하고 싶다면 다음과 같이 하면 된다.

FileOutputStream fos = new FileOutputStream("objectfile.txt");
ObjectOutputStream out = new ObjectOutputStream(fos);
ObjectInputStream in = new ObjectInputStream(fos);

out.writeObject(new User());
User u = (User) in.readObject();

writeObject(), readObject()를 통해 객체를 저장하고 읽어올 수 있다.

readObject()의 반환타입은 Object이므로, 원래 타입으로 형변환을 해줘야한다.

 

객체를 직렬화, 역직렬화하는 과정은 객체의 모든 인스턴스변수가 참조하는 모든 객체에 관한 내용이기 때문에,

상당히 복잡하고 시간도 오래 걸린다.

직렬화 작업 시간을 단축시키기 위해서는 직렬화하고자 하는 객체의 클래스에 다음과 같은 2개의 메서드를 구현해줘야한다.

private void writeObject(ObjectOutputStream out)

private void readObject(ObjectInputStream in)

 

Serializable, transient

java.io.Serializable 인터페이스를 구현하면 직렬화가 가능한 클래스가 된다.

Serializable 인터페이스는 아무 내용도 없는 빈 인터페이스이지만, 직렬화를 고려하여 작성한 클래스인지 판단하는 기준이 된다.

class User implements Serializable{
  private int age;
  private String name;
  private MyClass myClass; //직렬화 불가능
}

class MyClass{}

다음과 같은 User 클래스의 인스턴스를 직렬화하면 객체의 인스턴스 변수들이 직렬화 대상이 된다.

만약 인스턴스 변수 중 직렬화가 불가능한 클래스가 있다면 직렬화 시에 NotSerializableException이 발생한다.

class User implements Serializable{
  private int age;
  private String name;
  private Object myClass = new MyClassSerializable(); //직렬화 가능!

  transient private Object object = new Object(); // 직렬화 -> null
}

class MyClassSerializable implements Serializable{}

직렬화가 가능한지는 인스턴스 변수의 타입이 아닌 실제 연결된 객체의 종류에 의해 결정된다.

위와 같은 코드처럼 Object 타입 인스턴스 변수를 선언해도 실제 값이 직렬화 가능한 객체라면 직렬화가 가능하다.

 

또한 직렬화가 안되는 객체에 대한 참조를 포함하고 있다면 transient를 붙여서 직렬화 대상에서 제외할 수 있다.

transient가 붙은 인스턴스의 값은 그 타입의 기본값으로 직렬화된다.

 

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

직렬화된 객체를 역직렬화할 때는 직렬화 했을때와 같은 클래스를 사용해야한다.

그러나 클래스의 이름이 같더라도, 내용이 변경된 경우 역직렬화는 실패하며 예외가 발생한다.

 

객체가 직렬화될 때 객체는 클래스에 정의된 멤버들의 정보를 이용해서

serialVersionUID라는 클래스의 버전에 관한 식별자(UID)를 자동생성해서 직렬화 내용에 포함한다.

역직렬화시에 UID가 다르다면 이는 클래스의 내용이 수정되었다는 뜻이므로 에러를 발생시킨다.

 

그러나 static 변수, 상수, 또는 transient가 붙은 인스턴스 변수가 추가되는 경우 직렬화에는 영향을 끼치지 않는다.

이 경우 static final long serialVersionUID라는 스태틱 변수를 직접 추가해 버전을 수동으로 관리할 수 있다.

+ Recent posts