자바

[자바] Java 21 버전 특징과 주요 변경 사항

kyjdummy 2025. 5. 9. 21:56

[ 자바 21의 지원 일자 ]

  • 무료 지원 일자 : 2023-09 ~ 2028-09
  • 유료지원 일자 : 2031-09

[ 호환 스프링 부트 ]

  • Spring Boot 호환 : 3.2.x 이상(3.2가 21을 정식 지원)
  • 안정 적인 버전 :  자바 21은 3.2.1이상권장

[ 가비지 컬렉터 ]

  • 자바 21에서는 가비지 컬렉터의 성능이 크게 개선되었습니다. 특히 ZGC(Z Garbage Collector)와 G1(Garbage-First) 컬렉터에 많은 최적화가 적용되었습니다.

[ 특징 및 주요 변경 사항 ]

 

1. 가상 스레드 : 경량 스레드

  • 자바 21에서 정식 추가된 가상 스레드는 기존의 플랫폼 스레드(OS 스레드)와 다른 경량 스레드 구현입니다. 전통적인 플랫폼 스레드(OS 스레드)는 생성·컨텍스트 스위칭 비용이 크므로, 대규모 동시성 서버에서 “NIO + 쓰레드풀” 같은 복잡한 비동기를 강제했습니다. 가상 스레드는 JVM 내부 스케줄러가 수천~수백만 개의 스레드를 섬유처럼 가볍게 관리하도록 구현되어, 블로킹 I/O 코드를 ‘그대로’ 고성능으로 실행할 수 있습니다.
  • 가상 스레드 특징
    • 수백 바이트 수준의 메모리만 사용하여 가볍고, 수백만 개의 가상 스레드 생성 가능하며 기존 스레드 풀과 달리 스레드 생성 비용이 매우 낮아 풀링이 필요 없음
    • 가상 스레드는 Thread 클래스의 인스턴스로, 기존 코드와 호환성 유지
    • JVM에 의해 관리되며 실제 OS 스레드(캐리어 스레드) 위에서 실행되며, 블로킹 작업 시 다른 가상 스레드로 전환됨
    • Executors.newVirtualThreadPerTaskExecutor() 팩토리 메서드로 생성 가능
