자바 I/O 스트림

honeyricecake·2022년 6월 1일
0

학교 수업 - 자바

목록 보기
15/16

이 내용은 윤성우 선생님의 열혈 JAVA를 공부하고 정리한 내용임을 미리 밝힙니다.

I/O 스트림 모델의 소개

프로그램의 상당 부분은 입출력과 관련이 있다. 그리고 다음은 우리가 쉽게 접할 수 있는 입출력의 대상들이다.

  • 파일
  • 키보드와 모니터
  • 그래픽 카드, 사운드 카드
  • 프린터, 팩스와 같은 출력장치
  • 인터넷으로 연결되어 있는 서버 또는 클라이언트

이렇듯 데이터의 입출력 대상은 다양한데, 입출력 대상이 달라지면 코드상에서의 입출력 방법도 달라지는 것이 일반적이다 그런데 자바에서는 입출력 대상에 상관없이 동일한 방법으로 입출력을 진행할 수 있도록 I/O 스트림 모델이라는 것을 정의하였다.

이는 줄여서 I/O 모델이라고 한다.

I/O 모델과 스트림의 이해, 그리고 파일 대상의 입력 스트림 생성

I/O 모델의 스트림은 다음과 같이 크게 두가지로 나뉜다.

  • 입력 스트립 : 실행중인 자바 프로그램으로 데이터를 읽어 들이는 스트림

  • 출력 스트림 : 실행 중인 자바 프로그램으로부터 데이터를 내보내는 스트림

스트림이란 데이터의 흐름을 의미한다. 그리고 이는 데이터의 이동 통로로 의역을 하면 표현이 부드러워진다.

즉, 입력스트림은 입력 통로, 출력 스트림은 출력 통로를 의미한다.

그리고 여기서 말하는 입력, 출력의 주체는 실행 중인 자바 프로그램이다.

따라서 자바 프로그램으로 데이터를 읽어들이는 경우를 입력, 자바 프로그램이 데이터를 내보내는 경우를 출력이라 한다.

ex. 파일 대상으로 입력 스트림을 생성하는 경우

InputStream in = new FileInputStream("data.dat");

-> FileInputStream 쿨래스는 InputStream 클래스를 상속한다.

위의 문장이 보이듯이 입력 스트림의 생성도 (물론 출력 스트림의 생성도) 인스턴스의 생성을 통해서 이루어진다.

이렇게 통로가 만들어지면 다음 메소드 호출을 통해서 데이터를 읽어들일 수 있다.

public abstract int read() throws IOException // java.io.InputStream의 메소드

정리하면, 다음 두 문장을 통해서 data.dat 로부터 데이터를 읽을 수 있는 입력 스트림이 생성되고 또 첫번째 데이터를 읽어 들이게 된다.

InputStream in = new FileInputStream("data.dat"); // 입력스트립 생성
int data = in.read();  //  데이터 읽어 들임

이와 유사하게 다음 두 문장을 통해서 data.dat로 데이터를 전달할 수 있는 출력 스트림이 생성되고, 데이터 7을 저장할 수 있게 된다.

OutputStream out = new FileOutputStream("data.dat");  // 출력 스트림 생성
out.write(7);  // 데이터 7을 파일에 전달

-> FileOutputStream은 OutputStream 을 상속한다.

만약에 스트림을 생성할 대상이 파일이 아니라면 위 문장의 오른편에 생성하는 인스턴스의 종류가 그에 따라 달라진다. 그러나 이렇듯 스트림을 생성하고 나면, 이후로는 그 대상에 상관없이 다음의 방법으로 데이터를 저장할 수 있게 된다.

이것이 바로 입출력 대상에 상관없이 동일한 방법으로 입출력을 진행할 수 있도록 정의된 I/O 모델의 핵심이다.

out.write(7);

위 문장에서 호출한 OutputStream의 write메소드는 다음과 같다.

public abstract void write(int b) throws IOException

즉, 추상 메소드이다. 실제 파일로 데이터를 저장하도록 구현된 write메소드는 FileOutputStream에 정의되어 있다.

