Chapter 18 - 데이터 입출력

김태원·2023년 1월 31일
0

입출력 스트림

데이터는 키보드를 통해 입력될 수도 있고, 파일 또는 프로그램으로부터 입력될 수도 있다.
반대로 데이터는 모니터로 출력될 수도 있고, 파일에 저장되거나 다른 프로그램으로 전송될 수 있다.
이것을 총칭해서 데이터 입출력이라고 한다.

자바는 입력 스트림과 출력 스트림을 통해 데이터를 입출력한다. 스트림(Stream)은 단방향으로 데이터가 흐르는 것을 말하는데, 다음 그림과 같이 데이터는 출발지에서 나와 도착지로 흘러들어간다.

프로그램을 기준으로 데이터가 들어오면 입력 스트림, 데이터가 나가면 출력 스트림이 된다.
프로그램이 다른 프로그램과 데이터를 교환하려면 양쪽 모두 입력 스트림과 출력 스트림이 필요하다.

어떤 데이터를 입출력하느냐에 따라 스트림은 두 종류로 구분할 수 있다.

  • 바이트 스트림
    그림, 멀티미디어, 문자 등 모든 종류의 데이터를 입출력할 때 사용

  • 문자 스트림
    문자만 입출력할 때 사용

자바는 데이터 입출력과 관련된 라이브러리를 java.io 패키지에서 제공하고 있다. java.io 패키지는 바이트 스트림과 문자 스트림을 다음과 같이 이름으로 구분해서 제공한다.

바이트 입출력 스트림의 최상위 클래스는 InputStream과 OutputStream이다.
이 클래스를 상속받는 자식 클래스에는 접미사로 InputStream 또는 OutputStream이 붙는다.

바이트 출력 스트림

OutputStream은 바이트 출력 스트림의 최상위 클래스로 추상 클래스이다. 모든 바이트 출력 스트림 클래스는 이 OutputStream 클래스를 상속 받아서 만들어진다.

OutputStream 클래스에는 모든 바이트 출력 스트림이 기본적으로 가져야 할 메소드가 정의되어 있다.
다음은 OutputStream 클래스의 주요 메소드이다.

1 바이트 출력

write(int b) 메소드는 매개값 int(4byte) 에서 끝 1byte만 출력한다.
매개변수가 int 타입으므로 4byte 모두를 보내는 것은 아니다.

바이트 배열 출력

일반적으로 1바이트를 출력하는 경우는 드물고, 보통 바이트 배열을 통째로 출력하는 경우가 많다.
write(byte[] b) 메소드는 매개값으로 주어진 배열의 모든 바이트를 출력한다.

바이트 입력 스트림

InputStream은 바이트 입력 스트림의 최상위 클래스로, 추상 클래스이다. 모든 바이트 입력 스트림은 InputStream 클래스를 상속받아 만들어진다.


InputStream 클래스에는 바이트 입력 스트림이 기본적으로 가져야 할 메소드가 정의되어 있다.
다음은 InputStream 클래스의 주요 메소드이다.

1바이트 읽기

read() 메소드는 입력 스트림으로부터 1byte를 읽고 int(4byte) 타입으로 리턴한다. 따라서 리턴된 4byte 중 끝 1bye에만 데이터가 들어 있다. 예를 들어 입력 스트림에서 5개의 바이트가 들어온다면 다음과 같이 read() 메소드로 1byte씩 5번 읽을 수 있다.

더 이상 입력 스트림으로부터 바이트를 읽을 수 없다면 read()메소드는 -1을 리턴하는데, 이것을 이용하면 읽을 수 있는 마지막 바이트까지 반복해서 한 바이트씩 읽을 수 있다.

InputStream is = ...;

while (true) {
	int data = is.read(); // 1 바이트를 읽고 리턴
    if (data == -1) break; // -1을 리턴했을 경우 while 문 종료 
}

바이트 배열로 읽기