// 단일 가상 스레드 생성 및 실행
Thread virtualThread = Thread.startVirtualThread(() -> {
    System.out.println("가상 스레드에서 실행 중: " + Thread.currentThread());
    try {
        Thread.sleep(1000); // IO 작업을 시뮬레이션
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

// 가상 스레드의 완료를 기다림
try {
    virtualThread.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}

// ExecutorService를 사용한 가상 스레드 풀 생성
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // 10,000개의 동시 작업 실행
    List<Future<String>> futures = new ArrayList<>();
    
    for (int i = 0; i < 10_000; i++) {
        int taskId = i;
        futures.add(executor.submit(() -> {
            // 각 가상 스레드에서 실행되는 작업
            Thread.sleep(100); // 네트워크 호출 시뮬레이션
            return "Task " + taskId + " 완료";
        }));
    }
    
    // 결과 수집
    for (Future<String> future : futures) {
        System.out.println(future.get());
    }
}

 

 

 


 

2. 구조적 병행성 (Structured Concurrency)

  • 구조적 병행성은 관련된 여러 작업을 하나의 작업 단위로 관리할 수 있게 해주는 병행성 모델입니다. 자바 21에서는 미리 보기 기능으로 제공되고, java.util.concurrent.StructuredTaskScope 클래스를 통해 구현됩니다.
  • 구조적 병행성의 필요성
    • 관련된 여러 작업의 수명주기를 하나의 블록 안에서 관리 가능
    • 에러 처리와 취소가 간단함
    • 리소스 누수 가능성이 적음
    • 코드의 가독성이 올라감
// 구조적 병행성을 사용하기 위한 미리보기 기능 활성화 필요:
// --enable-preview 옵션 사용

// 사용자 정보를 가져오는 메서드
record User(int id, String name) {}
record Order(int id, String details) {}

User fetchUser(int userId) throws Exception {
    Thread.sleep(100); // 네트워크 호출 시뮬레이션
    return new User(userId, "사용자_" + userId);
}

List<Order> fetchOrders(int userId) throws Exception {
    Thread.sleep(150); // 네트워크 호출 시뮬레이션
    return List.of(
        new Order(1, "userId=" + userId + ", 상품=A"),
        new Order(2, "userId=" + userId + ", 상품=B")
    );
}

// 구조적 병행성을 사용한 병렬 데이터 조회
record UserData(User user, List<Order> orders) {}

UserData getUserData(int userId) throws Exception {
// ShutdownOnFailure: 한 작업 실패 시 나머지 모두 취소
// ShutdownOnSuccess: 한 작업 성공 시 나머지 모두 취소
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        // 두 작업을 병렬로 시작
        StructuredTaskScope.Subtask<User> userTask = 
            scope.fork(() -> fetchUser(userId));
        
        StructuredTaskScope.Subtask<List<Order>> ordersTask = 
            scope.fork(() -> fetchOrders(userId));
        
        // 모든 작업이 완료될 때까지 대기
        scope.join();
        
        // 실패 확인 및 예외 전파
        scope.throwIfFailed();
        
        // 결과 조합
        return new UserData(userTask.get(), ordersTask.get());
    }
}

 

 

 


 

3. Sequenced Collection

  • 자바 21에서 도입된 새로운 컬렉션 인터페이스로, 명시적인 순서를 가진 컬렉션을 표현합니다. 이 인터페이스들은 컬렉션의 첫 번째와 마지막 요소에 접근하고, 순방향 역방향 순회를 지원하는 메소드를 제공합니다. 기존 자바 컬렉션 API에서는 순서가 있는 컬렉션들(List, Deque 등)이 있었으나 이들 사이에 일관된 인터페이스가 없었습니다.
  • 주요 인터페이스
    • SequencedCollection: 순서가 있는 컬렉션의 기본 인터페이스
    • SequencedSet: 순서가 있는 Set
    • SequencedMap: 순서가 있는 Map
  • Sequenced Collections는 기존 컬렉션 클래스들(ArrayList, LinkedHashSet, LinkedHashMap 등)의 인터페이스를 확장한 것이므로, 별도의 새 구현을 필요로 하지 않고 기존 코드와 호환됩니다.
// SequencedCollection 사용 예제
List<String> fruits = new ArrayList<>(List.of("사과", "바나나", "오렌지", "포도"));

// 첫 번째와 마지막 요소 접근
String first = fruits.getFirst();  // "사과"
String last = fruits.getLast();    // "포도"

// 역순 컬렉션 얻기
List<String> reversed = fruits.reversed();  // [포도, 오렌지, 바나나, 사과]

// 첫 번째 요소 추가/제거
fruits.addFirst("딸기");    // [딸기, 사과, 바나나, 오렌지, 포도]
String removed = fruits.removeFirst();  // "딸기" 제거, [사과, 바나나, 오렌지, 포도]

// SequencedMap 사용 예제
LinkedHashMap<String, Integer> scores = new LinkedHashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 85);
scores.put("Charlie", 90);

// 첫 번째/마지막 항목 접근
Map.Entry<String, Integer> firstEntry = scores.firstEntry();  // Alice=95
Map.Entry<String, Integer> lastEntry = scores.lastEntry();    // Charlie=90

// 역순 맵 얻기
Map<String, Integer> reversedMap = scores.reversed();  // {Charlie=90, Bob=85, Alice=95}

// 키와 값의 시퀀스 컬렉션 얻기
SequencedSet<String> keys = scores.sequencedKeySet();    // [Alice, Bob, Charlie]
SequencedCollection<Integer> values = scores.sequencedValues();  // [95, 85, 90]

 

 


 

