F-LAB JAVA · 3주차 · Phase 8 · Stream 실전
🚀 Phase 8 시작 — Stream 코드 실전
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
System.in은 표준 입력의InputStream인스턴스로read()가 1바이트씩 반환하기 때문에 한글 (2~3바이트) 을 그대로 읽으면 깨진다.
인코딩 = 문자 ↔ 바이트 매핑 규칙으로, ASCII (1바이트), UTF-8 (1~4바이트), EUC-KR/MS949 (2바이트) 가 있으며,
한글을 올바르게 읽으려면 InputStream → InputStreamReader (인코딩 명시) → BufferedReader 또는 Scanner 사용.
Java 18+ 부터 기본 인코딩이 UTF-8 로 통일되었지만 OS 별 차이를 명시하는 게 안전.
실무에선Scanner의 간편함과BufferedReader.readLine()의 효율 +Console의 비밀번호 처리가 각각 자리가 있다.
System.in.read() (InputStream):
외국어 책을 글자 하나씩 펴서 그림으로 봄
- 영어 "A" = 1 그림 = 인식 OK
- 한글 "가" = 3 그림 = 따로 보면 의미 모름
- 글자가 깨짐
Reader (with encoding):
외국어 사전을 가지고 읽음
- "A" → 'A' (1 글자)
- "가" (3바이트) → '가' (1 문자)
- 인코딩 = 사전
- 문자 단위로 정확히 읽음
→ Reader = 인코딩 인식 + 문자 단위 처리.
1. System.in 의 정체
2. 표준 입출력 3총사 (in, out, err)
3. InputStream.read 의 1바이트 한계
4. 인코딩의 기초 (ASCII, UTF-8, EUC-KR)
5. 한글이 깨지는 정확한 메커니즘
6. Reader/Writer 로 해결
7. Scanner, BufferedReader, Console 비교
8. Java 18+ UTF-8 기본화와 실무
9. 면접 + 자기 점검
public final class System {
public static final InputStream in;
public static final PrintStream out;
public static final PrintStream err;
// 초기화는 native 코드로
}
핵심:
InputStream 타입// System.in 의 런타임 타입 확인
public class SystemInDemo {
public static void main(String[] args) {
InputStream in = System.in;
System.out.println(in.getClass().getName());
// 예: java.io.BufferedInputStream
// 내부적으로 BufferedInputStream 으로 감싸짐
System.out.println(in.getClass().getSuperclass().getName());
// java.io.FilterInputStream
// 실제 핵심: native FileInputStream (fd=0, 표준 입력)
}
}
// 참고:
// - JVM 시작 시 native 메서드로 초기화
// - 내부 구조는 JVM 구현 의존
// - 표준 입력 파일 디스크립터 (fd=0) 와 연결
System.in 의 5가지 특성:
1. InputStream (바이트 스트림)
- 1바이트씩 읽음
- 문자 변환 X (인코딩 처리 안 함)
2. Blocking
- 데이터 (사용자 입력) 올 때까지 대기
- Enter 키 누를 때까지 read 가 안 끝남
3. Buffered (대부분 JVM)
- 줄 단위 입력 (line-buffered)
- Enter 키 누르기 전엔 데이터 안 옴
4. 닫지 말 것
- 표준 입력 닫으면 복구 불가
- try-with-resources 사용 시 주의
5. JVM 단일 인스턴스
- 한 JVM 에 하나
- 모든 스레드 공유
// 1바이트 읽기
public class SimpleRead {
public static void main(String[] args) throws IOException {
System.out.print("Enter: ");
int b = System.in.read(); // 1바이트 + Enter (line-buffered)
// 결과: int 값 (0~255 또는 -1)
System.out.println("Got: " + b);
System.out.println("As char: " + (char) b);
// 영어 입력 OK
// "A" 입력 → 65 (A) + 13/10 (\r\n 또는 \n)
// 한글 입력
// "가" 입력 → 3바이트가 큐에 쌓임
// 첫 read() 는 첫 바이트만 가져옴
}
}
// System.in 을 다른 InputStream 으로 교체 가능
public class RedirectDemo {
public static void main(String[] args) throws IOException {
// 파일을 표준 입력으로
System.setIn(new FileInputStream("input.txt"));
// 이제 System.in.read() 가 파일을 읽음
int b = System.in.read();
// 활용:
// - 테스트 (입력 자동화)
// - 배치 처리
}
}
// 쉘 리다이렉트도 동일
// $ java MyApp < input.txt
// → System.in 이 input.txt
// 닫으면 복구 불가
// System.in.close(); // ★ 절대 하지 말 것
// 표준 입력 종료 신호:
// - Unix/Linux/Mac: Ctrl+D
// - Windows: Ctrl+Z + Enter
// 종료 시 read() 는 -1 반환
public class EofDetection {
public static void main(String[] args) throws IOException {
int b;
while ((b = System.in.read()) != -1) {
System.out.write(b);
}
// Ctrl+D 또는 Ctrl+Z 누르면 종료
}
}
// 1. 디버그 도구 — 콘솔 입력
public class ShipmentDebugTool {
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("=== ILIC Debug Tool ===");
System.out.println("Commands: info <id> | reset | exit");
String command;
while ((command = reader.readLine()) != null) {
if ("exit".equals(command)) break;
// 처리
}
}
}
// 2. 배치 스크립트 — 파일 리다이렉트
// shipment-batch.sh:
// java -jar shipment-batch.jar < input-ids.txt
// 3. 운영 도구 — 비밀번호 입력 (Console)
Console console = System.console();
if (console != null) {
char[] pass = console.readPassword("DB Password: ");
// 안전한 비밀번호 입력
}
System.in 의 정체와 특성은?
답:
1. 타입:
InputStream본질:
특성:
활용:
System.in:
- 표준 입력 (Standard Input, stdin)
- 파일 디스크립터 0
- 보통 키보드
- InputStream
System.out:
- 표준 출력 (Standard Output, stdout)
- 파일 디스크립터 1
- 보통 콘솔
- PrintStream
System.err:
- 표준 에러 (Standard Error, stderr)
- 파일 디스크립터 2
- 보통 콘솔 (out 과 별개)
- PrintStream
// System.out 과 System.err 의 타입
public final static PrintStream out;
public final static PrintStream err;
// PrintStream 은 OutputStream 의 자식
// 편리한 메서드 추가
System.out.println("Hello");
System.out.print("World");
System.out.printf("Value: %d%n", 42);
System.out.write(65); // 바이트 (low-level)
// flush
System.out.flush(); // 강제 출력
out 과 err 는 왜 분리?
이유:
- 정상 출력 vs 에러 메시지 분리
- 쉘에서 각각 리다이렉트 가능
- 로그와 데이터 분리
쉘 활용:
$ java MyApp 1> output.txt 2> errors.txt
- out → output.txt
- err → errors.txt
$ java MyApp 2>/dev/null
- 에러 메시지 무시
$ java MyApp 2>&1
- err 를 out 으로
// out 과 err 는 같은 콘솔로 나가지만 별개 스트림
System.out.println("Output");
System.err.println("Error");
// 순서 보장 안 됨
// 콘솔에서 섞일 수 있음
// 동기화 필요시
synchronized (System.out) {
System.out.println("...");
}
// println 의 동작
System.out.println("Hello");
// 1. "Hello" 출력
// 2. 시스템 줄바꿈 (line.separator) 추가
// - Unix: \n
// - Windows: \r\n
// 인코딩
// PrintStream 은 내부적으로 시스템 인코딩 사용
// Windows: MS949 (CP949)
// Linux/Mac: UTF-8
// → Cross-platform 문제 가능
// 명시적 인코딩
PrintStream out = new PrintStream(
new BufferedOutputStream(System.out),
true,
StandardCharsets.UTF_8);
// 표준 스트림을 다른 곳으로
System.setIn(new FileInputStream("input.txt"));
System.setOut(new PrintStream(new FileOutputStream("output.log")));
System.setErr(new PrintStream(new FileOutputStream("errors.log")));
// 이후 모든 println 이 파일로
System.out.println("To file");
// 복원 (원본 저장 후)
PrintStream originalOut = System.out;
System.setOut(new PrintStream(new ByteArrayOutputStream()));
// ... 임시 리다이렉트
System.setOut(originalOut);
// 콘솔 도구
public class ShipmentConsoleTool {
public static void main(String[] args) {
// out — 정상 메시지
System.out.println("Shipment Tool v1.0");
try {
processShipments();
} catch (Exception e) {
// err — 에러
System.err.println("Error: " + e.getMessage());
e.printStackTrace(System.err);
System.exit(1);
}
System.out.println("Done");
}
}
// 쉘 사용
// $ java ShipmentConsoleTool > result.log 2> error.log
System.in, out, err 의 차이와 활용은?
답:
1. System.in:
System.out:
System.err:
왜 분리?:
리다이렉트:
setIn, setOut, setErr>, 2>, 2>&1public abstract class InputStream {
public abstract int read() throws IOException;
// - 1바이트 읽기
// - 반환: 0~255 또는 -1 (EOF)
// - byte 가 아니라 int 인 이유: -1 표현
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
public int read(byte[] b, int off, int len) throws IOException {
// 여러 바이트 읽기
// 반환: 실제 읽은 수, -1 (EOF)
}
}
// 1바이트씩
InputStream in = System.in;
int b = in.read();
// 사용자가 "A" + Enter 입력:
// - "A" → 65
// - 첫 read(): 65 반환
// - 두 번째 read(): 10 (LF) 또는 13 (CR) — OS 따라
// 사용자가 "가" + Enter 입력 (UTF-8):
// - "가" → 0xEA, 0xB0, 0x80 (3바이트)
// - 첫 read(): 234 (0xEA)
// - 두 번째 read(): 176 (0xB0)
// - 세 번째 read(): 128 (0x80)
// - 네 번째 read(): 10 (LF)
// 각 read() 는 한 바이트만!
// 한글 1글자가 3번 read 로 분리됨
1바이트 (8비트) 의 표현 범위:
- 0 ~ 255 (unsigned)
- 256 가지 가능
문자 표현:
- ASCII: 0~127 (영문, 숫자, 기호)
- 한글: 한 글자 = 2~3바이트
- 중국어, 일본어: 비슷
문제:
- 1바이트로 모든 문자 X
- 한글 1글자가 여러 바이트
- 1바이트씩 읽으면 깨짐
// 영문 "Hello" — UTF-8 / ASCII 동일
// H = 72, e = 101, l = 108, l = 108, o = 111
// 5바이트
// 1바이트씩 read → 'H', 'e', 'l', 'l', 'o' 정상
// 한글 "안녕" — UTF-8
// "안" = 0xEC, 0x95, 0x88 (3바이트)
// "녕" = 0xEB, 0x85, 0x95 (3바이트)
// 총 6바이트
// 1바이트씩 read 결과:
// 236, 149, 136, 235, 133, 149
// (char) 변환: 'ì', '•', 'ˆ', 'ë', '…', '•'
// → 완전히 깨진 문자
// 한글 1글자 = 1 read() 로 못 읽음
// 정확히 3번 읽고 합쳐야
// 여러 바이트 한 번에
byte[] buf = new byte[1024];
int n = System.in.read(buf);
// n = 실제 읽은 바이트 수
// buf 의 0 ~ n-1 에 데이터
// 한글 "안녕" 입력 시:
// n = 6 또는 7 (with \n)
// buf[0..5] = [236, 149, 136, 235, 133, 149]
// 여전히 문자 단위 아님
// 인코딩 디코딩 필요
// 방법 1: 모든 바이트 모으기 + 인코딩 디코딩
byte[] buf = new byte[1024];
int n = System.in.read(buf);
String s = new String(buf, 0, n, StandardCharsets.UTF_8);
// "안녕\n"
// 방법 2: Reader 사용 (권장)
BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8));
String line = reader.readLine();
// "안녕"
// 방법 3: Scanner
Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8);
String line = scanner.nextLine();
// 왜 int 인가? (한 번 더)
// byte 는 -128 ~ 127 (signed)
// EOF (-1) 도 표현 가능하지만
// 정상 데이터 0xFF (= -1 as byte) 와 충돌
// int 로 확장:
// - 0~255: 정상 바이트 (unsigned 처리)
// - -1: EOF
int b = in.read();
if (b == -1) {
// 스트림 끝
} else {
// 정상 바이트 (0~255)
byte data = (byte) b; // 명시적 캐스트
char c = (char) b; // char (16비트로 확장)
}
// read(byte[]) 의 반환값
byte[] buf = new byte[10];
int n = in.read(buf);
// n 의 의미:
// - 양수: 실제 읽은 바이트 수
// - -1: EOF (스트림 끝)
// - 0: 가능하지만 드뭄 (Blocking 모드)
// 함정: 다음번 read 호출 시
// 첫 호출: 데이터 일부 (8바이트)
// 두 번째: 남은 데이터 (2바이트)
// 세 번째: -1 (EOF)
// 즉, 한 번에 다 안 읽힘
while ((n = in.read(buf)) != -1) {
// 0 ~ n-1 처리
process(buf, 0, n);
}
InputStream.read() 의 1바이트 한계와 한글 문제는?
답:
1. read() 의 동작:
한글 문제:
해결:
read(byte[]) 의 반환:
인코딩 (Encoding):
문자 (의미) ↔ 바이트 (저장) 의 매핑 규칙.
예:
- "A" 라는 문자 ↔ 65 (0x41) 바이트
- "안" 이라는 문자 ↔ 0xEC 0x95 0x88 (UTF-8) 또는 0xBE C8 (EUC-KR)
같은 문자도 인코딩에 따라 다른 바이트.
ASCII (American Standard Code for Information Interchange):
1963년 표준
- 7비트 (128 문자)
- 영문 대소문자, 숫자, 기본 기호
- 한글, 일본어, 중국어 X
문자 ↔ 바이트:
'A' = 65 (0x41)
'B' = 66 (0x42)
...
'Z' = 90 (0x5A)
'a' = 97 (0x61)
...
'z' = 122 (0x7A)
'0' = 48 (0x30)
...
'9' = 57 (0x39)
공백 = 32 (0x20)
'\n' = 10 (0x0A)
'\r' = 13 (0x0D)
특징:
- 1바이트당 1문자
- 영문만
- 다른 인코딩의 기반
EUC-KR (Extended Unix Code Korean):
1990년대 한국 표준
- 영문: ASCII 호환 (1바이트)
- 한글: 2바이트
- 한자: 2바이트
한글 표현:
- 첫 바이트: 0xA1~0xFE
- 둘째 바이트: 0xA1~0xFE
- 약 2,350자 (완성형)
문제:
- 한글 완성형만 (조합 X)
- "쀍", "긝" 같은 일부 한글 표현 X
확장: CP949 (Microsoft Codepage 949), Windows 949, MS949
- EUC-KR 확장
- 더 많은 한글 (11,172자)
- Windows 한국어의 기본
UTF-8 (Unicode Transformation Format, 8-bit):
1993년 표준
- 가변 길이 (1~4바이트)
- 모든 유니코드 문자 표현
- ASCII 호환 (1바이트 영문)
- 현재 인터넷 표준
가변 길이 규칙:
1바이트: 0xxxxxxx (영문, ASCII 호환)
2바이트: 110xxxxx 10xxxxxx (라틴, 그리스 등)
3바이트: 1110xxxx 10xxxxxx 10xxxxxx (한글, 일본어, 중국어)
4바이트: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx (이모지, 고대 문자)
한글 예:
"안" → 0xEC 0x95 0x88
이진:
0xEC = 11101100
0x95 = 10010101
0x88 = 10001000
앞 1110 = 3바이트 시그널
10 = continuation 표시
나머지: 1100 010101 001000
= U+C548 (유니코드 '안')
특징:
- 영문은 1바이트 (효율)
- 한글은 3바이트
- 표준
UTF-16:
- 2바이트 또는 4바이트
- 자바의 내부 char 표현
- String 의 내부도 UTF-16 (까지 Java 8, Java 9+ 는 압축)
- Windows 의 일부 API
UTF-32:
- 항상 4바이트
- 단순 (인덱스 직접)
- 메모리 비효율
- 거의 안 사용
비교:
"A" (UTF-8): 0x41 (1바이트)
"A" (UTF-16): 0x00 0x41 (2바이트)
"A" (UTF-32): 0x00 0x00 0x00 0x41 (4바이트)
"안" (UTF-8): 0xEC 0x95 0x88 (3바이트)
"안" (UTF-16): 0xC5 0x48 (2바이트)
"안" (UTF-32): 0x00 0x00 0xC5 0x48 (4바이트)
| 인코딩 | 크기 | 영문 | 한글 | 활용 |
|---|---|---|---|---|
| ASCII | 1바이트 | ✓ | X | 영문 전용 |
| EUC-KR | 1~2바이트 | 1B | 2B | 옛 한국 |
| CP949 (MS949) | 1~2바이트 | 1B | 2B | Windows 한국어 |
| UTF-8 | 1~4바이트 | 1B | 3B | 인터넷 표준 |
| UTF-16 | 2 또는 4바이트 | 2B | 2B | 자바 내부 |
| UTF-32 | 4바이트 | 4B | 4B | 드물게 |
// JVM 의 기본 인코딩
String defaultCharset = Charset.defaultCharset().name();
// Java 17 이하:
// Windows 한국어: x-windows-949 (MS949)
// Linux/Mac: UTF-8
// Java 18+:
// UTF-8 통일 (JEP 400)
// 명시적 인코딩
Charset.forName("UTF-8");
Charset.forName("EUC-KR");
Charset.forName("MS949");
// 표준 인코딩 상수
StandardCharsets.UTF_8;
StandardCharsets.UTF_16;
StandardCharsets.US_ASCII;
StandardCharsets.ISO_8859_1;
// String ↔ byte[]
String s = "안녕하세요";
byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);
// [236, 149, 136, 235, 133, 149, ...]
// 15바이트 (5글자 × 3바이트)
byte[] euckr = s.getBytes(Charset.forName("EUC-KR"));
// 10바이트 (5글자 × 2바이트)
// 디코딩 (바이트 → 문자열)
String s1 = new String(utf8, StandardCharsets.UTF_8);
// "안녕하세요" ✓
// 잘못된 디코딩
String s2 = new String(utf8, Charset.forName("EUC-KR"));
// 깨진 문자열
// 인코딩 변환
byte[] reEncoded = new String(utf8, StandardCharsets.UTF_8)
.getBytes(Charset.forName("EUC-KR"));
// UTF-8 → String → EUC-KR
인코딩의 기초와 한글 표현은?
답:
1. 인코딩: 문자 ↔ 바이트 매핑
주요 인코딩:
자바 기본:
변환:
String.getBytes(Charset)new String(bytes, Charset)// System.in 에서 "안" 입력 (UTF-8 환경)
InputStream in = System.in;
int b1 = in.read(); // 236 (0xEC)
int b2 = in.read(); // 149 (0x95)
int b3 = in.read(); // 136 (0x88)
// 잘못된 처리: 각각 char 로
char c1 = (char) b1; // 'ì' (Latin-1 의 236번)
char c2 = (char) b2; // '•'
char c3 = (char) b3; // 'ˆ'
System.out.println("" + c1 + c2 + c3);
// "안" — 깨진 문자
// 원인:
// - 한글 1글자 = 3바이트
// - 각 바이트를 따로 char 로 변환
// - 의미 잃음
// 파일이 UTF-8 인데 EUC-KR 로 읽기
byte[] data = Files.readAllBytes(Path.of("utf8-file.txt"));
String wrong = new String(data, Charset.forName("EUC-KR"));
// 깨진 문자열
String right = new String(data, StandardCharsets.UTF_8);
// 정상
// 반대로
byte[] data2 = Files.readAllBytes(Path.of("euckr-file.txt"));
String s = new String(data2, StandardCharsets.UTF_8);
// 깨짐
// 원인:
// - 인코딩이 다르면 다른 바이트 패턴
// - 잘못 해석하면 의미 잃음
// Windows (MS949) 에서 작성한 파일
// "안녕" → 0xBE 0xC8 0xB3 0xC4 (4바이트)
// Linux (UTF-8) 에서 읽기 (기본 인코딩)
FileReader reader = new FileReader("hello.txt");
// 기본 인코딩 = UTF-8
// MS949 바이트를 UTF-8 로 해석 → 깨짐
// 해결: 명시적 인코딩
Reader reader = new InputStreamReader(
new FileInputStream("hello.txt"),
Charset.forName("MS949"));
// HTTP 응답의 인코딩
// Content-Type: text/html; charset=UTF-8
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
// ❌ 기본 인코딩 사용
// 서버가 UTF-8 인데 클라이언트가 다른 인코딩이면 깨짐
// ✓ 명시적
BufferedReader reader = new BufferedReader(
new InputStreamReader(in, StandardCharsets.UTF_8));
한글 처리의 정확한 흐름:
1. 사용자 입력 (콘솔 또는 파일)
- OS 의 인코딩으로 저장
- Windows: MS949
- Linux/Mac: UTF-8
2. 바이트 스트림 (InputStream)
- 원본 바이트 그대로 읽음
- 의미 없음 (그냥 숫자)
3. 인코딩 인식 (InputStreamReader)
- 바이트 → 문자
- 인코딩 매핑 적용
- "안" (UTF-8 3바이트) → '안' (1 char)
4. 자바 내부 (String, char)
- UTF-16 (BMP) 또는 UTF-16 surrogate (확장)
- 항상 일관된 표현
5. 출력 (OutputStreamWriter)
- 문자 → 바이트
- 다시 인코딩 매핑
핵심:
- 입력/출력: 바이트 + 인코딩
- 내부: 항상 char (UTF-16)
// Java 8 까지: char[] (UTF-16)
String s = "안녕";
// 내부: ['안', '녕'] (각 char = 2바이트)
// 4바이트
// Java 9+ (JEP 254): Compact Strings
// - 영문만: byte[] (Latin-1, 1바이트)
// - 비영문 포함: byte[] (UTF-16, 2바이트)
// - 동일 API, 내부 최적화
String ascii = "Hello";
// Java 9+: byte[] (Latin-1, 5바이트)
String korean = "안녕";
// Java 9+: byte[] (UTF-16, 4바이트)
깨짐의 일반 패턴:
1. 시스템 인코딩 차이 (Windows ↔ Linux)
- 해결: 명시적 인코딩
2. 잘못된 디코딩
- UTF-8 바이트를 EUC-KR 로
- 해결: 인코딩 확인
3. 1바이트씩 처리
- read() 결과를 char 로 직접
- 해결: Reader 사용
4. 중간 변환
- 인코딩 변환 시 손실
- 해결: 단일 인코딩
5. HTTP/XML 의 charset 무시
- Content-Type 헤더 확인 안 함
- 해결: 명시적 charset
// 1. 파일 처리 (명시적 UTF-8)
try (BufferedReader reader = Files.newBufferedReader(
Path.of("shipments.csv"), StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
// 한글 안전하게 처리
}
}
// 2. HTTP 응답
@GetMapping(value = "/api/shipments", produces = "application/json;charset=UTF-8")
public ShipmentResponse get() {
return new ShipmentResponse("한글 데이터");
}
// 3. DB 인코딩
// application.yml
// spring:
// datasource:
// url: jdbc:postgresql://localhost:5432/ilic?characterEncoding=UTF-8
// 4. 콘솔 출력
// JVM 옵션
// -Dfile.encoding=UTF-8
// -Dconsole.encoding=UTF-8
한글이 깨지는 정확한 메커니즘은?
답:
1. 1바이트씩 처리:
잘못된 인코딩:
시스템 차이:
해결:
자바 내부:
public abstract class Reader implements Closeable, Readable {
public abstract int read(char[] cbuf, int off, int len) throws IOException;
public int read() throws IOException;
public int read(char[] cbuf) throws IOException;
public boolean ready() throws IOException;
public abstract void close() throws IOException;
// skip, mark, reset 등
}
핵심:
// InputStream → Reader 변환
InputStreamReader isr = new InputStreamReader(
System.in,
StandardCharsets.UTF_8); // ★ 인코딩 명시
// 한 문자씩 (UTF-16, 한글 1글자 = 1 char)
int c = isr.read();
char ch = (char) c;
// 한글 입력 "안":
// 1. 사용자가 "안" 입력 (UTF-8 3바이트)
// 2. InputStream 이 3바이트 받음
// 3. InputStreamReader 가 UTF-8 디코딩
// 4. char '안' (1 문자) 반환
// BufferedReader = Reader + 버퍼링 + readLine
BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8));
// 한 줄씩
String line = reader.readLine();
// 줄바꿈 (\n) 또는 EOF 까지
// 효율:
// - 버퍼링으로 OS 호출 ↓
// - readLine 으로 줄 단위 편의
// InputStream.read() — 1바이트
InputStream in = ...;
int b = in.read(); // 0~255 (byte), -1 (EOF)
// Reader.read() — 1문자 (UTF-16 단위)
Reader reader = ...;
int c = reader.read(); // 0~65535 (char), -1 (EOF)
// 차이:
// - InputStream: byte
// - Reader: char (16비트)
// - Reader 가 인코딩 처리
// 한글 처리:
// InputStream: 한글 1글자 = 3번 read
// Reader: 한글 1글자 = 1번 read
public abstract class Writer implements Closeable, Flushable, Appendable {
public abstract void write(char[] cbuf, int off, int len) throws IOException;
public void write(int c) throws IOException;
public void write(char[] cbuf) throws IOException;
public void write(String str) throws IOException;
public void write(String str, int off, int len) throws IOException;
public abstract void flush() throws IOException;
public abstract void close() throws IOException;
}
특징:
// OutputStream → Writer 변환
OutputStreamWriter osw = new OutputStreamWriter(
System.out,
StandardCharsets.UTF_8);
osw.write("안녕하세요\n");
osw.flush(); // 강제 출력
// 동작:
// 1. 사용자가 String 쓰기
// 2. OutputStreamWriter 가 UTF-8 인코딩
// 3. 바이트 시퀀스로 변환
// 4. OutputStream 에 쓰기
// 또는 BufferedWriter
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(System.out, StandardCharsets.UTF_8));
writer.write("한글\n");
writer.flush();
java.io 의 Reader/Writer 계층:
Reader (추상)
├── InputStreamReader
│ └── FileReader
├── BufferedReader
├── StringReader
├── CharArrayReader
├── PipedReader
└── FilterReader
└── PushbackReader
Writer (추상)
├── OutputStreamWriter
│ └── FileWriter
├── BufferedWriter
├── StringWriter
├── CharArrayWriter
├── PipedWriter
├── PrintWriter
└── FilterWriter
// 가장 일반적 패턴
InputStream is = ...;
// 1. InputStreamReader 로 변환
Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8);
// 2. BufferedReader 로 감싸기 (효율 + readLine)
BufferedReader br = new BufferedReader(reader);
// 한 번에
BufferedReader br = new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8));
// 또는 Files (NIO.2, 가장 권장)
BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8);
// 1. CSV 파일 한글 읽기
public List<Shipment> readShipments(Path file) throws IOException {
List<Shipment> shipments = new ArrayList<>();
try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
String line;
reader.readLine(); // 헤더
while ((line = reader.readLine()) != null) {
shipments.add(parseShipment(line));
// line 은 한글 정상 처리
}
}
return shipments;
}
// 2. CSV 파일 한글 쓰기
public void writeShipments(Path file, List<Shipment> shipments) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
writer.write("ID,선적번호,수하인,중량\n");
for (Shipment s : shipments) {
writer.write(s.getId() + ",");
writer.write(s.getBlNo() + ",");
writer.write(s.getConsignee() + ","); // 한글 OK
writer.write(s.getWeight() + "\n");
}
}
}
// 3. HTTP 클라이언트 (한글 응답)
public String fetchData(String url) throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
return reader.lines().collect(Collectors.joining("\n"));
}
}
Reader/Writer 의 본질과 활용은?
답:
1. 본질:
계층:
활용:
BufferedReader reader = new BufferedReader(
new InputStreamReader(input, StandardCharsets.UTF_8));
권장:
InputStream vs Reader:
표준 입력 읽기의 3가지 도구:
1. BufferedReader
- Reader 기반
- readLine 으로 한 줄씩
- 효율적, 표준
2. Scanner (Java 5+)
- 간편한 API
- 토큰 단위 (next, nextInt 등)
- 약간 느림
3. Console (Java 6+)
- 표준 입력의 특화
- readPassword 의 안전한 비밀번호
- IDE 에선 null 가능
// 가장 표준적
BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8));
System.out.print("Name: ");
String name = reader.readLine(); // 한 줄
System.out.print("Age: ");
int age = Integer.parseInt(reader.readLine());
System.out.print("Items: ");
String[] items = reader.readLine().split(",");
// 간편함
Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8);
System.out.print("Name: ");
String name = scanner.nextLine(); // 한 줄
System.out.print("Age: ");
int age = scanner.nextInt(); // 정수 자동 파싱
System.out.print("Price: ");
double price = scanner.nextDouble(); // 실수
System.out.print("Active: ");
boolean active = scanner.nextBoolean();
// 토큰
String token = scanner.next(); // 공백 구분 한 단어
// 함정: nextInt + nextLine
Scanner scanner = new Scanner(System.in);
System.out.print("Age: ");
int age = scanner.nextInt(); // 사용자가 "25" + Enter 입력
// nextInt 가 25 만 가져옴, \n 은 버퍼에 남음
System.out.print("Name: ");
String name = scanner.nextLine(); // ★ 빈 문자열 반환!
// 버퍼의 \n 을 읽음
// 해결: nextInt 후 nextLine 한 번 추가
int age = scanner.nextInt();
scanner.nextLine(); // \n 소비
String name = scanner.nextLine(); // 이제 정상
// 비밀번호 안전 입력 (화면에 표시 안 됨)
Console console = System.console();
if (console != null) {
String username = console.readLine("Username: ");
char[] password = console.readPassword("Password: ");
// password 는 char[] (보안)
// 사용 후 명시적 cleanup
Arrays.fill(password, '\0');
}
// IDE 에서는 null 가능
// 명령줄 (터미널) 에서만 정상
// printf 도 지원
console.printf("Hello, %s!%n", username);
// reader, writer 도
Reader reader = console.reader();
PrintWriter writer = console.writer();
| 항목 | BufferedReader | Scanner | Console |
|---|---|---|---|
| 시기 | Java 1.1 | Java 5 | Java 6 |
| 한 줄 읽기 | readLine() | nextLine() | readLine() |
| 정수 읽기 | parseInt | nextInt() | parseInt |
| 인코딩 명시 | ✓ | ✓ | (시스템) |
| 비밀번호 | X | X | ✓ (안전) |
| 속도 | 빠름 | 느림 | 보통 |
| 패턴 | 표준 | 간편 | 보안 |
| IDE | ✓ | ✓ | null 가능 |
선택 가이드:
BufferedReader 권장:
✓ 표준적
✓ 빠름
✓ 인코딩 명시 필요
✓ 한 줄 단위 처리
Scanner 권장:
✓ 알고리즘 문제 (코딩 테스트)
✓ 간편함 우선
✓ 다양한 타입 파싱
✗ 성능 중요 시 X
Console 권장:
✓ 비밀번호 입력
✓ 보안
✓ 터미널 전용 (IDE 에선 X)
// 1. 일반 콘솔 도구 — BufferedReader
public class ShipmentTool {
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8));
System.out.print("Shipment ID: ");
Long id = Long.parseLong(reader.readLine());
Shipment s = service.findById(id);
System.out.println(s);
}
}
// 2. 알고리즘/배치 — Scanner
public class BatchProcessor {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
for (int i = 0; i < n; i++) {
Long id = sc.nextLong();
process(id);
}
}
}
// 3. 운영 도구 (비밀번호) — Console
public class AdminTool {
public static void main(String[] args) {
Console console = System.console();
if (console == null) {
System.err.println("Run in terminal!");
System.exit(1);
}
String user = console.readLine("Username: ");
char[] pass = console.readPassword("Password: ");
try {
authenticate(user, pass);
} finally {
Arrays.fill(pass, '\0'); // 메모리 cleanup
}
}
}
Scanner, BufferedReader, Console 의 비교는?
답:
1. BufferedReader:
Scanner:
Console:
Scanner 의 함정:
JEP 400: UTF-8 by Default
Java 18 (2022) 부터 기본 인코딩이 UTF-8 로 통일.
이전 (Java 17 이하):
- Windows 한국어: x-windows-949 (MS949)
- Linux/Mac: UTF-8
- 시스템 의존, Cross-platform 문제
이후 (Java 18+):
- 모든 OS: UTF-8 통일
- file.encoding=UTF-8 가 기본
- Cross-platform 일관성
// 기본 인코딩 사용 API
new FileReader("file.txt") // 기본 인코딩
new FileWriter("file.txt")
new PrintStream(new FileOutputStream(...))
new InputStreamReader(in) // 기본 인코딩
new OutputStreamWriter(out)
String.getBytes() // 기본 인코딩
new String(bytes) // 기본 인코딩
Scanner(InputStream) // 기본 인코딩
// Java 18+ 부터 이 모두가 UTF-8
// Java 18+ 에서 옛 동작 복원
// JVM 옵션
// -Dfile.encoding=COMPAT
// 또는 -Dfile.encoding=MS949
// 시스템 프로퍼티 확인
String enc = System.getProperty("file.encoding");
// Java 18+: "UTF-8" (기본)
Charset.defaultCharset(); // UTF-8
Charset systemDefault = Charset.forName(System.getProperty("file.encoding"));
// Java 18+ 라도 명시적 인코딩 권장
// ❌ 의존성 있음
new FileReader("file.txt");
// ✓ 명시적
new FileReader("file.txt", StandardCharsets.UTF_8);
// ✓ NIO.2 (가장 권장)
Files.newBufferedReader(Path.of("file.txt"), StandardCharsets.UTF_8);
// 이유:
// 1. 다른 JVM 버전 호환
// 2. 의도 명확
// 3. 다른 인코딩 필요 시 대비
// 1. 항상 StandardCharsets 사용
import static java.nio.charset.StandardCharsets.UTF_8;
Files.newBufferedReader(path, UTF_8);
Files.newBufferedWriter(path, UTF_8);
Files.writeString(path, content, UTF_8);
Files.readString(path, UTF_8);
// 2. HTTP 통신
@RestController
public class Controller {
@GetMapping(value = "/api", produces = "application/json;charset=UTF-8")
public Response get() { ... }
@PostMapping(consumes = "application/json;charset=UTF-8")
public void post(@RequestBody Request req) { ... }
}
// 3. DB 인코딩
// application.yml
// spring:
// datasource:
// url: jdbc:postgresql://localhost:5432/db?characterEncoding=UTF-8
// 4. JVM 옵션 (확실히)
// -Dfile.encoding=UTF-8
// -Dconsole.encoding=UTF-8
실무 시나리오:
1. CSV 파일 한글 깨짐
- 원인: Excel 이 BOM 없는 UTF-8 못 읽음
- 해결: BOM 추가 또는 EUC-KR
2. HTTP 응답 한글 깨짐
- 원인: Content-Type charset 누락
- 해결: charset=UTF-8 명시
3. DB 조회 한글 깨짐
- 원인: DB 인코딩 vs 클라이언트 인코딩
- 해결: 통일 (UTF-8 권장)
4. 로그 파일 한글 깨짐
- 원인: 로그 프레임워크의 기본 인코딩
- 해결: 명시적 인코딩 설정
5. 파일명 한글 깨짐
- 원인: 파일 시스템의 인코딩
- 해결: -Dfile.encoding=UTF-8
BOM:
- UTF-8 BOM: 0xEF 0xBB 0xBF
- 파일이 UTF-8 임을 표시
- 일부 도구 (Excel 등) 가 인식
장단점:
+ Excel 에서 UTF-8 한글 정상
- 일부 도구 (스크립트 파서 등) 가 BOM 을 데이터로 인식
자바에서:
// BOM 쓰기
Files.write(path, new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF});
Files.write(path, content.getBytes(UTF_8), StandardOpenOption.APPEND);
// BOM 처리 라이브러리: Apache Commons IO 의 BOMInputStream
// 1. JVM 옵션 (모든 서비스 통일)
// -Dfile.encoding=UTF-8
// 2. application.yml
spring:
http:
encoding:
charset: UTF-8
enabled: true
force: true
datasource:
url: jdbc:postgresql://...?characterEncoding=UTF-8
// 3. 코드에서 명시
public class Constants {
public static final Charset CHARSET = StandardCharsets.UTF_8;
}
// 4. 일관된 사용
Files.writeString(path, content, Constants.CHARSET);
new String(bytes, Constants.CHARSET);
// 5. Excel 호환 (BOM)
public void exportForExcel(Path path, String csv) throws IOException {
try (OutputStream os = Files.newOutputStream(path)) {
os.write(new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF}); // BOM
os.write(csv.getBytes(StandardCharsets.UTF_8));
}
}
Java 18+ 의 인코딩 변화와 실무 권장은?
답:
1. JEP 400 (Java 18+):
호환성:
-Dfile.encoding=COMPAT실무 권장:
StandardCharsets.UTF_8 명시-Dfile.encoding=UTF-8흔한 문제:
| Q | 핵심 답변 |
|---|---|
| System.in 의 타입? | InputStream |
| System.in 의 특성? | Blocking, line-buffered, 단일 |
| read() 의 반환 int? | byte (-1) 충돌 회피, 0~255 |
| 한글이 깨지는 이유? | 1바이트씩 + 인코딩 무시 |
| UTF-8 한글 크기? | 3바이트 |
| EUC-KR vs UTF-8? | 2바이트 (한국 전용) vs 1~4바이트 (글로벌) |
| InputStream vs Reader? | byte vs char, 인코딩 처리 |
| BufferedReader 의 효과? | 버퍼링 + readLine |
| Scanner 의 nextInt 함정? | \n 버퍼 남음 |
| Console 의 특별 기능? | readPassword (안전) |
| Java 18+ 인코딩? | UTF-8 통일 |
| 자바 String 의 내부? | UTF-16 (Java 8), byte[] (Java 9+) |
답:
// ❌ 위험
try (InputStream in = System.in) {
// ...
}
// try-with-resources 가 close 호출
// 표준 입력 영구 종료
답:
답:
String s = "안녕";
s.length(); // 2 (문자 수, UTF-16 unit)
String emoji = "😀";
emoji.length(); // 2 (서로게이트 쌍, 4바이트 문자)
// 진짜 문자 수
emoji.codePointCount(0, emoji.length()); // 1
답:
답:
// 1. 자바 코드
System.getProperty("file.encoding"); // JVM 의 기본 인코딩
Charset.defaultCharset();
System.console().charset(); // Console 의 charset (Java 17+)
// 2. 쉘
$ locale # Linux/Mac
$ chcp # Windows
// 3. JVM 옵션
-Dfile.encoding=UTF-8
-Dconsole.encoding=UTF-8
1. System.in 의 정체
2. 한글이 깨지는 이유
3. 권장 패턴
BufferedReader + InputStreamReader + UTF-8이번 Unit에서 System.in 과 인코딩을 봤다면, 다음은 파일 바이트 스트림.
🚀 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 1 ~ 7 완주 (31 Unit)
🚀 Phase 8 — Stream 실전 (1/6 진행)
총: 32/43 Unit (약 74%)