JDK 9의 위치
2017년 출시된 JDK 9는 Java 플랫폼의 구조적 재설계를 단행한 릴리스입니다. 가장 큰 변화는 모듈 시스템(Project Jigsaw)의 도입이며, 이로 인해 JDK 자체가 약 90개의 모듈로 분리되었습니다. 동시에 JShell, 컬렉션 팩토리 메서드 등 개발 생산성을 높이는 기능도 함께 추가되었습니다.
모듈 시스템 (Project Jigsaw)
모듈 시스템은 패키지보다 상위 레벨의 캡슐화 단위입니다. 라이브러리를 빌딩 블록에 비유하면, 기존에는 모든 블록이 한 상자에 섞여 있었다면 모듈은 각 블록 세트를 별도 상자에 넣고 “이 상자는 저 상자가 필요하다”는 의존 관계를 명시하는 것입니다.
module-info.java 파일을 프로젝트 루트에 배치하여 모듈을 정의합니다.
// module-info.java — 모듈 선언 파일
// 이 모듈은 com.myapp이라는 이름으로 정의됨
module com.myapp {
// java.net.http 모듈을 사용하겠다고 선언
requires java.net.http;
// java.sql 모듈을 사용하겠다고 선언
requires java.sql;
// com.myapp.api 패키지를 외부에 공개
exports com.myapp.api;
// com.myapp.internal 패키지는 공개하지 않음 → 외부 접근 불가
}
모듈 시스템의 핵심 이점은 세 가지입니다. 첫째, exports하지 않은 패키지는 다른 모듈에서 접근할 수 없어 강한 캡슐화가 보장됩니다. 둘째, requires로 의존 관계를 명시하므로 순환 의존이 컴파일 타임에 검출됩니다. 셋째, jlink 도구로 필요한 모듈만 포함한 커스텀 런타임 이미지를 만들 수 있어 배포 크기가 대폭 줄어듭니다.
JShell — Java REPL
JDK 9 이전에는 한 줄의 코드를 테스트하려면 반드시 클래스를 만들고, main() 메서드를 작성하고, 컴파일하고, 실행해야 했습니다. JShell은 이 과정을 없애줍니다.
터미널에서 jshell을 실행하면 즉시 Java 코드를 입력하고 결과를 확인할 수 있습니다.
$ jshell
| Welcome to JShell -- Version 9
jshell> var list = List.of("Java", "Kotlin", "Scala")
list ==> [Java, Kotlin, Scala]
jshell> list.stream().filter(s -> s.length() > 4).toList()
$2 ==> [Kotlin, Scala]
jshell> /exit
API 동작을 빠르게 검증하거나, 알고리즘 로직을 프로토타이핑할 때 매우 유용합니다.
컬렉션 팩토리 메서드
JDK 8까지 불변 리스트를 만들려면 Collections.unmodifiableList(Arrays.asList(...))처럼 장황한 코드가 필요했습니다. JDK 9에서는 List.of(), Set.of(), Map.of()로 한 줄이면 충분합니다.
import java.util.List;
import java.util.Set;
import java.util.Map;
public class CollectionFactory {
public static void main(String[] args) {
// List.of() — 불변 리스트 생성
List<String> colors = List.of("빨강", "초록", "파랑");
System.out.println("색상: " + colors);
// 출력: 색상: [빨강, 초록, 파랑]
// Set.of() — 불변 셋 생성 (중복 시 IllegalArgumentException)
Set<Integer> primes = Set.of(2, 3, 5, 7, 11);
System.out.println("소수: " + primes);
// 출력: 소수: [2, 3, 5, 7, 11] (순서 보장 안 됨)
// Map.of() — 불변 맵 생성 (최대 10개 키-값 쌍)
Map<String, Integer> scores = Map.of(
"국어", 95,
"수학", 88,
"영어", 92
);
System.out.println("점수: " + scores);
// 출력: 점수: {국어=95, 수학=88, 영어=92} (순서 보장 안 됨)
// Map.ofEntries() — 10개 초과 시 사용
Map<String, String> config = Map.ofEntries(
Map.entry("host", "localhost"),
Map.entry("port", "8080"),
Map.entry("protocol", "https")
);
System.out.println("설정: " + config);
// 출력: 설정: {host=localhost, port=8080, protocol=https}
// 불변이므로 수정 시도하면 UnsupportedOperationException 발생
try {
colors.add("노랑");
} catch (UnsupportedOperationException e) {
System.out.println("불변 리스트는 수정 불가!");
// 출력: 불변 리스트는 수정 불가!
}
}
}
List.of()로 생성된 컬렉션은 null 요소를 허용하지 않습니다. null을 넣으면 NullPointerException이 즉시 발생하므로, null 관련 버그를 빠르게 발견할 수 있습니다.
Process API 개선
기존에는 OS 프로세스 정보를 얻으려면 네이티브 코드나 외부 라이브러리가 필요했습니다. JDK 9의 ProcessHandle API로 현재 프로세스와 자식 프로세스 정보를 쉽게 조회할 수 있습니다.
import java.time.Duration;
import java.time.Instant;
public class ProcessApiExample {
public static void main(String[] args) {
// 현재 프로세스 정보 조회
ProcessHandle current = ProcessHandle.current();
System.out.println("PID: " + current.pid());
// 출력: PID: 12345
current.info().command()
.ifPresent(cmd -> System.out.println("명령어: " + cmd));
// 출력: 명령어: /usr/bin/java
current.info().startInstant()
.ifPresent(start -> {
Duration uptime = Duration.between(start, Instant.now());
System.out.println("실행 시간: " + uptime.toMillis() + "ms");
});
// 출력: 실행 시간: 128ms
// 전체 프로세스 수 조회
long processCount = ProcessHandle.allProcesses().count();
System.out.println("실행 중인 프로세스 수: " + processCount);
// 출력: 실행 중인 프로세스 수: 287
}
}
try-with-resources 개선과 Private Interface Methods
JDK 9에서는 두 가지 작지만 유용한 문법 개선이 있었습니다.
import java.io.BufferedReader;
import java.io.StringReader;
public class SyntaxImprovements {
// Private Interface Method (JDK 9)
// 인터페이스의 default 메서드 간 공통 로직을 추출 가능
interface Logger {
default void logInfo(String msg) {
log("INFO", msg);
}
default void logError(String msg) {
log("ERROR", msg);
}
// private 메서드로 공통 로직 추출 — 외부에 노출되지 않음
private void log(String level, String msg) {
System.out.println("[" + level + "] " + msg);
}
}
public static void main(String[] args) throws Exception {
// try-with-resources 개선: effectively final 변수 직접 사용
BufferedReader reader = new BufferedReader(new StringReader("Hello JDK 9"));
// JDK 8: try (BufferedReader r = reader) { ... } — 새 변수 필요
// JDK 9: 기존 변수를 직접 사용 가능
try (reader) {
System.out.println("읽은 내용: " + reader.readLine());
// 출력: 읽은 내용: Hello JDK 9
}
// Private Interface Method 사용
Logger logger = new Logger() {};
logger.logInfo("서버 시작됨");
logger.logError("연결 실패");
// 출력:
// [INFO] 서버 시작됨
// [ERROR] 연결 실패
}
}
성능 개선
G1 GC 기본값 전환: JDK 9부터 G1(Garbage-First) GC가 기본 가비지 컬렉터가 되었습니다. 기존 Parallel GC 대비 Stop-the-World 시간이 짧아 대용량 힙에서 응답 시간이 개선됩니다.
Compact Strings: JDK 9 이전에는 String 내부가 char[](2바이트/문자)로 저장되었습니다. JDK 9부터 Latin-1 문자만 포함된 문자열은 byte[](1바이트/문자)로 저장됩니다. 영문 위주 애플리케이션에서 메모리 사용량이 크게 줄어듭니다. 한국어 등 멀티바이트 문자가 포함된 경우에는 기존과 동일하게 UTF-16으로 저장됩니다.
정리
| 기능 | 핵심 가치 |
|---|---|
| 모듈 시스템 | 강한 캡슐화, 의존 관계 명시, 경량 런타임 |
| JShell | 빠른 코드 실험, 프로토타이핑 |
| 컬렉션 팩토리 | List.of() 한 줄로 불변 컬렉션 생성 |
| Process API | OS 프로세스 정보 표준 API로 조회 |
| try-with-resources 개선 | effectively final 변수 직접 사용 |
| Private Interface Methods | 인터페이스 내 공통 로직 재사용 |
| G1 GC 기본값 | 대용량 힙 응답 시간 개선 |
| Compact Strings | 영문 문자열 메모리 50% 절감 |
JDK 9의 모듈 시스템은 도입 당시 논란이 많았지만, 라이브러리 개발자에게는 내부 API 보호라는 강력한 도구를 제공합니다. 일반 애플리케이션 개발에서는 모듈 시스템보다 List.of(), try-with-resources 개선, G1 GC 기본값 전환이 실무에 더 즉각적인 영향을 줍니다.