숑숑이의 개발일기

본글은 'Do it 자바 완전 정복' 책을 통한 공부내용을 정리한 글입니다.

 

앞에서 알아본 byte 단위의 입출력만으로도 파일, 콘솔, 네트워크 전송까지 전혀 문제 없이 입출력을 수행할 수 있지만, 실제 입출력에서 가장 많이 사용되는 데이터는 단연 문자열이다. 대표적으로 채팅 프로그램이 있다. char 단위의 입출력은 문자열 입출력을 위해 특화된 기법으로, char 단위로 입출력을 수행하는 기본 클래스는 ReaderWriter 추상 클래스다.

 

Reader와 Writer 상속 구조

Reader 클래스의 자식 클래스에는 FileReader, BufferedReader, InputStreamReader가 있다. Writer의 자식 클래스에는 FileWriter, BufferedWriter, OutputStreamWriter, PrintWriter가 있다. byte 단위의 입출력을 처리하는 클래스와 비교해 보면 InputStream과 OutputStream이 Reader와 Writer로 변경됐고, 다양한 타입으로 출력하는 Data(Input/Output)Stream에 대응되는 클래스가 빠져 있는 형태다.

대신 InputStreamReader와 OuputStreamReader 클래스가 추가되어 있는데, 이는 byte 단위의 데이터 입출력 클래스인 Input/OutputStream을 char 단위의 입출력 클래스 Reader/Writer로 변환하는 클래스다.

 

Reader의 주요 메서드

메서드 기능
int skip(long n) n개의 char 스킵(실제 스킵된 char 개수 리턴)
int read() int(4byte)의 하위 2byte에 읽은 데이터를 저장해 리턴
int read(char[] cbuf) 읽은 데이터를 char[] cbuf에 저장하고, 읽은 char 개수 리턴
abstract int read(char[] cbuf, int off, int len) len 개수만큼 읽은 데이터를 char[] cbuf의 off 위치부터 저장
abstract void close() Reader의 자원 반환

read() 메서드는 byte 단위의 입출력과 비교했을 때 byte[] 배열이 char[] 배열로 바뀐 것을 제외하면 동일하다. Reader는 추상 메서드를 포함하고 있는 추상 클래스이므로 객체를 스스로 생성하지 못한다. 직접 추상 메서드를 구현할수도 있지만, JNI로 오버라이딩 해야 하므로 직접 자식 클래스를 생성하는 것보다 read() 메서드를 구현한 자식 클래스로 Reader 객체를 생성해 사용하는 것이 효율적이다.

 

Writer의 주요 메서드

메서드 기능
abstract void flush() 메모리 버퍼에 저장된 데이터 내보내기(실제 출력 수행)
void write(int c) int(4byte)의 하위 2byte를 메모리 버퍼에 출력
void write(char[] cbuf) 매개변수로 넘겨진 char[] cbuf 데이터를 메모리 버퍼에 출력
void write(String str) 매개변수로 넘겨진 String 값을 메모리 버퍼에 출력
void write(String str, int off, int len) str의 off 위치에서부터 len 개수를 읽어 메모리 버퍼에 출력
abstract void write(char[] cbuf, int off, int len) char[]의 off 위치에서부터 len 개수를 읽어 출력
abstract void close() Writer의 자원 반환

Writer 클래스의 write() 메서드 또한 byte[] 배열 대신 char[] 배열이 사용되며, 추가로 String 타입도 매개변수로 사용할 수 있다. Reader 때와 마찬가지로 객체를 직접 생성하는 것 보다, 자바 API에서 제공하는 자식 클래스를 사용하여 객체를 생성하는 것이 효율적이다.

 

Reader/Writer 객체 생성 및 활용

먼저 FileReader와 FileWriter는 파일로부터 문자열을 읽거나 파일에 문자열을 저장하는 데 사용하는 클래스로, 입출력을 char 단위로 수행한다. char 단위로 입출력을 수행한다는 것은 문자열에 영문, 한글, 다국어 문자가 포함되어도 차이가 전혀 없다는 것을 의미한다. char 타입 자체가 다국어 문자를 저장할 수 있는 유니코드이기 때문이다.

 

