Theme:

콘솔에 한글이 깨져서 나올 때, 도대체 어디서부터 봐야 할까? "문ìžì—´" 같은 의문의 문자가 화면에 뜨면 당황스럽다. 분명 내 코드에는 "문자열"이라고 적었는데 말이다. 이 문제를 제대로 해결하려면 문자 인코딩이라는 개념부터 잡아야 한다. 이번 글에서는 ASCII부터 Unicode, UTF-8까지 인코딩의 역사를 훑고, 자바에서 문자를 어떻게 다루는지, 그리고 한글 깨짐을 어떻게 디버깅하는지까지 정리한다.

TIP 이 글의 코드 예제를 직접 실행해보고 싶다면 Java 기본기 핸드북을 확인해보세요.


1. 문자 인코딩이 왜 필요한가 — 컴퓨터는 숫자만 안다

핵심 한 줄

컴퓨터는 0과 1(바이트)만 이해하므로, 사람이 읽는 문자를 숫자로 변환하는 약속이 필요하다 — 그게 인코딩이다.

인코딩이란

  • 인코딩(Encoding): 문자 → 바이트(숫자) 변환 규칙
  • 디코딩(Decoding): 바이트(숫자) → 문자 변환 규칙

"가"라는 글자를 파일에 저장한다고 하자. 컴퓨터 입장에서 "가"는 의미가 없다. "가"를 어떤 숫자로 표현할지 정해야 저장할 수 있고, 그 숫자를 다시 "가"로 돌려놓을 수 있다.

PLAINTEXT
문자 "가"  ──인코딩──▸  바이트 [0xEA, 0xB0, 0x80]  (UTF-8 기준)
바이트 [0xEA, 0xB0, 0x80]  ──디코딩──▸  문자 "가"

인코딩과 디코딩에 같은 규칙을 사용해야 원래 문자가 복원된다. 다른 규칙을 쓰면? 그게 바로 글자가 깨지는 것이다.


2. ASCII — 모든 것의 시작, 7비트의 한계

핵심 한 줄

ASCII는 영문 알파벳·숫자·특수문자를 **7비트(128개)**로 표현하는 최초의 표준 인코딩이다.

ASCII 코드 구조

범위내용예시
0~31제어 문자\n(10), \t(9)
32~47특수 문자공백(32), !(33)
48~57숫자0(48), 9(57)
65~90대문자A(65), Z(90)
97~122소문자a(97), z(122)
JAVA
// ASCII 값 확인해보기
char ch = 'A';
System.out.println((int) ch); // 65 — 'A'의 ASCII 코드

// 대문자 → 소문자 변환 원리
char lower = (char) (ch + 32); // 'a' — 대소문자 차이는 32

7비트의 한계

7비트면 0~127, 총 128개의 문자만 표현할 수 있다. 영어와 기본 특수문자에는 충분하지만, 한글, 한자, 일본어, 아랍어 같은 문자는 전혀 담을 수 없다. 세상에는 영어만 있는 게 아닌데 말이다.

이 한계를 극복하기 위해 각 나라마다 자기들만의 인코딩을 만들기 시작했다. 8번째 비트(128~255)를 활용하거나, 아예 2바이트를 쓰는 방식으로 확장했다.


3. 한글 인코딩 — EUC-KR, CP949, 그리고 조합형 vs 완성형

핵심 한 줄

한글 인코딩은 조합형(자모 조합)과 완성형(완성된 글자에 코드 부여) 두 갈래로 발전했고, 실무에서 주로 만나는 것은 완성형 계열의 EUC-KRCP949이다.

조합형 vs 완성형

방식원리장점단점
조합형초성·중성·종성 각각에 코드 할당모든 한글 조합 가능 (11,172자)구현이 복잡
완성형"가", "나" 등 완성된 글자에 코드 할당구현이 단순표현 못하는 글자 존재

한글은 자모 조합으로 만들어지므로 이론적으로 11,172자가 가능하다. 초기 완성형(KS X 1001)은 이 중 2,350자만 수록해서, "똠"이나 "뷁" 같은 글자를 표현할 수 없었다. 이게 그 유명한 "완성형 한글의 한계"다.

EUC-KR

  • KS X 1001 기반, 2,350자 한글 + 한자 + 특수문자
  • 한글 한 글자는 2바이트
  • ASCII 영역(1바이트)과 호환
  • 문제: "똠방각하"의 "똠"을 표현할 수 없음