끝으로 데이터의 저장이 끝나면 out.close() 메소드의 호출을 통해서 생성했던 출력 스트림을 소멸해야 한다. 스트림이 소멸되면 열려 있던 파일은 닫히고 할당되었던 메모리 자원은 다시 사용할 수 있도록 반환이 된다.

입출력 스트림 관련 코드 개선

앞서 보인 예제의 경우 입력 및 출력 스트림의 생성 과정에서 예외가 발생하여 스트림의 생성에 실패할 수도 있다. 그런데 이러한 경우에는 스트림을 종료하는 close메소드의 호출을 생략해야 한다.

그렇지 않으면 이로 인해 또다른 예외가 발생할 수 있다. 따라서 앞서 제시한 두 예제를 다음과 같이 작성하는 것이 보다 안정적이다.

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class Write7ToFile {

	public static void main(String[] args) throws IOException {
		OutputStream out = null;
		
		try {
			out = new FileOutputStream("data.dat");
			out.write(7);	
		}
		
		finally {
			if(out != null) out.close();  // 안정적인 close문 호출을 보장
		}
		
		

	}

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

public class Read7FromFile {

	public static void main(String[] args) throws IOException {
		InputStream in = null;
		
		try
		{
			in = new FileInputStream("data.dat");
			int dat =  in.read();
			System.out.println(dat);
		}
		
		finally
		{
			if(in != null) in.close();
		}

	}

}

바이트 단위 입출 및 출력 스트림

가장 기본적인 데이터의 입출력 단위는 바이트이고, 바이트 단위로 데이터를 입력 및 출력하는 스트림을 가리켜 바이트 스트림이라 한다. 그리고 파일을 대상으로 하는 바이트 스트림의 생성 방법은 다음과 같다.

InputStream in = new FileInputStream("data.dat");
-> 파일을 대상으로 하는 바이트 단위의 입력 스트림 생성

OutputStream out = new FileOutputStream("data.dat");
-> 파일을 대상으로 하는 바이트 단위의 출력 스트림 생성

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Scanner;

public class BytesFileCopier {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		Scanner sc = new Scanner(System.in);
		System.out.println("대상 파일: ");
		String src = sc.nextLine();
		
		System.out.println("사본 이름: ");
		String dst = sc.nextLine();
		
		try(InputStream in = new FileInputStream(src))  // try with resource 문을 빠져나오면서 자동으로 종료
		{
			OutputStream out = new FileOutputStream(dst);
			int data;
			
			while(true)
			{
				data = in.read();  // 파일로부터 1바이트를 읽는다.
				if(data == -1)  // 더이상 읽어들일 데이터가 없다면 (EOF는 -1)
				{
					break;
				}
				out.write(data);  //  파일에 1바이트를 쓴다.
			}
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
		
	}

}

위 코드의 while문에서 호출하는 read 메소드는 다음과 같다.

public int read() throws IOException

이 메소드는 파일로부터 읽어 들인 1바이트의 유효한 데이터에 3바이트의 0을 채워서 4바이트 int형 데이터로 반환한다. 그래서 이 메소드가 반환하는 정상적인 값의 범위는 0 ~ 255이다. (아스키 코드와 일치)
반면 스트림의 끝에 도달해서 더 이상 읽어 들일 데이터가 없는 경우 -1을 리턴한다.

그리고 위의 while문에서 호출하는 write 메소드는 다음과 같다. 이 메소드도 read 메소드와 유사하게 인자로 전달되는 int형 데이터의 첫번째 바이트만을 파일에 저장한다.

public void write(int b) throws IOException

즉, 위의 예제는 파일의 크기에 상관없이 1바이트씩 쓰고 읽는 복사 프로그램이다.

따라서 크기가 어느 정도 되는 파일을 복사할 경우 제법 오랜 시간이 걸린다.

보다 빠른 속도의 파일 복사 프로그램

바이트 스트림이라 하여 1바이트씩만 읽고 써야 하는 것은 아니다.

다음 예제에서 보이듯이 byte 배열을 생성해서 이를 기반으로 많은 양의 데이터를 한번에 읽고 쓰는 것도 가능하다.

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Scanner;

public class BufferedFileCopier {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		Scanner sc = new Scanner(System.in);
		System.out.print("대상 파일: ");
		String src = sc.nextLine();
		
		System.out.print("사본 이름: ");
		String dst = sc.nextLine();
		
		try(InputStream in = new FileInputStream(src))
		{
			OutputStream out = new FileOutputStream(dst);
			byte buf[] = new byte[1024];
			int len;
			
			while(true)
			{
				len = in.read(buf);  // 배열 buf로 데이터를 읽어들이고
				if(len == -1) break;
				out.write(buf, 0, len); // len 바이트만클  데이터를 저장한다.
				// 원래는 in.read(), 즉, buf로 데이터를 읽어들이고, 이 때 read가 리턴하는 것은 데이터의 byte수구나.
				// 그럼 out.write(buf, 0, len)의 0은 뭐지??
				
			}
		}
		
		catch(IOException e)
		{
			e.printStackTrace();
		}

	}

}

