JDK 10 핵심 기능 — var 키워드와 G1 GC 병렬 처리

JDK 10과 새로운 릴리스 주기

2018년 3월 출시된 JDK 10은 Java 역사상 첫 번째 6개월 주기 릴리스입니다. 이전까지 Java는 2~3년마다 대규모 업데이트를 했지만, JDK 10부터는 6개월마다 정기 릴리스를 진행합니다. 대신 모든 릴리스가 LTS(Long-Term Support)는 아니며, JDK 10은 non-LTS 버전입니다.

이 릴리스 모델의 핵심은 “작은 기능을 빠르게 배포”하는 것입니다. 큰 기능이 준비될 때까지 전체 릴리스를 지연시키는 대신, 준비된 기능만 6개월 단위로 출시합니다.

릴리스 모델주기지원 기간
LTS (11, 17, 21)2년마다최소 8년
non-LTS (10, 12, 13…)6개월마다다음 릴리스까지 (6개월)

var — 지역 변수 타입 추론

JDK 10의 가장 눈에 띄는 변화는 var 키워드(JEP 286)입니다. 지역 변수 선언 시 컴파일러가 우변의 타입을 추론하므로, 반복적인 타입 선언을 줄일 수 있습니다.

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class VarExample {
    public static void main(String[] args) {
        // 기존 방식: 타입을 두 번 명시
        HashMap<String, List<Integer>> oldMap = new HashMap<String, List<Integer>>();

        // var 사용: 컴파일러가 우변에서 타입 추론
        var scores = new HashMap<String, List<Integer>>();
        scores.put("수학", List.of(95, 88, 92));
        scores.put("영어", List.of(85, 90, 78));

        // 복잡한 제네릭 타입에서 var가 빛남
        var averages = scores.entrySet().stream()
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> e.getValue().stream()
                    .mapToInt(Integer::intValue)
                    .average()
                    .orElse(0.0)
            ));
        System.out.println("과목별 평균: " + averages);
        // 출력: 과목별 평균: {수학=91.66666666666667, 영어=84.33333333333333}

        // for 루프에서도 사용 가능
        for (var entry : scores.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        // 출력:
        // 수학: [95, 88, 92]
        // 영어: [85, 90, 78]

        // try-with-resources에서도 사용 가능
        var list = new ArrayList<String>();
        list.add("Java");
        list.add("Kotlin");
        System.out.println("언어: " + list);
        // 출력: 언어: [Java, Kotlin]
    }
}

var 사용 가이드라인

var는 편리하지만 남용하면 가독성을 해칩니다. Oracle이 공식으로 제시한 스타일 가이드를 기반으로 정리합니다.

사용하기 좋은 경우:

  • 우변에서 타입이 명확한 경우: var list = new ArrayList<String>()
  • 제네릭이 복잡한 경우: var map = new HashMap<String, List<Map<String, Integer>>>()
  • for-each, try-with-resources 변수

피해야 하는 경우:

  • 우변만으로 타입을 알 수 없는 경우: var result = getResult() → 무슨 타입인지 불분명
  • 기본형 리터럴과 함께: var count = 0 → int인지 long인지 모호
public class VarGuideline {
    public static void main(String[] args) {
        // 좋은 예: 우변에서 타입이 명확
        var names = new ArrayList<String>();
        var count = names.size();  // int 반환이 명확

        // 나쁜 예: 타입이 불분명 (이런 경우 명시적 타입 선언 권장)
        // var data = fetchData();  // 무슨 타입? List? Map? String?
        // var x = 0;               // int? long? short?

        // var는 지역 변수에서만 사용 가능
        // 필드, 메서드 파라미터, 반환 타입에는 사용 불가
        System.out.println("var는 지역 변수 전용 키워드입니다.");
        // 출력: var는 지역 변수 전용 키워드입니다.
    }
}

var는 예약어(reserved word)가 아니라 예약된 타입 이름(reserved type name) 입니다. 따라서 var라는 이름의 변수는 선언할 수 없지만, var라는 이름의 메서드나 패키지는 가능합니다.

Optional.orElseThrow()

JDK 8의 Optional.get()은 값이 없으면 NoSuchElementException을 던지지만, 메서드 이름만 보면 예외가 발생할 것 같지 않아 혼란을 줍니다. JDK 10에서 추가된 orElseThrow()는 동일한 동작이지만 이름이 의도를 명확히 드러냅니다.

import java.util.Optional;

public class OrElseThrowExample {
    public static void main(String[] args) {
        Optional<String> present = Optional.of("Hello");
        Optional<String> empty = Optional.empty();

        // JDK 8: get() — 이름이 모호함
        String value1 = present.get();
        System.out.println("get(): " + value1);
        // 출력: get(): Hello

        // JDK 10: orElseThrow() — 의도가 명확함
        String value2 = present.orElseThrow();
        System.out.println("orElseThrow(): " + value2);
        // 출력: orElseThrow(): Hello

        // 빈 Optional에서 호출하면 NoSuchElementException 발생
        try {
            empty.orElseThrow();
        } catch (java.util.NoSuchElementException e) {
            System.out.println("예외 발생: " + e.getMessage());
            // 출력: 예외 발생: No value present
        }
    }
}

