[자바 기초] 입출력 I/O(2) - 바이트 기반 스트림

InputSteam과 OutputStream

InputStream과 OutputStream은 모든 바이트기반 스트림의 조상이다. 스트림의 종류에 따라서 mark()와 reset()를 사용하여 이미 읽은 데이터를 되돌려서 다시 읽을 수 있다. 이 기능을 지원하는 스트림인지는 markSupported()를 통해서 알 수 있다. flush()는 버퍼가 있는 출력스트림의 경우에만 의미가 있으며, OutputStream에 정의된 flush()는 아무런 일도 하지 않는다. 프로그램이 종료될 때, 사용하고 닫지 않은 스트림을 JVM이 자동적으로 닫아 주기는 하지만, 스트림을 사용해서 모든 작업을 마치고 난 후에는 close()를 호출해서 반드시 닫아주어야 한다. 그러나 ByteArrayInputStream과 같이 메모리를 사용하는 스트림과 System.in, System.out과 같은 표준 입출력 스트림은 닫아 주지 않아도 된다.

ByteArrayInputStream과 ByteArrayOutputStream

메모리, 즉 바이트배열에 데이터를 입출력 하는데 사용되는 스트림이다. 주로 다른 곳에 입출력하기 전에 데이터를 임시로 바이트배열에 담아서 변환 등의 작업을 하는데 사용된다.

    class IOEx1 {
    	public static void main(String[] args){
    		byte[] inSrc = {0,1,2,3,4,5,6,7,8,9};
    		byte[] outSrc = null;
    
    		ByteArrayInputStream input = null;
    		ByteArrayOutputStream output = null;
    
    		input = new ByteArrayInputStream(inSrc);
    		output = new ByteArrayOutputStream();
    
    		int data = 0;
    		
    		while((data = input.read()) != -1){
    			output.write(data);
    		}
    
    		outSrc = output.toByteArray();
    
    		System.out.println("Input Source :" + Arrays.toString(inSrc));
    		System.out.println("Output Source :" + Arrays.toString(outSrc));
    	}
    }
    
    // Input Source :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    // Output Source :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

바이트배열은 사용하는 자원이 메모리 밖에 없으므로 GC에 의해 자동적으로 자원을 반환한다. 따라서 close()를 이용해서 스트림을 닫지 않아도 된다. read()와 write(int b)를 사용하기 때문에 한 번에 1 byte만 읽고 쓰므로 작업효율이 떨어진다.

    class IOEx2 {
    	public static void main(String[] args){
    		byte[] inSrc = {0,1,2,3,4,5,6,7,8,9};
    		byte[] outSrc = null;
    		byte[] temp = new byte[10];
    
    		ByteArrayInputStream input = null;
    		ByteArrayOutputStream output = null;
    
    		input = new ByteArrayInputStream(inSrc);
    		output = new ByteArrayOutputStream();
    
    		input.read(temp, 0, temp.length) // 읽어 온 데이터를 배열 temp에 담는다.
    		output.write(temp, 5, 5); // temp[5]부터 5개의 데이터를 write한다.
    
    		outSrc = output.toByteArray();
    
    		System.out.println("Input Source :" + Arrays.toString(inSrc));
    		System.out.println("temp :" + Arrays.toString(temp));
    		System.out.println("Output Source :" + Arrays.toString(outSrc));
    	}
    }
    
    // Input Source :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    // temp :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    // Output Source :[5, 6, 7, 8, 9]

배열을 이용한 입출력은 작업의 효율을 증가시키므로 가능하면 입출력 대상에 따라 알맞은 크기의 배열을 사용하는 것이 좋다.

    class IOEx3 {
    	public static void main(String[] args){
    		byte[] inSrc = {0,1,2,3,4,5,6,7,8,9};
    		byte[] outSrc = null;
    		byte[] temp = new byte[4];
    
    		ByteArrayInputStream input = null;
    		ByteArrayOutputStream output = null;
    
    		input = new ByteArrayInputStream(inSrc);
    		output = new ByteArrayOutputStream();
    
    		try {
    			while(input.available() > 0){
    				int len = input.read(temp); // 읽어 온 데이터의 개수를 반환
    				output.write(temp, 0, len); // 읽어 온 만큼만 write
    			}
    		} catch(IOException e) {}
    
    		outSrc = output.toByteArray();
    
    		System.out.println("Input Source :" + Arrays.toString(inSrc));
    		System.out.println("temp :" + Arrays.toString(temp));
    		System.out.println("Output Source :" + Arrays.toString(outSrc));
    	}
    }
    
    // Input Source :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    // temp :[8, 9, 6, 7]
    // Output Source :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

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