read는 읽어 들인 바이트의 수를 리턴하는데, 스트림의 끝에 도달해서 더 이상 읽어들일 데이터가 없는 경우 -1을 리턴한다.

public void write(byte[] b, int off, int len) throws IOException
-> b로 전달된 배열의 데이터를 인덱스 off에서부터 len바이트만클 파일에 저장

Q. 그럼 read가 파일이 끝나기 전 100 바이트를 읽었는데 파일이 끝나면서 -1을 리턴하면 break에 걸리면서 앞의 이미 읽어들인 100바이트의 데이터를 못 읽게 되는게 아닌가요?

A.

필터 스트림의 이해와 활용

우리는 다음 요구 사항을 만족하는 프로그램을 작성할 수 있다.

"파일로부터 4바이트의 데이터를 읽어들인다."

그러나 다음 요구 사항을 만족하는 프로그램은 아직 작성할 줄 모른다.

"파일로부터 int형 데이터 하나를 읽어들인다."

내용만 놓고 보면 둘 다 4바이트의 데이터를 읽어 들이는 형태이지만 결과는 다르다
필자가 앞서 설명한 방식대로 4바이트의 데이터를 읽어들이는 방법은 다음과 같다.

InputStream in = new FileInputStream("data.dat");
byte buf[] = new byte[4];
in.read(buf); // 4바이트씩 읽어들인다!!

이렇게 읽어 들인 결과는 1바이트 데이터 4개를 읽어 들이는 형태이므로 코드 상에서 int데이터로 활용하지 못한다.

int형 데이터 하나를 읽어들이려면 다음 단계를 거쳐야 한다.

단계 1 : 파일로부터 1바이트 데이터 4개를 읽어들인다.
단계 2 : 읽어 들인 1바이트 데이터 4개를 하나의 int형 데이터로 조합한다.

이 중 두번째 단계의 일을 하는 스트림을 가리켜 '필터 스트림' 이라 한다.
이러한 필터 스트림은 입력 또는 출력 스트림에 더붙여서 데이터를 조합, 가공 및 분리하는 역할을 한다.

ex.

InputStream in = new FileInputStream("data.dat");
DataInputStream fIn = new DataInputStream(in); // 필터 스트림 생성 및 연결

DataInputStream : 기본 자료형 데이터의 입력을 위한 필터 스트림
DataOutputStream : 기본 자료형 데이터의 출력을 위한 필터 스트림

import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class DataFilterOutputStream {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		try(DataOutputStream out = new DataOutputStream(new FileOutputStream("data.dat")))
		{
			out.writeInt(370);
			out.writeDouble(3.14);
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}

	}

}
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class DataFilterInputStream {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		
		try(DataInputStream in = new DataInputStream(new FileInputStream("data.dat")))
		{
			int num1 = in.readInt();
			double num2 = in.readDouble();
			
			System.out.println(num1);
			System.out.println(num2);
		}
		
		catch(IOException e)
		{
			e.printStackTrace();
		}
		
		

	}

}

실행 결과

