본문 바로가기
TIL/Java | Spring Boot

[Java] 스터디 9주차_입출력 I/O

by yeon_zoo 2022. 8. 6.

1. 자바에서의 입출력

1.1 입출력이란? 

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

 

1.2 스트림

자바에서 입출력을 수행하려면 (어느 한 쪽에서 다른 쪽으로 데이터를 전달하려면) 두 대상을 연결하고 데이터를 전송할 수 있는 무언가가 필요한데 이것이 스트림이다. (람다와 스트림에서 배운 스트림과 다른 의미임)

스트림은 데이터를 운반하는데 사용되는 연결 통로이다.

 

스트림의 특징 

  • 단방향통신만 가능하다 (하나의 스트림으로 입력과 출력을 동시에 처리할 수 없음. )
  • 동시 수행을 위해서는 입력 스트림과 출력스트림 총 2개의 스트림이 필요하다.
  • 먼저 보낸 데이터를 먼저 받게 되어 있으며 중간에 건너뜀 없이 연속적으로 데이터를 주고받는다. (FIFO 구조)

1.3 바이트기반 스트림

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

InputStream, OutputStream의 추상 메서드들을 자신에 맞게 구현되어 있다. 

 

1.4 보조 스트림

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

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

// 기반 스트림을 이용해서 보조스트림을 생성한다.
BufferedInputStream bis = new BufferedInputStream(fis);

bis.read(); //보조스트림인 BufferedInputStream으로부터 데이터를 읽는다.

코드 상으로는 보조스트림인 BufferedInputStream이 입력기능을 수행하는 것처럼 보이지만 실제 입력기능은 BufferedInputStream과 연결된 FileInputStream이 수행하고 보조스트림으로는 버퍼만을 제공하고 있다. (버퍼 사용하면 성능이 훨씬 좋아지기 때문에 대부분의 경우에 버퍼를 사용)

 

모든 보조스트림 역시 InputStream, OutputStream의 자손들이므로 입출력 방법이 같다. 

 

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

바이트 기반(입출력의 단위가 1byte) 스트림 외에도 문자 기반의 스트림이 제공된다. 문자 데이터를 입출력 할 때는 바이트 기반 대신 문자기반을 사용한다. (자바에서는 char형이 1byte가 아니라 2byte기 때문에 바이트기반의 스트림으로 2 byte인 문자를 처리하는데는 어려움이 존재)

 

문자기반 스트림은 바이트기반 스트림과 활용방법이 거의 같다. 보조스트림 역시도 존재하며 바이트기반과 동일하다. 

 

2. 바이트기반 스트림

2.1 InputStream과 OutputStream

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

 

2.2 ByteArrayInputStream과 ByteArrayOutputStream

ByteArrayInputStream과 ByteArrayOutputStream은 주로 다른 곳에 입출력하기 전에 데이터를 임시로 바이트배열에 담아서 변환 등의 작업을 하는데 사용된다. 

바이트 배열은 사용하는 자원이 메모리 밖에 없으므로 가비지컬렉터에 의해 자동적으로 자원을 반환하므로 close()를 이용해서 스트림을 닫지 않아도 된다. read()와 write(int b)를 사용하기 떄문에 한번에 1 byte만 읽고 쓰므로 작업효율이 떨어진다. 이 때 배열을 이용하면 작업의 효율을 증가시킬 수 있다. 가능하면 입출력 대상에 따라 알맞은 크기의 배열을 사용하는 것이 좋다. 

 

read()나 write()이 IOException을 발생시킬 수 있기 때문에 try-catch문으로 감싸주는 것이 좋다. available()은 블락킹 없이 읽어올 수 있는 바이트의 수를 반환한다. 

 

2.3 FileInputStream과 FileOutputStream

파일에 입출력을 하기 위한 스트림이다. 실제 프로그래밍에서 많이 사용되는 스트림 중의 하나이다. 

 

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

3.1 FilterInputStream과 FilterOutputStream

이는 모든 보조스트림의 조상이다. 보조스트림은 자체적으로 입출력을 수행할 수 없기 때문에 기반스트림을 필요로 한다. FilterInputStream/FilterOutputStream은 상속을 통해 원하는 작업을 수행하도록 읽고 쓰는 메서드를 오버라이딩해야 한다. 생성자 FilterInputStream는 접근 제어자가 protected이기 때문에 Filter InputStream의 인스턴스를 생성해서 사용할 수 없고 상속을 통해서 오버라이딩되어야 한다. 

 