read(byte[] b) 메소드는 입력 스트림으로부터 주어진 배열의 길이만큼 바이트를 읽고 배열에 저장한 다음 읽은 바이트 수를 리턴한다. 예를들어 입력 스트림에 5개의 바이트가 들어오면 다음과 같이 길이 3인 배열로 두 번 읽을 수 있다.

read(byte[] b) 역시 입력 스트림으로부터 바이트를 더 이상 읽을 수 없다면 -1을 리턴하는데, 이것을 이용하면 읽을 수 있는 마지막 바이트까지 반복해서 읽을 수 있다.

InputStream is = ...;
byte[] data = new byte[100];

while (true) {
	int num = is.read(data); //최대 100byte를 읽고, 읽은 바이트는 배열 data 저장, 읽은 수는 리턴
    if (num == -1) break; //-1을 리턴하면 while 문 종료
}

문자 입출력 스트림

바이트 입출력 스트림인 InputStream과 OutputStream에 대응하는 문자 입출력 스트림으로 Reader와 Writer가 있다. 입출력되는 단위가 문자인 것을 제외하고는 바이트 입출력 스트림과 사용 방법은 동일하다.

문자 출력

Writer는 문자 출력 스트림의 최상위 클래스로, 추상 클래스이다. 모든 문자 출력 스트림 클래스는 Writer 클래스를 상속받아서 만들어진다.

Writer 클래스는 모든 문자 출력 스트림이 기본적으로 가져야 할 메소드가 정의되어 있다.
Writer 클래스의 주요 메소드는 다음과 같다.

Write는 OutPutStream과 사용 방법은 동일하지만, 출력 단위가 문자(char)이다.
그리고 문자열을 출력하는 write(String str) 메소드를 추가로 제공한다.

문자 읽기

Reader는 문자 입력 스트림의 최상위 클래스로, 추상 클래스이다. 모든 문자 입력 스트림 클래스는 Reader 클래스를 상속받아서 만들어진다.

Reader 클래스에는 문자 입력 스트림이 기본적으로 가져야 할 메소드가 정의되어 있다.
다음은 Reader 클래스의 주요 메소드이다.

Reader는 InputStream과 사용 방법은 동일하지만, 출력 단위가 문자(char)이다.

보조 스트림

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

보조 스트림은 자체적으로 입출력을 수행할 수 없기 때문에 입출력 소스로부터 직접 생성된 입출력 스트림에 연결해서 사용해야 한다.

입출력 스트림에 보조 스트림을 연결하려면 보조 스트림을 생성할 때 생성자 매개값으로 입출력 스트림을 제공하면 된다.

보조스트림 변수 = new 보조스트림(입출력스트림);

예를 들어 바이트 입력 스트림인 FileInputStream에 InputStreamReader 보조 스트림을 연결하는 코드는 다음과 같다.

InputStream is = new FileInputStream("...");
InputStreamReader reader = new InputStreamReader(is);

보조 스트림은 또 다른 보조 스트림과 연결되어 스트림 체인으로 구성할 수 있다.

예를 들어 문자 변환 보조 스트림인 InputStreamReader에 BufferedReader 보조 스트림을 연결하는 코드는 다음과 같다.

InputStream is = new FileInputStream("...");
InputStreamReader reader = new InputStreamReader(is);
BufferedReader br = new BufferReader(reader);

자주 사용되는 보조 스트림은 다음과 같다.

문자 변환 스트림

바이트 스트림(InputStream, OutputStream)에서 입출력할 데이터가 문자라면 문자 스트림(Reader, Writer)으로 변환해서 사용하는 것이 좋다. 그 이유는 문자로 바로 입출력하는 편리함이 있고, 문자셋의 종류를 지정할 수 있기 때문이다.

InputStream을 Reader로 변환

InputStream을 Reader로 변환하려면 InputStreamReader 보조 스트림을 연결하면 된다.

다음은 InputStream 을 Reader로 변환하는 예제이다.

InputStream is = new FileInputStream("C:/Temp/test.txt");
Reader reader = new InputStreamReader(is);