버퍼링 기능을 제공하는 필터 스트림

BufferedInputStream, BufferedOutputStream

필터 스트림을 구성하는 방법은 이미 설명하였으니, 위의 두 필터 스트림을 기반으로 많은 양의 데이터를 한꺼번에 읽고 쓰는 예제를 작성해보겠다.

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

public class BufferedStreamFileCopier {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		
		Scanner sc = new Scanner(System.in);
		System.out.print("대상 파일: ");
		String src = sc.nextLine();
		
		System.out.print("사본 파일: ");
		String dst = sc.nextLine();
		
		try(BufferedInputStream in = new BufferedInputStream(new FileInputStream(src));
			BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(dst)))
		{
			
			int data;
			
			while(true)
			{
				data = in.read();  // 버퍼링 기능을 제공하는 버퍼 입력 스트림!!
				if(data == -1) break;
				
				out.write(data);
				
			}
		}
		
		catch(IOException e)
		{
			e.printStackTrace();
		}

	}

}

Q. 위의 코드를

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

public class BufferedStreamFileCopier {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		
		Scanner sc = new Scanner(System.in);
		System.out.print("대상 파일: ");
		String src = sc.nextLine();
		
		System.out.print("사본 파일: ");
		String dst = sc.nextLine();
		
		try(BufferedInputStream in = new BufferedInputStream(new FileInputStream(src)))
		{
			BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(dst));
			int data;
			
			while(true)
			{
				data = in.read();  // 버퍼링 기능을 제공하는 버퍼 입력 스트림!!
				if(data == -1) break;
				
				out.write(data);
				
			}
		}
		
		catch(IOException e)
		{
			e.printStackTrace();
		}

	}

}

이와 같이 out을 선언하는 코드를 try의 괄호에서 삭제하고 try문의 중괄호에 작성하면 에러는 일어나지 않으나 파일이 작성되지 않는다. 그 이유가 뭘까?

A.

차이점은 out.close()가 호출되었나 안되었나 인데
out.close()를 호출하여 버퍼 스트림이 닫히면서 out.txt에 데이터가 쓰여지는 구조라고 보는 것이 타당하다.

버퍼링 기능에 대한 대책, flush 메소드의 호출

기본적으로 버퍼링은 성능 향상에 도움을 주지만 다음과 같은 상황이 발생할 수 있어서 주의가 필요하다. (버퍼 출력 스트림에만 해당하는 내용이다.)

"버퍼 스트림에 저장된 데이터가 파일에 저장되지 않은 상태에서 컴퓨터 다운!"

이 때 프로그램 상에서는 write 메소드 호출을 통해 데이터를 저장했음에도 불구하고 실제 파일에는 데이터가 저장되지 않는 일이 발생할 수 있다. 때문에 버퍼가 차지 않아도 파일에 저장해야 할 중요한 데이터가 있다면, 다음 메소드 호출을 통해서 명시적으로 버퍼를 비우라고 (파일로 데이터를 보내라고) 명령할 수 있다.

public void flush() throws IOException

그러나 이 메소드를 빈번히 호출하는 것은 버퍼링을 통한 성능 향상에 방해가 되는 일이기 때문에 제한적으로 호출하는 것이 좋다. 그리고 스트림이 종료되면 버퍼는 자동으로 비워진다.

따라서 스트림을 종료하기 직전에 굳이 이 메소드를 호출할 필요는 없다.

이를 이용하면 위에서 봤던 문제 역시 해결할 수 있다.

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

public class BufferedStreamFileCopier {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		
		Scanner sc = new Scanner(System.in);
		System.out.print("대상 파일: ");
		String src = sc.nextLine();
		
		System.out.print("사본 파일: ");
		String dst = sc.nextLine();
		
		try(BufferedInputStream in = new BufferedInputStream(new FileInputStream(src)))
		{
			BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(dst));
			
			int data;
			
			while(true)
			{
				data = in.read();  // 버퍼링 기능을 제공하는 버퍼 입력 스트림!!
				if(data == -1) break;
				
				out.write(data);
				
			}
			
