[Java] IO 기반 입출력 및 네트워킹 ⑤

kiteB·2022년 5월 2일
1

Java2

목록 보기
26/36
post-thumbnail

[ 보조 스트림 ]

다른 스트림과 연결되어 여러 가지 편리한 기능을 제공해주는 스트림

  • 보조 스트림의 일부가 FilterInputStream, FilterOutputStream의 하위 클래스이기 때문에 보조 스트림을 필터(filter) 스트림이라고도 하지만 전부가 그런 것은 아니므로 사용 목적에 맞게 보조 스트림이라고 하는 것이 좋다.
  • 보조 스트림은 자체적으로 입출력을 수행할 수 없기 때문에 입력 소스와 바로 연결되는 InputStream, FileInputStream, Reader, FileReader, 출력 소스와 바로 연결되는 OutputStream, FileOutputStream, Writer, FileWriter 등에 연결해서 입출력을 수행한다.
  • 보조 스트림은 문자 변환, 입출력 성능 향상, 기본 데이터 타입 입출력, 객체 입출력 등의 기능을 제공한다.

다음은 입력 스트림과 출력 스트림에 보조 스트림을 연결한 모습을 가상화시킨 것이다.

보조 스트림 생성

  • 보조 스트림을 생성할 때에는 자신이 연결된 스트림을 다음과 같이 생성자의 매개값으로 받는다.
보조스트림 변수 = new 보조스트림(연결스트림)
  • 예를 들어 콘솔 입력 스트림을 문자 변환 보조 스트림인 InputStreamReader에 연결하는 코드는 다음과 같다.
InputStream is = System.in;
InputStreamReader reader = new InputStreamReader(is);

✅ 스트림 체인

  • 보조 스트림은 또 다른 보조 스트림에도 연결되어 스트림 체인을 연결할 수 있다.

  • 예를 들어 문자 변환 보조 스트림인 InputStreamReader를 다시 성능 향상 보조 스트림인 BufferedReader에 연결하는 코드는 다음과 같다.
InputStream is = System.in;
InputStreamReader reader = new InputStreamReader(is);
BufferedReader br = new BufferedReader(reader);

1. 문자 변환 보조 스트림

소스 스트림이 바이트 기반 스트림(InputStream, OutputStream, FileInputStream, FileOutputStream)이면서 입출력 데이터가 문자라면 ReaderWriter로 변환해서 사용하는 것을 고려해야 한다.

ReaderWriter는 문자 단위로 입출력하기 때문에

  • 바이트 기반 스트림보다는 편리하고,
  • 문자셋의 종류를 지정할 수 있기 때문에 다양한 문자를 입출력할 수 있기 때문이다.

✅ InputStreamReader

바이트 입력 스트림에 연결되어 문자 입력 스트림인 Reader로 변환시키는 보조 스트림

Reader reader = new InputStreamReader(바이트입력스트림);
  • 콘솔 입력을 위한 InputStream을 다음과 같이 Reader 타입으로 변환할 수 있다.
InputStream is = System.in;
Reader reader = new InputStreamReader(is);
  • 파일 입력을 위한 FileInputStream도 다음과 같이 Reader 타입으로 변환시킬 수 있다.
FileInputStream fis = new FileInputStream("C:/Temp/file.txt");
Reader reader = new InputStreamReader(fis);
  • FileInputStreamInputStreamReader를 연결하지 않고 FileReader를 직접 생성할 수도 있다.
    FileReaderInputStreamReader의 하위 클래스이기 때문에 FileReader가 내부적으로 FileInputStreamInputStreamReader 보조 스트림을 연결한 것이라고 볼 수 있다.

✅ OutputStreamWriter

바이트 출력 스트림에 연결되어 문자 출력 스트림인 Writer로 변환시키는 보조 스트림

Writer writer = new OutputStreamWriter(바이트출력스트림);
  • 파일 출력을 위한 FileOutputStream을 다음과 같이 Writer 타입으로 변환할 수 있다.
FileOutputStream fos = new FileOutputStream("C:/Temp/file.txt");
Writer writer = new OutputStreamWriter(fos);
  • FileOutputStreamOutputStreamWriter를 연결하지 않고 FileWriter를 직접 생성할 수도 있다. (FileInputStream, FileReader와 같은 원리)

2. 성능 향상 보조 스트림

프로그램의 실행 성능은 입출력이 가장 늦은 장치를 따라간다.

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

이 문제에 대한 완전한 해결책은 될 수는 없지만, 프로그램이 입출력 소스와 직접 작업하지 않고 중간에 메모리 버퍼(buffer)와 작업함으로써 실행 성능을 향상시킬 수 있다.