FIleInputStream과 FileOutputStream

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

FileInputStream(String name), 지정된 파일이름(name)을 가진 실제 파일과 연결된 FIleInputStream을 생성

FileInputStream(File file), 파일의 이름이 String이 아닌 File 인스턴스로 지정해주어야 하는 점을 제외하고 FileInputStream(String name)과 같다.

FileInputStream(FileDescriptor fdObj), 파일 디스크립터(fdObj)로 FileInputStream을 생성

FileOutputStream(String name), 지정된 파일이름을 가진 실제 파일과 연결된 FIleOutputStream을 생성

FileOutputStream(String name, boolean append), 지정된 파일이름을 가진 실제 파일과 연결된 FileOutputStream을 생성한다. 두번째 인자인 append를 true로 하면, 출력 시 기존의 파일내용의 마지막에 덧붙인다. false면 기존의 파일내용을 덮어쓰게 된다.

FileOutputStream(File file)

FileOutputStream(File file, boolean append)

FileOutputStream(FileDescriptor fdObj)

    class FileViewer {
    	public static void main(String args[]) throws IOException{
    		FileInputStream fis = new FileInputStream(args[0]);
    		int data = 0;
    		
    		while((data = fis.read()) != -1){
    			char c = (char)data;
    			System.out.print(c);
    		}
    	}
    }
    
    // >java FileViewer FileViewer.java

커맨드라인으로부터 입력받은 파일의 내용을 읽어서 그대로 화면에 출력하는 간단한 예제이다. read()의 반환값이 int(4 byte)이긴 하지만, 더 이상 입력값이 없음을 알리는 -1을 제외하고는 0~255(1 byte) 범위의 정수값이기 때문에, char형(2 byte)으로 변환한다 해도 손실되는 값은 없다.

read()가 한 번에 1 byte씩 파일로부터 데이터를 읽어 들이긴 하지만, 데이터의 범위가 십진수로 0~255(16진수로는 0x00~0xff)범위의 정수값이고, 또 읽을 수 있는 입력값이 더 이상 알릴 수 있는 값(-1)도 필요하다. 그래서 다로 크긴 하지만 정수형 중에서는 연산이 가장 효율적이고 빠른 int형 값을 반환하도록 한 것이다.

    class FileCopy{
    	public static void main(String args[]){
    		try {
    			FileInputStream fis = new FileInputStream(args[0]);
    			FileOutputStream fos = new FileOutputStream(args[1]);
    
    			int data = 0;
    			while((data = fis.read()) != -1) {
    				fos.write(data);
    			}
    
    			fis.close();
    			fos.close();
    		} catch(IOException e){
    			e.printStackTrace();
    		}
    	}
    }
    
    // >java FileCopy FileCopy.java FileCopy.bak

텍스트파일을 다루는 경우에는 FileInputStream/FileOutputStream 보다 문자기반의 스트림인 FileReader/FileWriter를 사용하는 것이 더 좋다.

FilterInputStream과 FilterOutputStream

FilterInputStream/FilterOutputStream은 InputStream/OutputStream의 자손이면서 모든 보조스트림의 조상이다. 보조스트림은 자체적으로 입출력을 수행할 수 없기 때문에 기반스트림을 필요로 한다.

protected FilterInputStream(InputStream in)

public FilterOutputStream(OutputStream out)

BufferedInputStream과 BufferedOutputStream

BufferedInputStream/BufferedOutputStream은 스트림의 입출력 효율을 높이기 위해 버퍼를 사용하는 보조스트림이다. 한 바이트씩 입출력하는 것 보다는 버퍼(바이트배열)를 이용해서 한 번에 여러 바이트를 입출력하는 것이 빠르기 때문에 대부분의 입출력 작업에 사용된다. BufferedInputStream의 버퍼크기는 입력소스로부터 한 번에 가져올 수 있는 데이터의 크기로 지정하면 좋다. 보통 입력소스가 파일인 경우 4096 정도의 크기로 하는 것이 보통이며, 버퍼의 크기를 변경해가면서 테스트하면 최적의 버퍼크기를 알아낼 수 있다.