3.2 DataInputStream과 DataOutputStream

각각 FilterInputStream / FilterOutputStream의 자손이며 데이터를 읽고 쓰는데 있어서 byte단위가 아닌 8가지 기본 자료형의 단위로 읽고 쓸 수 있다는 장점이 있다. 

 

3.3 SequenceInputStream

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

 

3.4 PrintStream

데이터를 기반스트림에 다양한 형태로 출력할 수 있는 Print, println, printf와 같은 메서드를 오버로딩하여 제공한다. 이는 데이터를 적절한 문자로 출력하는 것이기 때문에 문자기반 스트림의 역할을 수행한다. PrintWriter로 더 향상된 기능의 문자기반 스트림이 추가 되었지만  자주 사용되던 System.out이 PrintStream이기 때문에 둘 다 사용한다. 

 

4. 문자기반 스트림

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

InputStream OutputStream 사용
Reader Writer byte 배열 대신 char 배열을 사용
FileReader FileWriter 파일로부터 텍스트데이터를 읽고 파일에 쓰는 데 사용
PipedReader PipedWriter 쓰레드 간에 데이터를 주고 받을 때 사용. 다른 스트림과 달리 입력과 출력 스트림을 하나의 스트림으로 연결해서 데이터를 주고 받는다. 
StringReader StringWriter 입출력 대상이 메모리인 스트림

 

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

InputStream OutputStream 사용
BufferedReader BufferedWriter 버퍼를 이용하여 입출력의 효율을 높일 수 있도록 해준다. 
InputStreamReader OutputStreamWriter 바이트기반 스트림을 문자기반 스트림으로 연결시켜주는 역할

 

6. 표준입출력과 File

6.1 표준입출력의 대상 변경

메서드 설명
static void setOut(PrintStream out) System.out의 출력을 지정된 PrintStream으로 변경
static void setErr(PrintStream err) System.err의 출력을 지정된 PrintStream으로 변경
static void setln(InputStream in) System.in의 입력을 지정된 InputStream으로 변경

이후 Scanner클래스가 제공되면서 System.in으로부터 데이터를 입력받아 작업하는 것이 편리해졌다. 

 

6.2 RandomAccessFile

RandomAccessFile은 하나의 클래스로 파일에 대한 입력과 출력을 모두 할 수 있도록 되어 있다. InputStream / OutputStream으로부터 상속받지 않고 DataInput인터페이스와 DataOutput인터페이스를 모두 구현했기 때문에 읽기와 쓰기가 모두 가능하다. 

가장 큰 장점은 파일의 어느 위치에나 읽기/쓰기가 가능하다는 것이다. 

 

7. 직렬화 (Serialization)

7.1 직렬화란?

객체를 데이터 스트림으로 만드는 것을 뜻한다. 객체에 저장된 데이터를 스트림에 쓰기위해 연속적인 데이터로 변환하는 것을 의미한다. 반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화라고 한다. 

 

7.2 ObjectInputStream, ObjectOutputStream

직렬화(=스트림에 객체를 출력)에는 ObjectOutputStream을, 역직렬화(스트림으로부터 객체를 입력)에는 ObjectInputStream을 사용한다. 

 

7.3 직렬화가 가능한 클래스 만들기 

직렬화가 가능한 클래스를 만드는 방법은 직렬화하고자 하는 클래스가 java.io.Serializable 인터페이스를 구현하도록 하면 된다. Serializable 인터페이스는 아무런 내용도 없는 빈 인터페이스이지만 직렬화를 고려하여 작성한 클래스인지를 판단하는 기준이 된다. 

 

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

직렬화된 객체를 역직렬화할 때는 직렬화 했을 때와 같은 클래스를 사용해야 한다. 그러나 클래스의 이름이 같더라도 클래스의 내용이 변경된 경우 역직렬화는 실패하며 예외가 발생한다. 그래서 역직렬화할 때 클래스의 버전을 비교함으로써 직렬화할 때의 클래스의 버전과 일치하는지 확인할 수 있다. 

댓글