소프트웨어 개발, 특히 웹 애플리케이션 개발 여정에서 개발자들은 마치 통과 의례처럼 '알 수 없는 외계어'와 마주하는 순간을 겪습니다. 분명 소스 코드에는 '안녕하세요'라고 아름답게 적어두었지만, 웹 브라우저 화면이나 터미널 콘솔, 로그 파일에는 '����', 'Ä¿³³', 'ÇѱÛ' 같은 기괴한 문자열이 출력되는 현상입니다. 이 지긋지긋한 문자 깨짐 현상은 단순한 오타나 로직 에러가 아닌, '인코딩(Encoding)'이라는 깊고 근본적인 문제에서 비롯됩니다.
과거 Eclipse나 IntelliJ와 같은 전통적인 통합 개발 환경(IDE)에서는 잘 동작하던 프로젝트가, 현대적인 개발 트렌드에 맞춰 Visual Studio Code(이하 VSCode)로 개발 환경을 이전했을 때 돌연 한글이 깨지기 시작하는 경험을 한 개발자가 적지 않습니다. 이는 각 도구가 가진 기본 설정의 차이와, 여러 컴포넌트(에디터, 컴파일러, JVM, 웹 서버, 데이터베이스 등)가 복잡하게 얽혀 돌아가는 개발 환경의 특성 때문입니다.
이 글에서는 스프링 부트(Spring Boot)와 VSCode를 사용하는 환경에서 발생하는 한글 깨짐 현상의 근본적인 원인을 A부터 Z까지 심층적으로 분석합니다. 단순히 '이 코드를 추가하면 해결됩니다' 식의 단편적인 해결책을 나열하는 것을 넘어, 데이터가 사용자의 키보드에서부터 시작해 데이터베이스에 저장되고 다시 화면에 표시되기까지의 전 과정에서 인코딩이 어떻게 작용하는지 추적합니다. 이를 통해 독자 여러분이 다시는 인코딩 문제로 귀중한 개발 시간을 낭비하지 않도록, 문제의 본질을 이해하고 어떤 상황에서도 적용할 수 있는 종합적이고 체계적인 해결 전략을 제시하고자 합니다.
1. 모든 문제의 시작: 문자 인코딩이란 무엇인가?
"컴퓨터는 0과 1밖에 모른다"는 말은 컴퓨터 과학의 가장 기본적인 명제입니다. 그렇다면 컴퓨터는 어떻게 '가', '나', '다'와 같은 한글이나 'A', 'B', 'C' 같은 알파벳, 그리고 😊와 같은 이모티콘을 이해하고 저장하고 표시할 수 있을까요? 바로 '문자 집합(Character Set)'과 '문자 인코딩(Character Encoding)'이라는 약속 덕분입니다.
1.1. 문자 집합 (Character Set): 문자의 목록
문자 집합은 우리가 사용하는 문자와 기호들을 모아놓고, 각 문자에 고유한 번호(코드 포인트, Code Point)를 부여한 '목록' 또는 '사전'에 비유할 수 있습니다. 예를 들어, 세계의 거의 모든 문자를 포함하는 표준인 유니코드(Unicode) 문자 집합에서는 '한'이라는 글자에 U+D55C
라는 고유 번호를 부여합니다.
- 아스키(ASCII): 가장 초기의 문자 집합 중 하나로, 128개의 영문 알파벳, 숫자, 특수 기호를 포함합니다. 7비트로 표현되어 용량이 작지만, 한글을 포함한 비영어권 문자는 전혀 표현할 수 없습니다.
- 유니코드(Unicode): 전 세계의 모든 문자를 일관되게 표현하고 다루기 위해 설계된 산업 표준입니다. 각 문자에 고유한 코드 포인트를 할당함으로써, 언어와 플랫폼에 상관없이 문자 호환성을 보장합니다. '가'는 `U+AC00`, '힣'은 `U+D7A3` 처럼 모든 한글 음절에 번호가 매겨져 있습니다.
1.2. 문자 인코딩 (Character Encoding): 문자를 0과 1로 바꾸는 규칙
문자 인코딩은 문자 집합에 정의된 문자들을 실제 컴퓨터가 저장하고 처리할 수 있는 0과 1의 연속, 즉 바이트(byte) 시퀀스로 변환하는 '규칙' 또는 '번역 방법'입니다. 하나의 문자 집합에 대해서도 여러 가지 인코딩 방식이 존재할 수 있습니다.
- EUC-KR (CP949): '확장 유닉스 코드-한국어(Extended Unix Code-Korea)'의 약자로, 과거 한국에서 널리 사용되던 한글 인코딩 방식입니다. 영문은 1바이트, 한글은 2바이트를 사용하여 표현합니다. 하지만 현대 한글의 모든 글자(11,172자)를 표현하지 못하고, 국제 표준이 아니라는 한계가 있습니다.
- UTF-8 (Unicode Transformation Format - 8-bit): 현재 웹과 소프트웨어 개발의 사실상 표준(de facto standard)으로 자리 잡은 인코딩 방식입니다. 유니코드 문자 집합을 위한 가장 대중적인 가변 길이 인코딩으로, 문자에 따라 1바이트에서 4바이트까지 다양한 크기를 사용합니다.
- 영어 알파벳 및 숫자는 ASCII와 동일하게 1바이트로 처리하여 호환성이 높습니다.
- 일반적인 한글은 대부분 3바이트로 표현됩니다.
- 전 세계 모든 언어를 표현할 수 있어 다국어 지원에 필수적입니다.
1.3. 왜 한글이 깨질까? "인코딩 불일치"의 비극
한글 깨짐 현상의 본질은 바로 이 '인코딩 불일치'에 있습니다. 데이터를 저장(인코딩)할 때 사용한 규칙과, 그 데이터를 읽어서 해석(디코딩)할 때 사용한 규칙이 서로 다른 경우 문자가 깨져 보이게 됩니다.
예를 들어, '한'이라는 글자를 각기 다른 방식으로 인코딩하고 디코딩해 보겠습니다.
- '한' 글자의 유니코드 코드 포인트는
U+D55C
입니다. - UTF-8로 인코딩: '한'은 UTF-8 규칙에 따라 3바이트인 `E D 9 5 9 C` (16진수)로 변환됩니다.
- EUC-KR로 인코딩: '한'은 EUC-KR 규칙에 따라 2바이트인 `C 7 D 1` (16진수)로 변환됩니다.
만약 UTF-8로 인코딩된 파일(ED 95 9C
)을 EUC-KR 방식으로 읽으려고 시도하면, 컴퓨터는 이 바이트들을 EUC-KR 규칙에 따라 해석하려 하고, 이는 전혀 다른 글자(예: `한`)로 보이거나 해석 불가능한 문자로 표시됩니다. 반대의 경우도 마찬가지입니다. 이것이 바로 우리가 마주하는 한글 깨짐의 실체입니다. 따라서 문제 해결의 핵심은 데이터가 흐르는 모든 경로의 인코딩 방식을 'UTF-8'로 통일하는 것입니다.
2. 범인 추적: 스프링 부트 애플리케이션의 인코딩 취약 지점
"모든 것을 UTF-8로 통일한다"는 목표는 단순하지만, 실제 애플리케이션 환경은 여러 컴포넌트가 얽힌 복잡계입니다. 인코딩 불일치는 이 사슬의 약한 고리 어디에서든 발생할 수 있습니다. 문제를 해결하기 위해서는 데이터의 흐름을 따라가며 각 단계별로 인코딩 설정을 점검해야 합니다.