프로그램에서 입력소스로부터 데이터를 읽기 위해 처음으로 read메서드를 호출하면, BufferedInputStream은 입력소스로부터 버퍼 크기만큼의 데이터를 읽어다 자신의 내부 버퍼에 저장한다. 이제 프로그램에서는 BufferedInputStream의 버퍼에 저장된 데이터를 읽으면 되는 것이다. 외부의 입력소스로 부터 읽는 것보다 내부의 버퍼로 부터 읽는 것이 훨씬 빠르기 때문에 그만큼 작업 효율이 높아진다. 프로그램에서 버퍼에 저장된 모든 데이터를 다 읽고 그 다음 데이터를 읽기위해 read메서드가 호출되면, BufferedInputStream은 입력소스로부터 다시 버퍼크기 만큼의 데이터를 읽어다 버퍼에 저장해 놓는다.

BufferedOutputStream 역시 버퍼를 이용해서 출력소스와 작업을 하게 되는데, 입력소스로부터 데이터를 읽을 때와는 반대로, 프로그램에서 write메서드를 이용한 출력이 BufferedOutputStream의 버퍼에 저장된다. 버퍼가 가득 차면, 그 때 버퍼의 모든 내용을 출력소스에 출력한다. 버퍼가 가득 찼을 때만 출력소스에 출력을 하기 때문에, 마지막 출력부분이 출력소스에 쓰이지 못하고 BufferedOutputStream의 버퍼에 남아있는 채로 프로그램이 종료될 수 있다는 점을 주의해야한다. 그래서 프로그램에서 모든 출력작업을 마친 후 BufferedOutputStream에 close()나 flush()를 호출해서 마지막에 버퍼에 있는 모든 내용이 출력소스에 출력되도록 해야 한다.

    class BufferedOutputStreamEx1 {
    	public static void main(String args[]){
    		try {
    			FileOutputStream fos = new FileOutputStream("123.txt");
    			BufferedOutputStream bos = new BufferedOutputStream(fos, 5);
    			for(int i = '1'; i <= '9'; i++){
    				bos.write(i);
    			}
    			bos.close(); // fos.close() 사용하면 12345, bos에 있는 내용이 버퍼에 남아있기 때문
    		} catch(IOException e){
    			e.printStackTrace();
    		}
    	}
    }
    
    // 123456789

BufferedOutputStream의 close()는 기반 스트림인 FileOutputStream의 close()를 호출하기 때문에 FileOutputStream의 close()는 따로 호출해주지 않아도 된다.

DataInputStream과 DataOutputStream

DataInputStream은 DataInput 인터페이스를, DataOutputStream은 DataOutput 인터페이스를 각각 구현하였기 때문에, 데이터를 읽고 쓰는데 있어서 byte 단위가 아닌, 8가지 기본 자료형의 단위로 읽고 쓸 수 있다는 장점이 있다. DataOutputStream이 출력하는 형식은 각 기본 자료형 값을 16진수로 표현하여 저장한다. 예를 들어 int값을 출력한다면, 4 byte의 16진수로 출력된다.