예를 들어 프로그램은 직접 하드 디스크에 데이터를 보내지 않고 메모리 버퍼에 데이터를 보냄으로써 쓰기 속도가 향상된다. 버퍼는 데이터가 쌓이기를 기다렸다가 꽉 차게 되면 데이터를 한꺼번에 하드 디스크로 보냄으로써 출력 횟수를 줄여준다.

보조 스트림 중에서는 위와 같이 메모리 버퍼를 제공하여 프로그램의 실행 성능을 향상시키는 것들이 있다.

  • 바이트 기반 스트림에는 BufferedInputStream, BufferedOutputStream이 있고,
  • 문자 기반 스트림에는 BufferedReader, BufferedWriter가 있다.

✅ BufferedInputStream과 BufferedReader

  • BufferedInputStream: 바이트 입력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림
  • BufferedReader: 문자 입력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림
  • BufferedInputStreamBufferReader입력 소스로부터 자신의 내부 버퍼 크기만큼 데이터를 미리 읽고 버퍼에 저장해 둔다. 프로그램은 외부의 입력 소스로부터 직접 읽는 대신 버퍼로부터 읽음으로써 읽기 성능이 향상된다.
  • BufferedInputStreamBufferedReader 보조 스트림은 다음과 같이 생성자의 매개값으로 준 입력 스트림과 연결되어 8192 내부 버퍼 사이즈를 가지게 된다.
    • BufferedInputStream은 최대 8192 바이트가, BufferedReader는 최대 8192 문자가 저장될 수 있다.
BufferedInputStream bis = new BufferedInputStream(바이트입력스트림);
BufferedReader br = new BufferedReader(문자입력스트림);
  • BufferedInputStreamBufferedReader로 데이터를 읽어들이는 방법은 InputStream 또는 Reader와 동일하다.
  • BufferedReaderreadLine() 메소드를 추가적으로 가지고 있다.
    • 이 메소드를 이용하면 캐리지리턴(\r) 라인피드(\n)로 구분된 행 단위의 문자열을 한꺼번에 읽을 수 있다.
  • 다음 코드는 Enter키를 입력하기 전까지 콘솔에서 입력한 모든 문자열을 한꺼번에 읽는다.
Reader reader = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(reader);
String inputStre = br.readLine();

✅ BufferedOutputStream과 BufferedWriter

  • BufferedOutputStream: 바이트 출력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림
  • BufferedWriter: 문자 출력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림
  • BufferedOutputStreamBufferedWriter프로그램에서 전송한 데이터를 내부 버퍼에 쌓아두었다가 버퍼가 꽉 차면, 버퍼의 모든 데이터를 한꺼번에 보낸다. 프로그램 입장에서 보면 직접 데이터를 보내는 것이 아니라, 메모리 버퍼로 데이터를 고속 전송하기 때문에 실행 성능이 향상되는 효과를 얻게 된다.
  • BufferedOutputStreamBufferedWriter 보조 스트림은 다음과 같이 생성자의 매개값으로 준 출력 스트림과 연결되어 8192 내부 버퍼 사이즈를 가지게 된다.
    • BufferedOutputStream은 8192 바이트가, BufferedWriter는 8192 문자가 최대 저장될 수 있다.
BufferedOutputStream bos = new BufferedOutputStream(바이트출력스트림);
BufferedWriter bw = new BufferedWriter(문자출력스트림);
  • BufferedOutputStreamBufferedWriter로 데이터를 출력하는 방법은 OutputStream 또는 Writer와 동일하다.

⭐ 버퍼가 가득 찼을 때만 출력을 하기 때문에 마지막 자투리 데이터 부분이 목적지로 가지 못하고 버퍼에 남아있을 수 있다. 그래서 마지막 출력 작업을 마친 후에는 flush() 메소드를 호출하여 버퍼에서 잔류하고 있는 데이터를 모두 보내도록 해야 한다.


3. 기본 타입 입출력 보조 스트림

  • 바이트 스트림은 바이트 단위로 입출력하기 때문의 자바의 기본 데이터 타입(boolean, char, short, int, long, float, double) 단위로 입출력할 수 없다.
  • 그러나 DataInputStreamDataOutputStream 보조 스트림을 연결하면 기본 데이터 타입으로 입출력이 가능하다.

✅ DataInputStream, DataOutputStream 객체 생성

  • 다른 보조 스트림과 마찬가지로 연결할 바이트 입출력 스트림을 생성자의 매개값으로 주면 된다.
DataInputStream dis = new DataInputStream(바이트입력스트림);
DataOutputStream dos = new DataOutputStream(바이트출력스트림);

✅ DataInputStream, DataOutputStream의 메소드

  • 데이터 타입의 크기가 모두 다르므로 DataOutputStream으로 출력한 데이터를 다시 DataInputStream으로 읽어올 때는 출력한 순서와 동일한 순서로 읽어야 한다.
  • 예를 들어 출력할 때의 순서가 int → boolean → double이라면 읽을 때의 순서도 int → boolean → double이어야 한다.

