// Developer · Debugging Reports

뉴스도 디버깅이 필요해

마주친 오류를 재현하고, 원인을 추적하고, 기록합니다. Java · Spring · DB · 실무 트러블슈팅.

debug — today.log
$ debug --trace "오늘의 뉴스"
> 헤드라인 파싱 중...
> 원인 추적: 근거 누락 · 출처 불명
> 패치 적용: 사실 확인 + 출처 보강
✓ RESOLVED
TOTAL
TODAY
YESTERDAY

Categories

웹개발/Oracle

AL32UTF8 KO16MSWIN949 한글 깨짐, Oracle 캐릭터셋 차이 정리

자바를잡아2026. 7. 10. 09:00
반응형

운영 DB에서 한글이 물음표로 보이거나, 로컬에서는 정상인데 서버에 올리면 글자가 깨지는 문제가 생기면 먼저 Oracle 캐릭터셋을 확인해야 한다. 특히 오래된 공공 SI나 레거시 시스템에서는 AL32UTF8KO16MSWIN949가 자주 나오고, 둘의 차이를 모르면 원인을 엉뚱한 곳에서 찾게 된다.

한글 깨짐은 단순히 Java 코드에서 UTF-8을 설정한다고 끝나는 문제가 아니다. DB가 어떤 문자 집합으로 데이터를 저장하는지, 클라이언트가 어떤 문자 집합으로 전달하는지, 이미 깨진 데이터가 저장됐는지를 나눠서 봐야 한다.

핵심 결론: AL32UTF8은 Oracle의 Unicode 기반 캐릭터셋이고, KO16MSWIN949는 한국어 Windows 코드페이지 기반 캐릭터셋이다. 신규 시스템은 보통 AL32UTF8이 유리하지만, 기존 DB가 KO16MSWIN949라면 데이터 이관과 외부 연계 인코딩을 반드시 확인해야 한다.

확인 순서: DB 캐릭터셋 확인 → 클라이언트/JDBC 경로 확인 → 실제 저장 데이터 확인 → 이관 또는 변환 여부 판단.

Oracle 캐릭터셋은 어디에 영향을 주나

Oracle 캐릭터셋은 VARCHAR2, CHAR, CLOB 같은 일반 문자 컬럼에 저장되는 문자의 기준이다. DB가 어떤 문자를 표현할 수 있는지, 한 글자가 몇 바이트로 저장되는지, 다른 시스템과 데이터를 주고받을 때 어떤 변환이 일어나는지에 영향을 준다.

예를 들어 같은 한글이라도 AL32UTF8에서는 보통 3바이트로 저장되고, KO16MSWIN949에서는 대체로 2바이트로 저장된다. 그래서 컬럼 길이를 바이트 기준으로 잡아 둔 오래된 테이블에서는 캐릭터셋 변경 후 길이 초과 문제가 나올 수 있다.

Oracle 오류 자체가 아니라 데이터 비교나 제약조건 문제라면 중복 키 오류처럼 별도 원인을 봐야 한다. 하지만 화면에 보이는 값이 깨지거나 물음표로 저장된다면 캐릭터셋부터 확인하는 편이 빠르다.

AL32UTF8과 KO16MSWIN949 차이

AL32UTF8은 Unicode 기반이다

AL32UTF8은 Oracle에서 권장되는 Unicode 기반 UTF-8 캐릭터셋이다. 한글뿐 아니라 영어, 일본어, 중국어, 특수문자, 이모지 일부처럼 다양한 문자를 한 DB에서 다루기 쉽다.

신규 시스템이나 여러 언어를 다룰 가능성이 있는 서비스라면 보통 AL32UTF8을 선택하는 편이 안전하다. Java, 웹, API, JSON, 외부 연계가 대부분 UTF-8 중심으로 돌아가기 때문이다.

KO16MSWIN949는 한국어 Windows 코드페이지 계열이다

KO16MSWIN949는 한국어 Windows 환경에서 많이 쓰던 문자 집합이다. 한글 중심의 오래된 업무 시스템, 내부망 시스템, 레거시 DB에서 자주 볼 수 있다.

한국어만 다루는 시스템에서는 오랫동안 큰 문제 없이 운영됐지만, UTF-8 기반 외부 API나 다국어 데이터가 들어오면 표현하지 못하는 문자가 생길 수 있다. 이 경우 문자가 물음표로 바뀌거나, 저장 단계에서 오류가 나거나, 조회할 때 깨져 보일 수 있다.

구분AL32UTF8KO16MSWIN949
기반Unicode UTF-8한국어 Windows 코드페이지
한글 저장보통 3바이트대체로 2바이트
다국어 처리유리함제한적
레거시 호환신규 시스템에 적합오래된 한국어 업무 시스템에 흔함
주의점바이트 길이 증가 가능UTF-8 외부 연계 시 깨짐 가능

