자바의정석 ch15

soso·2023년 3월 3일
0
post-thumbnail

Chapter15 입출력 I/O

입출력(I/O)과 스트림(stream)

입출력 I/O란 Input과 Output의 약자로 입력과 출력이다.
입출력은 컴퓨터 내부 또느 외부의 장치와 프로그램간의 데이터를 주고 받는것을 말한다.

키보드로 데이터를 입력받는것 또는 System.out.println()을 이용해 화면에 데이터를 출력하는것이 가장 기본적인 입출력의 예이다.

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

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

단방향통신만 가능하기 때문에 하나의 스트림으로 입력과 출력을 동시에 처리할 수 없다.
입력과 출력을 동시에 수행하려면 입력스트림(input stream)과 출력스트림(output stream) 2개의 스트림이 필요하다.

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

스트림은 바이트단위로 데이터를 전송하며 입출력 대상에 따라 다음과 같은 입출력스트림이 있다.
여러 종류의 입출력 스트림이 있으며, 어떠한 대상에 대해서 작업할것인지
입력, 출력을 할것인지에 따라 해당 스트림을 선택해서 사용하면 된다.

모두 InputStream 또는 OutputStream의 자손들이며,각각 읽고 쓰는데 필요한 추상메서드를 자신에 맞게 구현해 놓았다. InputStream의 read()와 OutputStream의 write(int b)는 입출력의 대상에 따라 읽고 쓰는 방법이 다르기 때문에 각 상황에 맞게 구현하라는 의미에서 추상메서드로 정의되어 있다.

보조 스트림

보조스트림은 실제 데이터를 주고받는 스트림이 아니기 때문에 데이터를 입출력할 수 있는 기능은 없지만, 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있다.
보조 스트림만으로는 입출력을 처리할 수 없고, 스트림을 먼저 생성한 다음에 이를 이용해 보조 스트림을 생성해야 한다.

문자기반 스트림 -Reader, Writer

Java에서는 한 문자를 의미하는 char형이 1byte가 아니라 2byte이기 때문에 바이트 기반의 스트림으로 2byte인 문자를 처리하는데 어려움이 있다.
이점을 보안라힉 위해 문자기반의 스트림이 제공된다.

InputStream → Reader
OutputStream → Writer

문자기반 스트림의 이름은 바이트기반 스트림의 이름에서
InputStream은 Reader로 OutputStream은 Writer로 바꾸면 된다.
단, ByteArrayInputStream에 대응하는 문자기반 스트림은 char배열을 시용하는 CharArrayReader이다.

바이트 기반 스트림과 문자 기반 스트림의 비교

아래의 표는 바이트기반 스트림과 문자 기반 스트림의 읽기와 쓰기에 사용되는 메서드를 비교한 것이다.
byte배열 대신 char배열을 사용한다는 것과 추상메서드가 달라졌다.

Reader와 Writer에서도 추상메서드가 아닌 메서드들은 추상메서드를 이용해서 작성되었으며, 프로그래밍적인 관점에서 볼 때 read()를 추상메서드로 하는 것보다 int read(charl] cbuf, int off, int len)를 추상메서드로 하는 것이 더 바람직하다.

바이트기반 스트림과 문자기반 스트림은 활용방법이 거의 같다.

InputStream과 OutputStream


스트림의 종류에 따라서 mark()와 reset()을 사용하여 이미 읽은 데이터를 되돌려서 다시 읽을 수 있다. 이 기능을 지원하는 스트림인지 확인하는 markSuppoprted()를 통해서 알 수 있다.

flush()는 버퍼가 있는 출력스트림의 경우에만 의미가 있으며, OutputStream에 정의된 flush()는 아무런 일도 하지 않는다.

프로그램이 종료될 때, 사용하고 닫지 않은 스트림을 JVM이 자동적으로 닫아 주기는 하지만, 스트림을 사용해서 모든 작업을 마치고 난 후에는 close()를 호출해서 반드시 닫아 주어 야 한다.

그러나 ByteArrayInputStream과 같이 메모리를 사용하는 스트림과 System.in, System.out과 같은 표준 입출력 스트림은 닫아 주지 않아도 된다.

FileInputStream과 FileOutputStream

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

FilterInputStream과 FilterOutputStream

FilterInputStream/ FilterOutputStream은 InputStream/ OutputStream의 자손이면서 모든 보조 스트림의 조상이다.

protected : FilterInputStream(InputStream in)
public : FilterOutputStream(OutputStream out)

FilterInputStream/ FilterOutputStream은 모든 메서드는 단순히 기반 스트림의 메서드를 그대로 호출할뿐, 아무런 일도 하지 않음을 의미한다.
FilterInputStream/ FilterOutputStream은 상속을 통해 원하는 작업을 수행하도록 읽고 쓰는 메서드를 오버라이딩 해야한다.

FilterInputStream/ FilterOutputStream을 상속받아서 기반스트림에 보조기능을 추가한 보조 스트림 클래스는 다음과 같다.

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