ex) 10 → 0, 0, 0, 0a / true → 01

    class DataOutputStreamEx1 {
    	public static void main(String args[]){
    		FileOutputStream fos = null;
    		DataOutputStream dos = null;
    
    		try {
    			fos = new FileOutputStream("sample.dat");
    			dos = new DataOutputStream(fos);
    			dos.writeInt(10);
    			dos.writeFloat(20.0f);
    			dos.writeBoolean(true);
    
    			dos.close();
    		} catch(IOException e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    class DataInputStreamEx1 {
    	public static void main(String args[]){
    		try {
    			FileArrayInputStream fis = new FileArrayInputStream("sample.dat");
    			DataInputStream dis = new DataInputStream(fis);
    
    			System.out.println(dis.readInt());
    			System.out.println(dis.readFloat());
    			System.out.println(dis.readBoolean());
    			dis.close();
    		} catch(IOException e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    // 10
    // 20.0
    // true

DataOutputStream에 의해서 어떻게 저장되는지 몰라도 DataOutputStream의 write메서드들로 기록한 데이터는 DataIntputStream의 read메서드들로 읽기만 하면 된다. 이 때 한 가지 주의해야 할 것은 이 예제와 같이 여러 가지 종류의 자료형으로 출력한 경우, 읽을 때는 반드시 쓰인 순서대로 읽어야 한다는 것이다.

문자로 데이터를 저장하면, 다시 데이터를 읽어 올 때 문자들을 실제 값으로 변환하는, 예를 들면 문자열 “100”을 숫자 100으로 변환하는, 과정을 거쳐야 하고, 또 읽어야 할 데이터의 개수를 결정해야하는 번거로움이 있다.

하지만 이처럼 DataInputStream과 DataOutputStream을 사용하면, 데이터를 변환할 필요도 없고, 자리수를 세어서 따지지 않아도 되므로 편리하고 빠르게 데이터를 저장하고 읽을 수 있게 된다.

    class DataOutputStreamEx3 {
    	public static void main(String args[]){
    		int[] score = {100, 90, 95, 85, 50};
    
    		try {
    			FileOutputStream fos = new FileOutputStream("sample.dat");
    			DataOutputStream dos = new DataOutputStream(fos);
    			
    			for(int i = 0; i < score.length; i++){
    				dos.writeInt(score[i]);
    			}
    
    			dos.close();
    		} catch(IOException e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    class DataInputStreamEx3 {
    	public static void main(String args[]){
    		int sum = 0;
    		int score = 0;
    
    		FileArrayInputStream fis = null;
    		DataInputStream dis = null;
    
    		try {
    			fis = new FileArrayInputStream("sample.dat");
    			dis = new DataInputStream(fis);
    			
    			while(true){
    				score = dis.readInt(
    				System.out.println(score);
    				sum += score;
    			}
    		} catch(EOFException e) {
    			System.out.println("점수의 총합은 " + sum + "입니다.");
    		} catch(IOException ie) {
    			ie.printStackTrace();
    		} finally {
    			try {
    				if(dis != null){
    					dis.close();
    				} catch (IOException ie){
    					ie.printStackTrace();
    				}
    			}
    		}
    	}
    }
    
    // 100
    // 90
    // 95
    // 85
    // 90
    // 점수의 총합은 420입니다.

JDK 1.8부터 try-with-resources문을 이용해서 close()를 직접 호출하지 않아도 자동호출되도록 할 수 있다.

    class DataInputStreamEx4 {
    	public static void main(String args[]){
    		int sum = 0;
    		int score = 0;
    
    		FileArrayInputStream fis = null;
    		DataInputStream dis = null;
    
    		try (FileArrayInputStream fis = new FileArrayInputStream("sample.dat");
    				DataInputStream dis = new DataInputStream(fis)) {
    			
    			while(true){
    				score = dis.readInt(
    				System.out.println(score);
    				sum += score;
    			}
    		} catch(EOFException e) {
    			System.out.println("점수의 총합은 " + sum + "입니다.");
    		} catch(IOException ie) {
    			ie.printStackTrace();
    		}
    	}
    }

SequenceInputStream

SequenceInputStream은 여러 개의 입력스트림을 연속적으로 연결해서 하나의 스트림으로부터 데이터를 읽는 것과 같이 처리할 수 있도록 도와준다.

    class SequenceInputStreamEx {
    	public static void main(String[] args){
    		byte[] arr1 = {0,1,2};
    		byte[] arr2 = {3,4,5};
    		byte[] outSrc = null;
    
    		Vector v = new Vector();
    		v.add(new ByteArrayInputStream(arr1));
    		v.add(new ByteArrayInputStream(arr2));
    
    		SequenceInputStream input = new SequenceInputStream(v.elements());
    		ByteArrayOutputStream output = new ByteArrayOutputStream();
    
    		int data = 0;
    
    		try {
    			while((data = input.read()) != -1) {
    				output.write(data);
    			}
    		} catch(IOException e) {}
    
    		outSrc - output.toByteArray();
    	}
    }

Vector에 저장된 순서대로 입력되므로 순서에 주의해야한다.

PrintStream

PrintStream은 데이터를 기반스트림에 다양한 형태로 출력할 수 있는 print, println, printf와 같은 메서드를 오버로딩하여 제공한다. PrintStream은 데이터를 적절한 문자로 출력하는 것이기 때문에 문자기반 스트림의 역할을 수행한다. PrintStream과 PrintWriter는 거의 같은 기능을 가지고 있지만 PrintWriter가 PrintStream에 비해 다양한 언어의 문자를 처리하는데 적합하기 때문에 가능하면 PrintWriter를 사용하는 것이 좋다.