출발점은 Jenkins의 실제 오류 기록입니다
운영자 블로그의 「jenkins build error : java encoding issue」(2026년 1월 14일)에는 Java 파일 컴파일 중 UTF-8로 해석할 수 없는 문자가 있다는 오류와 당시 환경 설정으로 대응한 경험이 있습니다. 이 글은 그 기록을 모든 프로젝트에 복사할 처방으로 쓰지 않고, 원인을 좁히는 절차로 확장합니다.
쭈니박스 본도메인은 Java나 Jenkins를 쓰지 않는 정적 사이트입니다. 아래 실험은 원본 업무 코드와 로그를 가져오지 않고 새로 작성한 예제입니다. 블로그 당시 환경을 그대로 재현한 것은 아니며, 파일과 읽는 쪽의 약속이 어긋나는 상황만 분리했습니다.
1. 깨진 글자와 컴파일 실패는 같은 문제가 아닙니다
인코딩은 바이트를 문자로 읽는 약속입니다. 파일이 저장된 방식과 읽는 쪽의 기대가 다르면 오류가 나거나 글자가 깨집니다. 중요한 것은 어디에서 그 해석이 일어났는지입니다.
| 보이는 증상 | 먼저 볼 위치 | 바로 하지 않을 일 |
|---|---|---|
| 컴파일 중 unmappable character | 오류가 가리킨 소스 파일과 컴파일러 encoding | DB 문자셋 변경 |
| 빌드는 성공, 웹 화면에서만 한글 깨짐 | 응답 바이트, Content-Type, HTML charset | 모든 Java 파일 재저장 |
| 특정 설정 파일만 잘못 읽힘 | 리소스 필터링과 해당 파일을 읽는 코드 | 운영체제 전체 로캘 변경 |
| 로그에서만 깨짐 | 로그 작성 인코딩과 터미널 해석 | 정상 DB 데이터 일괄 변환 |
CI 로그에서 마지막의 BUILD FAILURE만 보지 말고, 처음 실패한 파일과 줄·열, 실행된 작업을 기록합니다. 전체 로그를 공개하기보다 사용자 이름·내부 호스트·토큰을 가린 최소 오류 메시지를 공유합니다.
작은 Java 파일로 실패와 성공을 비교했습니다
2026년 9월 10일 Windows의 JDK 17.0.12에서 아래 파일 하나를 새로 만들어 검사했습니다. UTF-8로 저장한 한글을 US-ASCII로 읽게 한 첫 실행은 실패했고, 소스 바이트를 바꾸지 않고 읽는 옵션만 UTF-8로 맞춘 두 번째 실행은 성공했습니다.
따라 해보려면 기존 프로젝트 밖에 실험 폴더를 만들고 VS Code → File → New Text File에서 아래 코드를 붙여넣습니다. File → Save As로 EncodingProbe.java라는 이름을 정한 뒤 오른쪽 아래 인코딩 표시 → Save with Encoding → UTF-8로 저장합니다. 기존 업무 파일에 이 절차를 바로 적용하지 않습니다.
public class EncodingProbe {
public static void main(String[] args) {
String label = "쭈니박스";
System.out.println(label.codePointCount(0, label.length()));
}
}
VS Code → Terminal → New Terminal에서 PowerShell을 열고, 방금 만든 폴더가 현재 위치인지 확인합니다. 첫 명령은 오류를 확인하려는 의도적인 실패입니다. 바로 뒤의 $LASTEXITCODE로 종료 코드를 확인합니다.
javac -encoding US-ASCII EncodingProbe.java
$LASTEXITCODE
javac -encoding UTF-8 EncodingProbe.java
$LASTEXITCODE
# 두 번째 컴파일이 성공했을 때만 실행합니다.
java EncodingProbe
| 단계 | 실제 결과 | 알 수 있는 것 |
|---|---|---|
| US-ASCII로 컴파일 | 종료 1. unmappable character (0xEC) for encoding US-ASCII | UTF-8 한글 바이트를 해당 문자셋으로 읽지 못함 |
| UTF-8로 컴파일 | 종료 0, 컴파일 오류 없음 | 같은 소스의 읽기 규칙을 맞추면 컴파일됨 |
| 성공한 클래스 실행 | 4 출력 | 예제의 네 한글 음절이 네 코드 포인트로 읽힘 |
한글 자체 대신 문자 수를 출력한 이유는 콘솔의 글꼴이나 출력 인코딩 문제를 이번 검사에서 분리하기 위해서입니다. 이 결과만으로 모든 문자열이나 리소스 파일이 정상이라고 판정할 수는 없습니다. 원본 블로그에서는 UTF-8로 읽는 쪽에서 오류가 났고, 여기서는 반대로 UTF-8 파일을 ASCII로 읽게 했습니다. 불일치의 방향과 실제 원본 인코딩은 각 환경에서 다시 확인해야 합니다.
JDK 17 javac 문서의 -encoding은 소스 파일을 읽는 문자 인코딩을 지정합니다. 이번 실험에서 바꾼 것은 이 옵션 하나입니다. DB, CI 설정, 운영체제 로캘을 바꾸거나 기존 파일 전체를 변환하지 않았습니다.
추가 실험: 다시 열기와 잘못 읽은 내용을 저장하기는 다릅니다
컴파일러 옵션만 바꿔 고칠 수 있었던 앞의 사례는 원본 바이트가 남아 있었기 때문입니다. 그런데 잘못 읽힌 글자를 저장해 버리면 읽기 옵션만 바꿔도 원문이 돌아오지 않을 수 있습니다. 그 경계를 한 글자 가로 확인합니다.
Python 3가 있는 환경에서 VS Code → 새 텍스트 파일에 아래 코드를 붙여넣고 byte-loss.py로 저장한 뒤 Terminal → New Terminal에서 py -3 byte-loss.py를 실행합니다. macOS·Linux는 python3 byte-loss.py입니다. 바이트는 메모리 안에서만 만들며 기존 파일을 읽거나 변환하지 않습니다.
original = "가".encode("utf-8")
assert original.hex() == "eab080"
assert original.decode("utf-8") == "가"
try:
original.decode("ascii")
except UnicodeDecodeError:
print("strict ASCII: rejected")
else:
raise AssertionError("ASCII must reject these bytes")
wrong_text = original.decode("ascii", errors="replace")
saved = wrong_text.encode("utf-8")
assert wrong_text == "\ufffd" * 3
assert saved != original
assert saved.decode("utf-8") != "가"
print("original:", original.hex())
print("saved replacement:", saved.hex())
print("original preserved:", original.decode("utf-8") == "가")
strict ASCII: rejected
original: eab080
saved replacement: efbfbdefbfbdefbfbd
original preserved: True
UTF-8의 EA B0 80 세 바이트는 ASCII로 읽을 수 없습니다. 이번처럼 오류를 대체 문자로 바꾸는 설정을 쓰면 세 개의 U+FFFD가 만들어지고, 이를 UTF-8로 저장한 바이트는 EF BF BD 세 묶음이 됩니다. 이제 ‘UTF-8 파일’이라는 말은 맞지만 원래의 ‘가’는 아닙니다. 마지막 True는 별도로 보존한 original을 다시 읽은 결과이지 손상된 saved가 복구됐다는 뜻이 아닙니다.
여기서 세 개가 된 것은 이 바이트와 ASCII 디코더 조합의 결과입니다. 모든 잘못된 인코딩이 같은 대체 문자 개수나 같은 방식으로 손실되는 것은 아닙니다. Python Unicode HOWTO에서도 엄격한 오류 처리와 대체·무시 처리를 구분합니다.
문제가 있는 파일 한 개를 고르는 이유
원본을 보존한 채 재해석해 보는 단계와 바이트를 새로 쓰는 단계를 나누면, 실패해도 비교할 기준이 남습니다. 에디터에서 한글이 보이기 시작했다는 이유만으로 폴더 전체를 다시 저장하면 정상 파일까지 바뀔 수 있습니다. 이미 대체 문자로 저장된 경우에는 설정을 더 바꾸기보다 정상 커밋·백업·원본 산출물과 대조해야 합니다. 원본이 없는 상태에서 자동으로 복원할 수 있다고 약속할 수는 없습니다.
2. 로컬과 CI가 정말 같은 입력을 쓰는지 비교합니다
프로젝트 폴더에서 터미널을 열고 아래 읽기 전용 명령으로 기본 정보를 비교합니다. CI에서는 Jenkins → 해당 작업 → 빌드 번호 → Console Output에서 이미 남은 버전·커밋 정보를 먼저 찾습니다. 새로운 실행 단계 추가는 CI 변경이므로 별도로 검토합니다.
git rev-parse HEAD
git status --short
java -version
javac -version
mvn -version
java와 javac가 서로 다른 설치 경로를 가리킬 수도 있고, Maven이 별도의 Toolchain을 사용할 수도 있습니다. 명령 결과가 같다는 이유만으로 컴파일러까지 같다고 단정하지 말고 프로젝트의 기존 설정도 확인합니다.
다음으로 오류 파일 한 개의 인코딩을 확인합니다. VS Code → 문제 파일 열기 → 오른쪽 아래 인코딩 표시 → Reopen with Encoding에서 후보 인코딩으로 다시 열어봅니다. 이는 해석을 바꾸는 단계입니다. Save with Encoding은 파일 바이트를 바꾸므로 원본을 보존하고 별도로 진행해야 합니다. VS Code 파일 인코딩 안내에서도 두 동작을 구분합니다.
한 번 읽힌다고 실제 인코딩이 확정되는 것은 아닙니다. ASCII 문자만 있는 파일은 여러 인코딩에서 똑같이 보일 수 있습니다. 한글 주석·문자열과 원본 이력을 비교하고, 이미 대체 문자로 저장된 파일은 인코딩 선택만으로 원래 글자를 복원할 수 없다는 점도 확인합니다.
3. 파일을 보존할지, UTF-8로 정리할지 결정합니다
기존 소스 인코딩을 유지해야 하는 경우
파일들이 같은 레거시 인코딩으로 저장돼 있고 당장 파일을 바꾸기 어렵다면, 소스를 읽는 컴파일러의 인코딩을 해당 값으로 맞추는 방법을 검토합니다. JVM의 기본 문자셋, 운영체제 로캘, Maven 컴파일러의 명시적 설정은 서로 같은 항목이 아닙니다. 원문에서 해결된 환경 변수 설정이 모든 Maven 프로젝트에 그대로 적용되지는 않습니다.
Maven Compiler Plugin의 encoding은 Java 소스를 읽을 때 쓰며 기본값은 project.build.sourceEncoding을 참조합니다. 기존 POM에서 이 값과 플러그인별 덮어쓰기를 확인합니다. 공식 encoding 매개변수가 기준입니다.
UTF-8로 통일할 수 있는 경우
원본 인코딩을 올바르게 해석한 뒤 파일을 UTF-8로 저장하고, 관련 도구도 같은 규칙을 쓰도록 맞춥니다. 아래는 소스가 실제 UTF-8임을 확인한 경우 기존 POM의 properties에 병합할 예시입니다. 프로젝트 전체나 기존 properties를 덮어쓰는 템플릿이 아닙니다.
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
한글 파일 몇 개와 UTF-8 파일이 섞였다면 전역 옵션 하나로 해결하려 하지 않습니다. 바이트가 실제로 바뀌는 범위를 검토하고 소수 파일부터 변환해 차이를 확인합니다. 리소스 필터링은 컴파일러와 별도 설정일 수 있으므로 Maven Resources Plugin 인코딩 안내도 확인합니다.
이때 JDK, 라이브러리, 플러그인 버전을 동시에 전부 올리면 원인을 분리하기 어렵습니다. 먼저 인코딩 문제의 최소 변경을 검증하고, 보안상 필요한 버전 갱신은 별도 변경으로 검토하는 편이 결과를 설명하기 쉽습니다.
4. 성공 기준에 한글 내용도 포함합니다
- 변경 전후 파일 차이를 확인해 한글이 물음표나 대체 문자로 바뀌지 않았는지 봅니다.
- 프로젝트에서 원래 사용하던 빌드·테스트를 로컬과 CI에서 같은 커밋으로 실행합니다. 낯선 프로젝트의 빌드는 임의 코드 실행을 포함하므로 먼저 스크립트를 검토합니다.
- 산출물에서 한글 문자열과 리소스를 확인합니다. 로그만 정상인 것과 실제 화면·응답이 정상인 것은 다릅니다.
- 변경한 설정 하나와 해결한 증상을 대응시켜 기록합니다. 실패하면 시도한 값과 결과를 남기고 다음 가설을 분리합니다.
증상: 컴파일 / 리소스 / 응답 / 로그 중 어느 단계인가
입력: 커밋과 오류 파일, 확인한 원본 인코딩
환경: 로컬과 CI의 JDK·Maven·컴파일러 설정 차이
변경: 파일 변환 또는 읽는 쪽의 인코딩 변경
검증: 같은 커밋 빌드 결과, 한글 내용 비교
제외: 이번에 바꾸지 않은 DB·런타임·배포 설정
이 절차는 특정 인코딩을 정답으로 정하는 것이 아니라, 입력과 읽는 쪽의 약속을 맞추는 방법입니다. 외부에서 만든 사이트를 받을 때도 이런 환경 차이가 있으므로 마이그레이션 검토에 재현 명령과 파일 인코딩 정보를 함께 남겨두면 좋습니다.