일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 자바의 정석 기초편 ch7
- 자바 고급2편 - io
- 자바 기초
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch1
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch4
- 스프링 트랜잭션
- 자바로 계산기 만들기
- 자바 중급2편 - 컬렉션 프레임워크
- 스프링 mvc2 - 타임리프
- 자바 중급1편 - 날짜와 시간
- 2024 정보처리기사 시나공 필기
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch9
- 자바 고급2편 - 네트워크 프로그램
- 스프링 입문(무료)
- 2024 정보처리기사 수제비 실기
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch5
- 자바로 키오스크 만들기
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch11
- @Aspect
- 데이터 접근 기술
- 람다
- 스프링 mvc2 - 검증
- Today
- Total
개발공부기록
I/O 기본, 문자 다루기(시작, 스트림을 문자로, Reader, Writer, BufferedReader), 기타 스트림 본문
I/O 기본, 문자 다루기(시작, 스트림을 문자로, Reader, Writer, BufferedReader), 기타 스트림
소소한나구리 2025. 2. 21. 19:18출처 : 인프런 - 김영한의 실전 자바 - 고급2편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
문자 다루기
시작
스트림의 모든 데이터는 byte 단위를 사용하기 때문에 문자를 스트림에 직접 전달할 수 없으므로 String을 byte로 변환한 다음에 저장해야 함
TextConst
예제에서 공통으로 다룰 상수를 먼저 작성
package io.text;
public class TextConst {
public static final String FILE_NAME = "temp/hello.txt";
}
ReaderWriterMainV1
package io.text;
public class ReaderWriterMainV1 {
public static void main(String[] args) throws IOException {
String writeString = "ABC";
// 문자 - byte -> UTF-8 인코딩
byte[] writeBytes = writeString.getBytes(UTF_8);
System.out.println("writeString = " + writeString);
System.out.println("writeBytes = " + Arrays.toString(writeBytes));
// 파일에 쓰기
FileOutputStream fos = new FileOutputStream(FILE_NAME);
fos.write(writeBytes);
fos.close();
// 파일에서 읽기
FileInputStream fis = new FileInputStream(FILE_NAME);
byte[] readBytes = fis.readAllBytes();
fis.close();
// byte -> String UTF-8 디코딩
String readString = new String(readBytes, UTF_8);
System.out.println("readBytes = " + Arrays.toString(readBytes));
System.out.println("readString = " + readString);
}
}
/* 실행 결과
writeString = ABC
writeBytes = [65, 66, 67]
readBytes = [65, 66, 67]
readString = ABC
*/
hello.txt - 실행결과
byte[] writeBytes = writeString.getBytes(UTF_8);
- String을 byte로 변환할 때는 String.getBytes(Charset)을 사용하면 됨
- 이때 문자를 byte 숫자로 변경해야 하기 때문에 반드시 문자 집합(인코딩 셋)을 지정해야 함
- 예제에서는 UTF_8로 인코딩하였으며 ABC를 인코딩하면 65, 66, 67이 됨
이렇게 만든 byte[]을 FileOutputStream에 write()로 전달하면 65, 66, 67을 파일에 저장할 수 있게 되고 결과적으로 의도한 ABC 문자를 파일에 저장할 수 있음
String readString = new String(readBytes, UTF_8);
- 반대의 경우도 String 객체를 생성할 때 읽어 들인 byte[]과 디코딩할 문자 집합을 전달하면 byte[]를 String 문자로 복원할 수 있음
여기서 핵심은 스트림은 byte만으로 사용할 수 있으므로 String과 같은 문자는 직접 전달할 수 없다는 점임
그래서 개발자가 번거롭게 String + 문자집합 -> byte[], byte[] + 문자집합 -> String과 같은 변환 과정을 직접 호출해주어야 함
자바는 이런 번거로운 변환 과정을 대신 처리해 주는 기능을 제공함
스트림을 문자로
ReaderWriterMainV2
- OutputStreamWriter: 스트림에 byte 대신에 문자를 저장할 수 있게 지원
- InputStreamReader: 스트림에 byte 대신에 문자를 읽을 수 있게 지원
- 앞서 다루었던 BufferedXxx와 비슷하게 사용되며 bytes로 변환하지 않고 문자를 직접 사용하여 파일에 저장하고, 파일의 내용을 읽어오는 것을 확인할 수 있음
- 읽어올 때는 isr.read()는 반환 타입이 int이므로 char로 형변환 후 String에 담아서 출력해야 함
package io.text;
public class ReaderWriterMainV2 {
public static void main(String[] args) throws IOException {
String writeString = "가나다";
System.out.println("writeString = " + writeString);
// 파일에 쓰기
FileOutputStream fos = new FileOutputStream(FILE_NAME);
OutputStreamWriter osw = new OutputStreamWriter(fos, UTF_8);
osw.write(writeString);
osw.close();
// 파일에서 읽기
FileInputStream fis = new FileInputStream(FILE_NAME);
InputStreamReader isr = new InputStreamReader(fis, UTF_8);
StringBuilder content = new StringBuilder();
int ch;
while ((ch = isr.read()) != -1) {
content.append((char) ch); // 문자로 캐스팅해야 함
}
isr.close();
System.out.println("readString = " + content);
}
}
/* 실행 결과
writeString = 가나다
readString = 가나다
*/
hello.txt - 실행결과
OutputStreamWriter
- OutputStreamWriter는 입력받은 문자를 인코딩해서 byte[]로 변환함
- OutputStreamWriter는 변환한 byte[]을 전달할 OutputStream과 인코딩 문자 집합에 대한 정보가 필요하므로 두 정보를 생성자를 통해서 전달해야 함
- osw.write(writeString)으로 String 문자를 직접 전달하면 그림처럼 문자 인코딩을 통해 byte[]로 변환하고 변환 결과를 FileOutputStream에 전달되는 과정을 거쳐서 저장이 됨
InputStreamWriter
- 데이터를 읽을 때는 int ch = read() 메서드를 제공하는데, 이 메서드는 문자 하나인 char형으로 데이터를 받게 되지만 실제 반환타입은 int형이므로 최종적으로 char형으로 캐스팅해서 사용하면 됨
- read() 메서드가 int형으로 반환되는 이유는 자바의 char형은 파일의 끝인 -1을 표현할 수 없기 때문에 더 넓은 범위인 int형으로 문자를 반환받게 된 것임
- 즉, 그림에서 처럼 FileInputStream에서 byte[]을 읽어온 후 InputStreamReader는 읽은 byte[]을 지정한 문자 집합을 통해 char 형태로 변환해서 반환하지만, read()메서드의 반환타입이 int이므로 최종적으로 출력을 할 때는 char로 캐스팅해주면 됨
OutputStreamWriter, InputStreamReader 덕분에 편리하게 문자를 byte[]로 변경하고 그 반대의 경우도 가능하게 되어 쉽게 String 문자를 파일에 저장할 수 있음
스트림을 배울 때 byte 단위로 데이터를 읽고 쓰는 것을 확인했으며 write()의 경우에도 byte 단위로 데이터를 읽고 썼음
최상위 부모인 OutputStream의 경우 write()가 byte 단위로 입력하도록 되어있는데 OutputStreamWriter의 write()는 byte가 아니라 String이나 char를 사용하고 있음
그 이유는 OutputStreamWriter의 부모는 OutputStream이 아니기 때문임
Reader, Writer
자바는 byte를 다루는 I/O 클래스와 문자를 다루는 I/O 클래스를 둘로 나누어 두었음
byte를 다루는 클래스
- byte를 다루는 클래스는 OutputStream, InputStream의 자식임
- 부모 클래스의 기본 기능도 byte 단위를 다루며 클래스 이름 마지막에 보통 OutputStream, InputStream이 붙어있음
문자를 다루는 클래스
- 문자를 다루는 클래스는 Writer, Reader의 자식임
- 부모 클래스의 기본 기능은 String, char 같은 문자를 다루며 클래스 이름 마지막에 Writer, Reader가 붙어있음
- 문자는 너무 자주 사용하므로 편리하게 다룰 수 있도록 별도로 제공함
즉, 위에서 다루었던 OutputStreamWriter는 문자를 다루는 Writer 클래스의 자식이기 때문에 write(String)이 가능한 것임
OutputStreamWriter는 문자를 받아서 byte로 변경한 다음에 byte를 다루는 OutputStream으로 데이터를 전달했던 것임
꼭 기억해야 할 중요한 사실은 계속 언급되듯이 모든 데이터를 byte 단위(숫자)로 저장됨
Writer가 아무리 문자를 다룬다고 해도 문자를 바로 저장할 수는 없으며 클래스에 문자를 전달하면 결과적으로 내부에서 지정된 문자 집합을 사용하여 문자를 byte로 인코딩해서 저장함
ReaderWriterMainV3
Writer, Reader를 사용하는 다른 예시도 있음
package io.text;
public class ReaderWriterMainV3 {
public static void main(String[] args) throws IOException {
String writerString = "안녕하세요";
System.out.println("writerString = " + writerString);
// 파일에 쓰기
FileWriter fw = new FileWriter(FILE_NAME, UTF_8);
fw.write(writerString);
fw.close();
// 파일에서 읽기
StringBuilder content = new StringBuilder();
FileReader fr = new FileReader(FILE_NAME, UTF_8);
int ch;
while ((ch = fr.read()) != -1) {
content.append((char) ch);
}
fr.close();
System.out.println("readString = " + content);
}
}
/* 실행 결과
writerString = 안녕하세요
readString = 안녕하세요
*/
hello.txt - 실행결과
FileWriter fw = new FileWriter(FILE_NAME, UTF_8);
public FileWriter(String fileName, Charset charset) throws IOException {
super(new FileOutputStream(fileName), charset);
}
- FileWriter에 파일명과 문자 집합(인코딩 셋)을 전달할 수 있음
- 그러나 모든 데이터는 byte 단위로 저장되기 때문에 FileWriter를 들어가 보면 내부에서 스스로 FileOutputStream을 하나 생성해서 사용하는 것을 확인할 수 있음
fw.write(writeString)
- 이 메서드를 사용하면 문자를 파일에 저장할 수 있는데, 개발자가 느끼기에는 문자를 직접 파일에 쓰는 것처럼 느껴지지만 내부에서 문자를 byte로 변환하여 저장함
- FileWriter 내부에서 전달받은 인코딩 셋을 사용하여 문자를 byte로 변경하고 FileOutputStream을 사용하여 파일에 저장함
FileReader fr = new FileReader(FILE_NAME, UTF_8);
public FileReader(String fileName, Charset charset) throws IOException {
super(new FileInputStream(fileName), charset);
}
- FileReader도 마찬가지로 내부에서 FileInputStream을 생성해서 사용함
- ch = fr.read() 메서드도 내부에서는 FileInputStream을 사용하여 데이터를 byte 단위로 읽어 들인 후 문자 집합을 사용하여 byte[]을 char로 디코딩되어서 반환함
- 마찬가지로 문자를 반환하지만 반환타입이 int이므로 실제 출력할 때는 char로 형변환하여 문자열에 저장하고 출력하면 됨
FileWriter와 OutputStreamWriter
FileWriter 코드와 앞서 작성한 OutputStreamWriter를 사용한 코드는 매우 비슷한데 차이점은 이전 코드는 FileOutputStream을 직접 생성했는데 FileWriter는 생성자 내부에서 대신 FileOutputStream을 생성해 준다는 점임
FileWriter는 OutputStreamWriter을 상속하며 다른 추가 기능도 없으며 생성자에서 개발자 대신에 FileOutputStream을 생성해 주는 일만 대신 처리해 줌
즉, FileWriter는 OutputStreamWriter를 조금 편리하게 사용하도록 도와줄 뿐이며 FileReader도 마찬가지임
개발자는 복잡한 내부 개념은 알지 못해도 단순하게 문자로 파일을 저장하고 읽고 싶다면 FileWriter, FileReader만 알고 있다면 편리하게 사용할 수 있게 되도록 설계되어있는 것임
이러한 설계가 좋은 설계라고 볼 수 있음
정리
Writer, Reader 클래스를 사용하면 바이트 변환 없이 문자를 직접 다를 수 있어서 편리함
그러나 실제로는 모든 데이터는 바이트 단위로 다루며, 문자를 직접 저장할 수는 없음
즉, 내부에서 byte로 변환해서 저장한다는 점과 문자를 byte로 변경하려면 항상 문자 집합(인코딩 셋)이 필요하며, 문자 집합을 생략하면 시스템 기본 문자 집합이 사용된다는 점을 꼭 기억해야 함
BufferedReader
ReaderWriterMainV4
BufferedOutputStream, BufferedInputStream과 같이 Reader, Writer에도 버퍼 보조 기능을 제공하는 BufferedReader, BufferedWriter 클래스가 있음
추가로 문자를 다룰 때는 한 줄(라인) 단위로 다룰 때가 많은데 BufferedReader는 한 줄 단위로 문자를 읽는 기능을 추가로 제공함
package io.text;
public class ReaderWriterMainV4 {
private static final int BUFFER_SIZE = 8192;
public static void main(String[] args) throws IOException {
String writerString = "버퍼드를 써볼까요?\n버퍼를 써봤습니다";
System.out.println("== Write String == ");
System.out.println(writerString);
// 파일에 쓰기
FileWriter fw = new FileWriter(FILE_NAME, UTF_8);
BufferedWriter bw = new BufferedWriter(fw, BUFFER_SIZE);
bw.write(writerString);
bw.close();
// 파일에서 읽기
StringBuilder content = new StringBuilder();
FileReader fr = new FileReader(FILE_NAME, UTF_8);
BufferedReader br = new BufferedReader(fr, BUFFER_SIZE);
String line;
while ((line = br.readLine()) != null) {
content.append(line).append("\n");
}
br.close();
System.out.println("== Read String == ");
System.out.println(content);
}
}
/* 실행 결과
== Write String ==
버퍼드를 써볼까요?
버퍼를 써봤습니다
== Read String ==
버퍼드를 써볼까요?
버퍼를 써봤습니다
*/
br.readLine()
- 한 줄 단위로 문자를 읽고 String을 반환함
- String으로 반환하기 때문에 파일의 끝(EOF)을 -1로 표현할 수 없는 대신에 파일의 끝에 도달하면 null을 반환함
- 한 줄 단위로 글을 읽기 때문에 뒤에 개행 문자를 붙여서 줄을 구분해 주면서 출력하면 됨
기타 스트림
PrintStream
PrintStreamEtcMain
PrintStream은 우리가 자주 사용해 왔던 System.out에서 사용되는 스트림인데, PrintStream과 FileOutputStream을 조합하면 마치 콘솔에 출력하듯이 파일에 출력할 수 있음
마찬가지로 PrintStream 생성자의 두 번째 인자로 문자 집합을 입력할 수 있으며 생략하면 기본 시스템 문자 집합이 사용됨
package io.streams;
public class PrintStreamEtcMain {
public static void main(String[] args) throws FileNotFoundException {
FileOutputStream fos = new FileOutputStream("temp/print.txt");
PrintStream printStream = new PrintStream(fos);
printStream.println("Hello World");
printStream.println("10");
printStream.println(true);
printStream.printf("안녕? %s", "하세요!");
printStream.close();
}
}
print.txt - 실행 결과
PrintStream의 생성자에 FileOutputStream을 전달하면 이 스트림을 통해서 나가는 출력이 파일에 저장됨
이 기능을 사용하면 콘솔에 출력하는 것처럼 파일이나 다른 스트림에 문자를 출력할 수 있음
DataOutputStream
DataStreamEtcMain
DataOutputStream, DataInputStream을 사용하면 자바의 String, int, double, boolean 같은 데이터 형을 편리하게 다룰 수 있음
이 스트림과 FileOutputStream을 조합하면 파일에 자바 데이터 type을 편리하게 저장할 수 있음
자바 데이터 타입을 사용하면서 회원 데이터를 편리하게 저장하고 불러오는 것을 확인할 수 있음
이 스트림을 사용할 때 주의점으로는 꼭! 저장한 순서대로 읽어야 한다는 점인데 그렇게 하지 않으면 잘못된 데이터가 조회될 수 있음
package io.streams;
public class DataStreamEtcMain {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("temp/data.dat");
DataOutputStream dos = new DataOutputStream(fos);
dos.writeUTF("회원 A");
dos.writeInt(20);
dos.writeDouble(10.5);
dos.writeBoolean(true);
dos.close();
FileInputStream fis = new FileInputStream("temp/data.dat");
DataInputStream dis = new DataInputStream(fis);
// 저장한 순서대로 읽어야 함
System.out.println(dis.readUTF());
System.out.println(dis.readInt());
System.out.println(dis.readDouble());
System.out.println(dis.readBoolean());
}
}
/* 실행 결과
회원 A
20
10.5
true
*/
data.dat - 실행 결과
저장한 data.dat 파일을 직접 열어보면 입력한 내용이 제대로 보이지 않는데, writeUTF()의 경우 UTF-8 형식으로 저장하지만 나머지의 경우 문자가 아니라 각 타입에 맞는 byte 단위로 저장하기 때문임
예를 들어 자바의 int는 4byte를 묶어서 사용하는데 해당 byte 단위 그대로 저장됨
텍스트 편집기는 자신의 문자 집합을 사용해서 byte를 문자로 표현하려고 시도하지만 문자 집합에 없는 단어이거나 전혀 예상하지 않은 문자로 디코딩될 것임
DataOutputStream에 대해서는 뒤에 활용에서 더 자세히 다룸
정리
기본(기반, 메인) 스트림
- File, 메모리, 콘솔 등에 직접 접근하는 스트림
- 단독으로 사용할 수 있음
- FileInputStream, FileOutputStream, FileReader, FileWriter, ByteArrayInputStream, ByteArrayOutputStream
보조 스트림
- 기본 스트림을 도와주는 스트림
- 단독으로 사용할 수 없으므로 반드시 대상 스트림이 있어야 함
- BufferedInputStream, BufferedOutputStream, InputStreamReader, OutputStreamWriter, DataOutputStream, DataInputStream, PrintStream