BufferedInputStream

BufferedInputStream/ BufferedOutputStream은 스트림의 입출력 효율을 높이기 위해 버퍼를 사용하는 보조 스트림이다.
한 바이트씩 입출력하는 것 보다 버퍼(바이트배열)를 이용해서 한 번에 여러 바이트를 입출력하는 것이 빠르기 때문에 대부분 입출력 잡업에 사용된다. 프로그램에서 입력소스로부터 데이터를 읽기 위해 처음으로 read메서드를 호출하면, BufferedInputStream은 입력소스로부터 버퍼 크기만큼의 데이터를 읽어다 자신 내부 버퍼에 저장한다.
외부 입력소스로부터 읽는것보다 내부의 버퍼로부터 읽는 것이 훨신 빠르기 때문에 작업 효율이 높아진다.

BufferedOutputStream

BufferedOutputStream 역시 버퍼를 이용해서 출력소스와 작업을 하게 되는데, 입력소스 로부터 데이터를 읽을 때와는 반대로, 프로그램에서 write메서드를 이용한 출력이 Buffered OutputStream의 버퍼에 저장된다.

버퍼가 가득 차면, 그 때 버퍼의 모든 내용을 출력소스에 출력한다.
그리고는 버퍼를 비우고 다시 프로그램으로부터의 출력을 저장할 준비를 한다.

버퍼가 가득 찼을 때만 출력소스에 출력을 하기 때문에, 마지막 출력부분이 출력소스에 쓰이지 못하고 BufferedOutputStream의 버퍼에 남아있는 채로 프로그램이 종료될 수 있다는 점을 주의해야한다.

그래서 프로그램에서 모든 출력작업을 마친 후 BufferedOutputstream에 close()flush()를 호출해서 마지막에 버퍼에 있는 모든 내용이 출력소스에 출력되도록 해야 한다.

SequenceInputStream

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

PrintStream

데이터를 기반스트림에 다양한 형태로 출력할 수 있는 print, pritln, printf와 같은 메서드를 오버라이딩하여 제공하고 문자기반 스트림의 역활을 수행한다.

PrintStream과 PrintWriter는 거의 같은 기능을 가지고 있지만 PrintWriter가 PrintStream에 비해 다양한 언어의 문자를 처리하는데 적합하다.

문자 기반 스트림 -Reader

Reader/ Writer의 메서드인데 byte배열 대신 char배열을 사용한다는 것 외에는 InputStream/ OutputStream의 메서드와 다르지않다.

문자 기반 스트림 -Writer

문자기반 스트림이 단순히 2byte로 스트림을 처리하는 것을 의미하지 않는다.
문자 데이터를 다루는데 필요한 또 하나의 정보는 인코딩(encoding)이다.

문자기반 스트림, Reader/ Writer 그리고 그 자손들은 여러 종류의 인코딩과 자바에서 사용하는 유니코드(UTF-16)간의 변환을 자동적으로 처리해준다.
Reader는 특정인코딩을 읽어서 유니코드로 변환하고 Writer는 유니코드를 특정 인코딩으로 변환하여 저장한다.

StringReader와 StringWriter

StringReader/ StringWriter는 CharArrayReader/ CharArrayWriter와 같이 입출력 대산이 메모리인 스트림이다.

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

StringBuffer getBuffer() StringWriter에 출력한 데이터가 저장된 StringBuffer를 반환한다.
String toString StringWriter에 출력된(StringBuffer에 저장된) 문자열을 반환한다.

BufferedReader와 BufferedWriter

BufferedReader/ BufferedWriter는 버퍼를 이용해서 입출력의 효율을 높일수 있도록 해주는 역활을 한다.
BufferedReader의 readLine()을 사용하면 데이터를 라인단위로 읽을 수 있고 BufferedWriter는 newLine()이라는 줄바꿈 해주는 메서드를 가지고 있다.

InputStreamReader, OutputStreamWriter

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

표준 입출력(Standard I/O)

표준입력은 콘솔(console, 도스창)을 통한 데이터 입력과 콘솔로의 데이터 출력을 의미 한다.
자바에서는 표준 입출력(standard I/O)을 위해 3가지 입출력 스트림, System.in, System.out, System.err을 제공하는데, 이 들은 자바 어플리케이션의 실행과 동시에 사용할 수 있게 자동적으로 생성되기 때문에 개발자가 별도로 스트림을 생성하는 코드를 작성하지 않고도 사용이 가능하다.

System.in 콘솔로 데이터를 입력받는데 사용(표준 출력)
System.out 콘솔로 데이터를 출력하는데 사용(표준 입력)
System.err 콘솔로 데이터를 출력하는데 사용(표준 입력)