FileReader의 원리

FileInputStream에 InputStreamReader를 연결하지 않고 FileReader를 직접 생성할 수 있다. FileReader는 InputStreamReader의 자식 클래스이다. 이것은 FileReader가 내부적으로 FileInputStream에 InputStreamReader 보조 스트림을 연결한 것이라고 볼 수 있다.

OutputStream을 Writer로 변환

OutputStream을 Writer로 변환하려면 OutputStreamReader 보조 스트림을 연결하면 된다.

다음은 OutputStream을 Writer로 변환하는 예제이다.

OutputStream os = new FileOutputStream("C:/Temp/test.txt");
Writer writer = new OutputStreamWriter(os);

FileWriter의 원리

FIleOutputStream에 OutputStreamWriter를 연결하지 않고 FileWriter를 직접 생성할 수 있다. FileWriter는 OutputStreamWriter의 자식 클래스이다. 이것은 FileWriter가 내부적으로 FileOutputStream에 OutputStreamWriter 보조 스트림을 연결한 것이라고 볼 수 있다.

성능 향상 스트림

프로그램이 입출력 소스와 직접 작업하지 않고 중간에 메모리 버퍼(Buffer)와 작업함으로써 실행 성능을 향상시킬 수 있다.

출력 스트림과 입력 스트림은 하드 디스크가 아닌 메모리 버퍼에 데이터를 보내고, 받음으로써 입출력 속도를 향상시킬 수 있다.

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

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

BufferedInputStream bis = new BufferedInputStream(바이트 입력 스트림);
BufferedOutputStream bos = new BufferedOutputStream(바이트 출력 스트림);
BufferedReader br = new BufferedReader(문자 입력 스트림);
BufferedWriter bw = new BufferedWriter(문자 출력 스트림);

기본 타입 스트림

바이트 스트림에 DataInputStream과 DataOutputStream 보조 스트림을 연결하면 기본 타입인 boolean, char, short, int, long, float, double 값을 입출력할 수 있다.


다음과 같이 사용한다.

DataInputStream dis = new DataInputStream(바이트 입력 스트림);
DataOutputStream dos = new DataOutputStream(바이트 출력 스트림);

다음은 DataInputStream과 DataOutputStream이 제공하는 메소드이다.

데이터 타입의 크기가 모두 다르므로 DataOutputStream으로 출력한 데이터를 DataInputStream으로 읽어 올 때에는 출력한 순서와 동일한 순서로 읽어야 한다는 점에 주의하자

프린트 스트림

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

지금까지 우리는 콘솔에 출력하기 위해 System.out.println()을 사용하였는데, 그 이유는 out이 PrintStream 타입이기 때문이다.

PrintStream은 바이트 출력 스트림과 연결되고, PrintWriter는 문자 출력 스트림과 연결된다.

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

PrintStream과 PrintWriter는 거의 같은 메소드를 가지고 있다.
println() 메소드는 출력할 데이터 끝에 줄바꿈 문자인 \n을 더 추가시키기 때문에 콘솔이다 파일에 줄바꿈이 일어난다.
그러나 print() 메소드는 줄바꿈 없이 계속해서 문자를 출력시킨다.
println()과 print() 메소드는 출력할 데이터 타입에 따라 다음과 같이 재정의된다.

객체 스트림

자바는 메모리에 생성된 객체를 파일 또는 네트워크로 출력할 수 있다. 객체를 출력하려면 필드값을 일렬로 늘어선 바이트로 변경해야 하는데, 이것을 직렬화(Serialization)라고 한다. 반대로 직렬화된 바이트를 객체의 필드값으로 복원하는 것을 역직렬화(Deserialization)라고 한다.

ObjectInputStream과 ObjectOutputStream
은 객체를 입출력할 수 있는 보조 스트림이다. ObjectOutputStream은 바이트 출력 스트림과 연결되어 객체를 직렬화하고, ObjectInputStream은 바이트 입력 스트림과 연결되어 객체로 복원하는 역직렬화를 한다.

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