CP949 (= MS949)

  • 마이크로소프트가 EUC-KR을 확장한 인코딩
  • 11,172자 한글 모두 표현 가능
  • EUC-KR의 상위 호환
  • 윈도우의 기본 한글 인코딩으로 널리 사용됨
JAVA
// EUC-KR과 CP949의 차이 확인
import java.nio.charset.Charset;

String text = "똠"; // EUC-KR에 없는 글자

// EUC-KR로 변환하면 '?'로 바뀜 (표현 불가)
byte[] eucKr = text.getBytes(Charset.forName("EUC-KR"));
System.out.println(new String(eucKr, Charset.forName("EUC-KR"))); // ?

// CP949(MS949)로 변환하면 정상
byte[] cp949 = text.getBytes(Charset.forName("x-windows-949"));
System.out.println(new String(cp949, Charset.forName("x-windows-949"))); // 똠

면접에서 "한글 인코딩 문제를 만난 경험이 있나요?"라는 질문이 나오면, EUC-KR과 UTF-8의 불일치 사례를 들면서 원인과 해결법을 설명하면 좋다.


4. Unicode — 전 세계 문자를 하나의 코드 포인트로

핵심 한 줄

Unicode는 전 세계 모든 문자에 고유한 **코드 포인트(Code Point)**를 부여하는 문자 집합(Character Set) 표준이다.

Unicode가 해결하는 문제

각 나라마다 독자적인 인코딩을 만들다 보니 혼란이 생겼다. 한국은 EUC-KR, 일본은 Shift_JIS, 중국은 GB2312… 서로 다른 인코딩을 쓰면 당연히 깨진다. "하나의 표준으로 전 세계 문자를 통일하자" — 이게 Unicode의 목표다.

코드 포인트

Unicode는 각 문자에 U+XXXX 형식의 고유 번호를 부여한다.

문자코드 포인트설명
AU+0041라틴 대문자 A
U+AC00한글 첫 번째 글자
U+D55C한글 "한"
😀U+1F600이모지
JAVA
// 코드 포인트 확인하기
String text = "가";
int codePoint = text.codePointAt(0);
System.out.println(Integer.toHexString(codePoint)); // ac00
System.out.printf("U+%04X%n", codePoint);           // U+AC00

Unicode의 평면(Plane) 구조

Unicode는 총 17개 평면으로 나뉜다. 가장 중요한 것은 **BMP(Plane 0, U+0000U+FFFF)**로 한글·한자·라틴 등 자주 쓰는 문자가 여기에 있다. 이모지 같은 보충 문자는 **SMP(Plane 1, U+10000U+1FFFF)**에 해당한다.

중요한 포인트: Unicode 자체는 인코딩이 아니다. "이 문자의 번호는 U+AC00이다"라고 정의하는 것이지, "U+AC00을 바이트로 어떻게 저장할 것인가"는 별도의 문제다. 그 저장 방식이 바로 UTF-8, UTF-16, UTF-32다.


5. UTF-8, UTF-16, UTF-32 — 인코딩 방식의 차이

핵심 한 줄

UTF-8/16/32는 Unicode 코드 포인트를 실제 바이트로 변환하는 방식이며, 가변/고정 길이와 바이트 수에서 차이가 있다.

UTF-8

  • 가변 길이: 1~4바이트
  • ASCII 호환 — 영문은 1바이트, 그래서 기존 영문 텍스트와 호환성이 좋다
  • 웹 표준 — 전 세계 웹 페이지의 98% 이상이 UTF-8을 사용
코드 포인트 범위바이트 수바이트 패턴
U+0000 ~ U+007F1바이트0xxxxxxx
U+0080 ~ U+07FF2바이트110xxxxx 10xxxxxx
U+0800 ~ U+FFFF3바이트1110xxxx 10xxxxxx 10xxxxxx
U+10000 ~ U+10FFFF4바이트11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
JAVA
// "가"(U+AC00)를 UTF-8로 인코딩하면 3바이트
byte[] utf8 = "가".getBytes("UTF-8");
for (byte b : utf8) {
    System.out.printf("%02X ", b); // EA B0 80
}
// 3바이트: 0xEA = 11101010, 0xB0 = 10110000, 0x80 = 10000000