FileReader 객체는 아래의 2개 생성자 중 하나로 생성할 수 있다.

FileReader(File file) 매개변수로 넘어온 file을 읽기 위한 Reader 생성
FileReader(String name) 매개변수로 넘어온 name 위치의 파일을 읽기 위한 Reader 생성

 

FileWriter의 생성자는 다음 4개의 생성자 중 하나를 사용할 수 있으며, 매개변수로는 File 객체 또는 파일 경로의 문자열을 받을 수 있다.

FileWriter(File file) - 매개변수로 넘어온 file을 쓰기 위한 Writer 생성
- append = true일 때 이어쓰기
- append = false일 때 덮어쓰기(default = false)
FileWriter(File file, boolean append)
FileWriter(String name) - 매개변수로 넘어온 name 위치의 file을 쓰기 위한 Writer 생성
- append = true일 때 이어쓰기
- append = false일 때 덮어쓰기(default = false)
FileWriter(String name, boolean append)

파일에 이어쓰기 여부를 지정하는 boolean 타입 매개변수를 포함하고 있는 생성자를 제공하며, 이 값의 기본값은 false이므로 따로 지정하지 않으면 파일은 항상 덮어쓰기가 된다.

 

그럼 이들 객체를 이용해 한글과 영문이 혼용된 파일을 작성하고, 이를 다시 읽어 출력하는 코드를 살펴보자.

import java.io.*;

public class FileReaderWriter {
	public static void main(String[] args) {
    	File readerWriterFile = new File("src/files/text.txt");
        
        // 파일 쓰기
        try (Writer writer = new FileWrter(readerWriterFile)) {
        	writer.write("안녕하세요\n".toCharArray());
            writer.write("Hello");
            writer.write('\r');
            writer.write('\n');
            writer.write("반갑습니다.", 2, 3);
            writer.flush();
        } catch(IOException e) {}
        
        // 파일 읽기
        try(Reader reader = new FileReader(readerWriteFile)) {
        	int data;
            while((data = reader.read()) != -1) {
            	System.out.print((char)data);
                // 안녕하세요
                // Hello
                // 습니다
            }
        } catch(IOException e) {}
    }
}

getBytes() 메서드가 문자열 -> byte[]로 변환하는 메서드였다면 toCharArray() 메서드는 문자열 -> char[]로 변환하는 String 클래스의 메서드다.

 

BufferedReader/BufferedWriter로 속도 개선

char 단위의 입출력도 메모리 버퍼를 사용함으로써 속도를 향상할 수 있는 BufferedReader/Writer 클래스를 제공한다.

 

BufferedReader 객체는 아래의 2개의 생성자 중 하나를 사용해 생성할 수 있으며, 두 생성자 모두 Reader 객체를 매개변수로 지니고 있다. FilterReader/Writer 클래스의 하위 클래스가 아닌, 개념상으로는 여전히 Reader에 연결된 필터의 역할을 하기 때문이라고 볼 수 있다.

// BufferedReader의 생성자
BufferedReader(Reader in)
BufferedReader(Reader in, int size)	// 버퍼의 크기를 직접 지정 가능

두 번째 생성자에서는 내부 메모리 버퍼의 크기를 직접 지정할 수 있으며, 생략했을 때에는 기본값 버퍼 크기를 사용한다.

 

BufferedReader 클래스에서 특이한 점은 단순히 속도의 향상뿐 아니라 1줄의 데이터를 읽어 문자열로 리턴하는 메서드 readLine() 메서드가 추가됐다는 것이다. readLine() 메서드는 여러 줄로 구성된 텍스트 파일을 1줄씩 읽어 처리할 때 매우 유용하다. 해당 메서드 사용시 파일의 마지막에서 -1을 리턴하는 것이 아닌 null을 리턴하는 것에 유의한다.

 

BufferedWriter 생성자도 2개가 존재하며, 매개변수로 Writer 객체를 받는다.

// BufferedWriter 생성자
BufferedWriter(Writer out)
BufferedWriter(Writer out, int size) // 버퍼의 크기를 직접 지정 가능

 