System클래스의 소스에서 알 수 있듯이 in, out, err은 System 클래스에 선언된 클래스변수(static변수)이다.
선언부분만을 봐서는 out, err, in의 타입은 InputStream과 PrintStream이지만 실제로는 버퍼를 이용하는 BufferedInputStream과 BufferedOutputStream의 인스턴스를 사용한다.

public final class System {
  public final static InputStream in = nullInputStream();
  public final static PrintStream out = nullPrintStream();
  public final static PrintStream err = nullPrintStream();
  ...
}  

표준 입출력의 대상변경

초기에는 System.in, System.out, System.err의 입출력대상이 콘솔 화면이지만, setIn(), setOut(), setErr()를 사용하면 입출력을 콘솔 이외에 다른 입출력 대상으로 변경하는 것이 가능하다.

File

파일은 기본적이면서도 가장 많이 사용되는 입출력 대상이기 때문에 중요하다. 자바에서는 File클래스를 통해서 파일과 디렉토리를 다룰 수 있도록 하고 있다.
그래서 File 인스턴스는 파일일 수도 있고 디렉토리일 수도 있다.

직렬화(serialization)

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

객체는 클래스에 정의된 인스턴스변수의 집합이다.
객체에는 클래스변수나 메서드가 포함 되지 않고 오직 인스턴스변수들로만 구성되어 있다.
그래서 객체를 저장한다는 것은 객체의 모든 인스턴스변수의 값을 지장한다는 것과 같은 의미이다.
어떤 객체를 저장하고자 한다면, 현재 객체의 모든 인스턴스변수의 값을 저장하기만 하면 된다.
그리고 저장했던 객체를 다시 생성하려면, 객체를 생성한 후에 저장했던 값을 읽어서 생성한 객체의 인스턴스변수에 저장하면 된다.

ObjectInputStream, ObjectOutputStream

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

ObjectInputStream과 ObjectOutputStream은 각각InputStream과 OutputStream을 직접 상속받지만 기반스트림을 필요로 하는 보조스트림이다.
그래서 객체를 생성할 때 입출력(직렬화/역직렬화)할 스트림을 지정해주어야 한다.

ObjectInputStream(InputStream in)
ObjectoutputStream(OutputStream out)

만일 파일에 객체를 저장(직렬화)하고 싶다면 다음과 같이 하며 된다.

FileOutputStream fos = new FileOutputStream("objectfile.ser");
ObjectOutputStream out = new ObjectOutputStream(fos);

out.writeObject(new UserInfo());

역직렬화 방법은 직렬화할 때와는 달리 입력스트림을 사용하고 write Obiect(Obiect obj) 대신 readObiect()를 사용하여 저장된 데이터를 읽기만 하면 객체로 역직렬화된다.
다만 readObject()의 반환타입이 Object이기 때문에 객체 원래의 타입으로 형변환 해주어야 한다.

FileInputStream fis = new FileInputStream("objectfile.ser");
ObjectInputstream in = new ObjectInputstream(fis);
UserInfo info = (UserInfo)in.readobject();

ObjectinputStream과 ObjectOutputStream에는 readObject()와 writeObject() 이외에도 여러 가지 타입의 값을 입출력할 수 있는 메서드를 제공한다.
이 메서드들은 직렬화와 역직렬화를 직접 구현할 때 주로 사용되며, defaultReadObject()와 defaultWriteObject()는 자동 직렬화를 수행한다.
객체를 직렬화/역직렬화하는 작업은 객체의 모든 인스턴스변수가 참조하고 있는 모든 객체에 대한 것이기 때문에 상당히 복잡하며 시간도 오래 걸린다.
readObject()와 writeObject()를 사용한 자동 직렬화가 편리하기는 하지만 직렬화작업시간을 단축시키려면 직렬화하고자 하는 객체의 클래스에 추가적으로 다음과 같은 2개의 메서드를 직접 구현해주어야 한다.

private void writeObject(ObjectOutputStream out)
	throws IOException {
	//write메서드를 사용해서 직렬화를 수행한다.
}
private void readObject(ObjectInputStream in)
	throws IOException, ClassNotFoundException {
	// read메서드를 사용해서 역직렬화를 수행한다.
}

직렬화 대상에서 제외시키기 -transient

직렬화하고자 하는 객체의 클래스에 직렬화가 안 되는 객체에 대한 참조를 포함하고 있다면, 제어자 transient를 붙여서 직렬화 대상에서 제외되도록 할 수 있다.
또는 password와 같이 보안상 직렬화되면 안되는 값에 대해서 transient를 사용할 수 있다.
다르게 표현하면 transient가 붙은 인스턴스변수의 값은 그 타입의 기본값으로 직렬화된 다고 볼 수 있다.
즉, UserInfo객체를 역직렬화하면 참조변수인 obj와 password의 값은 null이 된다.

public class UserInfo implements Serializable {
  String name;
  transient String password;	//직렬화 대상에서 제외된다.
  int age;
  transient object obj = new Object();	//직렬화 대상에서 제외된다.
}
profile
오늘의 기록

0개의 댓글