UTF-16

  • 가변 길이: 2바이트 또는 4바이트
  • BMP 문자(U+0000~U+FFFF)는 2바이트
  • 보충 문자(U+10000 이상)는 서로게이트 페어(Surrogate Pair) — 4바이트
  • 자바의 내부 문자 처리가 바로 UTF-16이다
JAVA
// 이모지는 서로게이트 페어로 표현된다
String emoji = "😀"; // U+1F600
System.out.println(emoji.length());      // 2 — char 2개 (서로게이트 페어)
System.out.println(emoji.codePointCount(0, emoji.length())); // 1 — 실제 문자 1개

char high = emoji.charAt(0); // 상위 서로게이트
char low = emoji.charAt(1);  // 하위 서로게이트
System.out.printf("High: U+%04X, Low: U+%04X%n", (int) high, (int) low);
// High: U+D83D, Low: U+DE00

UTF-32

  • 고정 길이: 항상 4바이트
  • 구현이 가장 단순하지만 공간 낭비가 심함
  • 실무에서는 거의 안 쓴다

인코딩별 바이트 수 비교

문자코드 포인트UTF-8UTF-16UTF-32
AU+00411바이트2바이트4바이트
©U+00A92바이트2바이트4바이트
U+AC003바이트2바이트4바이트
😀U+1F6004바이트4바이트4바이트

한글 중심의 텍스트라면 UTF-16이 UTF-8보다 크기가 작을 수 있다(2바이트 vs 3바이트). 하지만 웹 환경에서는 호환성과 표준 때문에 UTF-8을 쓰는 게 일반적이다.


6. BOM (Byte Order Mark) — 왜 존재하고 언제 문제가 되는가

핵심 한 줄

BOM은 파일의 **바이트 순서(Endianness)**와 인코딩 방식을 알려주는 특수 마커인데, UTF-8에서는 불필요하며 오히려 문제를 일으킨다.

BOM이란

멀티바이트 인코딩(UTF-16, UTF-32)에서는 바이트 순서가 중요하다. 예를 들어 U+FEFF를 UTF-16으로 저장할 때:

  • Big Endian (BE): FE FF — 큰 바이트가 먼저
  • Little Endian (LE): FF FE — 작은 바이트가 먼저

파일 맨 앞에 이 마커를 넣어서 "이 파일은 BE/LE 중 어느 방식인지" 알려주는 것이다.

인코딩BOM 바이트
UTF-8EF BB BF
UTF-16 BEFE FF
UTF-16 LEFF FE
UTF-32 BE00 00 FE FF
UTF-32 LEFF FE 00 00

UTF-8에서 BOM이 문제가 되는 이유

UTF-8은 바이트 순서가 의미 없다(1바이트씩 순차적으로 읽으니까). 그런데 윈도우의 메모장(Notepad)이 UTF-8 파일을 저장할 때 BOM(EF BB BF)을 앞에 붙이는 경우가 있다.

JAVA
// BOM이 포함된 파일을 읽을 때 문제
// 파일 첫 줄: "hello" → 실제 바이트: EF BB BF 68 65 6C 6C 6F
BufferedReader reader = new BufferedReader(
    new InputStreamReader(new FileInputStream("bom_file.txt"), "UTF-8")
);
String firstLine = reader.readLine();
System.out.println(firstLine.length()); // 6 — "hello"는 5글자인데 BOM 때문에 6
System.out.println(firstLine.equals("hello")); // false!

// BOM 제거 방법
if (firstLine.charAt(0) == '\uFEFF') {
    firstLine = firstLine.substring(1);
}

이런 BOM 때문에 JSON 파싱이 실패하거나, CSV 파일의 첫 번째 컬럼을 못 읽거나, 셸 스크립트가 실행이 안 되는 문제가 발생한다. UTF-8 파일은 BOM 없이 저장하는 것이 권장된다.


7. 자바의 문자 처리 — char는 UTF-16, String은 내부적으로 byte[]

핵심 한 줄

자바의 charUTF-16 코드 유닛(2바이트)이고, Java 9부터 String 내부는 Compact Strings로 바뀌어 Latin-1 문자는 1바이트씩 저장한다.

char와 UTF-16

자바가 설계된 1990년대에는 Unicode가 U+FFFF 이내에 모든 문자를 담을 수 있을 것이라 예상했다. 그래서 char를 2바이트(16비트)로 만들었다. 하지만 이모지와 고대 문자 등이 추가되면서 U+FFFF를 넘어갔고, 서로게이트 페어라는 개념이 등장했다.