			out.flush();
		}
		
		catch(IOException e)
		{
			e.printStackTrace();
		}

	}

}

out.close()를 호출하는 대신 out.flush()를 호출하면 버퍼 스트림이 종료되지 않아 파일에 데이터가 출력되지 않는 문제를 해결할 수 있다.

그러나 out.close()를 호출하면 out.flush()를 굳이 호출할 필요가 없고, out.close()의 호출은 필수적이므로 out.close()를 이용하거나 try resource문의 ()에 out을 선언하는 부분을 넣는 것이 더 모범적이다.

파일에 기본 자료형 데이터를 저장하고 싶은데, 버퍼링 기능도 추가하면 좋을 때

파일에 기본 자료형 데이터를 저장하고자 한다면 다음 형태로 스트림을 생성하면 된다.

OutputStream out = new FileOutputStream("data.dat");
DataOutputStream fOut = new DataOutputStream(out);

버퍼링 기능이 있는 출력 스트림은 앞에서 보았듯이 하면된다.

그렇다면 파일을 대상으로 버퍼링 기능을 갖는 스트림을 생성하여 기본 자료형 데이터를 저장하려면 어떻게 스트림을 생성해야 할까?

  1. 출력 스트림 생성
  2. 출력 스트림으로 변환된 데이터를 보내야 하므로 자바 - 필터 스트림 - 출력 스트림 순서여야 함
  3. 버퍼에는 여러 유형의 데이터를 저장할 수 있으나, 필터 스트림은 한꺼번에 많은 데이터를 필터링할 수 없음.
    따라서 자바 - 필터 스트림 - 버퍼 스트림 - 출력 스트림 순서대로 연결되어야 한다.

즉 필터스트림(버퍼스트림(출력스트림)) 순서이어야 한다.

출력 예제

import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class BufferedDataOutputStream {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		try(FileOutputStream out = new FileOutputStream("data.dat");
				BufferedOutputStream bOut = new BufferedOutputStream(out);
				DataOutputStream dOut = new DataOutputStream(bOut))
		{
			dOut.writeInt(370);
			dOut.writeDouble(3.14);
		}
		
		catch(IOException e)
		{
			e.printStackTrace();
		}

	}

}

입력 예제

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class BufferedDataInputStream {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		try(FileInputStream in = new FileInputStream("data.dat");
				BufferedInputStream bIn = new BufferedInputStream(in);
				DataInputStream dIn = new DataInputStream(bIn))
		{
			int num1 = dIn.readInt();  // dIn은 파일의 내용들을 버퍼에 담아놓은 상태, 좀 더 빨리 읽을 수 있음
			double num2 = dIn.readDouble();
			
			System.out.println(num1);
			System.out.println(num2);
		}

	}

}

실행 결과

문자 스트림의 이해와 활용

앞서 설명한 입출력 스트림은 바이트 단위로 입출력이 이루어졌다.
그래서 이를 가리켜 바이트 스트림이라 한다.

반면 문자가 갖는 특성을 고려하여 입출력이 진행되는 문자스트림 이라는 것이 있다.

바이트 스트림과 문자 스트림의 차이

입출력 과정에서 데이터의 변화 없이 바이트 단위로 데이터를 입력 및 출력하는 것이 입출력의 기본이다. 그러나 문자를 입출력할 때에는 약간의 데이터 수정이 필요하다.

그래서 자바에서는 '문자 스트림'이라는 것을 별도로 지원한다. 그렇다면 문자를 입출력할 때에는 데이터의 수정이 왜 필요할까?

이와 관련하여 다음 두 변수에 저장된 문자를 파일에 저장한다고 가정해보자.

char ch1 = 'A';
char ch2 = '한';

자바는 모든 문자를 유니코드를 기준으로 표현한다.

이는 위의 두 변수에 각각 문자 'A'와 문자 '한'이 저장된다는 뜻이다.
그렇다면 이 두 문자를 파일에 저장하는 경우 파일에 유니코드 값이 그대로 저장이 되겠는가?