4. 레코드 패턴

  • 레코드 패턴은 자바 21에서 정식 추가된 기능으로, 레코드 타입의 구조를 분해하고 매칭할 수 있게 해주는 기능입니다. 패턴 매칭과 함께 사용하면 데이터 처리 코드를 더 간결하고 안전하게 작성할 수 있습니다.
// 중첩 레코드 정의
record Point(int x, int y) {}
record Rectangle(Point topLeft, Point bottomRight) {}
record Circle(Point center, int radius) {}

// 레코드 패턴과 instanceof 사용
void printShape(Object shape) {
    if (shape instanceof Rectangle(Point(int x1, int y1), Point(int x2, int y2))) {
        int width = x2 - x1;
        int height = y2 - y1;
        System.out.println("사각형: 너비=" + width + ", 높이=" + height);
    } 
    else if (shape instanceof Circle(Point(int x, int y), int r)) {
        System.out.println("원: 중심=(" + x + "," + y + "), 반지름=" + r);
    }
}

// switch 문에서의 레코드 패턴
String describeShape(Object shape) {
    return switch (shape) {
        case Rectangle(Point(var x1, var y1), Point(var x2, var y2)) -> {
            int width = x2 - x1;
            int height = y2 - y1;
            yield "사각형: 너비=" + width + ", 높이=" + height;
        }
        case Circle(Point(var x, var y), var r) -> 
            "원: 중심=(" + x + "," + y + "), 반지름=" + r;
        default -> "알 수 없는 도형";
    };
}

 

 

 


 

5. 패턴 매칭

  • 자바 21에서 패턴 매칭은 정식 추가된 기능으로, switch 문과 표현식에서 패턴 매칭을 사용할 수 있게 해줍니다. 기존 switch 문은 제한적 타입(정소, 열거형, 문자열)만 지원했고, 단순 값 비교만 가능했습니다. 변경 후에는 모든 참조 타입을 switch에서 사용할 수 있고, 타입 확인과 캐스팅이 한 번에 됩니다. 또한, null 값 처리가 자연스러워졌고, 가드 표현식(when)을 통한 조건부 매칭도 가능합니다.
// 객체 타입 패턴 매칭
Object obj = "Hello";
String result = switch (obj) {
    case Integer i -> "정수: " + i;
    case String s -> "문자열: " + s;
    case Double d when d > 0 -> "양의 실수: " + d;
    case Double d -> "음의 실수: " + d;
    case null -> "null 값";
    default -> "기타 타입";
};
System.out.println(result);  // "문자열: Hello"

// 레코드 패턴과 결합
sealed interface Shape permits Circle, Rectangle {}
record Circle(Point center, int radius) implements Shape {}
record Rectangle(Point topLeft, Point bottomRight) implements Shape {}
record Point(int x, int y) {}

double calculateArea(Shape shape) {
    return switch (shape) {
        case Circle(Point(var x, var y), var radius) -> Math.PI * radius * radius;
        case Rectangle(Point(var x1, var y1), Point(var x2, var y2)) -> {
            int width = x2 - x1;
            int height = y2 - y1;
            yield (double) width * height;
        }
    };
}

// null과 when 절 사용
String describe(Object obj) {
    return switch (obj) {
        case String s when s.length() > 5 -> "긴 문자열";
        case String s -> "짧은 문자열";
        case Integer i when i > 0 -> "양수";
        case Integer i when i < 0 -> "음수";
        case Integer i -> "0";
        case null -> "null 값";
        default -> obj.toString();
    };
}

 

 

 


 

6. 문자열 템플릿(String Templates)

  • 문자열 템플릿은 자바 21에서 미리 보기 기능으로 도입된 것으로, 문자열 안에 표현식을 직접 삽일할 수 있게 해주는 기능입니다. 기존에는 문자열 연결 시 (+) 연산자를 통해 연결하고, %s, %d 등을 사용했으나 문자열 템플릿은 이런 문제를 해결해 가독성과 유지 보수성을 높입니다.