현재 DB 캐릭터셋 확인 방법

NLS_DATABASE_PARAMETERS 확인

현재 DB 캐릭터셋은 아래 SQL로 확인한다.

SELECT parameter, value
FROM nls_database_parameters
WHERE parameter IN ('NLS_CHARACTERSET', 'NLS_NCHAR_CHARACTERSET');

NLS_CHARACTERSET은 VARCHAR2, CHAR, CLOB 같은 일반 문자열 컬럼 기준이다. NLS_NCHAR_CHARACTERSET은 NCHAR, NVARCHAR2, NCLOB 계열 기준이다. 일반적인 웹 개발에서 문제가 되는 한글 깨짐은 대부분 NLS_CHARACTERSET 쪽을 먼저 본다.

USERENV로 세션 언어 확인하기

현재 접속 세션의 언어 환경은 아래처럼 확인할 수 있다.

SELECT USERENV('LANGUAGE') AS session_language
FROM dual;

이 값은 클라이언트와 세션 환경을 볼 때 참고한다. 다만 Java 애플리케이션에서 Oracle JDBC Thin Driver를 쓰는 경우, 과거처럼 단순히 NLS_LANG 하나로 모든 문제가 해결된다고 보면 안 된다. 웹 요청 인코딩, JVM 파일 인코딩, JDBC 드라이버, DB 캐릭터셋, 저장된 데이터 상태를 나눠서 봐야 한다.

한글 깨짐 원인을 나누는 기준

조회할 때만 깨지는지, 저장 자체가 깨졌는지 확인한다

가장 먼저 구분할 것은 데이터가 DB에 정상 저장되어 있는데 화면에서만 깨지는지, 아니면 DB에 이미 깨진 값이 저장됐는지다. 이 둘은 해결 방법이 완전히 다르다.

SQL Developer나 DB 클라이언트에서 직접 조회했을 때도 ??? 또는 이상한 문자로 보이면 저장 단계에서 이미 문제가 생겼을 가능성이 크다. 반대로 DB 클라이언트에서는 정상인데 웹 화면에서만 깨지면 웹 응답 인코딩, JSP 설정, 필터, API 응답 헤더를 봐야 한다.

웹 계층에서 JSP와 Spring MVC가 함께 엮여 있다면 정적 리소스나 DispatcherServlet 설정도 같이 꼬일 수 있다. 리소스 경로 문제는 별도 이슈지만, 설정 파일 흐름을 볼 때는 MVC 리소스 설정처럼 servlet-context.xml과 web.xml 기준을 같이 확인하면 도움이 된다.

물음표로 저장된 데이터는 복구가 어렵다

중요한 점은 물음표로 변환되어 저장된 데이터다. 원래 문자가 였는데 저장 시점에 ?로 바뀌었다면, DB 안에는 더 이상 원래 문자를 복원할 정보가 없다. 백업, 원본 파일, 원천 시스템 데이터가 없으면 완전 복구는 어렵다.

그래서 한글 깨짐을 발견하면 바로 대량 UPDATE나 변환 스크립트를 돌리면 안 된다. 먼저 샘플 데이터를 잡고, 원본이 살아 있는지, 깨진 위치가 저장 전인지 저장 후인지 확인해야 한다.

실무에서 자주 나오는 시나리오

로컬은 정상인데 운영에서만 깨지는 경우

로컬 DB는 AL32UTF8이고 운영 DB는 KO16MSWIN949인 경우가 있다. 개발자는 로컬에서 UTF-8 기준으로 테스트했기 때문에 문제가 없어 보이지만, 운영 DB에 없는 문자가 들어가면 저장 또는 조회 단계에서 깨질 수 있다.

이때는 운영 DB의 NLS_CHARACTERSET, 애플리케이션 서버의 기본 인코딩, 배포 환경의 요청/응답 필터를 함께 확인한다. Tomcat 배포 경로와 설정이 같이 바뀌었다면 운영 배포 경로처럼 운영 배포 차이도 같이 봐야 한다.

엑셀 업로드 후 한글만 깨지는 경우

엑셀이나 CSV 업로드에서 한글만 깨지면 DB보다 파일 인코딩이 원인일 수 있다. CSV가 CP949인데 UTF-8로 읽거나, UTF-8 파일을 CP949로 읽으면 애플리케이션 단계에서 이미 깨진 문자열이 만들어진다. 그 상태로 DB에 INSERT하면 DB는 깨진 문자열을 그대로 저장한다.

