서론
이번에는 자바에서 입출력에 대해 알아보자.
코딩테스트를 준비하며, 왜 BufferedReader와 BufferedWriter가 더 빠른지 궁금했었는데,
이번 포스팅을 통해 제대로 알아보도록 하자.
Stream
- stream(스트림)의 특징 : 단방향이다.
- 데이터의 흐름을 의미한다.
- 프로그램의 관점에서 들어오는 것 : InputStream
- 프로그램의 관점에서 나가는 것 : OutputStream
종류
- 바이트(byte) 기반 스트림
: 그림, 멀티미디어 등의 바이너리 데이터를 읽고 출력할 때 사용 (모든 종류의 데이터를 보내고 받는 것이 가능) - 문자(character) 기반 스트림
: 문자 데이터를 일고 출력할때 사용
구분 | 바이트 기반 스트림 | 문자 기반 스트림 | ||
입력 스트림 | 출력 스트림 | 입력 스트림 | 출력 스트림 | |
최상위 클래스 | InputStream | OutputStream | Reader | Writer |
하위 클래스 | (XXX)InputStream | (XXX)OutputStream | (XXX)Reader | (XXX)Writer |
하위 클래스(예시) | FileInputStream | FileOutputStream | FileReader | FileWriter |
1. InputStream
주요 메소드
리턴 타입 | 메소드 | 설명 |
int | read() | 1byte를 읽고 읽은 바이트리턴 더 이상 입력 스트림으로부터 바이트를 읽을 수 없으면 -1 리턴 |
int | read(byte[] b) | 읽은 바이트를 매개값으로 주어진 배열에 저장하고, 읽은 바이트 수를 리턴 |
int | read(byte[] b, int off, int len) | len개의 바이트를 읽고 매개값으로 주어진 배열에서 b[off]부터 len개까지 저장합니다. 그리고 읽은 바이트 수를 리턴 |
void | close() | 입력 스트림을 닫습니다. |
여기서 알아둬야할 것이 있다.
아스키코드의 경우 문자 1개가 1byte로 이루어져있지만,
한글의 경우 유니코드 인코딩 방식 중 UTF-8인 경우에는 3byte가 필요하다.
또한, 자바에서 char는 2byte이기 때문에 char로 형변환을 한다면 한글이 깨져서 보일 것이다.
이를 위해서 어떻게 해아할지 알아보자.
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class Main {
public static void main(String[] args) throws IOException {
InputStream is = new FileInputStream(입력할 input.txt의 경로);
byte readBytes[] = new byte[3];
int byteNum;
String data = "";
while (true) {
if ((byteNum = is.read(readBytes)) == -1) {
break;
}
data = new String(readBytes, 0, byteNum);
System.out.print(data + "_");
}
}
}
위처럼 byte 배열을 3바이트로 설정해두고, 3바이트씩 끊어서 읽으면 된다!.
읽은 바이트를 String 생성자를 통해 문자열로 만들어주면 아래와 같은 결과를 얻을 수 있다.
2. OutputStream
주요 메소드
리턴 타입 | 메소드 | 설명 |
void | write(byte[] b) | 매개값으로 주어진 배열 b의 모든 바이트를 출력합니다. |
void | write(byte[] b, int off, int len) | 매개값으로 주어진 배열 b[off]부터 len개까지의 바이트를 출력합니다, |
void | flush() | 출력 버퍼에 잔류하는 모든 바이트를 출력합니다. |
void | close() | 출력 스트림을 닫습니다. |
write(byte[] b)를 통해 output.txt에 안녕하세요를 출력해보자.
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class OutputStreamExample {
public static void main(String[] args) throws IOException {
OutputStream os = new FileOutputStream(출력할 output.txt의 경로);
byte[] data = "안녕하세요".getBytes();
os.write(data);
os.flush();
os.close();
}
}
※ 하지만 OutputStream은 기존 파일에 추가되며 쓰는게 아니라,
기존의 내용을 없애고 써지기 때문에 파일 관련이면 File 클래스를 사용한다.
3. Reader
리턴 타입 | 메소드 | 설명 |
int | read() | 1개의 문자를 읽고 리턴합니다. |
int | read(char[] cbuf) | 읽은 문자들을 매개값으로 주어진 문자 배열에 저장하고, 읽은 문자 수를 리턴합니다. |
int | read(char[] cbuf, int off, int len) | len개의 문자를 읽고 매개값으로 주어진 문자 배열에서 cbuf[off]부터 len개까지 저장합니다. 그리고 읽은 문자 수를 리턴합니다. |
void | close() | 입력 스트림을 닫습니다. |
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
public class ReaderExample {
public static void main(String[] args) throws IOException {
Reader reader = new FileReader(read.txt의 파일 경로);
while (true) {
int readData = reader.read();
if (readData == -1) {
break;
}
System.out.print((char) readData);
}
}
}
//결과 : 안녕하세요
여기서 의문이 생겼다.
char는 2byte인데, 어떻게 3byte의 한글을 읽을 수 있을까?
이를 알아보기 위해서 FileReader의 생성자부터 차근차근 확인해보자.
해당 코드는 파일을 매개변수로 넣었을때의 생성자이다.
여기서 super는 위에서 상속구조를 통해 알아본 것처럼 InputStreamReader이므로, 이를 따라가보자.
이는 InputStreamReader의 생성자이다.
여기서의 super는 Reader이며, sd는 StreamDecoder의 객체이다.
궁금증을 해결하기 위해서는 StreamDecoder의 매개변수인 Charset.defaultCharset()을 확인해보아야 했다.
여기서 defaultCharset이 null인 경우에, 파일 인코딩 프로퍼티에서 인코딩 방식을 가져와서 사용하는 것 같다.
이를 매개변수로, 새로운 StreamDecoder객체를 생성하고 주소를 받게된다.
그렇다면, 이제 제일 궁금했던 부분을 알아보자.
StreamDecoder 클래스의 read0()메소드를 알아보자
- 기본적으로 char[2]만큼의 공간 (즉 4byte)를 할당해둔다.
- read(cbuf[], offset, length)를 호출하여, cb[1]에 저장된 문자가 있을 경
- InputStreamReader 클래스의 read메소드를 통해, 2개의 문자를 읽었을 때,
case 2에서 두번째 문자를 leftoverChar에 저장하고 haveLeftoverChar를 true로 설정합니다. - 이후, case에 break가 없기 때문에 case 1의 코드를 수행하면 cb[0] = "안"을 리턴해줍니다.
- 처음 코드에서 while이기 때문에, 다시 read()를 호출하면, haveLeftoverChar == true이기 때문에,
이전 반복문에서 저장해두었던 leftoverChar = "녕"을 반환합니다.
이렇게 보니, cb[0] = '안', cb[1] = '녕'이 들어있는 것을 볼 수 있다.
그렇다면 처음부터 알아보는 방식이 잘못된 것이다.
StreamDecoder 클래스의 read() 메소드에서 cb[2]로 설정되어 있어 4byte에 하나의 한글을 저장하는 것이라고 생각했다.
하지만, 자바의 기본 타입인 char 자체는 UTF-16으로 저장하여, 한글을 2byte로 저장한다고 한다.!
4. Writer
리턴 타입 | 메소드 | 설명 |
void | write(int c) | 매개값으로 주어진 한 문자를 보냅니다. |
void | write(char[] cbuf) | 매개값으로 주어진 배열의 모든 문자를 보냅니다. |
void | write(char[] cbuf, int off, int len) | 매개값으로 주어진 배열에서 cbuf[off]부터 len개까지의 문자를 보냅니다. |
void | write(String str) | 매개값으로 주어진 문자열을 보냅니다. |
void | write(String str, int off, int len) | 매개값으로 주어진 문자열에서 off순번부터 len개까지의 문자를 보냅니다. |
void | flush() | 버퍼에 잔류하는 모든 문자를 출력합니다. |
void | close() | 출력 스트림을 닫습니다. |
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
public class WriterExample {
public static void main(String[] args) throws IOException {
Writer writer = new FileWriter(출력할 파일 경로);
writer.write(new char[]{'안', '녕', '하', '세', '요'});
writer.write("\n안녕하세요");
writer.flush();
writer.close();
}
}
보조 스트림
- 다른 스트림과 연결되어 여러 가지 편리한 기능을 제공해주는 스트림
- 자체적으로 입출력을 수행할 수 없기 때문에, 입출력 소스와 바로 연결되는
InputStream, OutputStream, Reader, Writer와 연결해서 입출력을 수행한다.
- Buffered가 붙은 스트림은 성능 향상을 위한 보조스트림이다.
- 파일에서 입력을 받는 경우, 파일이 저장된 하드디스크에서 데이터를 받는데 하드디스크는 처리 속도가 메모리에 비해 느리다.
- 따라서, 입출력 소스 (예를들어 파일)과 직접 작업하지 않고 중간에 메모리 버퍼와 작업함으로써 성능을 향상시킬 수 있다.
1. InputStreamReader + BufferedReader & OutputStreamWriter + BufferedWriter
- InputStreamReader, OutputStreamWriter : 문자 변환 보조 스트림
- BufferedReader, BufferedWriter : 성능향상 보조 스트림
전송한 데이터를 내부 버퍼에 쌓아두고 버퍼가 꽉 차면, 모든 데이터를 한번에 보낸다.
import java.io.*;
public class BufferedReaderExample {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
bw.write(br.readLine());
br.close();
bw.flush();
bw.close();
}
}
2. DataInputStream + DataOutputStream
- 기본 타입인 boolean, char, short, int, long, float, double을 출력할 수 있다.
package DataInputOutputStream;
import java.io.*;
public class DataInputOutputStreamExample {
public static void main(String[] args) throws IOException {
DataOutputStream dos = new DataOutputStream(new FileOutputStream(.db파일 경로));
dos.writeInt(111);
dos.writeFloat(111.1111F);
dos.writeBoolean(true);
dos.writeUTF("안녕하세요");
DataInputStream dis = new DataInputStream(new FileInputStream(.db파일 경로));
int i = dis.readInt();
float f = dis.readFloat();
boolean b = dis.readBoolean();
String str = dis.readUTF();
System.out.println(i);
System.out.println(f);
System.out.println(b);
System.out.println(str);
dos.flush();
dis.close();
dos.close();
}
}
3. ObjectOutputStream + ObjectInputStream
- ObjectOutputStream : 객체를 직렬화
- ObjectInputStream : 객체를 역직렬화
- 가장 중요한 것은 직렬화할 객체가 Serializable 인터페이스를 implements한 클래스여야 한다!.
package ObjectStreamExample;
import java.io.Serializable;
public class User implements Serializable {
private String name;
private String id;
private String pwd;
public User(String name, String id, String pwd) {
this.name = name;
this.id = id;
this.pwd = pwd;
}
//getter.. setter..
}
import java.io.*;
public class ObjectStreamExample {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(.db 파일 경로));
User user = new User("홍길동", "test", "test");
oos.writeObject(user);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(.db 파일 경로);
User newUser = (User) ois.readObject();
System.out.println(newUser.getName());
System.out.println(newUser.getId());
System.out.println(newUser.getPwd());
}
}
잘 입출력 되는 것을 확인할 수 있다.
결론
이번에 공부하며, 문자 배열 cb[2]에 3바이트가 나눠서 들어가는 줄 알았는데,
알고보니 char 기본 저장 방식이 UTF-16이라 한글이 2바이트로 저장되어 가능한 것이라는 걸
코드를 따라가다보니, 잘못된 것을 발견하고 알게되었다.
하지만, 잘못된 것을 제대로 알아보기 전에는 잘못된 것도 모르고 그냥 그렇구나 하고 넘어갈 수 있었던 것을
제대로 파악할 수 있었던 것 같아서 끈기 있게 찾아본 것이 나름 뿌듯하다!.
그리고 객체 직렬화와 역직렬화, 코딩테스트를 준비할때 많이 사용하던 보조스트림 등을
왜 그리고 어떻게 사용하는지 알아볼 수 있어서 좋았던 것 같다!.
'프로그래밍 > Java' 카테고리의 다른 글
[Java] 6. Singleton 패턴 2 (3) | 2024.01.18 |
---|---|
[Java] 4. hashCode(), equals() (feat. HashMap, HashSet) (0) | 2023.11.08 |
[Java] 3. 싱글톤 패턴 (Singleton Pattern) (0) | 2023.10.27 |
[Java] 2. 람다 표현식 (Lambda Expression) (0) | 2023.08.07 |
[Java] 1. Comparable vs Comparator (feat. 정렬) (0) | 2023.08.04 |