JDK 18 핵심 기능 — UTF-8 기본값과 Simple Web Server

JDK 18 개요

Java 18은 2022년 3월에 출시된 비LTS 릴리스입니다. 큰 신규 기능보다는 오래된 불편함을 해소하는 개선에 집중한 버전입니다. UTF-8 기본 인코딩, 간이 웹 서버, Javadoc 스니펫 태그가 대표적입니다. JDK 17 LTS 직후의 릴리스답게 안정화 성격이 강합니다.

JEP기능상태
JEP 400UTF-8 기본 인코딩정식
JEP 408Simple Web Server정식
JEP 413Javadoc 코드 스니펫정식
JEP 416Method Handle을 사용한 리플렉션 재구현내부 개선
JEP 417Vector API (Third Incubator)인큐베이터
JEP 418Internet-Address Resolution SPI정식
JEP 421Finalization 제거 예고Deprecated

UTF-8 기본 인코딩 (JEP 400)

Java에서 가장 흔한 함정 중 하나가 플랫폼 기본 인코딩 문제였습니다. Windows에서 개발하면 MS949, macOS/Linux에서는 UTF-8이 기본값이어서, 같은 코드가 운영체제에 따라 다르게 동작했습니다.

JDK 18부터는 모든 플랫폼에서 UTF-8이 기본값입니다.

import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;

public class Utf8DefaultExample {
    public static void main(String[] args) throws IOException {
        // JDK 18부터 기본 인코딩 = UTF-8 (모든 OS에서 동일)
        System.out.println("기본 인코딩: " + Charset.defaultCharset());
        // JDK 17 이전 Windows: MS949 또는 EUC-KR
        // JDK 18 이후 Windows: UTF-8

        // 파일 쓰기 — 인코딩 명시 없이도 UTF-8
        Path tempFile = Files.createTempFile("test", ".txt");
        try (FileWriter writer = new FileWriter(tempFile.toFile())) {
            writer.write("한글 테스트 — UTF-8 기본값");
        }

        // 파일 읽기 — 역시 UTF-8
        String content = Files.readString(tempFile);
        System.out.println("파일 내용: " + content);
        // 파일 내용: 한글 테스트 — UTF-8 기본값

        // 바이트 배열 변환도 UTF-8 기본
        byte[] bytes = "자바 18".getBytes(); // 명시적 charset 없이도 UTF-8
        System.out.println("바이트 수: " + bytes.length);
        // 바이트 수: 8 (UTF-8에서 한글 3바이트 × 2 + ASCII 2바이트)

        // 정리
        Files.deleteIfExists(tempFile);
    }
}

이 변경이 기존 코드에 미치는 영향을 정리하면 다음과 같습니다.

상황JDK 17 이전 (Windows)JDK 18 이후
new FileWriter("a.txt")MS949로 쓰기UTF-8로 쓰기
new String(bytes)MS949로 해석UTF-8로 해석
System.out.println("한글")콘솔 인코딩 의존UTF-8

기존에 MS949/EUC-KR로 작성된 파일을 읽는 코드는 명시적으로 인코딩을 지정해야 합니다. 인코딩을 생략한 채 “이전 방식이 기본이겠지”라고 가정하면 깨진 문자가 나타날 수 있습니다.

Simple Web Server (JEP 408)

JDK 18에는 정적 파일을 서빙하는 간이 웹 서버가 내장되었습니다. Python의 python -m http.server와 유사한 역할입니다. 프로토타이핑, 테스트, 파일 공유 등에 유용합니다.

명령줄 사용

# 현재 디렉토리를 8080 포트에서 서빙
jwebserver -p 8080

# 특정 디렉토리 지정
jwebserver -d /path/to/docs -p 9000

# 바인드 주소 지정
jwebserver -b 0.0.0.0 -p 8080

Java 코드에서 사용

프로그래밍 방식으로도 웹 서버를 구성할 수 있습니다.

import java.net.InetSocketAddress;
import java.nio.file.Path;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.SimpleFileServer;
import com.sun.net.httpserver.SimpleFileServer.OutputLevel;

public class SimpleWebServerExample {
    public static void main(String[] args) throws Exception {
        // 정적 파일 서빙 서버 생성
        Path root = Path.of(System.getProperty("user.dir"));
        InetSocketAddress address = new InetSocketAddress(8080);

        HttpServer server = SimpleFileServer.createFileServer(
            address,
            root,
            OutputLevel.VERBOSE // 요청 로그 출력
        );

        server.start();
        System.out.println("서버 시작: http://localhost:8080");
        System.out.println("서빙 디렉토리: " + root);
        System.out.println("종료하려면 Ctrl+C를 누르세요");
        // 서버 시작: http://localhost:8080
        // 서빙 디렉토리: /home/user/project
        // 종료하려면 Ctrl+C를 누르세요
    }
}

Simple Web Server는 정적 파일 전용입니다. CGI, 서블릿, 동적 콘텐츠는 지원하지 않습니다. 프로덕션이 아니라 개발·테스트 용도로만 사용해야 합니다.

Javadoc 코드 스니펫 (JEP 413)

기존 {@code ...} 태그로는 복잡한 코드 예제를 Javadoc에 넣기 어려웠습니다. JDK 18의 @snippet 태그는 구문 강조, 외부 파일 참조, 영역 지정 등을 지원합니다.

/**
 * 사용자 정보를 담는 Record입니다.
 *
 * 사용 예제:
 * {@snippet :
 *     // User 인스턴스 생성
 *     User user = new User("홍길동", "hong@example.com"); // @highlight substring="new User"
 *     System.out.println(user.name());   // 홍길동
 *     System.out.println(user.email());  // hong@example.com
 * }
 *
 * 외부 파일에서 스니펫을 가져올 수도 있습니다:
 * {@snippet file="UserExample.java" region="creation"}
 */
