F-LAB JAVA · 3주차 · Phase 8 · Stream 실전
🏆 Phase 8 완주 — Stream 실전 마스터
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Writer는 문자 (char) 단위로 쓰는 추상 클래스로, 인코딩을 적용 하여 문자를 바이트 시퀀스로 변환한다.
FileWriter는 파일 전용 Writer 지만 Java 10 까지 인코딩 명시 불가 — Cross-platform 위험.
OutputStreamWriter는 인코딩 명시 가능한 안전한 변환 도구 — 실무 권장.
BufferedWriter를 결합하면 8KB 버퍼링 +newLine()메서드로 효율적 줄 단위 쓰기.
Java 11+ 부터FileWriter(File, Charset)추가로 안전 사용 가능, 가장 권장은Files.newBufferedWriter(path, UTF_8)또는Files.writeString(path, content, UTF_8).
OutputStream:
글자 사진을 한 장씩 인쇄
- 사진 1장 = 1바이트
- 한글 의미 모르고 그냥 인쇄
Writer:
사전 (인코딩) 으로 글자 → 인쇄
- "안" 입력
- 사전 보고 EC 95 88 (3바이트) 인쇄
- 의미 보존
FileWriter (옛):
사전을 시스템에서 가져옴
- 환경 따라 다름
- Cross-platform 위험
OutputStreamWriter (안전):
사전 명시 ("UTF-8" 사용)
- 어디서나 같은 결과
- 안전
→ Writer = 인코딩 적용 + 문자 단위.
1. Writer 의 정의와 OutputStream 과의 차이
2. FileWriter 의 정의와 한계
3. OutputStreamWriter 의 정의와 활용
4. 다양한 write 메서드
5. BufferedWriter 와의 결합
6. PrintWriter vs BufferedWriter
7. 한글 쓰기 권장 패턴
8. Phase 8 완주 정리
9. 면접 + 자기 점검 + Phase 8 졸업 시험
public abstract class Writer implements Appendable, Closeable, Flushable {
// 핵심 메서드
public void write(int c) throws IOException;
public void write(char[] cbuf) throws IOException;
public abstract void write(char[] cbuf, int off, int len) throws IOException;
public void write(String str) throws IOException;
public void write(String str, int off, int len) throws IOException;
// Appendable (Java 5+)
public Writer append(char c) throws IOException;
public Writer append(CharSequence csq) throws IOException;
public Writer append(CharSequence csq, int start, int end) throws IOException;
// 추가
public abstract void flush() throws IOException;
public abstract void close() throws IOException;
}
핵심:
java.io 패키지Writer (추상)
├── OutputStreamWriter ← OutputStream 을 Writer 로
│ └── FileWriter ← 파일 전용
├── BufferedWriter ← 버퍼링 + newLine
├── PrintWriter ← println, printf 편의
├── StringWriter ← String 으로
├── CharArrayWriter ← char[] 로
├── PipedWriter ← 파이프
└── FilterWriter
| 항목 | OutputStream | Writer |
|---|---|---|
| 단위 | byte (8비트) | char (16비트) |
| 인코딩 | X (raw) | ✓ (자동 변환) |
| write(int) | 하위 8비트만 | char 의 16비트 |
| 한글 쓰기 | 불가 (직접) | OK (인코딩 매핑) |
| 메서드 | write(int/byte[]) | write(int/char[]/String) |
| String 쓰기 | getBytes 필요 | 직접 |
// OutputStream.write(int) — 하위 8비트
OutputStream os = new FileOutputStream("file.txt");
os.write('A'); // 0x41 (정상)
os.write('안'); // 하위 8비트만! ('안' = 0xC548, 하위 = 0x48 = 'H')
// ❌ 한글 깨짐
// Writer.write(int) — char 의 16비트
Writer w = new OutputStreamWriter(new FileOutputStream("file.txt"), UTF_8);
w.write('A'); // 0x41 → UTF-8 0x41 (1바이트)
w.write('안'); // 0xC548 → UTF-8 EC 95 88 (3바이트)
// ✓ 정상
// OutputStream — getBytes 필요
OutputStream os = new FileOutputStream("file.txt");
os.write("안녕".getBytes(StandardCharsets.UTF_8));
// 매번 getBytes 호출
// Writer — 직접
Writer w = new OutputStreamWriter(
new FileOutputStream("file.txt"), UTF_8);
w.write("안녕");
// 인코딩 자동 처리
"안녕" 쓰기:
OutputStream:
매번 getBytes → 바이트 시퀀스 → write
복잡, 인코딩 매번 처리
Writer:
write("안녕")
내부에서 인코딩 적용
→ 0xEC 0x95 0x88 0xEB 0x85 0x95 (UTF-8 6바이트)
→ OutputStream 으로 전달
추상화 = Writer 가 인코딩 처리
// ❌ OutputStream 으로 한글 (귀찮음)
public void writeKoreanBad(Path path, String text) throws IOException {
try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
fos.write(text.getBytes(StandardCharsets.UTF_8));
// 매번 getBytes
}
}
// ✓ Writer 로 한글
public void writeKoreanGood(Path path, String text) throws IOException {
try (Writer w = new OutputStreamWriter(
new FileOutputStream(path.toFile()),
StandardCharsets.UTF_8)) {
w.write(text);
// 직접 String
}
}
// ✓✓ BufferedWriter (더 효율)
public void writeKoreanBest(Path path, String text) throws IOException {
try (BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
w.write(text);
}
}
Writer 와 OutputStream 의 결정적 차이는?
답:
1. 단위:
한글 처리:
write(int):
메서드 종류:
권장:
package java.io;
public class FileWriter extends OutputStreamWriter {
// Java 10 이하
public FileWriter(String fileName) throws IOException;
public FileWriter(String fileName, boolean append) throws IOException;
public FileWriter(File file) throws IOException;
public FileWriter(File file, boolean append) throws IOException;
public FileWriter(FileDescriptor fd);
// Java 11+ 추가
public FileWriter(String fileName, Charset charset) throws IOException;
public FileWriter(File file, Charset charset) throws IOException;
public FileWriter(String fileName, Charset charset, boolean append) throws IOException;
public FileWriter(File file, Charset charset, boolean append) throws IOException;
}
핵심:
// Java 10 이하 — 인코딩 명시 불가
try (FileWriter writer = new FileWriter("file.txt")) {
writer.write("Hello\n");
writer.write("안녕\n"); // ★ 시스템 인코딩 적용
}
// 문제:
// - 시스템 기본 인코딩
// - Windows MS949
// - Linux/Mac UTF-8
// - Cross-platform 위험
// Java 11+ 부터 안전
try (FileWriter writer = new FileWriter("file.txt", StandardCharsets.UTF_8)) {
writer.write("Hello\n");
writer.write("안녕\n");
// 항상 UTF-8
}
// 모든 생성자 조합
new FileWriter("file.txt"); // 기본 인코딩
new FileWriter("file.txt", true); // append + 기본
new FileWriter("file.txt", UTF_8); // UTF-8
new FileWriter("file.txt", UTF_8, true); // UTF-8 + append
// 덮어쓰기 (기본)
try (FileWriter writer = new FileWriter("log.txt", UTF_8)) {
writer.write("New log\n");
// 기존 내용 삭제
}
// 이어쓰기
try (FileWriter writer = new FileWriter("log.txt", UTF_8, true)) {
writer.write("Additional log\n");
// 기존 끝에 추가
}
// FileOutputStream 의 append 와 동일한 동작
한계:
1. 인코딩 명시 (Java 10 이하) 불가
- Java 11+ 에서 해결
2. NIO.2 API 어려움
- Path 대신 String/File
3. BufferedWriter 결합 권장
- 단독은 1문자씩 비효율
4. Files.newBufferedWriter 가 더 권장
- 더 간결
public class ShipmentLogger {
private final Path logFile;
public ShipmentLogger(Path logFile) {
this.logFile = logFile;
}
// Java 11+ FileWriter
public void log(String message) throws IOException {
try (FileWriter writer = new FileWriter(
logFile.toFile(), StandardCharsets.UTF_8, true)) {
writer.write(LocalDateTime.now() + " " + message + "\n");
}
}
// BufferedWriter 결합 (더 효율)
public void logBuffered(String message) throws IOException {
try (BufferedWriter writer = new BufferedWriter(
new FileWriter(logFile.toFile(), StandardCharsets.UTF_8, true))) {
writer.write(LocalDateTime.now() + " " + message);
writer.newLine();
}
}
// NIO.2 (가장 권장)
public void logModern(String message) throws IOException {
String line = LocalDateTime.now() + " " + message + "\n";
Files.writeString(logFile, line, StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
}
FileWriter 의 정의와 한계는?
답:
1. 정의:
한계 (Java 10 이하):
Java 11+ 개선:
FileWriter(File, Charset) 추가FileWriter(File, Charset, boolean append) 추가append 모드:
실무 권장:
package java.io;
public class OutputStreamWriter extends Writer {
public OutputStreamWriter(OutputStream out);
public OutputStreamWriter(OutputStream out, String charsetName)
throws UnsupportedEncodingException;
public OutputStreamWriter(OutputStream out, Charset cs);
public OutputStreamWriter(OutputStream out, CharsetEncoder enc);
}
핵심:
// 파일 — OutputStreamWriter
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("file.txt"),
StandardCharsets.UTF_8)) {
osw.write("Hello\n");
osw.write("안녕\n");
}
// 표준 출력
try (OutputStreamWriter osw = new OutputStreamWriter(
System.out, StandardCharsets.UTF_8)) {
osw.write("출력\n");
osw.flush();
}
// 메모리
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (OutputStreamWriter osw = new OutputStreamWriter(baos, StandardCharsets.UTF_8)) {
osw.write("안녕");
}
byte[] bytes = baos.toByteArray();
// UTF-8 인코딩된 바이트
// 1. 파일
new OutputStreamWriter(new FileOutputStream("file.txt"), UTF_8);
// 2. 표준 출력
new OutputStreamWriter(System.out, UTF_8);
// 3. 표준 에러
new OutputStreamWriter(System.err, UTF_8);
// 4. 메모리
new OutputStreamWriter(new ByteArrayOutputStream(), UTF_8);
// 5. 소켓 (네트워크)
new OutputStreamWriter(socket.getOutputStream(), UTF_8);
// 6. 압축
new OutputStreamWriter(new GZIPOutputStream(new FileOutputStream("file.gz")), UTF_8);
// 모든 OutputStream 을 Writer 로
// 1. 인코딩 없음 (위험, 시스템 기본)
new OutputStreamWriter(out);
// 2. 문자열 이름
new OutputStreamWriter(out, "UTF-8");
// UnsupportedEncodingException 가능
// 3. Charset 객체 (권장)
new OutputStreamWriter(out, StandardCharsets.UTF_8);
// 4. CharsetEncoder (고급)
CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder();
encoder.onMalformedInput(CodingErrorAction.REPLACE);
new OutputStreamWriter(out, encoder);
사용자 호출:
osw.write("안녕");
내부 동작:
1. CharsetEncoder 가 인코딩
- "안" (U+C548) → 0xEC 0x95 0x88 (UTF-8 3바이트)
- "녕" (U+B155) → 0xEB 0x85 0x95
2. OutputStream 으로 바이트 전달
- 6바이트 write 호출
3. flush 가 강제 출력
복잡한 처리를 추상화
사용자는 String 그대로
public class ShipmentMultiSource {
private static final Charset CHARSET = StandardCharsets.UTF_8;
// 1. 파일 쓰기
public void writeFile(Path path, String content) throws IOException {
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream(path.toFile()), CHARSET)) {
osw.write(content);
}
}
// 2. 표준 출력 한글
public void printKorean(String message) throws IOException {
try (OutputStreamWriter osw = new OutputStreamWriter(System.out, CHARSET)) {
osw.write(message);
osw.write('\n');
osw.flush();
}
}
// 3. 네트워크 응답
public void sendResponse(Socket socket, String content) throws IOException {
try (OutputStreamWriter osw = new OutputStreamWriter(
socket.getOutputStream(), CHARSET);
BufferedWriter bw = new BufferedWriter(osw)) {
bw.write(content);
bw.flush();
}
}
// 4. 압축 + 인코딩
public void writeGzip(Path path, String content) throws IOException {
try (OutputStreamWriter osw = new OutputStreamWriter(
new GZIPOutputStream(new FileOutputStream(path.toFile())),
CHARSET);
BufferedWriter bw = new BufferedWriter(osw)) {
bw.write(content);
}
}
// 5. 메모리에 인코딩
public byte[] encodeToBytes(String text) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (OutputStreamWriter osw = new OutputStreamWriter(baos, CHARSET)) {
osw.write(text);
}
return baos.toByteArray();
}
}
OutputStreamWriter 의 정의와 활용은?
답:
1. 정의:
인코딩 명시:
활용:
내부:
권장:
public abstract class Writer {
// 1. 1문자 쓰기
public void write(int c) throws IOException;
// 2. char[] 전체
public void write(char[] cbuf) throws IOException;
// 3. char[] 일부
public abstract void write(char[] cbuf, int off, int len) throws IOException;
// 4. String 전체
public void write(String str) throws IOException;
// 5. String 일부
public void write(String str, int off, int len) throws IOException;
}
// 1문자 쓰기
Writer w = ...;
w.write('A'); // 'A' → 0x41 (UTF-8 1바이트)
w.write(65); // 위와 동일
w.write('안'); // '안' → 0xEC 0x95 0x88 (UTF-8 3바이트)
w.write(0xC548); // 위와 동일
// 16비트 (char) 의 범위
// 0 ~ 65535
// 이모지 등 Supplementary 문자는 Surrogate Pair 두 번
// 전체
char[] chars = "Hello".toCharArray();
w.write(chars); // "Hello" 5문자
// 한글
char[] korean = "안녕".toCharArray();
w.write(korean); // 2 char (UTF-8 6바이트)
// 부분
w.write(chars, 0, 3); // "Hel"
// 가장 편리
w.write("Hello, World!");
// 부분
w.write("Hello, World!", 7, 5); // "World"
// 한글
w.write("안녕하세요");
// 인코딩 적용:
// "안" → 0xEC 0x95 0x88
// "녕" → 0xEB 0x85 0x95
// ...
// read(char[]) 와 짝
try (Reader r = Files.newBufferedReader(src);
Writer w = Files.newBufferedWriter(dest)) {
char[] buf = new char[8192];
int n;
while ((n = r.read(buf)) != -1) {
w.write(buf, 0, n); // ★ n 만큼만!
}
}
// ❌ 잘못
w.write(buf); // buf.length 전체
// 마지막 read 후 이전 데이터 누적
// Unit 8.3 의 함정과 동일
// Appendable 인터페이스
Writer w = ...;
w.append('A'); // == write('A')
w.append("Hello"); // == write("Hello")
w.append("Hello, World!", 7, 12); // "World"
// 체이닝
w.append("a").append("b").append("c");
// "abc" 쓰기
// CharSequence 호환
StringBuilder sb = new StringBuilder("Hello");
w.append(sb);
| 메서드 | 매개변수 | 특징 |
|---|---|---|
| write(int) | 1문자 | 단순, 비효율 |
| write(char[]) | 배열 전체 | 효율적 |
| write(char[], off, len) | 배열 일부 | 정확한 제어 |
| write(String) | 문자열 | 가장 편리 |
| write(String, off, len) | 문자열 일부 | substring 대안 |
| append | char/String | 체이닝, NPE 회피 |
public class ShipmentTextWriter {
private static final Charset CHARSET = StandardCharsets.UTF_8;
// 1. 1문자씩 (드물게)
public void writeChars(Path path, String text) throws IOException {
try (Writer w = Files.newBufferedWriter(path, CHARSET)) {
for (int i = 0; i < text.length(); i++) {
w.write(text.charAt(i));
}
}
}
// 2. char[]
public void writeCharArray(Path path, char[] data, int len) throws IOException {
try (Writer w = Files.newBufferedWriter(path, CHARSET)) {
w.write(data, 0, len);
}
}
// 3. String (가장 편리)
public void writeString(Path path, String content) throws IOException {
try (Writer w = Files.newBufferedWriter(path, CHARSET)) {
w.write(content);
}
}
// 4. 복사 (read + write)
public void copyText(Path src, Path dest) throws IOException {
try (BufferedReader r = Files.newBufferedReader(src, CHARSET);
BufferedWriter w = Files.newBufferedWriter(dest, CHARSET)) {
char[] buf = new char[8192];
int n;
while ((n = r.read(buf)) != -1) {
w.write(buf, 0, n); // ★ n 만큼만
}
}
}
// 5. append 체이닝
public void appendChained(Path path) throws IOException {
try (Writer w = Files.newBufferedWriter(path, CHARSET)) {
w.append("First line\n")
.append("Second line\n")
.append("Third line\n");
}
}
}
write 메서드 5가지의 차이는?
답:
1. write(int):
write(char[]):
write(char[], off, len):
write(String):
write(String, off, len):
append:
public class BufferedWriter extends Writer {
public BufferedWriter(Writer out);
public BufferedWriter(Writer out, int sz);
// 핵심 메서드
public void newLine() throws IOException;
// 상속
public void write(int c);
public void write(char[] cbuf, int off, int len);
public void write(String s, int off, int len);
public void flush();
public void close();
}
특징:
// 1. 기본 결합
try (BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("file.txt"),
StandardCharsets.UTF_8))) {
bw.write("Hello");
bw.newLine();
bw.write("안녕");
}
// 2. Java 11+ FileWriter
try (BufferedWriter bw = new BufferedWriter(
new FileWriter("file.txt", StandardCharsets.UTF_8))) {
bw.write("...");
}
// 3. NIO.2 (가장 권장)
try (BufferedWriter bw = Files.newBufferedWriter(
Path.of("file.txt"), StandardCharsets.UTF_8)) {
bw.write("...");
}
public void newLine() throws IOException;
// 시스템의 line.separator 사용
// - Unix/Mac: \n
// - Windows: \r\n
// 사용
bw.write("Line 1");
bw.newLine(); // 시스템 줄바꿈
bw.write("Line 2");
bw.newLine();
// 또는 직접
bw.write("Line 1\n"); // Unix
bw.write("Line 1\r\n"); // Windows
// 어느 게 좋나?
// - 텍스트 파일: \n (대부분 OS 인식)
// - Windows 네이티브: \r\n
// - newLine(): 시스템 의존 (Cross-platform 텍스트는 \n 권장)
일반 Writer:
write() → OutputStreamWriter → CharsetEncoder → OutputStream → OS
매 write 가 system call
BufferedWriter (8KB):
write() → BufferedWriter 의 char[8192]
버퍼 가득 시 flush
대부분 메모리 접근
효과:
- write 호출 빠름
- 매 write 마다 system call X
- flush() 또는 close() 시 강제 출력
// ❌ flush 누락
BufferedWriter bw = new BufferedWriter(new FileWriter("file.txt"));
bw.write("important data");
// JVM 종료 시 8KB 버퍼 내용 손실 가능!
// ✓ try-with-resources (자동 flush + close)
try (BufferedWriter bw = new BufferedWriter(new FileWriter("file.txt"))) {
bw.write("important data");
}
// try 종료 시 close → 자동 flush
// ✓ 명시적 flush (진행 중)
try (BufferedWriter bw = ...) {
for (int i = 0; i < 100; i++) {
bw.write(data[i]);
bw.newLine();
if (i % 10 == 0) {
bw.flush(); // 10개마다 강제
}
}
}
| 항목 | BufferedWriter | PrintWriter |
|---|---|---|
| 줄바꿈 | newLine() | println() |
| printf | X | ✓ |
| 예외 | IOException 던짐 | 안 던짐 (checkError) |
| autoFlush | X | 옵션 |
| 권장 | 일반 텍스트 | 콘솔, 디버깅 |
public class ShipmentReportWriter {
private static final Charset CHARSET = StandardCharsets.UTF_8;
// 1. 기본 패턴
public void writeReport(Path path, List<Shipment> shipments) throws IOException {
try (BufferedWriter w = Files.newBufferedWriter(path, CHARSET)) {
w.write("ID,선적번호,수하인,중량");
w.newLine();
for (Shipment s : shipments) {
w.write(s.getId() + ",");
w.write(s.getBlNo() + ",");
w.write(s.getConsignee() + ",");
w.write(s.getWeight().toString());
w.newLine();
}
}
}
// 2. 큰 데이터 (주기적 flush)
public void writeLargeReport(Path path, Iterator<Shipment> shipments) throws IOException {
try (BufferedWriter w = Files.newBufferedWriter(path, CHARSET)) {
int count = 0;
while (shipments.hasNext()) {
Shipment s = shipments.next();
w.write(s.toCsvLine());
w.newLine();
count++;
if (count % 1000 == 0) {
w.flush();
log.info("Written {} records", count);
}
}
}
}
// 3. append 모드
public void appendLog(Path logFile, String message) throws IOException {
try (BufferedWriter w = Files.newBufferedWriter(
logFile, CHARSET,
StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
w.write(LocalDateTime.now() + " " + message);
w.newLine();
}
}
}
BufferedWriter 의 효과는?
답:
1. 정의:
핵심 메서드:
버퍼링 효과:
결합 패턴:
flush:
public class PrintWriter extends Writer {
public PrintWriter(Writer out);
public PrintWriter(Writer out, boolean autoFlush);
public PrintWriter(OutputStream out);
public PrintWriter(OutputStream out, boolean autoFlush);
public PrintWriter(OutputStream out, boolean autoFlush, Charset charset);
public PrintWriter(File file);
public PrintWriter(File file, Charset charset);
// ... 기타
// 편의 메서드
public void print(...);
public void println(...);
public PrintWriter printf(String format, Object... args);
public PrintWriter format(String format, Object... args);
// 예외 안 던짐 — checkError 로 확인
public boolean checkError();
}
// BufferedWriter — 표준
try (BufferedWriter bw = Files.newBufferedWriter(path)) {
bw.write("Hello");
bw.newLine();
// printf 없음
}
// PrintWriter — 편의
try (PrintWriter pw = new PrintWriter(
new FileWriter("file.txt", UTF_8))) {
pw.println("Hello");
pw.printf("Value: %d%n", 42);
pw.print("No newline");
// 예외 안 던짐
}
public void println();
public void println(boolean b);
public void println(char c);
public void println(int i);
public void println(long l);
public void println(float f);
public void println(double d);
public void println(char[] x);
public void println(String x);
public void println(Object x);
// 동작:
// 1. print 호출
// 2. line.separator 추가 (\n 또는 \r\n)
pw.println("Hello");
// "Hello\n" (Unix) 또는 "Hello\r\n" (Windows)
PrintWriter pw = new PrintWriter("file.txt");
// 다양한 형식
pw.printf("Name: %s%n", "Alice");
pw.printf("Age: %d%n", 30);
pw.printf("Price: %.2f%n", 19.99);
pw.printf("%-10s%5d%n", "Apple", 100);
// %n = 시스템 줄바꿈
// %s = String
// %d = int
// %f = float/double
// %.2f = 소수점 2자리
// %-10s = 왼쪽 정렬 10칸
// %5d = 오른쪽 정렬 5칸
// 함정 1: 예외 안 던짐
PrintWriter pw = new PrintWriter("file.txt");
pw.println("Hello");
// 디스크 풀이어도 예외 X
// 데이터 손실 가능
// 명시적 검사
pw.println("Hello");
if (pw.checkError()) {
System.err.println("Write failed!");
}
// 함정 2: 인코딩 명시 안 함 (옛 생성자)
new PrintWriter("file.txt"); // 시스템 인코딩
// Java 11+ 권장:
new PrintWriter("file.txt", StandardCharsets.UTF_8);
// 함정 3: autoFlush
PrintWriter pw1 = new PrintWriter(out); // autoFlush = false
PrintWriter pw2 = new PrintWriter(out, true); // autoFlush = true
// pw2 는 println 후 자동 flush
System.out 의 정체:
public static final PrintStream out;
// PrintStream 은 PrintWriter 와 유사
// 차이:
// - PrintStream: byte 기반 (OutputStream 의 자식)
// - PrintWriter: char 기반 (Writer 의 자식)
// 둘 다 println, printf 지원
System.out.println("Hello");
// 내부적으로 인코딩 적용
// 시스템 콘솔로
BufferedWriter 권장:
✓ 표준
✓ 명시적 newLine
✓ 예외 처리 명확
✓ 일반 텍스트 파일
PrintWriter 권장:
✓ printf 필요
✓ println 의 편의
✓ 콘솔 출력 스타일
PrintStream 권장:
✓ System.out 처럼
✓ byte 기반 출력
✓ println 의 편의
선택:
- 일반: BufferedWriter
- 형식: PrintWriter
- 콘솔: System.out (PrintStream)
public class ShipmentReportFormatter {
// BufferedWriter — 표준 텍스트
public void writeCsv(Path path, List<Shipment> shipments) throws IOException {
try (BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
w.write("ID,BL,Weight");
w.newLine();
for (Shipment s : shipments) {
w.write(s.toCsvLine());
w.newLine();
}
}
}
// PrintWriter — 형식 있는 보고서
public void writeFormatted(Path path, List<Shipment> shipments) throws IOException {
try (PrintWriter pw = new PrintWriter(
new FileWriter(path.toFile(), StandardCharsets.UTF_8))) {
pw.println("=== Shipment Report ===");
pw.printf("Generated: %s%n", LocalDateTime.now());
pw.printf("Total: %d%n", shipments.size());
pw.println();
pw.printf("%-10s %-20s %10s%n", "ID", "BL", "Weight");
pw.println("-".repeat(40));
for (Shipment s : shipments) {
pw.printf("%-10d %-20s %10.2f%n",
s.getId(), s.getBlNo(), s.getWeight());
}
// 검사
if (pw.checkError()) {
throw new IOException("Write failed");
}
}
}
}
PrintWriter vs BufferedWriter?
답:
1. BufferedWriter:
PrintWriter:
선택:
함정:
// 1. 가장 권장 — NIO.2 + BufferedWriter
try (BufferedWriter w = Files.newBufferedWriter(
Path.of("file.txt"), StandardCharsets.UTF_8)) {
w.write("안녕하세요");
w.newLine();
}
// 2. 짧은 콘텐츠 — Files.writeString
Files.writeString(Path.of("file.txt"), "안녕하세요\n",
StandardCharsets.UTF_8);
// 3. 여러 줄 — Files.write
List<String> lines = List.of("첫 줄", "둘째 줄", "셋째 줄");
Files.write(Path.of("file.txt"), lines, StandardCharsets.UTF_8);
// 4. append — Files.writeString + APPEND
Files.writeString(Path.of("log.txt"), "추가 로그\n",
StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
// 5. 다양한 OutputStream — OutputStreamWriter
try (OutputStreamWriter osw = new OutputStreamWriter(
socket.getOutputStream(), StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw)) {
bw.write("응답");
}
public class IoConstants {
public static final Charset UTF8 = StandardCharsets.UTF_8;
public static final int BUFFER_SIZE = 8192;
}
// 일관된 사용
Files.newBufferedWriter(path, IoConstants.UTF8);
Files.writeString(path, content, IoConstants.UTF8);
1. CSV Excel 에서 한글 깨짐
원인: Excel 의 BOM 요구
해결: UTF-8 BOM 추가
2. HTTP 응답 한글 깨짐
원인: Content-Type charset 누락
해결: charset=UTF-8 명시
3. DB 저장 후 깨짐
원인: DB 인코딩 불일치
해결: 모두 UTF-8
4. 파일명 한글 깨짐
원인: 파일 시스템 인코딩
해결: -Dfile.encoding=UTF-8
5. 컴파일러 인코딩
원인: 소스 파일 인코딩
해결: -encoding UTF-8 (javac)
// Excel 이 UTF-8 BOM 을 인식해서 한글 정상 표시
public void exportCsvForExcel(Path path, List<Shipment> shipments) throws IOException {
try (OutputStream os = Files.newOutputStream(path);
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(os, StandardCharsets.UTF_8))) {
// BOM 쓰기 (Excel 한글 인식)
os.write(0xEF);
os.write(0xBB);
os.write(0xBF);
// ★ Writer 가 아닌 OutputStream 으로 직접 BOM
// 데이터
bw.write("ID,선적번호,수하인");
bw.newLine();
for (Shipment s : shipments) {
bw.write(s.toCsvLine());
bw.newLine();
}
}
}
// 또는 더 간단
public void exportCsvForExcel2(Path path, String csvContent) throws IOException {
byte[] bom = new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF};
byte[] data = csvContent.getBytes(StandardCharsets.UTF_8);
try (OutputStream os = Files.newOutputStream(path)) {
os.write(bom);
os.write(data);
}
}
public boolean isValidKorean(Path path) throws IOException {
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = br.readLine()) != null) {
// U+FFFD (replacement character) 검사
if (line.contains("\uFFFD")) {
return false;
}
}
return true;
}
}
public void writeAndVerify(Path path, String content) throws IOException {
// 쓰기
Files.writeString(path, content, StandardCharsets.UTF_8);
// 검증 (라운드 트립)
String readBack = Files.readString(path, StandardCharsets.UTF_8);
if (!content.equals(readBack)) {
throw new IOException("Round-trip failed");
}
}
@Service
public class ShipmentExportService {
private static final Charset CHARSET = StandardCharsets.UTF_8;
// 1. 일반 한글 CSV
public Path exportCsv(List<Shipment> shipments) throws IOException {
Path path = Path.of("/var/shipment/exports/shipments.csv");
Files.createDirectories(path.getParent());
try (BufferedWriter w = Files.newBufferedWriter(path, CHARSET)) {
w.write("ID,선적번호,수하인,중량(kg)");
w.newLine();
for (Shipment s : shipments) {
w.write(s.getId() + ",");
w.write(s.getBlNo() + ",");
w.write(s.getConsignee() + ",");
w.write(s.getWeight().toString());
w.newLine();
}
}
return path;
}
// 2. Excel 호환 CSV (BOM)
public Path exportCsvForExcel(List<Shipment> shipments) throws IOException {
Path path = Path.of("/var/shipment/exports/shipments_excel.csv");
Files.createDirectories(path.getParent());
try (OutputStream os = Files.newOutputStream(path);
OutputStreamWriter osw = new OutputStreamWriter(os, CHARSET);
BufferedWriter w = new BufferedWriter(osw)) {
// BOM (Writer 거치지 않고)
os.write(new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF});
// 데이터
w.write("ID,선적번호,수하인,중량(kg)");
w.newLine();
for (Shipment s : shipments) {
w.write(s.toCsvLine());
w.newLine();
}
}
return path;
}
// 3. JSON 한글
public Path exportJson(Shipment shipment) throws IOException {
Path path = Path.of("/var/shipment/exports/shipment.json");
Files.createDirectories(path.getParent());
String json = String.format(
"{\"id\":%d,\"수하인\":\"%s\",\"비고\":\"%s\"}",
shipment.getId(),
escape(shipment.getConsignee()),
escape(shipment.getNotes()));
Files.writeString(path, json, CHARSET);
return path;
}
// 4. 로그 (append + 한글)
public void log(String level, String message) throws IOException {
Path logFile = Path.of("/var/log/shipment/app.log");
Files.createDirectories(logFile.getParent());
String line = String.format("[%s] [%s] %s%n",
LocalDateTime.now(), level, message);
Files.writeString(logFile, line, CHARSET,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
// 5. HTTP 응답 (네트워크)
public void writeResponse(OutputStream out, Shipment s) throws IOException {
try (OutputStreamWriter osw = new OutputStreamWriter(out, CHARSET);
BufferedWriter w = new BufferedWriter(osw)) {
w.write("HTTP/1.1 200 OK\r\n");
w.write("Content-Type: application/json; charset=UTF-8\r\n");
w.write("\r\n");
w.write(String.format("{\"id\":%d}", s.getId()));
}
}
private String escape(String s) {
return s == null ? "" : s.replace("\"", "\\\"");
}
}
한글 쓰기의 권장 패턴은?
답:
1. 가장 권장:
Files.newBufferedWriter(path, UTF_8)Files.writeString(path, content, UTF_8)인코딩 명시:
Excel 호환:
append:
흔한 버그 5가지:
Phase 8 — Stream 실전
Unit 8.1 — System.in (한글 안 되는 이유)
- System.in 의 정체 (InputStream)
- 1바이트 read 의 한계
- 인코딩 기초
- Scanner, BufferedReader, Console 비교
Unit 8.2 — FileInputStream
- InputStream 의 파일 전용
- read() 의 정확한 동작
- EOF (-1) 의 이유
- try-with-resources 필수
Unit 8.3 — byte[] 배열로 효율적 읽기
- read(byte[]) 의 효율
- 마지막 읽기 함정 (n 만큼만)
- for-each 위험성
- 버퍼 크기 (8KB sweet spot)
Unit 8.4 — FileOutputStream
- OutputStream 의 파일 전용
- 덮어쓰기 vs append
- write 메서드 종류
- 바이트가 문자로 보이는 이유
Unit 8.5 — 한글 처리 (FileReader, InputStreamReader)
- Reader vs InputStream
- FileReader 의 한계
- InputStreamReader 의 Bridge 패턴
- BufferedReader 결합
Unit 8.6 — FileWriter (한글 쓰기) ← 여기
- Writer vs OutputStream
- FileWriter 의 한계
- OutputStreamWriter
- BufferedWriter, PrintWriter
- Phase 8 완주
1. 파일 입출력 자유자재
- 텍스트 / 바이너리
- 한글 안전
- 인코딩 명시
2. 효율적 처리
- byte[] 활용
- 버퍼링
- Stream API
3. 다양한 소스
- 파일, 네트워크, 메모리
- InputStream/Reader 추상화
- OutputStream/Writer 추상화
4. 실무 함정 회피
- 1바이트 read 한계
- 마지막 읽기 n
- for-each 위험
- 인코딩 명시
- 디렉토리 자동 생성 X
5. NIO.2 활용
- Files.newBufferedReader/Writer
- Files.readString/writeString
- Stream 통합
Stream 의 두 가지 차원:
1. 바이트 vs 문자
- byte: InputStream/OutputStream
- char: Reader/Writer
- 한글: 반드시 Reader/Writer
2. 입력 vs 출력
- 입력: InputStream/Reader
- 출력: OutputStream/Writer
- 짝: write(buf, 0, n)
핵심 패턴:
- try-with-resources
- 인코딩 명시 (UTF-8)
- BufferedXxx 결합
- NIO.2 Files 활용
Phase 8: 기본 Stream
↓
Phase 9: Stream 의 강화
- try-with-resources 정밀
- BufferedInput/OutputStream
- DataInput/OutputStream
- Serialization (직렬화)
- serialVersionUID
Phase 8 의 흔한 함정 10가지:
1. System.in.close()
→ 표준 입력 영구 종료, 절대 X
2. 1바이트 read 로 한글 처리
→ 깨짐. Reader 사용
3. 인코딩 명시 안 함
→ 시스템 의존, Cross-platform 위험
4. 마지막 read 후 buf.length 사용
→ 이전 데이터 처리
5. for-each (byte b : buf)
→ buf.length 전체 순회
6. write(buf) 후 write(buf, 0, n) 누락
→ 이전 데이터 누적
7. close() 누락
→ 파일 핸들 누수
8. flush() 누락 (Buffered)
→ 데이터 손실 가능
9. 부모 디렉토리 자동 생성 가정
→ FileNotFoundException
10. PrintWriter 의 예외 마스킹
→ checkError() 누락 시 손실
✅ Phase 1 — Pass by Value (3 Unit)
✅ Phase 2 — 컬렉션 프레임워크 (6 Unit)
✅ Phase 3 — 해시의 원리 (4 Unit)
✅ Phase 4 — 추상화의 두 도구 (4 Unit)
✅ Phase 5 — 제네릭과 와일드카드 (5 Unit)
✅ Phase 6 — 객체 비교 (4 Unit)
✅ Phase 7 — I/O 시스템 큰 그림 (5 Unit)
✅ Phase 8 — Stream 실전 (6 Unit) ← 여기, 완주
⏭ Phase 9 — I/O 강화 (5 Unit)
⏭ Phase 10 — 함수형 프로그래밍 (4 Unit)
총: 37/43 Unit (Phase 8 완주, 약 86%)
Phase 8 학습의 종합은?
답:
1. 6개 Unit 학습:
두 가지 차원:
핵심 패턴:
흔한 함정 10가지:
마스터 후:
| Q | 핵심 답변 |
|---|---|
| Writer 정의? | char 단위, 인코딩 적용 |
| Writer vs OutputStream? | char vs byte, 인코딩 |
| FileWriter 한계? | Java 10 이하 인코딩 X |
| OutputStreamWriter 역할? | Bridge (OutputStream → Writer) |
| BufferedWriter newLine? | 시스템 줄바꿈 (\n / \r\n) |
| BufferedWriter vs PrintWriter? | 표준 vs printf/예외 마스킹 |
| 한글 쓰기 권장? | Files.newBufferedWriter + UTF_8 |
| flush() 의 역할? | 버퍼 → OS 강제 |
| Excel 한글 BOM? | UTF-8 BOM 0xEF 0xBB 0xBF |
Q1. System.in 의 타입? → InputStream
Q2. System.in 의 특성? → Blocking, line-buffered
Q3. read() 반환 int 이유? → byte (-1) 와 충돌
Q4. System.in.close()? → 표준 입력 영구 종료, X
Q5. Scanner 의 nextInt + nextLine 함정? → \n 버퍼 남음
Q6. Console 의 특별 기능? → readPassword
Q7. ASCII 의 범위? → 0~127
Q8. UTF-8 의 한글 크기? → 3바이트
Q9. EUC-KR 한글? → 2바이트
Q10. Java 18+ 기본 인코딩? → UTF-8
Q11. FileInputStream 정의? → InputStream 의 파일 전용
Q12. read() 반환? → 0~255, -1 EOF
Q13. -1 의 이유? → byte (0xFF) 와 충돌 회피
Q14. FileNotFoundException? → IOException 자식
Q15. try-with-resources? → AutoCloseable 자동 close
Q16. available() 의 의미? → 즉시 읽기 가능 (대략)
Q17. skip() 보장? → 요청한 만큼 X
Q18. mark/reset 지원? → FileInputStream X
Q19. getChannel()? → FileChannel 변환
Q20. NIO.2 대안? → Files.newInputStream
Q21. read(byte[]) 반환? → 실제 읽은 수, -1 EOF
Q22. 1바이트 vs byte[8KB] 성능? → 약 2000배
Q23. 마지막 읽기 함정? → n < buf.length, 이전 데이터
Q24. for-each 위험? → buf.length 전체
Q25. 올바른 처리? → for (i=0; i<n; i++)
Q26. write(buf, 0, n)? → n 만큼만
Q27. 권장 버퍼? → 8KB
Q28. OS 페이지 크기? → 4KB
Q29. readNBytes (Java 9+)? → 정확한 크기
Q30. transferTo (Java 9+)? → 다른 OutputStream
Q31. FileOutputStream 정의? → OutputStream 의 파일 전용
Q32. append 모드? → new FOS(path, true)
Q33. 덮어쓰기 vs append? → truncate vs 끝에 추가
Q34. write(int) 의 한계? → 하위 8비트만
Q35. 한글 write(int)? → 깨짐
Q36. flush()의 역할? → 버퍼 → OS
Q37. FOS 의 flush? → 자체 버퍼링 X
Q38. BufferedOutputStream flush? → 8KB → OS
Q39. 바이트가 문자로? → ASCII/UTF-8 매핑
Q40. 부모 디렉토리 자동? → X, mkdirs 필요
Q41. Reader vs InputStream? → char vs byte
Q42. FileReader 한계? → Java 10 이하 인코딩 X
Q43. InputStreamReader 역할? → Bridge
Q44. BufferedReader readLine? → 한 줄, \n/\r/\r\n 제외
Q45. char 16비트? → UTF-16 한 단위
Q46. Supplementary 문자? → 이모지, Surrogate Pair
Q47. Writer vs OutputStream? → 인코딩 적용
Q48. FileWriter 한계? → 동일
Q49. BufferedWriter newLine? → 시스템 줄바꿈
Q50. 권장 패턴? → Files.newBufferedReader/Writer + UTF_8
50 / 50 → Phase 8 마스터
45-49 → 거의 마스터
40-44 → 복습
< 40 → Unit 8.1 ~ 8.6 재학습
답:
답:
bw.newLine(); // 시스템 의존 (\n 또는 \r\n)
bw.write("\n"); // 항상 \n
// Cross-platform 텍스트:
// - 대부분 \n 권장 (대부분 OS 인식)
// - newLine 은 플랫폼 의존
// Windows 네이티브:
// - newLine() 사용 (\r\n)
답:
PrintWriter pw1 = new PrintWriter(out); // autoFlush=false
PrintWriter pw2 = new PrintWriter(out, true); // autoFlush=true
// autoFlush=true:
// - println 후 flush
// - 문자 배열 쓴 후 flush
// - print 후엔 flush 안 함
// 디버깅에 유용 (println 후 즉시 출력)
답:
// 이모지 😀 (U+1F600)
char[] chars = Character.toChars(0x1F600);
// chars = [0xD83D, 0xDE00] (Surrogate Pair)
w.write(chars);
// 또는
w.write(0xD83D);
w.write(0xDE00);
답:
// Files.writeString — 작은 콘텐츠
Files.writeString(path, "Hello\n안녕\n", UTF_8);
// 내부적으로 BufferedWriter 사용
// 더 간단
// BufferedWriter — 큰 콘텐츠, 점진적
try (BufferedWriter w = Files.newBufferedWriter(path, UTF_8)) {
for (int i = 0; i < 1000000; i++) {
w.write(data[i]);
w.newLine();
}
}
// 점진적 쓰기
// flush 제어
1. Writer
2. FileWriter vs OutputStreamWriter
3. 한글 쓰기
🚀 Phase 8 — Stream 실전
✅ Unit 8.1 System.in (한글 안 되는 이유)
✅ Unit 8.2 FileInputStream
✅ Unit 8.3 byte[] 배열로 효율적 읽기
✅ Unit 8.4 FileOutputStream
✅ Unit 8.5 한글 처리 (FileReader, InputStreamReader)
✅ Unit 8.6 FileWriter (한글 쓰기) ← 여기, Phase 8 완주
→ 파일 입출력 자유자재
→ 한글 안전 처리
→ 인코딩 명시 패턴
→ 실무 함정 회피
→ NIO.2 활용
Phase 9 — I/O 강화 (5 Unit)
Unit 9.1 — try-with-resources
- Java 7+ 자원 관리
- AutoCloseable 인터페이스
- Suppressed Exception
Unit 9.2 — BufferedInputStream / BufferedOutputStream
- 버퍼링의 정밀
- Decorator 패턴
- 성능 효과
Unit 9.3 — DataInputStream / DataOutputStream
- 기본 타입 입출력
- 바이너리 형식
- readInt/writeInt 등
Unit 9.4 — Serialization (직렬화)
- Serializable 인터페이스
- ObjectInputStream/OutputStream
- transient 키워드
Unit 9.5 — serialVersionUID
- 버전 관리
- 직렬화 호환성
- 보안 고려
✅ Phase 1 ~ 8 완주 (37 Unit)
⏭ Phase 9 — I/O 강화 (5 Unit)
⏭ Phase 10 — 함수형 프로그래밍 (4 Unit)
총: 37/43 Unit (Phase 8 완주, 약 86%)
🏆 Phase 8 완주 — Stream 실전 마스터 달성