ObjectOutputStream으로 객체를 직렬화하기 위해서는 writeObject() 메소드를 사용한다.

oos.writeObject(객체);

반대로 ObjectInputStream 의 readObject()메소드는 읽은 바이트를 역직렬화해서 객체로 생성한다. readObject() 메소드의 리턴타입은 Object이므로 구체적인 타입으로 강제 타입 변환해야 한다.

객체타입 변수 = (객체타입) ois.readObject();

Serializable 인터페이스

자바는 Serializable 인터페이스를 구현한 클래스만 직렬화할 수 있도록 제한한다.
Serializable 인터페이스는 멤버가 없는 빈 인터페이스이지만, 객체를 직렬화할 수 있다고 표시하는 역할을 한다.
객체가 직렬화될 때 인스턴스 필드값은 직렬화 대상이지만 정적 필드값과 transient로 선언된 필드값은 직렬화에서 제외되므로 출력되지 않는다.

SerialVersionUID 필드

직렬화할 때 사용된 클래스와 역직렬화할 때 사용된 클래스는 기본적으로 동일한 클래스여야 한다.
만약 클래스의 이름이 같더라도 클래스의 내용이 다르면 역직렬화에 실패한다.

클래스 내용이 다르다 할지라고 직렬화된 필드를 공통으로 포함하고 있다면 역직렬화할 수 있는 방법이 있다.
두 클래스가 동일한 SerialVersionUID 상수값을 가지고 있으면 된다.

SerialVersionUID의 값은 개발자가 임의로 줄 수있지만 가능한 클래스마다 다른 유일한 값을 갖도록 하는 것이 좋다.

File과 Files 클래스

java.io 패키지와 java.nio.file 패키지는 파일과 디렉터리 정보를 가지고 있는 File과 Files 클래스를 제공한다. Files는 File을 개선한 클래스로, 좀 더 많은 기능을 가지고 있다.

File 클래스

File 클래스로부터 File 객체를 생성하는 방법은 다음과 같다.

File file = new File("경로");

경로 구분자는 운영체제마다 조금씩 다르다.
윈도우에서는 \\ 또는 / 둘다 사용할 수 있고 맥OS 및 리눅스에서는 /를 사용한다.

File 객체를 생성했다고 해서 파일이나 디렉토리가 생성되는 것은 아니다.
그리고 경로에 실제 파일이나 디렉토리가 없더라도 예외가 발생하지 않는다.
파일이나 디렉토리가 실제로 있는지 확인하고 싶다면 File객체를 생성하고 나서 exists() 메소드를 호출해보면 된다.

boolean isExist = file.exists(); // 파일이나 폴더가 존재한다면 true를 리턴

exists() 메소드가 false를 리턴할 경우, 다음 메소드로 파일 또는 폴더를 생성할 수 있다.

exists() 메소드 리턴값이 true 라면 다음 메소드를 사용할 수 있다.

Files 클래스

Files 클래스는 정적 메소드로 구성되어 있기 때문에 File 클래스처럼 객체로 만들 필요가 없다.
Files의 정적 메소드는 운영체제의 파일 시스템에게 파일 작업을 수행하도록 위임한다.

다음은 Files 클래스가 가지고 있는 정적 메소드를 기능별로 정리한 표이다.

이 메소드들은 매개값으로 Path 객체를 받는다. Path 객체는 파일이나 디렉토리를 찾기 위한 경로 정보를 가지고 있는데, 정적 메소드인 get() 메소드로 다음과 같이 얻을 수 있다.

Path path = Paths.get(String first, String ... more)

get() 메소드의 매개값은 파일 경로인데, 전체 경로를 한꺼번에 지정해도 좋고 상위 디렉토리와 하위 디렉토리를 나열해서 지정해도 좋다.

profile
개발이 재밌어서 하는 Junior Backend Developer

0개의 댓글