JAVA
// char는 BMP 문자만 담을 수 있다
char koreanChar = '가'; // OK — U+AC00은 BMP
// char emoji = '😀';   // 컴파일 에러 — U+1F600은 BMP 밖

// BMP 밖의 문자는 int(코드 포인트)로 다루기
int emojiCodePoint = 0x1F600;
String emoji = new String(Character.toChars(emojiCodePoint));
System.out.println(emoji); // 😀

String의 내부 구조 변화 (Java 9+: Compact Strings)

Java 8까지 String 내부는 char[] 배열이었다. 모든 문자를 2바이트씩 저장했으므로 ASCII 텍스트도 메모리를 2배 사용했다.

Java 9부터 Compact Strings가 도입되었다.

JAVA
// String 내부 구조 (Java 9+, 간략화)
public final class String {
    private final byte[] value; // char[]가 아니라 byte[]
    private final byte coder;   // 0 = LATIN1 (1바이트), 1 = UTF16 (2바이트)
}
  • 문자열이 Latin-1(영문, 숫자 등) 범위 안이면 → byte[]1바이트씩 저장
  • 한글 등 Latin-1 밖의 문자가 포함되면 → byte[]UTF-16으로 2바이트씩 저장

영문만 있는 "hello"는 Latin-1으로 5바이트, 한글이 섞인 "hello안녕"은 UTF-16으로 전체가 2바이트씩 저장된다. 하나라도 Latin-1 밖이면 전체가 UTF-16 모드가 된다.

String.length()의 함정

length()char 개수를 반환하지, 문자 개수를 반환하는 게 아니다.

JAVA
String text = "Hello😀World";
System.out.println(text.length());      // 12 — 😀이 서로게이트 페어(char 2개)
System.out.println(text.codePointCount(0, text.length())); // 11 — 실제 문자 수

// 코드 포인트 단위로 순회하기
text.codePoints().forEach(cp -> {
    System.out.printf("U+%04X: %s%n", cp, new String(Character.toChars(cp)));
});

면접에서 "자바의 char가 모든 유니코드 문자를 표현할 수 있나요?"라는 질문이 나올 수 있다. 정답은 아니오 — BMP 밖의 문자는 char 하나로 표현할 수 없고, 서로게이트 페어가 필요하다.


8. Charset과 CharsetEncoder/Decoder — 인코딩 변환

핵심 한 줄

java.nio.charset.Charset은 인코딩 방식을 나타내는 클래스이며, CharsetEncoder/CharsetDecoder로 바이트와 문자 간의 변환을 세밀하게 제어할 수 있다.

Charset 기본 사용법

JAVA
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

// StandardCharsets 상수 사용 (권장)
Charset utf8 = StandardCharsets.UTF_8;
Charset eucKr = Charset.forName("EUC-KR"); // 이름으로도 가능

String과 Charset

JAVA
String text = "한글 테스트";

// 인코딩: String → byte[]
byte[] utf8Bytes = text.getBytes(StandardCharsets.UTF_8);    // 15바이트 (한글 3 × 5)
byte[] eucKrBytes = text.getBytes(Charset.forName("EUC-KR")); // 10바이트 (한글 2 × 5)

// 디코딩: byte[] → String — 반드시 같은 Charset으로!
String decoded = new String(utf8Bytes, StandardCharsets.UTF_8);       // 정상
String broken = new String(utf8Bytes, Charset.forName("EUC-KR"));    // 깨짐!

CharsetEncoder로 에러 처리

인코딩 변환 시 표현할 수 없는 문자가 있을 때의 처리 전략을 지정할 수 있다.

JAVA
import java.nio.charset.CodingErrorAction;

CharsetEncoder encoder = StandardCharsets.US_ASCII.newEncoder()
    .onUnmappableCharacter(CodingErrorAction.REPLACE); // 매핑 불가 → '?'로 대체

// REPORT: 예외 발생 | REPLACE: 대체 문자 | IGNORE: 건너뜀

9. InputStreamReader의 인코딩 지정 — 파일 읽기에서의 실수

핵심 한 줄

파일을 읽을 때 인코딩을 명시하지 않으면 JVM의 기본 인코딩에 의존하게 되어, 환경이 바뀌면 한글이 깨진다.

