JDK 9 핵심 기능 — 모듈 시스템과 JShell의 등장

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 APIOS 프로세스 정보 표준 API로 조회
try-with-resources 개선effectively final 변수 직접 사용
Private Interface Methods인터페이스 내 공통 로직 재사용
G1 GC 기본값대용량 힙 응답 시간 개선
Compact Strings영문 문자열 메모리 50% 절감

JDK 9의 모듈 시스템은 도입 당시 논란이 많았지만, 라이브러리 개발자에게는 내부 API 보호라는 강력한 도구를 제공합니다. 일반 애플리케이션 개발에서는 모듈 시스템보다 List.of(), try-with-resources 개선, G1 GC 기본값 전환이 실무에 더 즉각적인 영향을 줍니다.

이 글이 도움이 되었나요?