F-LAB JAVA · 3주차 · Phase 8 · Stream 실전
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Reader는 문자 (char) 단위로 읽는 추상 클래스로, 인코딩을 인식 하여 바이트 시퀀스를 문자로 변환한다.
FileReader는 파일 전용 Reader 지만 Java 10 까지 인코딩 명시 불가 (시스템 기본만) — Cross-platform 위험.
InputStreamReader는InputStream을Reader로 변환하며 인코딩 명시 가능 — 실무 권장.
BufferedReader를 결합하면 readLine 으로 줄 단위 처리 + 버퍼링 효율.
Java 11+ 부터FileReader(File, Charset)생성자 추가로 FileReader 도 안전하게 사용 가능, 하지만Files.newBufferedReader(path, UTF_8)가 가장 권장.
InputStream:
외국어 책의 글자 사진을 한 장씩 보는 사람
- 사진 1장 = 1바이트
- 한글 "안" = 사진 3장 따로
- 의미 모름
Reader:
사전 (인코딩) 을 가진 번역가
- 사진 1~4장을 보고 1 문자
- 한글 "안" = "안" 으로 인식
- 의미 있음
FileReader (옛):
사전을 항상 한국에서 가져옴 (시스템 기본)
- 외국 가면 사전이 달라짐
- Cross-platform 위험
InputStreamReader (안전):
사전을 명시 ("UTF-8" 사전 사용)
- 어디서든 같은 사전
- 안전
→ Reader = 인코딩 인식 + 문자 단위.
1. Reader 의 정의와 InputStream 과의 차이
2. FileReader 의 정의와 한계
3. InputStreamReader 의 정의와 활용
4. FileReader vs InputStreamReader 비교
5. 인코딩 명시의 중요성
6. BufferedReader 와의 결합
7. char 의 한계 (Supplementary 문자)
8. Java 11+ 의 개선과 실무 패턴
9. 면접 + 자기 점검
public abstract class Reader implements Readable, Closeable {
// 핵심 메서드
public int read() throws IOException;
public int read(char[] cbuf) throws IOException;
public abstract int read(char[] cbuf, int off, int len) throws IOException;
// 추가
public long skip(long n) throws IOException;
public boolean ready() throws IOException;
public boolean markSupported();
public void mark(int readAheadLimit) throws IOException;
public void reset() throws IOException;
public abstract void close() throws IOException;
}
핵심:
java.io 패키지Reader (추상)
├── InputStreamReader ← InputStream 을 Reader 로
│ └── FileReader ← 파일 전용
├── BufferedReader ← 버퍼링 + readLine
├── StringReader ← String 을
├── CharArrayReader ← char[] 를
├── PipedReader ← 파이프
└── FilterReader
└── PushbackReader
핵심:
- 모두 char 처리
- InputStream 의 byte 와 대응
| 항목 | InputStream | Reader |
|---|---|---|
| 단위 | byte (8비트) | char (16비트) |
| 인코딩 | X (raw) | ✓ (자동 변환) |
| 반환 (read) | 0~255, -1 | 0~65535, -1 |
| 한글 처리 | 불가 (1바이트) | OK (인코딩 매핑) |
| 패키지 | java.io | java.io |
| 자식 | FileInputStream 등 | FileReader 등 |
// InputStream.read()
InputStream is = new FileInputStream("file.txt");
int b = is.read();
// 반환: 0~255 (byte 의 unsigned 값) 또는 -1
// 1바이트 (한글이면 분리됨)
// Reader.read()
Reader r = new InputStreamReader(new FileInputStream("file.txt"), "UTF-8");
int c = r.read();
// 반환: 0~65535 (char 의 값) 또는 -1
// 1문자 (한글 1글자 = 1 read)
// 파일 내용: "안" (UTF-8: EC 95 88)
// InputStream — 3번 read
try (FileInputStream fis = new FileInputStream("an.txt")) {
int b1 = fis.read(); // 236 (0xEC)
int b2 = fis.read(); // 149 (0x95)
int b3 = fis.read(); // 136 (0x88)
// 각각 따로 → 의미 잃음
}
// Reader — 1번 read
try (Reader r = new InputStreamReader(
new FileInputStream("an.txt"), "UTF-8")) {
int c = r.read(); // 51008 (0xC548 = '안')
char ch = (char) c; // '안'
// 한 글자 완성
}
파일: "안녕" (UTF-8, 6바이트)
InputStream.read() 시퀀스:
236, 149, 136, ← "안" 의 3바이트
235, 133, 149, ← "녕" 의 3바이트
-1 ← EOF
Reader.read() 시퀀스:
'안' (51008), ← "안" 1문자
'녕' (45397), ← "녕" 1문자
-1 ← EOF
// ❌ InputStream 으로 한글 (잘못)
public String readKoreanBad(Path path) throws IOException {
StringBuilder sb = new StringBuilder();
try (FileInputStream fis = new FileInputStream(path.toFile())) {
int b;
while ((b = fis.read()) != -1) {
sb.append((char) b);
// 한글 깨짐!
}
}
return sb.toString();
}
// ✓ Reader 로 한글
public String readKoreanGood(Path path) throws IOException {
StringBuilder sb = new StringBuilder();
try (Reader r = new InputStreamReader(
new FileInputStream(path.toFile()),
StandardCharsets.UTF_8)) {
int c;
while ((c = r.read()) != -1) {
sb.append((char) c);
}
}
return sb.toString();
}
// ✓✓ BufferedReader (더 효율적)
public String readKoreanBest(Path path) throws IOException {
try (BufferedReader r = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
return r.lines().collect(Collectors.joining("\n"));
}
}
Reader 와 InputStream 의 결정적 차이는?
답:
1. 단위:
인코딩:
한글 처리:
반환:
권장:
package java.io;
public class FileReader extends InputStreamReader {
public FileReader(String fileName) throws FileNotFoundException;
public FileReader(File file) throws FileNotFoundException;
public FileReader(FileDescriptor fd);
// Java 11+ 추가
public FileReader(String fileName, Charset charset) throws IOException;
public FileReader(File file, Charset charset) throws IOException;
}
핵심:
// Java 10 이하 — 인코딩 명시 불가
try (FileReader reader = new FileReader("file.txt")) {
int c;
while ((c = reader.read()) != -1) {
System.out.print((char) c);
}
}
// 문제:
// - 시스템 기본 인코딩 사용
// - Windows: MS949
// - Linux/Mac: UTF-8
// - Cross-platform 위험!
// 시나리오: UTF-8 파일을 Windows 에서 읽기
// 파일: "안녕" (UTF-8 으로 저장됨)
// Windows 의 자바 (MS949)
try (FileReader reader = new FileReader("hello.txt")) {
// 기본 인코딩 = MS949
// UTF-8 파일을 MS949 로 해석
// → 깨짐!
}
// Linux 의 자바 (UTF-8)
try (FileReader reader = new FileReader("hello.txt")) {
// 기본 인코딩 = UTF-8
// 정상 처리
}
// 같은 코드, 다른 결과
// → FileReader 의 큰 함정
// Java 11+ 부터 인코딩 명시 가능
try (FileReader reader = new FileReader("file.txt", StandardCharsets.UTF_8)) {
int c;
while ((c = reader.read()) != -1) {
System.out.print((char) c);
}
}
// 이제 안전:
// - 모든 OS 에서 UTF-8
// - Cross-platform 일관성
한계:
1. 인코딩 명시 (Java 10 이하) 불가
- Java 11+ 에서 해결
2. NIO.2 API 활용 어려움
- Path 대신 String/File 만
3. BufferedReader 와 결합해야 효율
- 단독으로는 1문자씩
4. Files.newBufferedReader 가 더 권장
- 더 간결, NIO.2 통합
// 짧은 텍스트 파일 — 간단할 때
try (FileReader reader = new FileReader("config.txt", StandardCharsets.UTF_8)) {
// 간단한 한 문자씩 처리
}
// 더 권장 — BufferedReader 결합
try (BufferedReader reader = new BufferedReader(
new FileReader("file.txt", StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
// 한 줄씩 처리
}
}
// 가장 권장 — NIO.2
try (BufferedReader reader = Files.newBufferedReader(
Path.of("file.txt"), StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
// 처리
}
}
public class ShipmentTextReader {
// Java 11+ 에서 안전
public String readShipmentText(Path path) throws IOException {
try (FileReader reader = new FileReader(path.toFile(), StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(reader)) {
return br.lines().collect(Collectors.joining("\n"));
}
}
// 권장 (NIO.2)
public String readShipmentTextModern(Path path) throws IOException {
return Files.readString(path, StandardCharsets.UTF_8);
}
// Stream
public List<Shipment> readShipmentsStream(Path path) throws IOException {
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
return lines.skip(1).map(this::parse).toList();
}
}
}
FileReader 의 정의와 한계는?
답:
1. 정의:
한계 (Java 10 이하):
Java 11+ 개선:
FileReader(File, Charset) 추가실무 권장:
Files.newBufferedReader(path, UTF_8) 가 더 좋음활용:
package java.io;
public class InputStreamReader extends Reader {
// 생성자
public InputStreamReader(InputStream in);
public InputStreamReader(InputStream in, String charsetName)
throws UnsupportedEncodingException;
public InputStreamReader(InputStream in, Charset cs);
public InputStreamReader(InputStream in, CharsetDecoder dec);
}
핵심:
// InputStream → Reader 변환
try (FileInputStream fis = new FileInputStream("file.txt");
InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
int c;
while ((c = isr.read()) != -1) {
System.out.print((char) c);
}
}
// 줄 단위 (BufferedReader 결합)
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream("file.txt"), StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
// 1. 파일
new InputStreamReader(new FileInputStream("file.txt"), UTF_8);
// 2. 표준 입력
new InputStreamReader(System.in, UTF_8);
// 3. URL (네트워크)
URL url = new URL("https://example.com");
new InputStreamReader(url.openStream(), UTF_8);
// 4. 메모리
byte[] data = "안녕".getBytes(UTF_8);
new InputStreamReader(new ByteArrayInputStream(data), UTF_8);
// 5. 소켓
new InputStreamReader(socket.getInputStream(), UTF_8);
// 모든 InputStream 을 Reader 로
// 1. 인코딩 안 함 (위험)
new InputStreamReader(in);
// 시스템 기본 인코딩
// 2. 문자열 인코딩 이름
new InputStreamReader(in, "UTF-8");
// UnsupportedEncodingException 가능
// 3. Charset 객체 (권장)
new InputStreamReader(in, StandardCharsets.UTF_8);
// 컴파일 타임 검증
// 4. CharsetDecoder (고급)
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
decoder.onMalformedInput(CodingErrorAction.REPLACE);
new InputStreamReader(in, decoder);
// 에러 처리 옵션
// 같은 InputStream 을 다른 인코딩으로
public void readMultipleEncodings(byte[] data) throws IOException {
// 데이터가 어떤 인코딩인지 모를 때
Charset[] candidates = {
StandardCharsets.UTF_8,
Charset.forName("MS949"),
Charset.forName("EUC-KR"),
StandardCharsets.ISO_8859_1
};
for (Charset cs : candidates) {
try (InputStreamReader isr = new InputStreamReader(
new ByteArrayInputStream(data), cs)) {
CharBuffer buffer = CharBuffer.allocate(1024);
isr.read(buffer);
System.out.println("As " + cs + ": " + buffer.flip());
}
}
}
사용자 호출:
isr.read()
내부 동작:
1. InputStream 에서 N 바이트 읽음 (필요한 만큼)
- UTF-8 의 첫 바이트로 길이 판단
- 1바이트 (ASCII), 2/3/4 바이트 (다국어)
2. CharsetDecoder 가 디코딩
- 바이트 시퀀스 → char
3. char 반환
복잡한 처리를 추상화
사용자는 단순히 char 받음
public class ShipmentEncodingReader {
// 1. 단일 인코딩
public String readUtf8(Path path) throws IOException {
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream(path.toFile()), StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr)) {
return br.lines().collect(Collectors.joining("\n"));
}
}
// 2. 옛 데이터 (EUC-KR)
public String readEucKr(Path path) throws IOException {
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream(path.toFile()), Charset.forName("EUC-KR"));
BufferedReader br = new BufferedReader(isr)) {
return br.lines().collect(Collectors.joining("\n"));
}
}
// 3. HTTP 응답 (Content-Type 기반)
public String readHttpResponse(URL url) throws IOException {
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
String contentType = conn.getContentType();
// "text/html; charset=UTF-8"
Charset charset = extractCharset(contentType);
try (InputStreamReader isr = new InputStreamReader(
conn.getInputStream(), charset);
BufferedReader br = new BufferedReader(isr)) {
return br.lines().collect(Collectors.joining("\n"));
}
}
private Charset extractCharset(String contentType) {
// "text/html; charset=UTF-8" → UTF-8
if (contentType != null && contentType.contains("charset=")) {
String charsetName = contentType.split("charset=")[1].trim();
return Charset.forName(charsetName);
}
return StandardCharsets.UTF_8; // 기본
}
}
InputStreamReader 의 정의와 활용은?
답:
1. 정의:
본질:
인코딩 명시:
활용:
| 항목 | FileReader | InputStreamReader |
|---|---|---|
| 정의 | InputStreamReader 의 자식 | Reader 의 직접 자식 |
| 입력 | 파일만 | 모든 InputStream |
| 인코딩 (Java 10-) | X | ✓ |
| 인코딩 (Java 11+) | ✓ | ✓ |
| 유연성 | ↓ | ↑↑ |
| 코드 길이 | 짧음 | 약간 길음 |
| 활용 | 단순 파일 | 다양 |
// FileReader (Java 11+)
try (FileReader fr = new FileReader("file.txt", StandardCharsets.UTF_8)) {
int c;
while ((c = fr.read()) != -1) {
System.out.print((char) c);
}
}
// InputStreamReader (Java 1.1+)
try (FileInputStream fis = new FileInputStream("file.txt");
InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
int c;
while ((c = isr.read()) != -1) {
System.out.print((char) c);
}
}
// 결과 동일
// 코드는 FileReader 가 조금 짧음
// InputStreamReader 가 더 유연 (네트워크 등도 OK)
// FileReader 는 사실상 InputStreamReader + FileInputStream
// Java 11+ FileReader 구현 (대략)
public class FileReader extends InputStreamReader {
public FileReader(File file, Charset charset) throws IOException {
super(new FileInputStream(file), charset);
}
public FileReader(String fileName, Charset charset) throws IOException {
super(new FileInputStream(fileName), charset);
}
}
// 즉:
// FileReader = InputStreamReader + FileInputStream 의 편의 래퍼
FileReader 권장:
✓ 파일만 처리
✓ Java 11+ 환경
✓ 짧은 코드 우선
✓ 인코딩 명시
InputStreamReader 권장:
✓ 파일 외에도 처리 (네트워크, 메모리 등)
✓ Java 10 이하 호환
✓ 다양한 InputStream
✓ 학습/이해 (Bridge 패턴)
가장 권장 (NIO.2):
✓ Files.newBufferedReader(path, UTF_8)
✓ 가장 간결
✓ Path 활용
// 가장 권장 — Files.newBufferedReader
try (BufferedReader reader = Files.newBufferedReader(
Path.of("file.txt"), StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
process(line);
}
}
// 또는 Stream
try (Stream<String> lines = Files.lines(
Path.of("file.txt"), StandardCharsets.UTF_8)) {
lines.forEach(this::process);
}
// 또는 한 번에 (작은 파일)
String content = Files.readString(Path.of("file.txt"), StandardCharsets.UTF_8);
같은 효과의 표현:
Files.newBufferedReader(path, UTF_8)
≡ new BufferedReader(new InputStreamReader(new FileInputStream(path.toFile()), UTF_8))
≡ new BufferedReader(new FileReader(path.toFile(), UTF_8)) // Java 11+
내부 동작:
1. FileInputStream — 바이트 스트림
2. InputStreamReader — 인코딩 적용
3. BufferedReader — 버퍼링 + readLine
public class ShipmentReader {
private final Charset charset = StandardCharsets.UTF_8;
// 1. 가장 권장 — NIO.2
public List<Shipment> readAllModern(Path path) throws IOException {
try (Stream<String> lines = Files.lines(path, charset)) {
return lines.skip(1).map(this::parse).toList();
}
}
// 2. Java 11+ FileReader (간결)
public List<Shipment> readAllFileReader(Path path) throws IOException {
try (BufferedReader br = new BufferedReader(
new FileReader(path.toFile(), charset))) {
return br.lines().skip(1).map(this::parse).toList();
}
}
// 3. InputStreamReader (전통)
public List<Shipment> readAllInputStreamReader(Path path) throws IOException {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(path.toFile()),
charset))) {
return br.lines().skip(1).map(this::parse).toList();
}
}
// 4. 네트워크 (InputStreamReader 만 가능)
public List<Shipment> readFromUrl(URL url) throws IOException {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(url.openStream(), charset))) {
return br.lines().skip(1).map(this::parse).toList();
}
}
}
FileReader 와 InputStreamReader 의 선택?
답:
1. FileReader:
InputStreamReader:
차이:
선택:
실무:
// ❌ 인코딩 없음
new InputStreamReader(is);
new FileReader("file.txt"); // Java 10 이하
// 시스템 기본 인코딩 사용
// - Windows 한국어: MS949
// - Linux: UTF-8
// - Mac: UTF-8
// 문제:
// - 환경 따라 다름
// - Cross-platform 위험
// - 디버깅 어려움
시나리오:
- 개발자 로컬: Mac (UTF-8)
- 운영 서버: Linux (UTF-8)
- 옛 윈도우 서버: Windows (MS949)
UTF-8 로 저장된 파일을 읽기:
new FileReader("data.txt"):
- Mac: UTF-8 → OK
- Linux: UTF-8 → OK
- Windows: MS949 로 해석 → 깨짐!
같은 코드, 다른 결과
JEP 400 (Java 18+):
- 기본 인코딩 UTF-8 통일
- 모든 OS
영향:
- Java 18+ 에선 FileReader 가 UTF-8 기본
- Cross-platform 일관성
하지만:
- 명시적 인코딩이 여전히 권장
- 의도 명확
- 다른 JVM 버전 호환
- 다른 인코딩 필요 시 대비
// ✓ 명시적
try (Reader r = new InputStreamReader(is, StandardCharsets.UTF_8)) {
// 안전:
// - 모든 OS
// - 모든 Java 버전
// - 의도 명확
}
// UTF-8 파일을 EUC-KR 로 읽기
byte[] utf8 = "안녕".getBytes(StandardCharsets.UTF_8);
// [0xEC, 0x95, 0x88, 0xEB, 0x85, 0x95]
try (Reader r = new InputStreamReader(
new ByteArrayInputStream(utf8), Charset.forName("EUC-KR"))) {
int c;
while ((c = r.read()) != -1) {
System.out.print((char) c);
}
}
// 출력: 깨진 한자 또는 ?
// EUC-KR 의 0xEC95 가 어떤 한자에 매핑되든
// 정상 한글 아님
// 인코딩 추정 라이브러리 활용
// 예: ICU4J, juniversalchardet
import org.mozilla.universalchardet.UniversalDetector;
public Charset detectCharset(byte[] data) throws IOException {
UniversalDetector detector = new UniversalDetector(null);
detector.handleData(data, 0, data.length);
detector.dataEnd();
String charsetName = detector.getDetectedCharset();
return charsetName != null
? Charset.forName(charsetName)
: StandardCharsets.UTF_8;
}
// 한계:
// - 100% 정확 X
// - 통계적 추정
// - 짧은 데이터 부정확
// - 명시적 인코딩이 더 안전
// UTF-8 BOM: 0xEF 0xBB 0xBF
// UTF-16 BE BOM: 0xFE 0xFF
// UTF-16 LE BOM: 0xFF 0xFE
// 파일 첫 부분에 BOM 이 있으면 인코딩 추정 가능
// 자바 표준은 BOM 자동 처리 X
// BOMInputStream (Apache Commons IO) 활용
import org.apache.commons.io.input.BOMInputStream;
try (InputStream is = new FileInputStream("file.txt");
BOMInputStream bis = new BOMInputStream(is, false); // BOM 제거
InputStreamReader isr = new InputStreamReader(bis, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr)) {
// BOM 자동 제거
String line;
while ((line = br.readLine()) != null) {
// BOM 없는 깨끗한 데이터
}
}
public class EncodingConstants {
public static final Charset DEFAULT = StandardCharsets.UTF_8;
public static final Charset LEGACY_KOREAN = Charset.forName("MS949");
public static final Charset LATIN = StandardCharsets.ISO_8859_1;
}
@Service
public class ShipmentEncodingService {
// 항상 명시적 인코딩
public String readFile(Path path) throws IOException {
return Files.readString(path, EncodingConstants.DEFAULT);
}
// 옛 시스템 데이터
public String readLegacy(Path path) throws IOException {
return Files.readString(path, EncodingConstants.LEGACY_KOREAN);
}
// 변환
public void convertEncoding(Path src, Path dest,
Charset srcCharset, Charset destCharset) throws IOException {
String content = Files.readString(src, srcCharset);
Files.writeString(dest, content, destCharset);
}
// HTTP 응답
public String fetchUrl(String url) throws IOException {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
try {
HttpResponse<String> response = client.send(request,
BodyHandlers.ofString(EncodingConstants.DEFAULT));
return response.body();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
}
}
}
인코딩 명시의 중요성은?
답:
1. 명시 안 하면:
Java 18+:
잘못된 인코딩 결과:
권장:
BOM:
public class BufferedReader extends Reader {
public BufferedReader(Reader in);
public BufferedReader(Reader in, int sz);
// 핵심 메서드
public String readLine() throws IOException;
// 상속
public int read();
public int read(char[] cbuf, int off, int len);
// Java 8+
public Stream<String> lines();
}
특징:
// 기본 결합
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("file.txt"),
StandardCharsets.UTF_8))) {
// ...
}
// Java 11+ FileReader
try (BufferedReader br = new BufferedReader(
new FileReader("file.txt", StandardCharsets.UTF_8))) {
// ...
}
// NIO.2 (가장 권장)
try (BufferedReader br = Files.newBufferedReader(
Path.of("file.txt"), StandardCharsets.UTF_8)) {
// ...
}
// 모두 같은 효과
// readLine 의 동작
public String readLine() throws IOException;
// 반환:
// - 한 줄 (\n, \r, \r\n 제외)
// - null (EOF)
// 사용
try (BufferedReader br = Files.newBufferedReader(path)) {
String line;
while ((line = br.readLine()) != null) {
process(line);
}
}
// readLine 의 구분자:
// \n (LF, Unix)
// \r (CR, Old Mac)
// \r\n (CRLF, Windows)
// 모두 처리, 결과에서 제외
일반 Reader (8KB 버퍼 없음):
read() → InputStreamReader → InputStream → OS
매 호출이 system call
BufferedReader (8KB 버퍼):
read() → BufferedReader 의 char[8192]
버퍼 비면 InputStreamReader 에서 채움
대부분 메모리 접근
효과:
- read 호출 빠름
- readLine 효율적
// lines() 메서드
try (BufferedReader br = Files.newBufferedReader(path)) {
br.lines()
.filter(l -> !l.isBlank())
.map(String::trim)
.forEach(System.out::println);
}
// Files.lines (더 간단)
try (Stream<String> lines = Files.lines(path)) {
lines.filter(...)
.map(...)
.forEach(...);
}
// 차이:
// - BufferedReader.lines(): 자기가 만든 reader
// - Files.lines(): NIO.2 의 정적 메서드
// - 둘 다 try-with-resources 필수
// 한 줄씩 — readLine
String line;
while ((line = br.readLine()) != null) {
process(line);
}
// 가독성 ↑
// 메모리: 한 줄 정도
// 한 문자씩 — read
int c;
while ((c = br.read()) != -1) {
process((char) c);
}
// 더 세밀한 제어
// 토큰 단위 파싱 등
// 청크 단위 — read(char[])
char[] buf = new char[1024];
int n;
while ((n = br.read(buf)) != -1) {
process(buf, 0, n); // ★ n 만큼만!
}
// 효율 ↑
// 마지막 함정 주의
public class ShipmentLogReader {
// 1. 한 줄씩 처리
public List<String> readLogLines(Path logFile) throws IOException {
List<String> lines = new ArrayList<>();
try (BufferedReader br = Files.newBufferedReader(logFile, StandardCharsets.UTF_8)) {
String line;
while ((line = br.readLine()) != null) {
lines.add(line);
}
}
return lines;
}
// 2. Stream + 필터링
public List<String> findErrorLogs(Path logFile) throws IOException {
try (Stream<String> lines = Files.lines(logFile, StandardCharsets.UTF_8)) {
return lines
.filter(l -> l.contains("ERROR"))
.toList();
}
}
// 3. 그룹핑
public Map<String, Long> countByLevel(Path logFile) throws IOException {
try (Stream<String> lines = Files.lines(logFile, StandardCharsets.UTF_8)) {
return lines
.map(this::extractLevel)
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()));
}
}
// 4. 대용량 처리
public long countLines(Path logFile) throws IOException {
try (Stream<String> lines = Files.lines(logFile, StandardCharsets.UTF_8)) {
return lines.count();
}
}
private String extractLevel(String line) {
// "2026-05-19 [INFO] message"
int start = line.indexOf('[');
int end = line.indexOf(']');
return (start >= 0 && end > start) ? line.substring(start + 1, end) : "UNKNOWN";
}
}
BufferedReader 의 효과는?
답:
1. 정의:
핵심 메서드:
결합:
효과:
권장:
Java 의 char:
- 16비트 (2바이트)
- 0 ~ 65535
- UTF-16 의 한 단위 (code unit)
표현 가능 범위:
- BMP (Basic Multilingual Plane): 0x0000 ~ 0xFFFF
- 65,536 코드 포인트
유니코드의 분류:
BMP (Basic Multilingual Plane):
- 0x0000 ~ 0xFFFF
- 대부분의 문자
- 영어, 한글, 한자 (기본), 일본어, 그리스어 등
- char 1개로 표현
Supplementary Planes:
- 0x10000 ~ 0x10FFFF
- 이모지 😀
- 고대 문자
- 일부 한자 (확장)
- char 2개로 표현 (Surrogate Pair)
Surrogate Pair:
Supplementary 문자 (BMP 밖) 을
char 2개로 표현하는 방법.
High Surrogate: 0xD800 ~ 0xDBFF
Low Surrogate: 0xDC00 ~ 0xDFFF
예: 😀 (U+1F600, GRINNING FACE)
UTF-16: 0xD83D 0xDE00 (2 char)
"😀".length() == 2 // char 길이
"😀".codePointCount(0, 2) == 1 // 코드 포인트 수
// Reader.read() 는 1 char 반환
// Supplementary 문자는 두 번 read 필요
try (Reader r = new InputStreamReader(
new ByteArrayInputStream("😀".getBytes(UTF_8)),
UTF_8)) {
int c1 = r.read(); // 0xD83D (High Surrogate)
int c2 = r.read(); // 0xDE00 (Low Surrogate)
int c3 = r.read(); // -1 (EOF)
// 단일 char 로는 불완전
String s = "" + (char) c1 + (char) c2;
// "😀" (정상)
}
// Code Point 단위 처리
String s = "Hello 😀 World";
// 잘못 — char 기반
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
System.out.println(c);
// 😀 가 두 char 로 나옴 (깨진 표시)
}
// 올바름 — Code Point
s.codePoints().forEach(cp -> {
String character = new String(Character.toChars(cp));
System.out.println(character);
// 😀 가 한 문자
});
// 또는 iterator
for (int i = 0; i < s.length(); ) {
int cp = s.codePointAt(i);
String character = new String(Character.toChars(cp));
System.out.println(character);
i += Character.charCount(cp); // 1 또는 2
}
// 이모지가 있는 파일
String text = "Hello 😀\n안녕 🌍";
Files.writeString(Path.of("emoji.txt"), text, StandardCharsets.UTF_8);
// 읽기 — 정상
try (BufferedReader br = Files.newBufferedReader(
Path.of("emoji.txt"), StandardCharsets.UTF_8)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
// 이모지 포함 정상 출력
}
}
// BufferedReader.readLine 은 String 반환
// String 은 Surrogate Pair 안전하게 처리
// 단, length(), charAt() 사용 시 주의
일반 한글 (BMP):
'안' = U+C548 (49992)
char 1개
한자 확장 (Supplementary):
'𠮷' (U+20BB7) — 일부 한자
char 2개 (Surrogate Pair)
영향:
- 일반 한글: char 안전
- 확장 한자, 이모지: Surrogate Pair 필요
- Code Point 기반 처리 권장
public class TextProcessor {
// 정확한 문자 수 계산
public int countCharacters(String text) {
return text.codePointCount(0, text.length());
// Surrogate Pair 를 1로 계산
}
// 안전한 길이 자르기
public String truncate(String text, int maxChars) {
if (text.codePointCount(0, text.length()) <= maxChars) {
return text;
}
StringBuilder sb = new StringBuilder();
int count = 0;
int i = 0;
while (i < text.length() && count < maxChars) {
int cp = text.codePointAt(i);
sb.appendCodePoint(cp);
i += Character.charCount(cp);
count++;
}
return sb.toString();
}
// 이모지 제거 (예시)
public String removeEmoji(String text) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < text.length(); ) {
int cp = text.codePointAt(i);
if (!isEmoji(cp)) {
sb.appendCodePoint(cp);
}
i += Character.charCount(cp);
}
return sb.toString();
}
private boolean isEmoji(int cp) {
// 이모지 범위 (대략)
return cp >= 0x1F600 && cp <= 0x1F64F;
}
}
char 의 한계와 Supplementary 문자는?
답:
1. char 의 크기:
BMP:
Supplementary:
처리:
codePoint 기반Character.charCountcodePoints().forEach영향:
Java 11 의 FileReader 개선:
이전 (Java 10 이하):
new FileReader("file.txt") // 시스템 기본 인코딩
Java 11+:
new FileReader("file.txt") // 시스템 기본 (호환)
new FileReader("file.txt", StandardCharsets.UTF_8) // 인코딩 명시 ✓
새 생성자:
FileReader(String, Charset)
FileReader(File, Charset)
이제 FileReader 도 안전하게 사용 가능
JEP 400 (Java 18+):
기본 인코딩 UTF-8 통일.
영향:
- new FileReader("file.txt") 가 UTF-8 (모든 OS)
- new InputStreamReader(is) 가 UTF-8
- 시스템 기본 의존 사라짐
호환:
- -Dfile.encoding=COMPAT (옛 동작)
- 명시적 인코딩 여전히 권장
// 1. 가장 권장 — Files.newBufferedReader
<try (BufferedReader reader = Files.newBufferedReader(
Path.of("file.txt"), StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
process(line);
}
}
// 2. Stream 처리
try (Stream<String> lines = Files.lines(
Path.of("file.txt"), StandardCharsets.UTF_8)) {
lines.filter(...)
.map(...)
.forEach(...);
}
// 3. 한 번에 읽기 (작은 파일)
String content = Files.readString(
Path.of("file.txt"), StandardCharsets.UTF_8);
List<String> allLines = Files.readAllLines(
Path.of("file.txt"), StandardCharsets.UTF_8);
// 4. 다양한 InputStream
try (Reader r = new InputStreamReader(
url.openStream(), StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(r)) {
// URL 응답 처리
}
// 5. 표준 입력
try (BufferedReader br = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
String line = br.readLine();
}
// Constants 클래스
public class IoConstants {
public static final Charset UTF8 = StandardCharsets.UTF_8;
public static final Charset MS949 = Charset.forName("MS949");
public static final Charset EUC_KR = Charset.forName("EUC-KR");
public static final int BUFFER_SIZE = 8192;
}
// 활용
try (BufferedReader br = Files.newBufferedReader(path, IoConstants.UTF8)) {
// ...
}
// 일관성:
// - 모든 코드에서 같은 인코딩
// - 변경 용이
// - 의도 명확
@Service
public class ShipmentTextService {
private static final Charset CHARSET = StandardCharsets.UTF_8;
// 1. CSV 읽기
public List<Shipment> importCsv(Path path) throws IOException {
try (Stream<String> lines = Files.lines(path, CHARSET)) {
return lines
.skip(1) // 헤더
.map(this::parseShipment)
.toList();
}
}
// 2. 큰 파일 청크 처리
public void processLargeFile(Path path, LineProcessor processor) throws IOException {
try (BufferedReader br = Files.newBufferedReader(path, CHARSET)) {
String line;
long lineNum = 0;
while ((line = br.readLine()) != null) {
processor.process(lineNum++, line);
}
}
}
// 3. 한글 검증
public boolean isValidKorean(Path path) throws IOException {
try (BufferedReader br = Files.newBufferedReader(path, CHARSET)) {
String line;
while ((line = br.readLine()) != null) {
if (containsCorruptedChars(line)) {
return false;
}
}
return true;
}
}
// 4. HTTP 응답 처리
public String fetchShipmentData(String url) throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
// Content-Type 의 charset 활용
String contentType = conn.getContentType();
Charset charset = extractCharset(contentType, CHARSET);
try (BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream(), charset))) {
return br.lines().collect(Collectors.joining("\n"));
}
}
private boolean containsCorruptedChars(String line) {
// U+FFFD (replacement character) 검사
return line.contains("\uFFFD");
}
private Charset extractCharset(String contentType, Charset defaultCharset) {
if (contentType != null && contentType.contains("charset=")) {
try {
return Charset.forName(contentType.split("charset=")[1].trim());
} catch (Exception e) {
// 무시
}
}
return defaultCharset;
}
@FunctionalInterface
interface LineProcessor {
void process(long lineNum, String line);
}
}
실무 인코딩 버그 5가지:
1. CSV Excel 에서 한글 깨짐
- 원인: Excel 의 BOM 요구
- 해결: UTF-8 BOM 추가
2. HTTP 응답 한글 깨짐
- 원인: Content-Type charset 누락
- 해결: 명시 또는 기본 UTF-8
3. DB 조회 한글 깨짐
- 원인: DB 인코딩 ↔ 클라이언트 인코딩 불일치
- 해결: 모두 UTF-8
4. 파일 이름 한글 깨짐
- 원인: 파일 시스템 인코딩
- 해결: -Dfile.encoding=UTF-8
5. 컴파일러 인코딩
- 원인: 소스 파일 인코딩
- 해결: -encoding UTF-8 (javac)
// 인코딩 확인
String defaultCharset = Charset.defaultCharset().name();
String fileEncoding = System.getProperty("file.encoding");
String consoleEncoding = System.getProperty("console.encoding");
System.out.println("Default: " + defaultCharset);
System.out.println("File: " + fileEncoding);
System.out.println("Console: " + consoleEncoding);
// 바이트 hex 출력
byte[] bytes = "안녕".getBytes(StandardCharsets.UTF_8);
for (byte b : bytes) {
System.out.printf("%02X ", b);
}
// EC 95 88 EB 85 95
// 다양한 인코딩으로 해석
byte[] data = ...;
for (Charset cs : List.of(UTF_8, UTF_16, Charset.forName("MS949"), Charset.forName("EUC-KR"))) {
System.out.println(cs + ": " + new String(data, cs));
}
Java 11+ 의 개선과 권장 패턴은?
답:
1. Java 11+ 개선:
FileReader(File, Charset) 추가Java 18+ 변화:
가장 권장:
Files.newBufferedReader(path, UTF_8)명시적 인코딩:
실무 5가지 버그:
| Q | 핵심 답변 |
|---|---|
| Reader 정의? | 문자 단위, 인코딩 인식 |
| Reader vs InputStream? | char vs byte, 인코딩 |
| FileReader 한계? | Java 10 이하 인코딩 명시 X |
| InputStreamReader 역할? | Bridge (InputStream → Reader) |
| 인코딩 명시 안 하면? | 시스템 기본, Cross-platform 위험 |
| BufferedReader 효과? | 버퍼링 + readLine |
| readLine 의 줄 구분? | \n, \r, \r\n |
| Reader.read() 반환? | 0~65535 또는 -1 |
| char 16비트? | UTF-16 한 단위 |
| Supplementary 문자? | 이모지, char 2개 (Surrogate) |
| Java 18+ 변화? | UTF-8 기본 |
| 가장 권장? | Files.newBufferedReader(path, UTF_8) |
답:
답:
// 기본 동작
Charset.forName("UTF-8");
// 잘못된 시퀀스 → U+FFFD (replacement character) 로 치환
// 엄격하게
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
decoder.onMalformedInput(CodingErrorAction.REPORT); // 예외
decoder.onUnmappableCharacter(CodingErrorAction.REPORT);
try (Reader r = new InputStreamReader(is, decoder)) {
// MalformedInputException
}
답:
답:
char[] buf = new char[1024];
int n = br.read(buf);
// n < buf.length 가능
// ❌ for-each
for (char c : buf) { ... }
// ✓ n 까지만
for (int i = 0; i < n; i++) { ... }
// InputStream 의 read(byte[]) 와 동일한 함정
답:
1. Reader vs InputStream
2. FileReader vs InputStreamReader
3. 권장 패턴
이번 Unit에서 한글 읽기를 봤다면, 다음은 한글 쓰기 + Phase 8 완주.
🚀 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 완주
✅ Phase 1 ~ 7 완주 (31 Unit)
🚀 Phase 8 — Stream 실전 (5/6 진행)
총: 36/43 Unit (약 84%)