public record User(String name, String email) {
    // 컴팩트 생성자로 유효성 검증
    public User {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("이름은 필수입니다");
        }
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("유효한 이메일이 필요합니다");
        }
    }
}

class SnippetDemo {
    public static void main(String[] args) {
        User user = new User("홍길동", "hong@example.com");
        System.out.println(user);
        // User[name=홍길동, email=hong@example.com]

        // 유효성 검증 확인
        try {
            new User("", "invalid");
        } catch (IllegalArgumentException e) {
            System.out.println("검증 실패: " + e.getMessage());
            // 검증 실패: 이름은 필수입니다
        }
    }
}

@snippet 태그의 주요 기능은 다음과 같습니다.

기능문법설명
인라인 스니펫{@snippet : ... }Javadoc 안에 직접 코드 작성
외부 파일 참조{@snippet file="..." }별도 파일에서 가져오기
영역 지정region="name"파일의 특정 구간만 표시
강조@highlight substring="..."특정 부분 강조 표시
교체@replace regex="..." replacement="..."표시 시 문자열 교체

Internet-Address Resolution SPI (JEP 418)

InetAddress.getByName() 등 호스트명 해석에 사용되는 내부 구현을 SPI(Service Provider Interface)로 교체할 수 있게 되었습니다. 테스트 환경에서 DNS 응답을 모킹하거나, 커스텀 DNS 해석 로직을 주입할 때 유용합니다.

import java.net.InetAddress;
import java.net.UnknownHostException;

public class AddressResolutionExample {
    public static void main(String[] args) {
        try {
            // 기본 DNS 해석 — SPI를 통해 커스텀 구현 교체 가능
            InetAddress address = InetAddress.getByName("localhost");
            System.out.println("호스트: " + address.getHostName());
            System.out.println("주소: " + address.getHostAddress());
            // 호스트: localhost
            // 주소: 127.0.0.1

            // 여러 주소 조회
            InetAddress[] addresses = InetAddress.getAllByName("google.com");
            System.out.println("google.com 주소 수: " + addresses.length);
            for (InetAddress addr : addresses) {
                System.out.println("  " + addr.getHostAddress());
            }
        } catch (UnknownHostException e) {
            System.out.println("호스트를 찾을 수 없습니다: " + e.getMessage());
        }
    }
}

실제 SPI 구현체를 만들려면 java.net.spi.InetAddressResolverProvider를 상속하면 됩니다. 테스트 프레임워크에서 외부 의존성 없이 DNS 응답을 제어할 수 있어 통합 테스트 작성이 편리해집니다.

Finalization 제거 예고 (JEP 421)

Object.finalize() 메서드가 forRemoval로 지정되었습니다. finalize()는 GC가 호출 시점을 보장하지 않고, 성능 저하와 메모리 누수의 원인이 되어 왔습니다.

import java.lang.ref.Cleaner;

public class CleanerExample {
    // finalize() 대신 Cleaner 사용 (JDK 9+)
    private static final Cleaner cleaner = Cleaner.create();

    static class Resource implements AutoCloseable {
        private final Cleaner.Cleanable cleanable;
        private final ResourceState state;

        // 정리 로직을 별도 static 클래스에 분리 (this 참조 방지)
        private static class ResourceState implements Runnable {
            private final String name;

            ResourceState(String name) {
                this.name = name;
            }

            @Override
            public void run() {
                System.out.println("리소스 정리: " + name);
            }
        }

        Resource(String name) {
            this.state = new ResourceState(name);
            this.cleanable = cleaner.register(this, state);
            System.out.println("리소스 생성: " + name);
        }

        @Override
        public void close() {
            cleanable.clean(); // 명시적 정리
        }
    }

    public static void main(String[] args) {
        // try-with-resources로 명시적 정리 (권장)
        try (Resource res = new Resource("DB 커넥션")) {
            System.out.println("리소스 사용 중...");
        }
        // 리소스 생성: DB 커넥션
        // 리소스 사용 중...
        // 리소스 정리: DB 커넥션

        System.out.println("프로그램 종료");
    }
}

finalize() 대신 사용할 수 있는 대안은 다음과 같습니다.

방식용도호출 보장
try-with-resources + AutoCloseable명시적 리소스 해제보장됨
Cleaner (JDK 9+)안전망(fallback) 정리GC 의존
PhantomReference + ReferenceQueue저수준 정리 제어GC 의존

정리

JDK 18은 화려한 신규 기능보다 개발 편의성 개선에 초점을 맞춘 버전입니다.

  • UTF-8 기본 인코딩: Windows/macOS/Linux 간 인코딩 불일치 문제 해결. 한국어 개발 환경에서 특히 반가운 변경
  • Simple Web Server: jwebserver 명령 한 줄로 정적 파일 서빙. 프로토타이핑과 테스트에 유용
  • @snippet 태그: Javadoc에 구문 강조, 외부 파일 참조가 가능한 코드 예제 삽입
  • Internet-Address Resolution SPI: DNS 해석을 커스터마이즈할 수 있는 확장 포인트
  • Finalization 제거 예고: finalize() 대신 Cleanertry-with-resources 사용 권장

JDK 18 자체는 비LTS이므로 프로덕션에서 직접 사용하기보다는, JDK 17 LTS에서 다음 LTS(JDK 21)로 가는 중간 징검다리로 보는 것이 적절합니다. 다만 UTF-8 기본값 변경은 JDK 21로 마이그레이션할 때 반드시 인지해야 할 핵심 변경 사항입니다.

이 글이 도움이 되었나요?