Unmodifiable Collections

JDK 10에서는 기존 컬렉션을 불변으로 복사하는 copyOf()와, 스트림 결과를 불변 컬렉션으로 수집하는 toUnmodifiable* 컬렉터가 추가되었습니다.

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class UnmodifiableCollections {
    public static void main(String[] args) {
        // List.copyOf() — 기존 리스트의 불변 복사본 생성
        var mutable = new ArrayList<>(List.of("A", "B", "C"));
        var immutable = List.copyOf(mutable);

        mutable.add("D");  // 원본은 수정 가능
        System.out.println("원본: " + mutable);
        // 출력: 원본: [A, B, C, D]
        System.out.println("복사본: " + immutable);
        // 출력: 복사본: [A, B, C] — 원본 변경에 영향 없음

        try {
            immutable.add("E");
        } catch (UnsupportedOperationException e) {
            System.out.println("불변 복사본은 수정 불가!");
            // 출력: 불변 복사본은 수정 불가!
        }

        // Collectors.toUnmodifiableList() — 스트림 결과를 불변 리스트로 수집
        var numbers = Stream.of(3, 1, 4, 1, 5, 9)
            .sorted()
            .collect(Collectors.toUnmodifiableList());
        System.out.println("정렬된 불변 리스트: " + numbers);
        // 출력: 정렬된 불변 리스트: [1, 1, 3, 4, 5, 9]

        // Collectors.toUnmodifiableMap()
        var lengthMap = List.of("Java", "Go", "Rust").stream()
            .collect(Collectors.toUnmodifiableMap(
                s -> s,
                String::length
            ));
        System.out.println("문자열 길이 맵: " + lengthMap);
        // 출력: 문자열 길이 맵: {Java=4, Go=2, Rust=4}
    }
}

List.copyOf()는 원본이 이미 불변 리스트인 경우 복사하지 않고 원본을 그대로 반환합니다. 불필요한 메모리 할당을 피하는 최적화입니다.

G1 GC Parallel Full GC

JDK 9에서 기본 GC가 된 G1은 대부분의 경우 Full GC를 피하도록 설계되었습니다. 그러나 혼합(Mixed) GC가 메모리 회수 속도를 따라가지 못하면 Full GC가 발생합니다. JDK 9까지 G1의 Full GC는 싱글 스레드로 동작하여 심각한 지연을 초래했습니다.

JDK 10(JEP 307)에서 G1의 Full GC가 병렬(Parallel) 로 전환되었습니다. -XX:ParallelGCThreads 옵션으로 스레드 수를 조절할 수 있으며, Full GC 발생 시 Stop-the-World 시간이 크게 단축됩니다.

Application Class-Data Sharing

CDS(Class-Data Sharing)는 JVM 시작 시 로딩하는 클래스 메타데이터를 아카이브 파일로 저장해두고, 다음 시작 시 재사용하여 기동 시간을 단축하는 기술입니다. JDK 10(JEP 310)에서는 이 기능이 애플리케이션 클래스까지 확장되었습니다.

# 1단계: 클래스 리스트 생성
java -Xshare:off -XX:DumpLoadedClassList=classes.lst -jar myapp.jar

# 2단계: 아카이브 생성
java -Xshare:dump -XX:SharedClassListFile=classes.lst -XX:SharedArchiveFile=app-cds.jsa -jar myapp.jar

# 3단계: 아카이브 사용하여 빠른 기동
java -Xshare:on -XX:SharedArchiveFile=app-cds.jsa -jar myapp.jar

마이크로서비스처럼 같은 애플리케이션 인스턴스를 여러 개 띄우는 환경에서 메모리 절약과 기동 시간 단축 효과가 큽니다.

정리

기능핵심 가치
var장황한 타입 선언 제거, 제네릭 코드 가독성 향상
G1 Parallel Full GCFull GC 발생 시 Stop-the-World 시간 단축
Application CDSJVM 기동 시간 단축, 메모리 절약
Optional.orElseThrow()get()보다 의도가 명확한 API
Unmodifiable Collections방어적 복사와 불변 수집을 한 줄로 처리
6개월 릴리스 주기작은 기능을 빠르게, LTS는 안정적으로

JDK 10은 non-LTS 릴리스이지만, var와 Unmodifiable Collections는 이후 모든 Java 버전에서 일상적으로 사용되는 기능입니다. 특히 var는 “타입을 두 번 쓰지 않는다”는 간단한 원칙만 기억하면 됩니다. 다만 우변에서 타입이 명확하지 않다면 명시적 타입 선언을 유지하는 것이 팀 전체의 가독성을 위한 현명한 선택입니다.

이 글이 도움이 되었나요?