그림 1: 데이터 흐름에 따른 인코딩 검사 지점
- 소스 파일 자체 (File Layer): 우리가 코드를 작성하는
.java
,.properties
,.html
파일 자체가 어떤 인코딩으로 저장되었는가? VSCode가 파일을 저장할 때 사용하는 기본 인코딩이 여기에 해당합니다. - 컴파일 (Compile Layer): 소스 코드를 바이트 코드로 변환하는 Java 컴파일러(javac)가 어떤 인코딩을 기준으로 소스 파일을 읽는가? Maven이나 Gradle 같은 빌드 도구가 이 과정을 제어합니다.
- JVM 실행 (Runtime Layer): 컴파일된 코드를 실행하는 Java 가상 머신(JVM)이 파일 I/O나 문자열 처리 시 사용하는 기본 문자 집합(Charset)은 무엇인가? 이는 JVM 시작 옵션에 의해 결정되며, 운영체제(OS)의 기본 설정에 영향을 받습니다.
- 웹 서버 (Web Server Layer): 내장된 Tomcat 같은 서블릿 컨테이너가 클라이언트로부터 받은 HTTP 요청(Request)의 파라미터를 어떤 인코딩으로 해석하고, 클라이언트에게 보낼 HTTP 응답(Response)을 어떤 인코딩으로 생성하는가? 스프링 부트의 서버 설정이 이 부분을 담당합니다.
- 데이터베이스 (Persistence Layer): 데이터를 영구적으로 저장하는 데이터베이스와의 연결(Connection) 시 어떤 인코딩을 사용하며, 데이터베이스 서버 자체, 테이블, 컬럼은 어떤 문자 집합으로 설정되어 있는가? JDBC 드라이버 설정이 중요합니다.
- 클라이언트/뷰 (Client/View Layer): 최종적으로 웹 브라우저가 서버로부터 받은 HTML을 어떤 인코딩으로 해석해야 하는지 알려주는 `Content-Type` 헤더나 HTML 내부의 `` 태그가 올바르게 설정되었는가?
VSCode 환경에서 문제가 자주 발생하는 이유는, 특히 3번 JVM 실행 단계와 관련이 깊습니다. Eclipse나 IntelliJ 같은 Java 전문 IDE는 프로젝트 설정에 따라 자동으로 JVM 실행 옵션에 인코딩 설정을 포함해주는 경우가 많습니다. 반면, 범용 에디터인 VSCode는 Java 확장 프로그램을 통해 기능을 제공하므로, 이러한 세밀한 JVM 옵션을 사용자가 직접 명시적으로 설정해야 할 때가 많습니다. 특히 Windows 운영체제는 기본 시스템 로케일 인코딩이 CP949(EUC-KR의 확장)인 경우가 많아, JVM이 별도 설정 없이 실행되면 OS의 설정을 따라 EUC-KR을 기본 인코딩으로 사용하게 되고, 이는 UTF-8로 작성된 소스 코드나 설정 파일과 충돌을 일으키는 주된 원인이 됩니다.
3. 완전 정복: 단계별 UTF-8 통합 솔루션
이제 이론을 바탕으로 실제적인 해결책을 적용해 보겠습니다. 위에서 분석한 각 계층에 맞춰 하나씩 설정을 바로잡아, 견고한 UTF-8 환경을 구축하는 방법을 안내합니다.
Step 1: VSCode 편집기 설정 (파일 계층)
가장 먼저 우리 손으로 만드는 소스 파일들이 처음부터 UTF-8로 올바르게 저장되도록 VSCode 설정을 확인하고 수정해야 합니다.
- VSCode에서
Ctrl + ,
(macOS:Cmd + ,
)를 눌러 설정(Settings) 창을 엽니다. - 우측 상단의 파일 아이콘(
Open Settings (JSON)
)을 클릭하여settings.json
파일을 직접 편집합니다. - 아래 내용을
settings.json
파일에 추가하거나 기존 설정을 수정합니다. 이 설정은 VSCode가 모든 파일을 기본적으로 UTF-8로 저장하고 읽도록 강제합니다.{ // ... 다른 설정들 ... // Files: 기본 파일 인코딩을 UTF-8로 설정합니다. "files.encoding": "utf8", // Files: 여러 인코딩이 혼재된 오래된 프로젝트의 경우, // 이 설정을 켜면 VSCode가 파일 인코딩을 추측하지만, // 새로 시작하는 프로젝트에서는 명시적으로 utf8만 사용하는 것이 좋습니다. // "files.autoGuessEncoding": false, // 터미널 인코딩 설정 (PowerShell 등 Windows 터미널에서 한글이 깨질 때) "terminal.integrated.shell.windows": "C:\\Windows\\System32\\cmd.exe", "terminal.integrated.shellArgs.windows": ["/K", "chcp 65001"], "terminal.integrated.profiles.windows": { "PowerShell": { "source": "PowerShell", "icon": "terminal-powershell", "args": ["-NoExit", "-Command", "chcp 65001"] } } }
특히 files.encoding: "utf8"
은 필수적인 설정입니다. 이미 EUC-KR 등으로 저장된 파일이 있다면, VSCode 우측 하단의 인코딩 상태(예: 'UTF-8')를 클릭하여 'Save with Encoding' 옵션을 통해 UTF-8로 변환하여 다시 저장해주어야 합니다.
Step 2: JVM 실행 옵션 설정 (런타임 계층) - 가장 핵심적인 해결책
앞서 언급했듯이, JVM이 운영체제의 기본값을 따르지 않고 항상 UTF-8을 기본 인코딩으로 사용하도록 명시적으로 지시하는 것이 매우 중요합니다. 이것이 바로 원문에서 제시된 -Dfile.encoding=UTF-8
옵션의 역할입니다. 이 옵션은 VSCode에서 Java 코드를 실행하거나 디버깅할 때 JVM에 전달되어야 합니다.
VSCode에서 이 설정을 적용하는 방법은 두 가지입니다. 전역 설정과 프로젝트별 실행 설정입니다.
방법 A: VSCode 사용자 또는 작업 공간 설정에 추가 (권장)
이 방법은 VSCode의 Java Language Server 자체에 옵션을 적용하여, 해당 VSCode 환경에서 실행되는 모든 Java 애플리케이션에 일괄적으로 적용됩니다.
- 다시
settings.json
파일을 엽니다. - 아래와 같이
java.jdt.ls.vmargs
설정을 추가합니다. 이 설정은 Java Language Server가 사용하는 JVM에 인자를 전달합니다.
{
// ... 다른 설정들 ...
// Java Language Server에 JVM 인자 전달
// 서버 시작 시 파일 인코딩을 UTF-8로 강제합니다.
"java.jdt.ls.vmargs": "-Dfile.encoding=UTF-8 -Xmx1G -XX:+UseG1GC -XX:+UseStringDeduplication"
}
참고: 위 예시처럼 -Dfile.encoding=UTF-8
외에도 메모리 설정(-Xmx1G
)이나 가비지 컬렉터 설정 등 다른 JVM 옵션을 함께 추가할 수 있습니다. 각 옵션은 공백으로 구분합니다.
방법 B: 프로젝트의 launch.json 파일에 추가
만약 특정 프로젝트에만 이 설정을 적용하고 싶다면, 프로젝트 루트의 .vscode
폴더 안에 있는 launch.json
파일을 수정합니다. 이 파일은 디버깅 및 실행 구성을 관리합니다.
- VSCode의 'Run and Debug' 탭(
Ctrl+Shift+D
)으로 이동합니다. - 톱니바퀴 아이콘을 클릭하여
launch.json
파일을 생성하거나 엽니다. - "Launch Java" 또는 "Spring Boot" 관련 구성(configuration)을 찾아
vmArgs
속성을 추가합니다.
{
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Spring Boot-Application<my-project>",
"request": "launch",
"mainClass": "com.example.myproject.Application",
"projectName": "my-project",
"args": "",
"envFile": "${workspaceFolder}/.env",
// 여기에 JVM 인자를 추가합니다.
"vmArgs": "-Dfile.encoding=UTF-8"
}
]
}
대부분의 경우 방법 A가 더 편리하고 확실한 해결책입니다. 한 번 설정해두면 모든 자바 프로젝트에 적용되어 인코딩 문제를 예방할 수 있습니다.
Step 3: 빌드 도구 설정 (컴파일 계층)
컴파일러가 소스 코드를 읽을 때부터 UTF-8을 사용하도록 빌드 도구에 명시해주는 것이 좋습니다. 이는 팀원 간의 협업이나 CI/CD 환경에서의 빌드 안정성을 높여줍니다.
Maven 사용 시 (pom.xml)
pom.xml
파일의
섹션에 다음을 추가합니다.
...
17
UTF-8
UTF-8
...
Gradle 사용 시 (build.gradle)
Groovy DSL 기반의 build.gradle
파일에는 다음 설정을 추가합니다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.5'
id 'io.spring.dependency-management' version '1.1.3'
}
// ...
// Java 컴파일러 옵션 설정
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
Kotlin DSL 기반의 build.gradle.kts
파일에는 다음과 같이 작성합니다.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
// ...
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "17"
}
}
Step 4: 스프링 부트 애플리케이션 설정 (웹 서버 계층)
스프링 부트 애플리케이션 자체에서 HTTP 요청과 응답에 대한 인코딩을 명확하게 지정해야 합니다. src/main/resources/
폴더 아래의 application.properties
또는 application.yml
파일에 설정합니다.
application.properties 사용 시
# ===================================================================
# 서버 및 서블릿 인코딩 설정
# ===================================================================
# 서버가 HTTP 요청과 응답에 사용할 기본 문자 인코딩
server.servlet.encoding.charset=UTF-8
# 인코딩 설정을 항상 강제로 적용할지 여부
# Tomcat의 경우 기본값이 true 이므로 굳이 명시할 필요는 없지만, 명확성을 위해 추가
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true
application.yml 사용 시
server:
servlet:
encoding:
charset: UTF-8
enabled: true
force: true
최신 스프링 부트 버전(2.2 이상)에서는 server.servlet.encoding.charset=UTF-8
하나만 설정해도 대부분의 경우 충분합니다. 이 설정은 내장 톰캣이 모든 HTTP 요청을 UTF-8로 디코딩하고, 응답의 Content-Type
헤더에도 `charset=UTF-8`을 포함시키도록 하는 역할을 합니다.
Step 5: 데이터베이스 연결 설정 (영속성 계층)
애플리케이션 내부에서 모든 것이 완벽해도, 데이터가 저장되는 최종 목적지인 데이터베이스의 인코딩이 다르면 도루묵입니다.
-
데이터베이스 자체의 문자 집합 확인:
사용하는 데이터베이스(MySQL, MariaDB, PostgreSQL 등)의 기본 문자 집합과 콜레이션(collation)이
utf8mb4
(이모티콘까지 지원) 또는 최소한utf8
로 설정되어 있는지 확인해야 합니다.-- MySQL/MariaDB 에서 데이터베이스 문자 집합 확인 SHOW VARIABLES LIKE 'c%'; -- character_set_database, character_set_server 등이 utf8mb4 인지 확인 -- 테이블 및 컬럼 문자 집합 확인 SHOW CREATE TABLE your_table_name;
-
JDBC 연결 URL에 인코딩 옵션 추가:
스프링 부트의
application.properties
(또는.yml
) 파일에서 데이터베이스 연결 URL에 인코딩 관련 파라미터를 명시적으로 추가합니다. 이는 드라이버가 서버와 통신할 때 사용할 문자 집합을 지정합니다.
MySQL/MariaDB JDBC URL 예시 (application.properties)
# ===================================================================
# 데이터소스 설정
# ===================================================================
spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
useUnicode=true&characterEncoding=UTF-8
부분이 핵심입니다. 이를 통해 클라이언트(애플리케이션)와 DB 서버 간의 모든 데이터 전송이 UTF-8로 이루어지도록 보장합니다.
4. 최종 검증 및 결론
위의 1단계부터 5단계까지의 모든 설정을 마쳤다면, 이제 애플리케이션의 거의 모든 계층에서 인코딩이 UTF-8로 통일되었습니다. 간단한 테스트 코드를 작성하여 모든 것이 정상적으로 작동하는지 확인해 봅시다.
검증용 RestController 예제
package com.example.myproject.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class EncodingTestController {
@GetMapping("/test/encoding")
public Map<String, String> testEncoding(
@RequestParam(defaultValue = "기본값 한글 파라미터") String param) {
String message = "컨트롤러에서 생성된 한글 메시지입니다. 😊";
System.out.println("서버 콘솔 출력: " + message);
System.out.println("수신된 파라미터: " + param);
Map<String, String> response = new HashMap<>();
response.put("staticMessage", message);
response.put("receivedParam", param);
return response;
}
}
애플리케이션을 실행한 후 다음을 확인합니다.
- 콘솔 로그: VSCode의 'TERMINAL' 또는 'DEBUG CONSOLE'에 "서버 콘솔 출력: 컨트롤러에서 생성된 한글 메시지입니다. 😊"와 같이 한글과 이모티콘이 깨지지 않고 정상적으로 출력되는지 확인합니다.
- API 요청/응답: 웹 브라우저나 Postman 같은 API 클라이언트로 다음 두 URL을 호출해 봅니다.
http://localhost:8080/test/encoding
http://localhost:8080/test/encoding?param=브라우저에서 보낸 한글
만약 모든 과정에서 한글이 올바르게 보인다면, 마침내 지긋지긋한 한글 깨짐의 늪에서 벗어나게 된 것입니다.
결론: 일관성이 답이다
스프링 부트와 VSCode 환경에서의 한글 인코딩 문제는 복잡해 보이지만, 그 본질은 '인코딩 규칙의 불일치'라는 하나의 원인으로 귀결됩니다. 해결책 역시 '모든 계층에서 UTF-8로 일관성을 유지'하는 단 하나의 원칙으로 요약할 수 있습니다.
오늘 살펴본 것처럼 파일 저장부터 컴파일, JVM 실행, 웹 서버, 데이터베이스에 이르기까지 데이터가 거쳐가는 모든 관문의 인코딩 설정을 체계적으로 점검하고 통일하는 것이 중요합니다. 특히 VSCode 환경에서는 JVM 실행 옵션(-Dfile.encoding=UTF-8
)을 settings.json
에 명시적으로 설정하는 것이 다른 IDE에서 넘어온 개발자들이 가장 쉽게 놓치는 부분이자, 가장 극적인 효과를 보는 해결책입니다.
이 가이드가 여러분의 프로젝트에서 발생하는 인코딩 문제를 해결하는 데 명확한 길잡이가 되기를 바랍니다. 한 번 제대로 된 UTF-8 환경을 구축해두면, 앞으로는 인코딩 걱정 없이 오롯이 비즈니스 로직 개발에만 집중할 수 있을 것입니다.
와... 감사합니다 저도 찾고 있었어요 ㅠㅠ
ReplyDelete그런데 저거 설정은 어디서 하는지 알수 있을까요???