4. 프린터 보조 스트림

PrintStreamPrintWriter는 프린터와 유사하게 출력하는 print(), println() 메소드를 가지고 있는 보조 스트림이다.

  • 지금까지 사용했던 System.out이 바로 PrintStream 타입이기 때문에 print(), println() 메소드를 사용할 수 있었다.
  • PrintStream은 바이트 출력 스트림과 연결되고, PrintWriter()는 문자 출력 스트림과 연결된다. PrintStreamPrintWriter는 거의 같은 기능을 가지고 있다.

✅ PrintStream, PrintWriter 객체 생성

PrintStream ps = new PrintStream(바이트출력스트림);
PrintWriter pw = new PrintWriter(문자출력스트림);

✅ println(), print() 오버 로딩

  • print() 메소드는 출력할 데이터 끝에 개행 문자인 '\n'을 더 추가시켜주기 때문에 콘솔이나 파일에서 줄 바꿈이 일어나지만, print() 메소드는 개행 없이 계속해서 문자를 출력시킨다.
  • 출력할 데이터 타입에 따라 다음과 같이 오버 로딩 되어 있다.


✅ printf()

  • printf() 메소드는 형식화된 문자열(format string)을 출력할 수 있도록 하기 위해 자바 5부터 추가된 메소드이다.
  • 첫 번째 매개값으로 형식화된 문자열을 지정하고, 두 번째 매개값부터 형식화된 문자열에 들어갈 값을 나열해주면 된다.
  • 형식화된 문자열에서 %conversion(변환문자)은 필수적으로 작성하고 그 외의 항목은 생략할 수 있다.
  • argument_index$는 적용할 매개값의 순번인데 1$는 첫 번째 매개값, 2$는 두 번째 매개값을 말한다.
  • flags는 빈공간을 채우는 방법인데, 생략되면 왼쪽이 공백으로 채워지고, -는 오른쪽이 공백으로 채워진다. 0은 왼쪽이 0으로 채워진다.
  • width는 전체 자릿수이며, .precision은 소수자릿수를 뜻한다.
  • 변환문자에는 정수(d), 실수(f), 문자열(s)과 시간과 관련된 문자가 와서 매개값을 해당 타입으로 출력한다.

형식화된 문자열에서 자주 사용되는 것을 정리해보면 다음과 같다.


5. 객체 입출력 보조 스트림

자바는 메모리에 생성된 객체를 파일 또는 네트워크로 출력할 수 있다.

  • 객체는 문자가 아니기 때문에 바이트 기반 스트림으로 출력해야 한다.
  • 객체를 출력하기 위해서는 객체의 데이터(필드값)를 일렬로 늘어선 연속적인 바이트로 변경해야 하는데, 이것을 객체 직렬화(serialization)라고 한다.
  • 반대로 파일에 저장되어 있거나 네트워크에서 전송된 객체를 읽을 수도 있는데, 입력 스트림으로부터 읽어들인 연속적인 바이트를 객체로 복원하는 것을 역직렬화(deserialization)라고 한다.

✅ ObjectInputStream, ObjectOutputStream

  • 자바는 객체를 입출력할 수 있는 두 개의 보조 스트림인 ObjectInputStreamObjectOutputStream을 제공한다.
  • ObjectOutputStream은 바이트 출력 스트림과 연결되어 객체를 직렬화하는 역할을 하고,
  • ObjectInputStream은 바이트 입력 스트림과 연결되어 객체로 역직렬화하는 역할을 한다.

✅ ObjectInputStream, ObjectOutputStream 객체 생성

ObjectInputStream ois = new ObjectInputStream(바이트입력스트림);
ObjectOutputStream oos = new ObjectOutputStrema(바이트출력스트림);

✅ 직렬화, 역직렬화

  • ObjectOutputStream으로 객체를 직렬화하기 위해서는 writeObject() 메소드를 사용한다.
oos.writeObject(객체);
  • ObjectInputStreamreadObject() 메소드는 입력 스트림에서 읽은 바이트를 역직렬화해서 객체로 생성한다.
  • readObject() 메소드의 리턴 타입은 Object 타입이기 때문에 객체 원래의 타입으로 변환해야 한다.
객체타입 변수 = (객체 타입) ois.readObject();
  • 복수의 객체를 저장할 경우, 출력된 객체 순서와 동일한 순서로 객체를 읽어야 한다.

✅ 직렬화가 가능한 클래스(Serializable)

자바는 Serialzable 인터페이스를 구현한 클래스만 직렬화할 수 있도록 제한하고 있다. Serializable 인터페이스는 필드나 메소드가 없는 빈 인터페이스이지만, 객체를 직렬화할 때 private 필드를 포함한 모든 필드를 바이트로 변환해도 좋다는 표시 역할을 한다.

