JDK 18 개요
Java 18은 2022년 3월에 출시된 비LTS 릴리스입니다. 큰 신규 기능보다는 오래된 불편함을 해소하는 개선에 집중한 버전입니다. UTF-8 기본 인코딩, 간이 웹 서버, Javadoc 스니펫 태그가 대표적입니다. JDK 17 LTS 직후의 릴리스답게 안정화 성격이 강합니다.
| JEP | 기능 | 상태 |
|---|---|---|
| JEP 400 | UTF-8 기본 인코딩 | 정식 |
| JEP 408 | Simple Web Server | 정식 |
| JEP 413 | Javadoc 코드 스니펫 | 정식 |
| JEP 416 | Method Handle을 사용한 리플렉션 재구현 | 내부 개선 |
| JEP 417 | Vector API (Third Incubator) | 인큐베이터 |
| JEP 418 | Internet-Address Resolution SPI | 정식 |
| JEP 421 | Finalization 제거 예고 | 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()대신Cleaner나try-with-resources사용 권장
JDK 18 자체는 비LTS이므로 프로덕션에서 직접 사용하기보다는, JDK 17 LTS에서 다음 LTS(JDK 21)로 가는 중간 징검다리로 보는 것이 적절합니다. 다만 UTF-8 기본값 변경은 JDK 21로 마이그레이션할 때 반드시 인지해야 할 핵심 변경 사항입니다.