예제코드

import java.io.*;

public class BufferedReaderWriter {
	public static void main(String[] args) {
    	File readerWriterFile = new File("src/files/Buffered.txt");
        
        // 파일 쓰기
        try (Writer writer = new FileWriter(readerWriterFile);
        	BufferedWriter bw = new BufferedWriter(writer);) {
            bw.write("안녕하세요\n".toCharArray());
            bw.write("Hello");
            bw.write('\r');
            bw.write('\n');
            bw.write("반갑습니다.", 2, 3);
            bw.flush();
		} catch(IOException e) {}
        
        // 파일 읽기
        try (Reader reader = new FileReader(readerWriterFile);
        	BufferedReader br = new BufferedReader(reader);) {
            String data;
            while((data=br.readLine()) != null) {
            // 파일의 마지막이 -1이아닌 null
            	System.out.println(data);
            }
		} catch(IOException e) {}
    }
}

 

InputStreamReader/OutputStreamWriter로 Reader/Writer 객체 생성

이미 byte 단위로 입출력을 수행한 객체(InputStream, OutputStream)를 char 단위의 입출력 객체(Reader, Writer)로 변환해야 할 때가 있다. 이때 사용하는 클래스가 바로 InputStreamReader와 OutputStreamWriter다. 이름에서도 유추할 수 있듯이 InputStreamReader는 InputStream을 Reader로 변환하고 OutputStreamWriter는 OutputStream을 Writer로 변환한다.

 

InputStreamReader 객체는 아래의 3개 생성자 중 1개를 이용해 생성한다.

// InputStreamReader의 생성자
InputStreamReader(InputStream in)
InputStreamReader(InputStream in, Charset cs)	// 매개변수로 전달되는 문자셋을 기준으로 byte->char로 변환
InputStreamReader(InputStream in, String charsetName)

 

객체를 생성할 때 중요한 점은 문자셋이다. byte단위를 char단위 즉, 문자 단위로 변환하는 과정에서 어떤 문자셋을 사용해야 하는지 알려줘야 한다. 문자셋은 Charset 객체를 생성해 넘겨주거나, 문자열로 문자셋의 이름을 넘겨줄 수도 있다. 문자셋을 생략한 첫 번째 생성자에는 기본값 문자셋이 적용된다.

 

OutputStreamWriter도 아래의 3개 생성자 중 1개를 이용해 생성하고, 매개변수로 OutputStream과 문자셋을 넘겨받는다.

// OutputStreamWriter의 생성자
OutputStreamWriter(OutputStream out)
OutputStreamWriter(OutputStream out, Charset cs) // 매개변수로 전달되는 문자셋을 기준으로 byte->char로 변환
OutputStreamWriter(OutputStream out, String charsetName)

 

여기서 하나 짚고 넘어가야할 점은 Reader와 Writer 클래스는 char 단위로 입출력이 수행되므로 쓰고 읽는 과정에서 문자셋을 지정할 필요가 없었다. 그러나 InputStreamReaderOutputStreamWriter는 이미 문자셋에 영향을 받는 byte 단위로 분할된 객체를 다시 char 단위로 조합해야 하기 때문에 이 조합 과정에서 문자셋의 지정이 필요한 것이다.

 

아래 코드에서 콘솔 입력일 경우를 살펴본다. 콘솔 입력을 위해 제공되는 객체 System.in은 InputStream 타입으로, 콘솔의 입력은 byte 단위로 이뤄진다. 이를 char 단위로 처리하기 위해서는 InputStreamReader 클래스를 사용해야 한다.

import java.io.*;

public class InputStreamReader2 {
	public static void main(String[] args) {
    	// 콘솔로 입력(UTF-8)
    	try {
        	InputStreamReader isr = new InputStreamReader(System.in, "UTF-8");
            int data;
            while((data = isr.read()) != '\r') {
            	System.out.print((char)data);
            }
            System.out.println("\n" + isr.getEncoding());
        } catch (IOException e) {}
        
        // 콘솔로 입력(MS949)
        try {
        	InputStreamReader isr = new InputStreamReader(System.in, "MS949");
            int data;
            while((data = isr.read()) != '\r') {
            	System.out.print((char)data);
            }
            System.out.println("\n" + isr.getEncoding());
        } catch (IOException e) {}
    }
}