유니코드 값이 그대로 저장된다고 해서 잘못은 아니다. 그러나 그렇게 저장된 두 문자는 운영체제 상에서 확인할 수 없다. 예를 들어서 윈도우의 메모장 프로그램으로 해당 파일을 열어보았을 때 제대로 된 두 문자를 확인할 수 없다. 이유는 운영체제의 문자 표현 방식이 (인코딩 방식이) 자바의 문자 표현방식과 다르기 때문이다.

운영체제의 문자 표현 방식은 운영체제별로 다르다. 예를 들어서 한글 윈도우의 경우 '코드 페이지949'라는 인코딩 방식을 기준으로 문자를 표현하는데 이 인코딩 방식의 특성은 다음과 같다.

  • 영문과 특수문자는 1바이트로 표현
  • 한글은 2바이트로 표현

결론은 한글 윈도우의 문자 표현 방식이 자바의 문자 표현방식과 다르다는 것이고 따라서 자바 프로그램에서 파일에 문자를 저장하고, 해당 파일을 메모장과 같은 프로그램으로 확인을 하려면 다음과 같은 방식으로 문자를 저장해야 한다.

"유니코드로 표현된 문자를 해당 운영체제의 문자 표현방식으로 바꾸어서 저장한다."

예를 들어서 한글 윈도우에서 확인할 수 있는 형태로 파일에 문자를 저장하려면 자바에서 표현한 문자를 다음과 같이 저장해야 한다.

"유니코드로 표현된 문자를 코드페이지 949 기반으로 바꿔서 저장한다."

그런데 이러한 작업을 프로그래머가 직접 하기엔 많이 번거롭다.
운영체제별로 문자 표현 방식이 다르므로 더욱 번거롭다. 따라서 자바에서 운영체제의 문자 표현 방식에 맞춰서 유니코드로 표현된 문자를 알아서 변환해주면 좋겠다. 그렇다면 다음 예제에서 보이듯이 문자출력 스트림을 생성해서 문자를 저장하면 된다.

import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class SimpleWriter {

	public static void main(String[] args) throws IOException {
		try(Writer out = new FileWriter("data.txt"))
		{
			out.write('A');
			out.write('한');
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}

	}

}

위 예제에서는 data.txt라는 파일을 생성해서 문자 'A'와 '한'을 저장하였다.
따라서 두 문자는 유니코드로 표현되어 write에 전달되지만 스트림이 문자 스트림인 관계로 (FileWriter) 해당 운영체제의 문자 표현방식에 근거하여 각 문자가 인코딩 되어 저장된다. 따라서 한글 윈도우의 경우 다음과 같이 메모장을 통해서 파일의 내용을 확인할 수 있다.

지금까지 문자 출력 스트림에 대해 설명했지만 문자 입력 스트림도 동일한 개념에서 이해하면 된다.
즉, 문자 입력 스트림은 파일에 저장된 데이터를 읽어 들일 때 자바의 문자 표현 방식인 유니코드로 해당 문자 인코딩을 변경하는 스트림이다.

FileReader vs FileWriter

문자 스트림의 생성과 관련된 클래스가 상속하는 클래스는 각각 다음과 같다.

Reader 문자 입력 스트림의 상위 클래스
Writer 문자 출력 스트림의 상위 클래스

파일을 대상으로 하는 문자 입출력 스트림을 생성하는 클래스는 다음과 같다.

FileReader : 파일 대상 문자 입력 스트림 생성
FileWriter : 파일 대상 문자 출력 스트림 생성

이어서 관련 예제를 제시할 텐데, 이에 앞서 어떠한 유형의 스트림을 생성해야 하는지 묻는 다음 질문에 답해보자.

Q1. 문자만 저장되어 있는 파일을 복사하려고 한다. 이 때 필요한 스트림은?

A. 물론 문자 스트림을 통해서도 복사를 진행할 수 있다. 그러나 기본적으로 파일 복사는 파일 내용에 상관없이 있는 그대로의 바이트 정보가 저장된 파일을 하나 더 만드는 일이다. 따라서 바이트 스트림을 생성해서 복사를 진행하는 것이 원칙이다.