// CSV를 읽을 때는 원본 파일 인코딩을 먼저 확인한다.
try (BufferedReader reader = Files.newBufferedReader(path, Charset.forName("MS949"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        // parse and insert
    }
}

파일 업로드는 DB 캐릭터셋과 별개로 입력 파일 인코딩을 먼저 확인해야 한다. DB가 AL32UTF8이어도 애플리케이션에서 이미 깨진 문자열을 넘기면 정상 저장할 수 없다.

MyBatis 조회 결과가 깨지는 경우

MyBatis는 JDBC 위에서 동작하므로 한글 깨짐의 직접 원인이라기보다 DB 연결, SQL, Java 문자열 처리 흐름 안에 있다. 같은 컬럼을 SQL Developer에서는 정상 조회하는데 MyBatis 결과만 깨진다면 응답 인코딩이나 화면 출력 쪽을 의심한다.

반대로 Mapper XML의 컬럼 매핑이 틀려서 다른 값을 보여주는 문제라면 인코딩이 아니라 매핑 문제다. resultType과 resultMap 기준이 헷갈리면 조회 매핑 기준을 먼저 확인한다.

바이트 길이 문제도 같이 봐야 한다

VARCHAR2(100)은 항상 100글자가 아니다

Oracle에서 문자열 길이는 CHAR 기준인지 BYTE 기준인지에 따라 달라진다. 오래된 시스템은 VARCHAR2(100 BYTE)처럼 바이트 기준인 경우가 많다. 날짜 조건이 하루 밀리는 것처럼 SQL 조건 자체가 문제인 경우는 날짜 조건 문제를 따로 봐야 하지만, 한글 입력 시 길이 초과가 난다면 캐릭터셋과 바이트 길이가 우선이다.

KO16MSWIN949에서 한글 50자가 들어가던 컬럼이 AL32UTF8에서는 한글 33자 정도에서 막힐 수 있다. 같은 업무 화면이라도 DB 캐릭터셋이 바뀌면 입력 가능 글자 수가 달라질 수 있다.

SELECT column_name, data_type, char_used, char_length, data_length
FROM user_tab_columns
WHERE table_name = 'MEMBER';

CHAR_USEDB이면 BYTE 기준, C이면 CHAR 기준이다. 캐릭터셋 변경이나 데이터 이관을 검토할 때는 이 값을 반드시 본다.

LENGTH와 LENGTHB로 차이를 확인한다

문자 수와 바이트 수 차이는 아래 SQL로 바로 확인할 수 있다.

SELECT
    LENGTH('한글') AS char_len,
    LENGTHB('한글') AS byte_len
FROM dual;

AL32UTF8 DB에서는 한글 2글자가 보통 6바이트로 계산된다. KO16MSWIN949에서는 보통 4바이트로 계산된다. 이 차이 때문에 이관 후 value too large 계열 오류가 새로 나타날 수 있다.

캐릭터셋 변경은 간단한 설정 변경이 아니다

운영 DB에서 바로 ALTER로 바꾸면 위험하다

DB 캐릭터셋은 운영 중 간단히 바꾸는 설정값이 아니다. 데이터가 이미 들어 있는 DB에서 캐릭터셋을 바꾸면 기존 데이터 변환, 컬럼 길이, 외부 연계, 배치, 파일 업로드, 리포트 출력까지 영향을 받는다.

특히 KO16MSWIN949에서 AL32UTF8로 옮길 때는 한글 바이트 수가 늘어날 수 있다. 컬럼 길이, 인덱스 길이, 외부 파일 포맷, 인터페이스 전문 길이까지 확인해야 한다.

이관은 샘플 검증부터 한다

실무에서는 전체 DB를 바로 변환하기보다 샘플 테이블을 정하고, 대표 데이터를 뽑아 검증한다. 이름, 주소, 특수문자, 괄호, 원문 복사 데이터, 외부 기관에서 받은 파일 데이터를 섞어 보는 것이 좋다.

SELECT member_id, member_name, address
FROM member
WHERE REGEXP_LIKE(member_name || address, '[^ -~]');

위 SQL은 ASCII 범위 밖 문자를 포함한 데이터를 찾는 단순 예시다. 실제 검증에서는 업무 데이터 특성에 맞게 한글, 특수문자, 외국어, 기호를 따로 샘플링한다.

Java와 JDBC에서 확인할 부분

Oracle JDBC URL에 characterEncoding을 붙이지 않는다

MySQL 예제를 보고 Oracle JDBC URL에 characterEncoding=UTF-8 같은 값을 붙이려는 경우가 있다. Oracle JDBC는 그런 방식으로 해결하는 구조가 아니다. DB 캐릭터셋과 드라이버 변환, Java 문자열 처리 흐름을 기준으로 봐야 한다.

// Oracle JDBC URL 예시
jdbc:oracle:thin:@//localhost:1521/ORCLPDB1

Oracle에서 한글 깨짐을 볼 때는 JDBC URL에 임의 파라미터를 붙이기보다 DB 캐릭터셋, 입력 파일 인코딩, 웹 요청/응답 인코딩, 저장된 데이터 상태를 순서대로 확인한다.

JSP와 Servlet 응답 인코딩도 확인한다

DB에는 정상 저장되어 있는데 화면에서만 깨진다면 응답 인코딩을 봐야 한다. JSP라면 page 지시자와 응답 헤더, Servlet Filter를 확인한다.

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");

단, 이 설정은 화면 출력 문제를 해결하는 쪽이다. DB에 이미 물음표로 저장된 값은 JSP 설정으로 되돌릴 수 없다.

확인 순서 체크리스트

순서확인 항목판단 기준
1DB 캐릭터셋NLS_CHARACTERSET이 AL32UTF8인지 KO16MSWIN949인지 확인
2DB 원본 조회SQL Developer 등에서 직접 조회해도 깨지는지 확인
3입력 경로웹 폼, API, CSV, 엑셀 중 어디서 들어오는지 확인
4저장 전 문자열Java 로그에서 INSERT 전 문자열이 이미 깨졌는지 확인
5컬럼 길이BYTE 기준 컬럼에서 바이트 초과가 나는지 확인
6출력 경로JSP, API 응답, 파일 다운로드 인코딩 확인

실무에서 바로 쓰는 팁

  • 먼저 DB에 실제로 어떻게 저장됐는지 본다. 저장 전 문제인지 조회/화면 문제인지 나눠야 한다.
  • NLS_CHARACTERSET과 NLS_NCHAR_CHARACTERSET을 구분한다. 일반 문자열은 대부분 NLS_CHARACTERSET 기준이다.
  • KO16MSWIN949에서 AL32UTF8로 갈 때는 컬럼 길이를 확인한다. 한글 바이트 수가 늘어나는 구간이 생길 수 있다.
  • 물음표로 저장된 데이터는 원본 없이는 복구가 어렵다. 변환 스크립트보다 백업과 원천 데이터 확인이 먼저다.
  • Oracle JDBC는 MySQL식 characterEncoding 파라미터로 해결하지 않는다. DB, 입력, 출력, 저장 데이터 상태를 분리해서 본다.

자주 묻는 질문

  • Q. AL32UTF8과 UTF8은 같은 건가요? A. Oracle에서는 AL32UTF8 사용을 기준으로 보는 것이 안전합니다. 오래된 UTF8 캐릭터셋과 혼동하지 말고, 신규 DB는 보통 AL32UTF8을 선택합니다.
  • Q. KO16MSWIN949는 무조건 나쁜가요? A. 그렇지는 않습니다. 한국어 레거시 시스템에서는 오래 운영된 사례가 많습니다. 다만 UTF-8 외부 연계와 다국어 데이터에는 제약이 있습니다.
  • Q. DB 캐릭터셋만 AL32UTF8로 바꾸면 한글 깨짐이 해결되나요? A. 아닙니다. 이미 깨져 저장된 데이터, 입력 파일 인코딩, 웹 응답 인코딩, 컬럼 길이 문제가 남을 수 있습니다.
  • Q. SQL Developer에서는 정상인데 웹에서만 깨집니다. A. DB 저장은 정상일 가능성이 큽니다. JSP pageEncoding, 응답 Content-Type, 필터, API 응답 헤더를 확인해야 합니다.
  • Q. 웹에서는 정상인데 DB에서 물음표로 보입니다. A. 저장 단계에서 이미 손상됐을 가능성이 큽니다. 원본 데이터나 백업이 없으면 완전 복구가 어렵습니다.
  • Q. 컬럼 길이를 BYTE에서 CHAR로 바꾸면 해결되나요? A. 일부 길이 문제에는 도움이 되지만 전체 해결책은 아닙니다. 인덱스 길이, 기존 데이터, 애플리케이션 검증 로직도 같이 봐야 합니다.

정리

AL32UTF8과 KO16MSWIN949 차이는 단순한 이름 차이가 아니라 Oracle이 문자를 저장하고 변환하는 기준의 차이다. AL32UTF8은 Unicode 기반이라 신규 웹 시스템과 외부 API 연계에 유리하고, KO16MSWIN949는 한국어 레거시 환경에서 자주 쓰이지만 UTF-8 데이터와 만나면 깨짐 문제가 생길 수 있다.

한글 깨짐을 해결할 때는 먼저 NLS_CHARACTERSET을 확인하고, DB에 실제 저장된 값이 정상인지 본다. 그다음 입력 파일, Java 문자열, JDBC, JSP 응답 인코딩, 컬럼 BYTE/CHAR 길이를 순서대로 확인한다. 무엇보다 물음표로 저장된 데이터는 원본 없이는 되돌리기 어렵기 때문에, 변환보다 원인 분리가 먼저다.

Oracle 한글 깨짐은 “UTF-8로 맞추면 끝”이 아니라, 어느 단계에서 문자가 깨졌는지 찾는 문제다.
반응형