콘솔에 입력되는 문자는 UTF-8 기준으로 쓰이고, 첫번째 코드에서는 문자셋을 UTF-8로 지정함으로써 입력한 문자가 정상적으로 저장되었을 것이다. 반면 두 번째 코드에서는 문자셋을 MS949로 지정했다. UTF-8로 쓰여진 문자를 MS949로 읽었으므로, 한글을 입력했을 때에는 모두 깨진 상태로 저장될 것이다.

 

아래 예제코드에서는 OutputStreamWriter로 파일을 쓰는 것에 대해 알아본다.

import java.io.*;

public class OutputStreamWriter1 {
	public static void main(String[] args) {
    
    	// 1. FileWriter을 이용해 데이터 쓰기(문자셋 지정x)
    	File outputStreamWriter1 = new File("src/files/OutputStreamWriter1.txt");
        try (Writer writer = new FileWriter(outputStreamWriter1);) {
        	writer.write("예제파일\n".toCharArray());
            writer.write("한글+영문 모두 포함");
            writer.flush();
        } catch(IOException e) {}
        
        // 2. FileWriter을 이용해 데이터 쓰기(문자셋 지정o)
        File outputStreamWriter2 = new File("src/files/OutputStreamWriter2.txt");
        try (OutputStream os = new FileOutputStream(outputStreamWriter2, false);
        	OutputStreamWriter osw = new OutputStreamWriter(os, "MS949");) {
            osw.write("예제파일\n".toCharArray());
            osw.write("한글+영문 모두 포함");
            osw.flush();
		} catch(IOException e) {}
    }
}

첫번째 코드에서는 기본값 문자셋이 적용된다(UTF-8). 두번째 코드에서는 byte 단위의 FileOutputStream을 먼저 생성하고 이를 char 단위로 변환하는 OutputStreamWriter 클래스를 사용해 객체 생성 과정에서 문자셋을 지정했기 때문에 이때 생성된 파일은 MS949(ANSI) 문자셋으로 인코딩되어 저장된다.

 

PrinterWriter로 Writer 객체 생성

PrinterWriter는 PrinterStream과 같이 다양한 타입의 출력에 특화된 케이스로, 자동 flush 기능이 추가되어 있으므로 flush 메서드를 호출할 필요가 없다.

 

PrintWriter 클래스의 생성자는 아래와 같이 6개가 있고, byte 단위의 입출력 클래스인 PrintStream과 비교하면 Writer 객체를 매개변수로 포함하고 있는 생성자가 추가된 형태다.

// PrintWriter 생성자
PrintWriter(File file)
PrintWriter(String fileName)
PrintWriter(OutputStream out)
PrintWriter(OutputStream out, boolean autoFlush)
PrintWriter(Writer out)
PrintWriter(Writer out, boolean autoFlush)

생성자에서 주의깊게 살펴볼 사항은 autoFlush이다. autoFlush=treu라면 말 그대로 flush가 수행된다. 기본값이 false 이므로 자동 flush를 사용하고 싶다면 매개변수를 true로 넘겨 줘야 한다.

 

PrintWriter의 대표적인 메서드

PrintStream와 동일하게 print(), println(), printf() 메서드를 들 수 있다.

개행 포함 개행 미포함
void println(boolean b)
void println(char c)
void println(int i)
void println(long l)
void println(float f)
void println(double d)
void println(String s)
void println(Object obj)
void println() -> 출력값 없이 개행만 수행
void print(boolean b)
void print(char c)
void print(int i)
void print(long l)
void print(float f)
void print(double d)
void print(String s)
void print(Object obj)
PrintWriter printf(String format, Object.. args) -> 연속호출가능

printf() 메서드가 PrintWriter 타입을 리턴한 점을 제외하면 PrintStream 메서드와 사용법이 동일하므로 별도의 설명은 생략한다. 

profile

숑숑이의 개발일기

@숑숑-

풀스택 개발자 준비중입니다