Java 파일 I/O 정리

Bennie97·2022년 1월 30일
2
post-thumbnail

java의 입출력 I/O에 대해 정리합니다. (주로 파일 I/O를 위주로 살펴보겠습니다.)
항상 자바나 다른 언어 공부를 하면서 txt파일을 어떻게 읽어와야하는건지 몰라서 답답할때가 있었습니다. 그러나 항상 블로그나 책을 살펴보면 시간은 없는데 방대한 설명만 있고 방법은 너무많고.. 그래서 뭐가 제일 좋은데?
어떤 방법을 써서 txt파일을 읽어서 내가 콘솔에 출력하거나 사용할수 있는데? 이런 생각이 들었습니다. 그런분들은 스크롤을 내려서 제일 밑부분을 참고해주세요.

자바에서 입출력를 수행하려면 두 노드(키보드, 모니터, 메모리, 파일 등)사이를 연결하려는 무엇인가가 필요하고 이를 스트림(Stream)이라고 한다.
간단히 하자면 데이터를 운반하는데 사용되는 연결통로라고도 볼수 있다.
시냇물이 졸졸 흘러가는 것처럼 노드 사이에 연결되어서 데이터가 졸졸 흘러가는것이라고 비유해볼수 있을것 같다.

이때 스트림은 단방향으로 통신이 가능하며 하나의 스트림으로 입출력을 같이 처리할수 없다.
따라서 입력 스트림, 출력 스트림 두개의 스트림이 반드시 필요하다.

이때 스트림이 처리하는 데이터의 타입에 따라 따라 바이트 기반스트림(XXXStream) 캐릭터 기반 스트림(XXXer)으로 나뉘어 진다.

바이트 기반 스트림은 데이터를 바이트 단위(8bit)로 처리하는 스트림이다.
클래스 이름에 XXXStream이 들어간것은 바이트 단위 처리하기 위한 스트림이구나 하고 생각하면 된다. 먼저 바이트 기반 스트림의 I/O에 대해 알아보자.

Byte기반 Stream

위 표는 바이트 기반 입출력에 대한 클래스들의 표이다. -> 표시는 상속의 의미이다.
InputStream과 OutputStream은 모든 바이트 기반 스트림의 조상이다.
파일 입력을 위한 FileInputStream클래스도 InputStream 클래스를 상속받은 클래스이다.

InputStream은 추상클래스로 read 메서드가 추상메서드로 지정되어있다.
InputStream을 상속받는 각 노드별 스트림들이 read를 구현하여 사용한다.

InputStream

public abstract class InputStream extends Object implements Closeable{
   
    // 자식 클래스들이 구현해야할 read 추상 메서드  
    // 바이트 하나를 읽어서 int로 반환하되, 더 이상 읽을 값이 없으면 -1을 리턴.
    public abstract int read() throws IOException;
   
    // len 바이트의 데이터를 읽어서 배열 b에 off 위치부터 집어넣기 (off위치는 배열 b의 index라고 생각하면 됨)
    // 읽은 바이트 개수를 반환하되, 더이상 읽을 값이 없으면 -1을 리턴
    public int read(byte[] b, int off, int len){
    	...
    }
   
    //byte b의 길이만큼 데이터를 InputStream으로부터 읽어들여 byte 배열 b에 삽입.
    //읽은 바이트 개수를 반환하되, 더이상 읽을 값이 없으면 -1을 리턴
    public int read(byte[] b) throws IOException {
    	...
       
    }
    // InputStream을 닫는역할.
    public void close() throws IOException{
    	...
    }
    ...

}

InputStream 클래스와 자주 사용하는 메서드들을 살펴보았다.
참고로 InputStream은 추상(abstract)클래스이므로 new InputStream() 이런식으로 객체를 생성할수 없다. 각 목적에 따라 InputStream을 상속받아 구현하고 있는 여러 자식 클래스들을 이용해야한다.
우리는 파일 입출력에 대해 알아보고 싶으니 FileInputStream을 이용하면 된다.

FileInputStream

public class FileInputStream extends InputStream{
	
    //생성자 목록
    public FileInputStream(File file){
		...    
    }
    public FileInputStream(String name){
    	...
    }
    ...

    //메서드
    public int read(){
    	...
    }
    public int read(byte[] b){
    	...
    }
    public int read(byte[] b, int off, int len){
    	..
    }
    ...


}

