본문 바로가기
JAVA

JAVA 17 vs 21 vs 25

by 요료료룡 2026. 3. 4.

Java 17, 21, 25는 모두 장기지원 LTS(Long-Term Support)이다.

각각에 대한 특징을 알아보고 싶어 작성하게 되었다.

 

Java 17

Java17은 안정성이 특징이다. Record와 상속 제한으로 불변 데이터 보장 및 무분별한 상속 차단하여 버그 발생을 최소화한다.

  • Record 클래스 : DTO나 VO 만들 때 Getter, 생성자, toString(), equals(), hasCode()를 직접 만들지 않는다.

before

public class UserDTO {
    private final String name;
    private final int age;

    public UserDTO(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public boolean equals(Object o) { ... }
    @Override
    public int hashCode() { ... }
    @Override
    public String toString() 
{ return "UserDTO{name='" + name + "', age=" + age + "}"; }
}

after

public record UserDTO(String name, int age) {}
  • Sealed Classes (상속 제한) : 아무나 상속받지 못하도록 설계자가 허용한 클래스만 상속을 허용한다.

before

public abstract class Payment {} 

public class Card extends Payment {}
public class Cash extends Payment {}
public class Coin extends Payment {}
-> 누가 실수로 다른 걸 추가해도 막을 방법이 없는 문제

after

public sealed abstract class Payment permits Card, Cash {}

public final class Card extends Payment {}
public final class Cash extends Payment {}

// public class Coin extends Payment {} // Error
> 상속 받지 못함

sealed: 상속 제한
permits: 상속 허용 권한
  • Switch & Text Blocks Switch : case -> 문법을 도입하여 가독성을 높이고 값을 직접 반환할 수 있다.

before

public class SwitchBefore {
    public void printDayInfo(String day) {
        int status;
        switch (day) {
            case "MON":
            case "TUE":
                status = 1;
                break; 
            case "WED":
                status = 2;
                break;
            default:
                throw new IllegalArgumentException("Unknown day");
        }
        System.out.println(status);
    }
}

after

public class SwitchAfter {
    public void printDayInfo(String day) {
         int status = switch (day) {
            case "MON", "TUE" -> 1; 
            case "WED" -> 2;
            default -> throw new IllegalArgumentException("Unknown day");
        };
        System.out.println(status);
    }
}

Java 21

Java21은 동시성이 특징이다. 가상 스레드로 기존 동기식 코드 유지하고, 대규모 트래픽 처리한다.

  • 가상 스레드(virtual threads) : JVM이 직접 관리하는 초경량 스레드이다.

before

ExecutorService pool = Executors.newFixedThreadPool(100);
for (int i = 0; i < 10_000; i++) {
    pool.submit(() -> {
        Thread.sleep(1000); // 1초 대기하는 동안 귀중한 스레드 1개 낭비
        return "완료";
    });
}

after

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        executor.submit(() -> {
            Thread.sleep(1000); // 대기할 때 OS 스레드를 반납하므로 자원 낭비 0%
            return "완료";
        });
    }
}
  • 시퀀스 컬렉션(Sequence Collections) : 순서를 보장하는 SequencedCollection 인터페이스 도입하였다.

before

List<String> list = new ArrayList<>(List.of("A", "B", "C"));
String lastList = list.get(list.size() - 1); // 사이즈를 구해서 1을 빼야 함

SortedSet<String> set = new TreeSet<>(List.of("A", "B", "C"));
String lastSet = set.last(); // 여기선 또 메서드 이름이 다름

after

List<String> list = new ArrayList<>(List.of("A", "B", "C"));
String lastList = list.getLast(); // 깔끔하고 직관적임

SortedSet<String> set = new TreeSet<>(List.of("A", "B", "C"));
String lastSet = set.getLast(); // 어떤 컬렉션이든 getLast()로 통일!
  • 레코드 & Switch 패턴 매칭 : Switch 문에서 타입 검사와 구조 분해를 동시에 수행한다.

before

public String process(Object obj) {
    if (obj instanceof User) {
        User u = (User) obj; // 명시적 캐스팅 필수
        if (u.getAge() >= 20) {
            return "성인: " + u.getName(); // 일일이 getter로 꺼내야 함
        }
    }
    return "기타";
}