public class XXX implements Serializable { }

객체를 직렬화하면 바이트로 변환되는 것은 필드들이고, 생성자 및 메소드는 직렬화에 포함되지 않는다. 따라서 역직렬화할 때에는 필드의 값만 복원된다. 하지만 모든 필드가 직렬화 대상이 되는 것은 아니다. 필드 선언에 static 또는 transient가 붙어 있을 경우에는 직렬화가 되지 않는다.

public class XXX implements Serializable {
    public int field1;
    protected int field2;
    int field3;
    private int field4;
    public static int field5;	//직렬화에서 제외
    transient int field6;		//직렬화에서 제외
}

✅ serialVersionUID 필드

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

  • 클래스의 이름이 같더라도 클래스의 내용이 변경되면, 역직렬화는 실패하며 다음과 같은 예외가 발생한다.
java.io.InvalidException: XXX; local class incompatible: stream classdesc
serialVersionUID: -9130799490637378756, local class serialVersionUID = -1174725809595957294
  • 위 예외의 내용은 직렬화할 때와 역직렬화할 때 사용된 클래스의 serialVersionUDI가 다르다는 것이다.
  • serialVersionUID는 같은 클래스임을 알려주는 식별자 역할을 하는데, Serializable 인터페이스를 구현한 클래스를 컴파일하면 자동적으로 serialVersionUID 정적 필드가 추가된다.
    • 하지만 클래스를 재컴파일하면 serialVersionUID의 값이 달라지기 때문에 문제가 발생한다.
    • 네트워크로 객체를 직렬화하여 전송하는 경우, 보내는 쪽과 받는 쪽이 모두 같은 serialVersionUID를 갖는 클래스를 가지고 있어야 하는데 한 쪽에서 클래스를 변경해서 재컴파일하면 다른 serialVersionUID를 가지게 되므로 역직렬화에 실패하게 된다.
  • 클래스에 serialVersionUID가 명시적으로 선언되어 있으면 컴파일 시에 serialVersionUID 필드가 추가되지 않기 때문에 동일한 serialVersionUID 값을 유지할 수 있다.
  • serialVersionUID의 값은 개발자가 임의로 줄 수 있지만 가능하다면 클래스마다 다른 값을 갖도록 하는 것이 좋다.

✅ writeObject()와 readObject() 메소드

두 클래스가 상속 관계에 있다고 가정해보자. 부모 클래스가 Serializable 인터페이스를 구현하고 있으면 자식 클래스는 Serializable 인터페이스를 구현하지 않아도 자식 객체를 직렬화하면 부모 필드 및 자식 필드가 모두 직렬화된다. 하지만 그 반대로 부모 클래스가 Serializable 인터페이스를 구현하고 있지 않고, 자식 클래스만 Serializable 인터페이스를 구현하고 있다면 자식 객체를 직렬화할 때 부모의 필드는 직렬화에서 제외된다. 이 경우 부모 클래스의 필드를 직렬화하고 싶다면 다음 두 가지 방법 중 하나를 선택해야 한다.

  1. 부모 클래스가 Serializable 인터페이스를 구현하도록 해야 한다.
  2. 자식 클래스에서 writeObject()readObject() 메소드를 선언해서 부모 객체의 필드를 직접 출력시킨다.

첫 번째 방법이 제일 좋은 방법이 되겠지만, 부모 클래스의 소스를 수정할 수 없는 경우에는 두 번째 방법을 사용해야 한다. writeObject() 메소드는 직렬화될 때 자동으로 호출되고, readObject() 메소드는 역직렬화될 때 자동적으로 호출된다.

✅ writeObject(), readObject() 메소드 선언 방법

private void writeObject(ObjectOutputStream out) throws IOException {
    out.writeXXX(부모필드);		//부모 객체의 필드값을 출력함
    ...
    out.defaultWriteObject();	//자식 객체의 필드값을 직렬화
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    부모필드 = in.readXXX();	//부모 객체의 필드값을 읽어옴
    ...
    in.defaultReadObject();	//자식 객체의 필드값을 역직렬화
}

두 메소드를 작성할 때 주의할 점은 접근 제한자가 private가 아니면 자동 호출되지 않기 때문에 반드시 private을 붙여주어야 한다. writeObject()readObject() 메소드의 매개값인 ObjectOutputStreamObjectInputStream은 다양한 종류의 writeXXX(), readXXX() 메소드를 제공하므로 부모 필드 타입에 맞는 것을 선택해서 사용하면 된다. defaultWriteObejct()defaultReadObject()는 자식 클래스에 정의된 필드들을 모두 직렬화하고 역직렬화한다.


[ 참고자료 ]

이것이 자바다 책

profile
🚧 https://coji.tistory.com/ 🏠

0개의 댓글