흔한 실수

JAVA
// ❌ 나쁜 예: 인코딩을 명시하지 않음
BufferedReader reader = new BufferedReader(
    new InputStreamReader(new FileInputStream("data.txt"))
);
// → JVM 기본 인코딩에 의존 — 로컬에서는 되지만 서버에서 깨질 수 있음

// ✅ 좋은 예: 인코딩을 명시
BufferedReader reader = new BufferedReader(
    new InputStreamReader(new FileInputStream("data.txt"), StandardCharsets.UTF_8)
);

Files API 활용 (Java 7+)

java.nio.file.Files를 쓰면 더 간결하고 안전하다.

JAVA
// 파일 읽기/쓰기 — 인코딩 명시
String content = Files.readString(Path.of("data.txt"), StandardCharsets.UTF_8); // Java 11+
List<String> lines = Files.readAllLines(Path.of("data.txt"), StandardCharsets.UTF_8);
Files.writeString(Path.of("output.txt"), "한글 내용", StandardCharsets.UTF_8);

// EUC-KR 레거시 파일 읽어서 UTF-8로 변환 저장
List<String> legacy = Files.readAllLines(Path.of("old.csv"), Charset.forName("EUC-KR"));
Files.write(Path.of("new.csv"), legacy, StandardCharsets.UTF_8);

콘솔 출력 깨짐

JAVA
// JVM 실행 시 인코딩 지정
// java -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 MyApp

// 코드에서 기본 인코딩 확인
System.out.println("file.encoding: " + System.getProperty("file.encoding"));

10. 한글 깨짐 디버깅 — HTTP Content-Type, DB charset, 파일 인코딩 체크리스트

핵심 한 줄

한글이 깨지면 **"어디서 인코딩이 불일치하는가"**를 찾는 것이 핵심이다 — 파일, HTTP, DB, 콘솔 네 군데를 순서대로 확인하자.

깨짐 패턴으로 원인 추정하기

깨진 모양을 보면 원인을 추정할 수 있다.

깨진 모양추정 원인
문ìžì—´UTF-8 바이트를 Latin-1/ISO-8859-1로 읽음
?????해당 인코딩에서 표현 불가 (매핑 실패)
???EUC-KR/CP949로 읽어야 할 데이터를 다른 인코딩으로 읽음
¹®ÀÚ¿­UTF-8 데이터를 EUC-KR로 읽음
\uFFFD (�)디코딩 실패 시 대체 문자

한글 깨짐 디버깅 체크리스트

1단계 — 파일 인코딩: 터미널에서 file -bi data.txt로 확인. 자바에서는 Files.readAllBytes()로 바이트를 직접 확인한다.

2단계 — HTTP Content-Type: 응답 헤더에 charset=UTF-8이 있는지 확인.

JAVA
// Spring에서 인코딩 명시
@GetMapping(value = "/api/data", produces = "application/json; charset=UTF-8")
public String getData() { return "{\"name\": \"홍길동\"}"; }

// 서블릿에서 인코딩 설정
response.setCharacterEncoding("UTF-8");
request.setCharacterEncoding("UTF-8");

3단계 — DB 캐릭터셋: MySQL은 SHOW VARIABLES LIKE 'character_set%'로 확인. JDBC URL에 characterEncoding=UTF-8을 명시한다.

MySQL에서 utf8은 3바이트까지만 지원한다(이모지 불가). 반드시 **utf8mb4**를 사용해야 한다.

4단계 — 콘솔/IDE: IntelliJ File Encodings → UTF-8, JVM 옵션 -Dfile.encoding=UTF-8.

인코딩 불일치 복원 트릭

깨진 문자열은 **"깨진 과정의 역순"**으로 바이트를 복원할 수 있다.

JAVA
// UTF-8 바이트를 Latin-1로 잘못 읽은 경우 복원
String broken = "문ìžì—´";
byte[] rawBytes = broken.getBytes(StandardCharsets.ISO_8859_1); // 원래 바이트 복원
String fixed = new String(rawBytes, StandardCharsets.UTF_8);     // 올바른 인코딩으로 디코딩
System.out.println(fixed); // 문자열

11. 정리 테이블

인코딩 방식 비교