after

public String process(Object obj) {
    return switch (obj) {
        // 객체가 User면 포장을 뜯어 name, age 변수에 담고, age가 20 이상인지 검사
        case User(String name, int age) when age >= 20 
            -> "성인: " + name; 
        default -> "기타";
    };
}

Java 25

Java25은 생산성(개발 및 운영 효율 극대화)이 특징이다. 불필요한 문법을 제거하고, 메모리 구조를 압축하여 서버 메모리를 최적화한다.

즉, 타이핑해야 하는 불필요한 코드를 줄이고, 안전성을 높여 생산성을 극대화하는 것이다.

  • 생성자 유연화 : Super() 호출 전에 로직 넣을 수 있다.

before

// 부모 생성자(super)를 무조건 첫 줄에 써야 함
public class PremiumAccount extends Account {
    public PremiumAccount(String id) {
        super(id); // 일단 부모부터 초기화해야 함
        
        // 문제: id가 null이면 이미 부모 객체는 헛수고로 생성된 후 예외 발생
        if (id == null) throw new IllegalArgumentException("ID 필수"); 
    }
}

after

// super() 호출 전에 데이터 검증 및 가공 가능
public class PremiumAccount extends Account {
    public PremiumAccount(String id) {
        // 부모를 초기화하기 전에 안전하게 검증(Validation)부터 수행!
        if (id == null) throw new IllegalArgumentException("ID 필수");
        
        String cleanId = id.trim().toUpperCase(); // 데이터 전처리
        super(cleanId); // 모든 준비가 끝난 후 깔끔하게 부모 생성자 호출
    }
}
  • 클래스 없는 메인

before

// 단 한 줄을 출력하기 위한 거대한 껍데기
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

after

// 클래스 선언도, public static도 필요 없음
// 파일명: HelloWorld.java
void main() {
    System.out.println("Hello, World!");
}
  • 모듈 임포트 핵심 모듈로 모든 기본 패키지 사용 가능하다.

before

// 파일 상단을 가득 채우는 import 문들
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.io.File;
import java.nio.file.Files;
// ... 

after

// 핵심 모듈 하나면 모든 기본 패키지 준비 끝
import module java.base;

void main() {
    // List, Map, File, Files, Stream 등 java.base 안의 모든 클래스 즉시 사용 가능
    List<String> list = List.of("Java", "25"); 
}
  • 안전한 데이터 공유

before

// ThreadLocal의 메모리 누수 위험과 불변성 파괴
public static final ThreadLocal<String> USER = new ThreadLocal<>();

USER.set("admin");
processOrder(); 
// 치명적 단점: 명시적으로 USER.remove()를 안 하면 
// 가상 스레드 환경에서 메모리가 터짐!

after

// 블록을 벗어나면 자동 소멸되는 불변의 ScopedValue
public static final ScopedValue<String> USER = ScopedValue.newInstance();

// ScopedValue.where() 로 값을 세팅하고 run() 블록 안에서만 안전하게 사용
ScopedValue.where(USER, "admin").run(() -> {
    processOrder(); // 이 메서드 내부 어디서든 USER.get()으로 "admin"을 꺼낼 수 있음
}); 
// 블록이 끝나는 순간 "admin" 데이터는 메모리에서 완벽히, 자동으로 소멸됨 (remove 불필요)

 

위에서 각각의 버전별 특징을 알아보았는데요.

17 → 21으로 변경했을 때 특징과 21 → 25로 변경했을 때의 특징을 알아보겠습니다.

 

17 -> 21

  • 가상 스레드로 대규모 동시성 처리가 필요한 애플리케이션에서 스레드 자원 낭비 ↓ 성능 ↑
  • 인코딩 방식 변경 (OS와 상관없이 자바의 표준 기본 인코딩이 무조건 UTF-8로 강제 고정)

21 -> 25

  • 객체 헤더 압축(헤더의 크기 8바이트로 압축 > 서버의 전체적인 처리량 ↑)

이렇게 자바의 LTS버전으로 알아보았는데요, 다음번엔 더 자세하게 JAVA25에 대해 가지고 오겠습니다.

긴 글 읽어주셔서 감사합니다~