FileInputStream 클래스와 자주 사용하는 메서드들을 살펴보았다.
read 메서드들은 위의 InputStream 클래스에 주석달아놓은것과 동일한 역할을 한다.
생성자로는 파일의 주소를 String 타입으로도 매개변수로 받으며
File타입으로도 받는다.
그럼 FileInputStream을 이용하여 txt파일을 읽어보는 예제를 살펴보겠다.

txt파일 위치. MyJavaBasicProject바로 밑에 있는것을 볼수있다. test1.txt파일 내용. 한글과 영어가 섞여있다.


console에 print 결과. 영어는 제대로 출력이되었으나 한글은 깨져있다.

package iotest;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class InputStreamtest {
	public static void main(String[] args) {
		try {
			FileInputStream fin1 = new FileInputStream("./test1.txt");
            /* 
            1.  .txt파일의 위치에 주목!
            첫번째 첨부사진을 보면 test1.txt파일은 
            MyJavaBasicProject라는 프로젝트 밑에 존재한다.
            이클립스에서는 디폴트 경로가 프로젝트이므로 ./test1.txt 로 접근가능하다.
            아니면 ./ 을 생략하고 test1.txt로도 접근 가능
		
            2. 다형성의 성질을 이용해 
            FileInputStream fin1 = new FileInputStream("./test1.txt"); 대신
            InputStream fin1 = new FileInputStream("./test1.txt"); 으로 사용할수도 있음
    
            3. 생성자로 String 타입 대신 File타입을 사용할수도 있음.
            */
  
	    int data;
	    while((data = fin1.read()) != -1)	//파일 다 읽으면 read()는 -1반환한다.
		System.out.print((char)data);

	    } catch (IOException e) {
		e.printStackTrace();
	    }

	}

}

test1.txt 코드는 영어와 한글로 이루어져 있다.
그런데 FileInputStream으로 읽어들이면 영어는 잘 읽히는데 한글은 깨져서 나온다..
그 이유는 바로 Stream은 바이트 기반이기때문이다.
알다시피 1byte는 범위가 0~255까지이며 알파벳 대,소문자의 아스키드 값은 다 저 범위 안에 들어간다.
그러나 한글을 나타내려면 2byte가 필요하므로 바이트 기반인 Stream에서는 깨지는것이다.
따라서 한글 file을 읽고쓰려면 캐릭터 기반 스트림인 Reader나 Writer을 사용해야한다.

그럼 이제 Stream으로 파일읽기를 마쳤으니 Stream으로 txt파일에 쓰기를 해보자.
파일에 쓰려면 FileOutputStream 클래스가 필요하다.
먼저 FileOutputStream의 조상인 OutputStream부터 살펴보자.

OutputStream

public abstract class OutputStream extends Object implements Closeable, Flushable{

	//자식들이 구현해야할 write(int b) 추상 메서드
	// 주어진 값 b를 노드에 write.
	public abstract void write(int b) throws IOException;
   
	//배열 b에 있는 데이터 전부를 노드에 write.
	public void write(byte[] b) throws IOException {
    	...
   	}
   
	// 바이트 배열 b에 저장된 데이터 중 off위치부터 len개를 읽어서 노드에 write
   	public void write(byte[] b, int off, int len) throws IOException {
    	...
   	}
	
   	// 출력 스트림에 있는 모든 데이터를 노드에 출력하고 버퍼를 비운다.
	public void flush() throws IOException {
    	...
	}
   
   	// OutputStream을 닫는 역할
	public void close() throws IOException{
    	...
    }
	...
}

OutputStream 클래스와 자주 사용하는 메서드들을 살펴보았다.
OutputStream은 추상(abstract)클래스이므로 new OutputStream() 이런식으로 객체를 생성할수 없다. 각 목적에 따라 OutputStream을 상속받아 구현하고 있는 여러 자식 클래스들을 이용해야한다.
우리는 파일 입출력에 대해 알아보고 싶으니 FileOutputStream을 이용하면 된다.

FileOutputStream의 생성자는 FileInputStream처럼
매개변수로 String으로도 사용가능하며 File타입으로도 사용가능하다.

그외에 write메서드들은 InputStream과 사용방법이 동일하니 이번엔 따로 코드를 살펴보지 않고 바로 사용하는 예를 살펴보겠다.