// 미리보기 기능 활성화 필요:
// --enable-preview 옵션 사용

// StringTemplate 클래스를 static import
import static java.lang.StringTemplate.STR;

// 기본 사용법
String name = "홍길동";
int age = 30;
String greeting = STR."안녕하세요, \{name}님! 당신은 \{age}세입니다.";
System.out.println(greeting);  // "안녕하세요, 홍길동님! 당신은 30세입니다."

// 표현식 사용
int x = 10, y = 20;
String calculation = STR."\{x} + \{y} = \{x + y}";
System.out.println(calculation);  // "10 + 20 = 30"

// 멀티라인 템플릿
String html = STR."""
    <html>
        <body>
            <h1>환영합니다, \{name}님!</h1>
            <p>현재 나이: \{age}</p>
            <p>5년 후 나이: \{age + 5}</p>
        </body>
    </html>
    """;

// 커스텀 포맷터 사용
record Person(String name, int age) {}
List<Person> people = List.of(
    new Person("홍길동", 30),
    new Person("김철수", 25),
    new Person("이영희", 35)
);

// JSON 포맷터 구현 예시
StringTemplate.Processor<String> JSON = template -> {
    StringBuilder sb = new StringBuilder();
    sb.append("{");
    boolean first = true;
    for (StringTemplate.Fragment fragment : template.fragments()) {
        if (!first) sb.append(",");
        first = false;
        
        String key = fragment.value().trim();
        if (!key.isEmpty()) {
            sb.append("\"").append(key).append("\":");
        }
    }
    sb.append("}");
    return sb.toString();
};

// 커스텀 포맷터 사용
String json = JSON."name:\{person.name()}age:\{person.age()}";

 

 

 


 

7. 키-값 스토어 API 기능

  • 자바 21에서 미리 보기 기능으로 도입된 것으로, 간단한 키-값 데이터를 효율적으로 저장하고 관리할 수 있는 API입니다. 기존에는 자바에서 키-값 데이터를 영구적으로 저장하기 위해 외부 데이터베이스나 서드파티 라이브러리를 사용해야 했습니다. 이제는 표준화된 인터페이스 제공으로 인해, 간단한 키-값 데이터를 위한 내장 솔루션을 제공합니다. 이 API는 프리뷰 기능이므로, 실제 사용 시에는--enable-preview 옵션을 지정해야 합니다.
// 프리뷰 기능 활성화 필요:
// --enable-preview 옵션 사용

import java.lang.runtime.KeyValueStore;
import java.lang.runtime.KeyValueStores;

// 키-값 스토어 생성
Path storePath = Path.of("/path/to/store");
try (KeyValueStore store = KeyValueStores.newKeyValueStore(storePath)) {
    // 값 저장
    store.put("user.1.name", "홍길동");
    store.put("user.1.email", "hong@example.com");
    store.put("user.1.age", "30");
    
    store.put("user.2.name", "김철수");
    store.put("user.2.email", "kim@example.com");
    store.put("user.2.age", "25");
    
    // 값 조회
    String name = store.get("user.1.name");
    System.out.println("사용자 이름: " + name);  // "홍길동"
    
    // 키 존재 여부 확인
    boolean exists = store.containsKey("user.1.phone");
    System.out.println("전화번호 존재: " + exists);  // false
    
    // 특정 패턴의 모든 키 조회
    List<String> userKeys = store.keys("user.1.*");
    System.out.println("사용자1 키: " + userKeys);  // [user.1.name, user.1.email, user.1.age]
    
    // 값 삭제
    store.remove("user.2.email");
    
    // 트랜잭션 사용
    store.transaction(() -> {
        store.put("counter", "1");
        String counter = store.get("counter");
        store.put("counter", String.valueOf(Integer.parseInt(counter) + 1));
        
        // 트랜잭션 내에서 예외 발생 시 롤백
        if (someCondition()) {
            throw new RuntimeException("트랜잭션 롤백");
        }
    });
}