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에 대해 가지고 오겠습니다.
긴 글 읽어주셔서 감사합니다~
'JAVA' 카테고리의 다른 글
| Java 멀티스레딩 입문: Thread, ExecutorService, CompletableFuture (0) | 2026.04.08 |
|---|---|
| Spring Boot 핵심 개념 정리: DI, IoC, AOP를 쉽게 이해해보자 (0) | 2026.04.07 |
| [JAVA] Recode (0) | 2024.04.18 |
| JAVA Collection (0) | 2024.04.18 |
| [SPRING] @Transactional (0) | 2024.04.16 |