package iotest;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class InputStreamtest {

	public static void main(String[] args) {
		try {
			FileInputStream fin1 = new FileInputStream("./test1.txt");
			FileOutputStream fout1 = new FileOutputStream("./test2.txt");
			
			int data;
			while((data = fin1.read()) != -1) {
//				System.out.print((char)data);
				fout1.write(data);
			}
			
		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}

}

FileInputStream으로 test1.txt 파일을 읽어들여
FileOutputStream을 통해 test2.txt에 써보는 예제이다.

결과로 test2.txt가 아무문제 없이 잘 생성되는 모습을 볼 수 있다.

그러나 이런 FileInputStream이나 FileOutputStream을 사용하여
입출력 하는것은 기본적으로 한 바이트씩 입, 출력 하는것이기 때문에 기본적으로 시간이 오래걸린다. 따라서 대표적으로 버퍼를 이용하여 속도를 빠르게 할 수 있다. 그 역할을 해주는 것이
BufferedInputStream과 BufferedOutputStream이다.

읽을때는 위와 같이 BufferedInputStream 내부의 버퍼만 Java App이 재빨리 읽으면 된다. 그동안 InputStream이 Source로 부터 버퍼를 채워놓는다.

쓸때는 JavaApp에서 BufferedOutputStream의 버퍼에 출력할것을 저장해놓으면 OutputStream이 재빨리 Target에다 출력을 한다. 참고로 BufferedOutputStream이나 BufferedInputStream 모두 보조스트림으로 자체적으로 입/출력을 수행할 수 없고
InputStream이나 Outputstream 같은 기반 스트림을 필요로 한다. 그래서 밑에 코드를 보듯이 생성자로 InputStream과 OutputStream을 매개변수로 받는것을 볼수 있다.

그럼 이제
빠르게 BufferedInputStream과 BufferedInputStream을 살펴보자.

BufferedInputStream

public class BufferedInputStream extends FilterInputStream{

  // 생성자 
  // 주어진 InputStream 객체를 매개변수로 받으며 버퍼의 크기를 지정해주지 않았으므로
  // 기본적으로 8192byte 크기의 버퍼를 가짐.
  public BufferedInputStream(InputStream in){
  ...
  }
  // 주어진 InputStream 객체를 매개변수로 받으며 지정된 size크기의 버퍼를 갖는
  // BufferedInputStream객체를 생성한다.
  public BufferedInputStream(InputStream in, int size){
  ...
  }

  //메서드 (InputStream과 하는 역할 동일)
  public int read() throws IOException{
   ...
  }
  //(InputStream과 하는 역할 동일)
  public int read(byte[] b, int off, int len) throws IOException{
   ...
  }
   ...

}

BufferedOutputStream

public class BufferedOutputStream extends FilterOutputStream{

    // 생성자 
    // OutputStream 객체를 매개변수로 받으며 버퍼의 크기를 지정해주지 않으면
    // 8192 byte 크기의 버퍼를 갖게 된다.
	public BufferedOutputStream(OutputStream out){
    	...
    }
    // OutputStream 객체를 매개변수으로 받으며 지정된 size크기의 버퍼를 갖는
    // BufferedOutputStream 객체를 생성한다.
    public BufferedOutputStream(OutputStream out, int size){
   		...
    }
   
    //메서드 (OutputStream과 하는 역할 동일)
	public void write(int b) throws IOException{
    	...
    }
    // (OutputStream과 하는 역할 동일)
    public void write(byte[] b, int off, int len) throws IOException{
    	...
    }
    //버퍼의 모든 내용을 출력한후 버퍼 비움
    public void flush() throws IOException{
    	...
    }
	...    

}

한가지 짚고 넘어가야 할 점은 BufferedOutputStream에서 버퍼가 가득 찼을때만
출력하기 때문에 flush()나 close()등을 이용하여 남아있는 버퍼의 내용들을 출력하게 해야한다.

package iotest;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Bufferedstreamtest {

	public static void main(String[] args) {
		try {
			long start = System.currentTimeMillis();
			FileInputStream fin = new FileInputStream("./test3.txt");
			BufferedInputStream bin = new BufferedInputStream(fin);
		
			FileOutputStream fout = new FileOutputStream("./test4.txt");
			BufferedOutputStream bout = new BufferedOutputStream(fout);
		
			int data;
			while((data =bin.read()) != -1) {
				bout.write(data);
			}
					
			long end = System.currentTimeMillis();
			System.out.println( "실행 시간 : " + ( end - start )/1000.0 + "초");
		
		} catch (IOException e) {
			e.printStackTrace();
		}
	
	}

}

실행시간
0.828초

package iotest;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Bufferedstreamtest {

	public static void main(String[] args) {
		try {
			long start = System.currentTimeMillis();
			FileInputStream fin = new FileInputStream("./test3.txt");
			//BufferedInputStream bin = new BufferedInputStream(fin);
		
			FileOutputStream fout = new FileOutputStream("./test4.txt");
			//BufferedOutputStream bout = new BufferedOutputStream(fout);
	
			int data;
//			while((data =bin.read()) != -1) {
//				bout.write(data);
//			}
//			bout.close();
	
			while((data =fin.read()) != -1) {
				fout.write(data);
			}
	
			long end = System.currentTimeMillis();
			System.out.println( "실행 시간 : " + ( end - start )/1000.0 + "초");

		} catch (IOException e) {
			e.printStackTrace();
		}

	}

}

실행시간
151.218초

반면 BufferedInputStream, BufferedOutputStream을 사용하지 않고 FileInputStream과 FileOutputStream만을 사용했을때는 151.218초나 걸렸다.
참고로 읽기에 사용된 test3.txt파일은 약 34메가정도 였다.
정말 속도 면에서 어마어마한 차이가 나는것을 볼수 있었다.

Character 기반 Stream

character 기반 stream은 끝이 ~er이 붙는다.
byte기반 stream과 마찬가지로 각 클래스들은 기본적으로 입력 스트림은 Reader와 출력 스트림인 Writer를 상속받는다. 따라서 Reader와 Writer의 메서드 구현부와 설명을 보면 그것을 상속받는 클래스들에서도 그대로 사용할수 있다.
그럼 일단 Reader와 Writer의 코드를 살펴보자

Reader

public abstract class Reader extends Object implements Readable, Closeable{
    // 주요 메서드
    // 캐릭터 하나를 읽는다. char 범위인 0~65535범위의 읽은 정수를 반환하며
    // 입력스트림 마지막에 도달하면 -1을 반환한다.
    public int read() throws IOException {
    	...
    }
    // 입력소스로부터 매개변수로 주어진 배열 c의 크기만큼을 읽어서 배열 cbuf에 저장
    // 반환값은 읽어온 데이터의 개수이다. 
    // 입력스트림의 마지막에 도달하면 -1을 반환한다.
    public int read(char[] cbuf) throws IOException{
    	...
    }
    // 입력소스로부터 len개의 문자를 읽어서 배열 cbuf의 지정된 위치(off)로 부터 읽은만큼 저장한다. 
    // 읽어들인 데이터의 개수를 반환하되, 입력스트림 마지막에 도달시 -1을 반환한다.
    public abstract int read(char[] cbuf, int off, int len) throws IOException{
		...    
    }
    // 입력스트림을 닫음으로써 사용하고 있던 자원을 반환한다.
    public abstract void close() throws IOException {
    	...
    }
	...
}

Writer

public abstract class Writer extends Object implements Appendable, Closeable, Flushable{
	// 주요 메서드
    // 지정된 문자 c를 출력소스에 출력한다.
    public Writer append(char c) throws IOException{
    	...
    }
    // 지정된 문자열 csq를 출력소스에 출력한다. 
    public Writer append(CharSequence csq) throws IOException{
    	...
    }
    // 지정된 문자열 csq중 일부(start <= < end)를 출력소스에 출력한다. 
    public Writer append(CharSequence csq, int start, int end) throws IOException{
    	...
    }
    // 출력스트림을 닫는다.
    public abstract void close() throws IOException{
    	...
	}
    // 스트림의 버퍼에 있는 모든 내용을 출력소스에 쓴다. (버퍼가 있는 스트림에만 해당)
    public abstract void flush() throws IOException{
    	...
    }
    // 주어진 문자열(str)을 출력소스에 쓴다.
    public void write(String str) throws IOException{
    	...
    }
    // 주어진 값(c)를 출력소스에 쓴다.
    public void write(int c) throws IOException{
    	...
    }
    // 주어진 배열 cbuf에 저장된 모든 내용을 출력소스에 쓴다.
    public void write(char[] cbuf) throws IOException{
    	...
    }
    // 주어진 배열 cbuf에 저장된 내용 중에서 off번째부터 len 길이만큼만 출력소스에 쓴다.
    public abstract void write(char[] cbuf, int off, nt len) hrows IOException{
    	...
    }
    // 주어진 문자열 str의 off번째 문자부터 len개만큼의 문자열을 출력소스에 쓴다.
    public void write(String str, int off, int len) throws IOException{
    	...
    }
    ...
}

이로써 Reader와 Writer에 대해서 자주 사용되는 메서드들을 알아보았다.
우리는 파일입출력에 관심이 있으니
FileReader와 FileWriter을 이용하여 입출력을 해보자.
FileReader와 FilerWrtier의 생성자는
FileInputStream과 FileOutputStream과 비슷하게 File타입과 String 타입 모두 허용한다.

package iotest;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class FileReaderWriterTest {

	public static void main(String[] args) {
		try(FileReader fin = new FileReader("./test1.txt"); FileWriter fout = new FileWriter("./test2.txt");) {
			int data;
			while((data = fin.read()) != -1){
				System.out.print((char)data);
				fout.write(data);
			}
			
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

}

test1.txt에서 FileReader스트림을 통해 데이터를 읽어들여 test2.txt에 FileWriter스트림을 통하여 출력하는 예제이다.

위의 FileInputStream의 예제와는 다르게 한글도 안깨지고 잘 출력되는 모습을 볼수있다.!!

근데 FileInputStream과 FileOutputStream과 마찬가지로 이 방법은 데이터를 한바이트 씩 입출력 하는것이므로 비효율적이며 시간이 오래걸린다. 그래서 우리는 BufferedInputStream과 BufferedOutputStream을 사용했었다.

마찬가지로 캐릭터 기반 스트림에서도 Buffer을 사용한 BufferedReader와 BufferedWriter을 사용하면
보다 더 빠르게 입출력을 할 수 있다!!

BufferedReader

public class BufferedReader extends Reader{
	// 생성자
    public BufferedReader(Reader in, int sz){
    	...
    }
    public BufferedReader(Reader in){
    	...
    }
    // 메서드 
    // 한줄을 읽어들여 String으로 반환한다. 
    // 스트림의 끝에 도달시 null을 반환.
    public String readLine() throws IOException{
    	...
    }
    ...
}

BufferedReader에서는 새롭게 ReadLine이라는 메서드가 추가 되었다. 한줄로 읽어들이는 메서드이며 매우 유용하게 사용된다. 나머지 read메서드들은 Reader와 사용법과 설명이 동일하다.

BufferedWriter

public class BufferedWriter extends Writer{
	// 생성자
    BufferedWriter(Writer out){
    	...
    }
    BufferedWriter(Writer out, int sz){
    	...
    }
   	// 메서드
    // 줄바꿈 문자를 쓴다.
    void newLine(){
    	...
    }
    ...
}

그럼 이제 BufferedReader나 BufferedWriter을 이용한 예제를 살펴보자.

package iotest;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;

public class BufferedReaderWriterTest {
	public static void main(String[] args) {
		
		long start = System.currentTimeMillis();
		
		try(BufferedReader bin = new BufferedReader(new FileReader("./test3.txt")); 
		    BufferedWriter bout = new BufferedWriter(new FileWriter("./test5.txt"));) {
			String line = "";
			while((line = bin.readLine()) != null) {
				bout.write(line);
				bout.newLine();
			}
			
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		long end = System.currentTimeMillis();
		System.out.println( "실행 시간 : " + ( end - start )/1000.0 + "초");
		
	}
}

실행 시간 : 0.149초

BufferedInputStream과 BufferedOutputStream을 사용했을때보다 8배나 빠른것을 볼 수 있었다.
앞으로 파일 입출력시 한글도 안깨지고 속도도 빠른 BufferedReader와 BufferedWriter를 쓰도록 하자!!

이미지 출처
https://o7planning.org/13357/java-bufferedinputstream
참고자료
Java의 정석(3판)

profile
현명한개발자가되자

0개의 댓글