Q2. 자바 프로그램에서 문자 하나를 파일에 저장했다가 다시 읽어 들이려 한다. 이 때 필요한 스트림은?

A. 이 경우 파일에 문자룰 저장하는 주체도, 저장된 문자를 읽는 주체도 자바 프로그램이다. 따라서 문자를 유니코드로 저장하고 읽어 들이면 충분하므로 바이트 스트림을 생성하는 것이 옳다. 물론 문자 스트림을 생성해서 이 일을 처리할 수도 있으나 그 과정에서 불필요하게 문자의 인코딩을 변경하는 일만 생기게 된다.

(사람이 이를 열람할 일이 있다면 운영체제의 문자 표현 방식으로 바꾸어 저장하는게 맞으므로 문자 스트림을 생성해서 처리하는 것이 맞다.)

Q3. 운영체제상에서 만든 텍스트 파일의 내용을 자바 프로그램에서 읽어서 출력하려 한다. 이 때 필요한 스트림은?

A. 이 때는 운영체제의 기본 문자 인코딩 방식을 따르는 문자들을 유니코드로 인코딩해야하므로 문자 스트림을 생성해야 한다.

텍스트 파일을 읽는 예제

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Scanner;

public class TextReader {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		Scanner sc = new Scanner(System.in);
		System.out.print("읽을 파일: ");
		String src = sc.nextLine();
		
		try(Reader in = new FileReader(src))  // 문자 스트림 FileReader
		{
			int ch;
			
			while(true)
			{
				ch = in.read();
				if(ch == -1) break;
				
				System.out.print((char)ch);
			}
		}
		
		catch(IOException e)
		{
			e.printStackTrace();
		}

	}

}

public int read() throws IOException : 하나의 문자를 반환, 반환할 문자 없으면 -1 반환

Tip. throws IOException의 의미 : 저 예외를 던질 수 있으므로 처리해줘야 한다는 뜻

그런데 이 메소드의 반환형이 int이다. 이렇듯 반환형이 char가 아닌 int인 이유는 반환할 문자가 없을 때 -1을 반환하기 위함이다. 따라서 문자를 출력할 때에는 다음과 같이 char형으로의 형 변환이 필요하다. 형 변환을 하지 않으면 int형 데이터로 인식되어 정수가 출력된다.

파일 출력 예제

import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class TextWriter {

	public static void main(String[] args) {
		try(Writer out = new FileWriter("out.txt"))
		{
			for(int i = 'A'; i <= 'Z'; i++)
			{
				out.write(i);
			}
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}

	}

}

실행 결과

BufferedReader & BufferedWriter

문자 스트림에 버퍼링 기능을 제공하는 필터 스트림 둘

  • BufferedReader
  • BufferedWriter

특히 위의 두 클래스에는 다음과 같이 문자열 단위로 입력 및 출력을 진행할 수 있는 메소드가 정의되어 있다.

public String readLine() throws IOException -> 문자열 반환, 반환할 문자열 없으면 null 반환

public void write(String s, int off, int len) -> 문자열 s를 인덱스 off에서부터 len개의 문자까지 출력

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class StringWriter {

	public static void main(String[] args) throws IOException {
		String ks = "공부에 있어서 돈이 꼭 필요한 것은 아니다.";
		String es = "Life is long if you know how to use it.";
		
		try(BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt")))
		{
			bw.write(ks, 0, ks.length());
			bw.newLine(); // 줄바꿈 문자를 삽입
			bw.write(es, 0, es.length());
		}
		
		catch(IOException e)
		{
			e.printStackTrace();
		}

	}

}

파일로부터 문자열을 입력받는 예제

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

public class StringReader {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		try(BufferedReader in = new BufferedReader(new FileReader("in.txt")))
		{
			String str;
			while(true)
			{
				str = in.readLine();
				if(str == null) break;  // 개행문자라도 있으면 null 리턴하지 X
				System.out.println(str);
			}
		}
		
		catch(IOException e)
		{
			e.printStackTrace();
		}

	}

}

0개의 댓글