인코딩바이트 수ASCII 호환한글 지원주요 사용처
ASCII1 (고정)OX레거시 시스템
EUC-KR1~2 (가변)O2,350자오래된 한국 웹사이트
CP9491~2 (가변)O11,172자윈도우 한국어 환경
UTF-81~4 (가변)OO (3바이트)웹, API, 파일 (표준)
UTF-162~4 (가변)XO (2바이트)자바 내부, 윈도우 API
UTF-324 (고정)XO (4바이트)거의 안 씀

한글 깨짐 디버깅 요약

체크 포인트확인 항목
소스 파일IDE 인코딩 설정 (UTF-8)
파일 I/OInputStreamReader, Files API에 Charset 명시
HTTPContent-Type 헤더에 charset=UTF-8
DB테이블 utf8mb4, JDBC URL에 characterEncoding=UTF-8
콘솔-Dfile.encoding=UTF-8 JVM 옵션
BOMUTF-8 BOM(EF BB BF) 제거

마무리

인코딩 문제는 원리를 이해하면 어렵지 않다. 핵심은 딱 하나 — **"인코딩과 디코딩에 같은 규칙을 쓰고 있는가?"**다.

기억해둘 포인트를 정리하면:

  • 컴퓨터는 숫자만 안다. 문자를 숫자로 바꾸는 약속이 인코딩이다.
  • ASCII → EUC-KR → Unicode로 발전해왔고, 현재 표준은 UTF-8이다.
  • 자바의 char는 UTF-16 코드 유닛이며, BMP 밖의 문자는 서로게이트 페어가 필요하다.
  • String.length()는 char 개수이지 문자 개수가 아니다. codePointCount()를 쓰자.
  • 파일 읽기에서 인코딩을 명시하지 않는 것이 한글 깨짐의 가장 흔한 원인이다.
  • 한글이 깨지면 파일 → HTTP → DB → 콘솔 순으로 인코딩 불일치를 추적하자.

면접에서 "인코딩에 대해 설명해주세요"라는 질문이 나오면, ASCII의 한계 → Unicode의 등장 → UTF-8의 가변 길이 인코딩 → 자바의 UTF-16 선택 이유까지 흐름을 잡아서 설명하면 깔끔하다.


다음 글에서는 JDBC와 커넥션 풀을 다룬다. 자바가 데이터베이스에 접근하는 원리부터, 커넥션 풀이 왜 필요한지, HikariCP는 어떻게 동작하는지까지 정리할 예정이다.

댓글 로딩 중...
On This Page
1. 문자 인코딩이 왜 필요한가 — 컴퓨터는 숫자만 안다핵심 한 줄인코딩이란2. ASCII — 모든 것의 시작, 7비트의 한계핵심 한 줄ASCII 코드 구조7비트의 한계3. 한글 인코딩 — EUC-KR, CP949, 그리고 조합형 vs 완성형핵심 한 줄조합형 vs 완성형EUC-KRCP949 (= MS949)4. Unicode — 전 세계 문자를 하나의 코드 포인트로핵심 한 줄Unicode가 해결하는 문제코드 포인트Unicode의 평면(Plane) 구조5. UTF-8, UTF-16, UTF-32 — 인코딩 방식의 차이핵심 한 줄UTF-8UTF-16UTF-32인코딩별 바이트 수 비교6. BOM (Byte Order Mark) — 왜 존재하고 언제 문제가 되는가핵심 한 줄BOM이란UTF-8에서 BOM이 문제가 되는 이유7. 자바의 문자 처리 — char는 UTF-16, String은 내부적으로 byte[]핵심 한 줄char와 UTF-16String의 내부 구조 변화 (Java 9+: Compact Strings)String.length()의 함정8. Charset과 CharsetEncoder/Decoder — 인코딩 변환핵심 한 줄Charset 기본 사용법String과 CharsetCharsetEncoder로 에러 처리9. InputStreamReader의 인코딩 지정 — 파일 읽기에서의 실수핵심 한 줄흔한 실수Files API 활용 (Java 7+)콘솔 출력 깨짐10. 한글 깨짐 디버깅 — HTTP Content-Type, DB charset, 파일 인코딩 체크리스트핵심 한 줄깨짐 패턴으로 원인 추정하기한글 깨짐 디버깅 체크리스트인코딩 불일치 복원 트릭11. 정리 테이블인코딩 방식 비교한글 깨짐 디버깅 요약마무리