Java Category/Java

[Java] 보조 스트림(문자 변환, 성능 향상, 기본 타입, 프린트, 객체)

ReBugs 2023. 8. 9.

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


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

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

출처 : 이것이 자바다 유튜브 동영상 강의

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

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

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

 

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

출처 : 이것이 자바다 유튜브 동영상 강의

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

 

자주 사용되는 보조 스트림은 아래와 같다.

출처 : 이것이 자바다 유튜브 동영상 강의

보조 스트림을 닫으면 자동적으로 주 스트림도 닫힌다.

 

문자 변환 스트림

바이트 스트림(InputStream, OutputStream)에서 입출력할 데이터가 문자라면 문자 스트림(Reader, Writer)로 변환해서 사용하는 것이 좋다.

문자로 바로 입출력하는 편리함이 있고, 문자셋의 종류를 지정할 수 있기 때문이다.

 

InputStream을 Reader로 변환

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

출처 : 이것이 자바다 유튜브 동영상 강의

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

 

아래는 InputStream을 Reader로 변환하는 코드를 나타낸다.

InputStream is = new FileInputStream("C:/Temp/test.txt");
Reader reader = new InputStreamReader(is);
보통 성능 향상 스트림인 BufferedReader을 추가적으로 장착하여 더 편리하게 문자열을 읽는다.

 

OutputStream을 Writer로 변환

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

출처 : 이것이 자바다 유튜브 동영상 강의

OutputStream os = new FileOutputStream("C:/Temp/test.txt");
Writer writer = new OutputStreamWriter(os);
FileWriter의 원리
FileOutputStream에 OutputStreamWriter를 연결하지 않고 FileWriter를 직접 생성할 수 있다.
FileWriter는 OutputStreamWriter의 자식 클래스이다.
이것은 FileWriter가 내부적으로 FileOutputStream에 OutputStreamWriter 보조 스트림을 연결한 것이라고 볼 수 있다.

 


 

사용 예제

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;

public class CharacterConvertStreamExample {
	public static void main(String[] args) throws Exception {
		write("문자 변환 스트림을 사용합니다.");
		String data = read();
		System.out.println(data);
	}

	public static void write(String str) throws Exception {
		OutputStream os = new FileOutputStream("C:/Temp/test.txt");
		Writer writer = new OutputStreamWriter(os, "UTF-8"); //인코딩 문자셋 지정
		writer.write(str);
		writer.flush();
		writer.close();
	}
	
	public static String read() throws Exception {
		InputStream is = new FileInputStream("C:/Temp/test.txt");
		Reader reader = new InputStreamReader(is, "UTF-8"); //디코딩 문자셋 지정
		char[] data = new char[100];
		int num = reader.read(data);
		reader.close();
		String str = new String(data, 0, num);
		return str;
	}
}
/*
문자 변환 스트림을 사용합니다.
*/

정상적으로 파일이 작성된 것을 볼 수 있다.

 


 

성능 향상 스트림

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

 

출력 스트림의 경우 직접 하드 디스크에 데이터를 보내지 않고 메모리 버퍼에 데이터를 보냄으로써 출력 속도를 향상 시킬 수 있다.

버퍼는 데이터가 쌓이기를 기다렸다가 꽉 차게 되면 데이터를 한꺼번에 하드 디스크로 보냄으로써 출력 횟수를 줄여준다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

입력 스트림에서도 버퍼를 사용하면 읽기 성능이 좋아진다.

하드 디스크로부터 직접 읽는 것보다는 메모리 버퍼로부터 읽는 것이 빠르다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

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

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

 

BufferedRead
버퍼의 사이즈가 Scanner가 1024 char인데 비해, BufferedReader는 8192  char이기 때문에 입력이 많을 때 BufferedReader가 유리하다.
또한 BufferedReader는 동기화 되기 때문에 멀티 쓰레드 환경에서 안전하고, Scanner는 동기화가 되지 않기 때문에 멀티 쓰레드 환경에서 안전하지 않다.

 

아래의 더보기를 누르면 성능 향상 보조 스트림을 사용하지 않았을 때와 했을 때의 파일 복사 성능 차이 예제를 볼 수 있다.

더보기

BufferExample.java

public class BufferExample {
    public static void main(String[] args) throws Exception {
        //입출력 스트림 생성
        String originalFilePath1 = BufferExample.class.getResource("originalFile1.jpg").getPath();
        String targetFilePath1 = "C:/Temp/targetFile1.jpg";
        FileInputStream fis = new FileInputStream(originalFilePath1);
        FileOutputStream fos = new FileOutputStream(targetFilePath1);

        //입출력 스트림 + 버퍼 스트림 생성
        String originalFilePath2 = BufferExample.class.getResource("originalFile2.jpg").getPath();
        String targetFilePath2 = "C:/Temp/targetFile2.jpg";
        FileInputStream fis2 = new FileInputStream(originalFilePath2);
        FileOutputStream fos2 = new FileOutputStream(targetFilePath2);
        BufferedInputStream bis = new BufferedInputStream(fis2);
        BufferedOutputStream bos = new BufferedOutputStream(fos2);

        //버퍼를 사용하지 않고 복사
        long nonBufferTime = copy(fis, fos);
        System.out.println("버퍼 미사용:\t" + nonBufferTime + " ns");

        //버퍼를 사용하고 복사
        long bufferTime = copy(bis, bos);
        System.out.println("버퍼 사용:\t" + bufferTime + " ns");

        fis.close();
        fos.close();
        bis.close();
        bos.close();
    }

    public static long copy(InputStream is, OutputStream os) throws Exception {
        //시작 시간 저장
        long start = System.nanoTime();
        //1 바이트를 읽고 1 바이트를 출력
        while(true) {
            int data = is.read();
            if(data == -1) break;
            os.write(data);
        }
        os.flush();
        //끝 시간 저장
        long end = System.nanoTime();
        //복사 시간 리턴
        return (end-start);
    }
/*
버퍼 미사용:	102628400 ns
버퍼 사용:	1826600 ns
*/
}

 

문자 입력 스트림 Reader에 BufferedReader를 연결하면 성능 향상뿐만 아니라 행 단위로 문자열을 읽는 매우 편리한 readLine() 메소드를 제공한다.

import java.io.*;

public class ReadLineExample {
	public static void main(String[] args) throws Exception {
		BufferedReader br = new BufferedReader(
			new FileReader("src/ch18/sec07/exam02/ReadLineExample.java")
		);
		
		int lineNo = 1;
		while(true) {
			String str = br.readLine();
			if(str == null) break;
			System.out.println(lineNo + "\t" + str);
			lineNo++;
		}
		
		br.close();
	}
}
//출력 내용
/*
1	package ch18.sec07.exam02;
2	
3	import java.io.*;
4	
5	public class ReadLineExample {
6		public static void main(String[] args) throws Exception {
7			BufferedReader br = new BufferedReader(
8				new FileReader("src/ch18/sec07/exam02/ReadLineExample.java")
9			);
10			
11			int lineNo = 1;
12			while(true) {
13				String str = br.readLine();
14				if(str == null) break;
15				System.out.println(lineNo + "\t" + str);
16				lineNo++;
17			}
18			
19			br.close();
20		}
21	}
*/
BufferedReader는 파일을 다 읽었다면 null을 리턴한다.

 


 

기본 타입 스트림

바이트 스트림에 DataInputStream과 DataOutputStream 보조 스트림을 연결하면 기본 타입 값을 입출력할 수 있다.

출처 : 이것이 자바다 유튜브 동영상 강의

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

 

아래는 DataInputStream과 DataOutputStream이 제공하는 메소드이다.

출처 : 이것이 자바다 유튜브 동영상 강의

주의 사항
데이터 타입의 크기가 모두 다르므로 DataOutputStream으로 출력한 데이터를 다시 DataInputStream으로 읽어 올 때에는 출력한 순서와 동일한 순서로 읽어야 한다.
출력할 때 순서가 int -> boolean -> double이라면 읽을 때의 순서도 동일해야 한다.

 

아래의 더보기를 누르면 예제를 볼 수 있다.

더보기

DataInputOutputStreamExample.java

import java.io.*;
	
public class DataInputOutputStreamExample {
	public static void main(String[] args) throws Exception {
		//DataOutputStream 생성
		FileOutputStream fos = new FileOutputStream("C:/Temp/primitive.db");
		DataOutputStream dos = new DataOutputStream(fos);

		//기본 타입 출력
		dos.writeUTF("홍길동");
		dos.writeDouble(95.5);
		dos.writeInt(1);

		dos.writeUTF("감자바");
		dos.writeDouble(90.3);
		dos.writeInt(2);
		
		dos.flush(); dos.close(); fos.close();
		
		//DataInputStream 생성
		FileInputStream fis = new FileInputStream("C:/Temp/primitive.db");
		DataInputStream dis = new DataInputStream(fis);
		
		//기본 타입 입력
		for(int i=0; i<2; i++) {
			String name = dis.readUTF();
			double score = dis.readDouble();
			int order = dis.readInt();
			System.out.println(name + " : " + score + " : " + order);
		}

		dis.close(); fis.close();
	}
}
/*
홍길동 : 95.5 : 1
감자바 : 90.3 : 2
*/

 


 

프린트 스트림

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

콘솔에 출력하기 위해 System.out.println()를 사용하는데, 그 이유는 out이 PrintStream 타입 이기 때문이다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

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

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

 

PrintStream과 PrintWriter는 거의 같은 메소드를 가지고 있다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

아래의 예제는 FileOutputStream에 보조로 PrintStream을 연결해서 문자열을 출력하는 예제이다.

import java.io.FileOutputStream;
import java.io.PrintStream;

public class PrintStreamExample {
	public static void main(String[] args) throws Exception {
		FileOutputStream fos = new FileOutputStream("C:/Temp/printstream.txt");
		PrintStream ps = new PrintStream(fos);
		
		ps.print("마치 ");
		ps.println("프린터가 출력하는 것처럼 ");
		ps.println("데이터를 출력합니다.");
		ps.printf("| %6d | %-10s | %10s | \n", 1, "홍길동", "도적");
		ps.printf("| %6d | %-10s | %10s | \n", 2, "감자바", "학생");
		
		ps.flush();
		ps.close();
	}
}

정상적으로 출력된 것을 볼 수 있다.

 


 

객체 스트림

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

객체를 출력하려면 필드값을 일렬로 늘어선 바이트로 변경해야 하는데, 이것을 직렬화라고 한다.

반대로 직렬화된 바이트를 객체의 필드값으로 복원하는 것을 역직렬화라고 한다.

 

ObjectInputStream과 ObjectOutputStream은 객체를 입출력할 수 있는 보조 스트림이다.

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

출처 : 이것이 자바다 유튜브 동영상 강의

 

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

 

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

oos.writeObject(객체);

 

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

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

 

Serializable 인터페이스

자바는 Serializable 인터페이스를 구현한 클래스만 직렬화할 수 있도록 제한한다.

Serializable 인터페이스는 멤버가 없는 빈 인터페이스이지만, 객체를 직렬화할 수 있다고 표시하는 역할을 한다.

 

객체가 직렬화될 때 인스턴스 필드값은 직렬화 대상이지만 정적 필드값과 transient로 선언된 필드값은 직렬화에서 제외되므로 출력되지 않는다.

출처 : 이것이 자바다 유튜브 동영상 강의

필드의 타입 또한 Serializable 인터페이스를 구현한 타입이어야 직렬화가 가능하다.

 


 

serialVersionUID 필드

직렬화할 때 사용된 클래스와 역직렬화할 때 사용된 클래스는 기본적으로 동일한 클래스여야 한다.

만약 클래스의 이름이 같더라도 클래스의 내용이 다르면 역직렬화에 실패한다.

 

아래의 코드를 보면, 왼쪽 Member 클래스로 생성한 객체를 직렬화하면 오른쪽 Member 클래스로 역직렬화할 수 없다.

이유는 오른쪽 Member 클래스에는 field3이 있기 때문이다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

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

왼쪽이 오른쪽보다 필드의 수가 적다. 반대로 왼쪽이 오른쪽보다 필드의 수가 많으면 역직렬화가 불가능하다.

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

이클립스는 serialVersionUID 필드를 자동 생성하는 기능을 제공한다.

출처 : 이것이 자바다 유튜브 동영상 강의

 


 

사용 예제

Member.java

import java.io.Serializable;
	
public class Member implements Serializable {
	private static final long serialVersionUID = -622284561026719240L;
	private String id;
	private String name;
	
	public Member(String id, String name) {
		this.id = id;
		this.name = name;
	}
		
	@Override
	public String toString() { return id + ": " + name; }
}

 

Product.java

import java.io.Serializable;

public class Product implements Serializable {
	private static final long serialVersionUID = -621812868470078544L;
	private String name;
	private int price;

	public Product(String name, int price) {
		this.name = name;
		this.price = price;
	}
	
	@Override
	public String toString() { return name + ": " + price; }
}

 

ObjectInputOutputStreamExample.java

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;
 
public class ObjectInputOutputStreamExample {
	public static void main(String[] args) throws Exception {
		//FileOutputStream에 ObjectOutputStream 보조 스트림 연결
		FileOutputStream fos = new FileOutputStream("C:/Temp/object.dat");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		
		//객체 생성
		Member m1 = new Member("fall", "단풍이");
		Product p1 = new Product("노트북", 1500000);
		int[] arr1 = { 1, 2, 3 };
		
		//객체를 역직렬화해서 파일에 저장
		oos.writeObject(m1);
		oos.writeObject(p1);
		oos.writeObject(arr1);
		
		oos.flush(); oos.close(); fos.close();
			
		//FileInputStream에 ObjectInputStream 보조 스트림 연결
		FileInputStream fis = new FileInputStream("C:/Temp/object.dat");
		ObjectInputStream ois = new ObjectInputStream(fis);
		
		//파일을 읽고 역질렬화해서 객체로 복원
		Member m2 = (Member) ois.readObject();
		Product p2 = (Product) ois.readObject();
		int[] arr2 = (int[]) ois.readObject();
		
		ois.close(); fis.close();
		
		//복원된 객체 내용 확인
		System.out.println(m2);
		System.out.println(p2);
		System.out.println(Arrays.toString(arr2));
	}
}
/*
fall: 단풍이
노트북: 1500000
[1, 2, 3]
*/

댓글