자바는 조용히 계속 좋아지고 있다
6개월 주기 릴리스 덕분에 자바는 패턴 매칭, 가상 스레드, 레코드로 완전히 달라졌습니다.

자바는 프로그래밍 언어계의 로드니 데인저필드입니다. 존중을 받지 못하죠. 30대 미만 개발자 대부분에게 자바를 말하면 XML 설정 파일, 엔터프라이즈 빈, 그리고 AbstractSingletonProxyFactoryBean 같은 이름이 떠오를 겁니다. 자바의 평판은 Java 6이 최신 버전이던 2008년에 멈춰 있습니다. 그 시절 자바 코드는 보일러플레이트에 파묻혀 쓰는 게 고역이었죠.
그런데 자바가 2017년 6개월 릴리스 주기를 도입한 이후 놀라운 일이 벌어졌습니다. Java 6, 7, 8 사이에 4년씩 걸리던 시절에는 상상도 못 하던 속도로 언어가 발전하고 있어요. 레코드, 봉인된 클래스, 패턴 매칭, 가상 스레드, 문자열 템플릿, 구조적 동시성까지. 릴리스마다 자바 코드를 더 짧고 표현력 있게, 그리고 더 즐겁게 작성할 수 있는 기능이 추가됩니다. Java 26도 이 흐름을 이어가고 있고, ‘지금의 자바’와 ‘사람들이 생각하는 자바’의 간극은 그 어느 때보다 벌어졌습니다.
판도를 바꾼 기능들
Java 8 이후로 자바를 써보지 않았다면 십 년치 개선을 놓친 셈입니다. 지금 자바가 어떤 모습인지 살펴보죠.
레코드(Java 14)는 불변 데이터 클래스입니다. 필드, 생성자, 게터, equals, hashCode, toString 등 50줄짜리 보일러플레이트가 필요했던 코드가 이제 한 줄이면 됩니다.
// Java 8: 50 lines of boilerplate
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() { return Objects.hash(x, y); }
@Override
public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}
// Java 14+: one line
record Point(int x, int y) {}
패턴 매칭(Java 16~21, 아직 발전 중)은 switch 표현식과 instanceof 검사에서 객체를 분해(디스트럭처링)할 수 있게 해 줍니다. 자바 코드를 괴롭히던 if-else-instanceof 연쇄 구조를 없애 줍니다.
// Pattern matching with sealed types and records
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
// Exhaustive pattern matching — compiler verifies all cases covered
double area(Shape shape) {
return switch (shape) {
case Circle(var r) -> Math.PI * r * r;
case Rectangle(var w, var h) -> w * h;
case Triangle(var b, var h) -> 0.5 * b * h;
};
}
가상 스레드(Java 21)는 OS가 아닌 JVM이 관리하는 경량 스레드입니다. OS 스레드나 메모리가 바닥나지 않고 수백만 개를 만들 수 있어요. 덕분에 가장 단순한 동시성 모델인 ‘요청당 스레드 하나’ 방식을 대규모에서도 현실적으로 쓸 수 있게 됐습니다.
// Create a million concurrent tasks — try this with OS threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1_000_000).forEach(i -> {
executor.submit(() -> {
// Each task gets its own virtual thread
// Blocking I/O (HTTP, DB, file) automatically yields
var response = httpClient.send(request, BodyHandlers.ofString());
return response.body();
});
});
}
// All done — executor auto-closes, waits for completion
가상 스레드는 Go의 고루틴, 코틀린의 코루틴에 대한 자바의 답입니다. 차이점은 특별한 문법이 필요 없고 함수에 색을 입힐 필요도 없다는 것입니다. 기존의 블로킹 코드가 그대로 동작하고, JVM이 블로킹 지점에서 가상 스레드를 알아서 멈췄다 다시 재개합니다. 기존 블로킹 HTTP 클라이언트, JDBC 드라이버, 파일 I/O 코드를 한 줄도 고치지 않고 그 혜택을 봅니다.
왜 아무도 눈치채지 못할까
자바의 개선 흐름은 정말 인상적입니다. 그런데 왜 인식은 아직 2008년에 머물러 있을까요?
첫째, 엔터프라이즈의 관성입니다. 많은 조직이 대규모 코드베이스를 업그레이드하는 게 비싸고 위험하다는 이유로 Java 8이나 11을 계속 씁니다. 개발자들이 ‘자바’를 이야기할 때는 최신 릴리스가 아니라 회사가 쓰는 버전을 말하는 셈이죠. Java 8 개발자는 레코드, 패턴 매칭, 가상 스레드를 아직 경험해 본 적이 없습니다.
둘째, 프레임워크 계층입니다. Spring Boot, Jakarta EE 같은 엔터프라이즈 프레임워크가 언어 위에 자체적인 복잡성을 얹습니다. 개발자들은 Spring의 어노테이션 의식을 두고 ‘자바’를 탓하지만, 그건 언어가 아니라 프레임워크의 문제입니다. 무거운 프레임워크 없이 쓰는 현대 자바는 놀라울 만큼 가볍습니다.
셋째, 문화적 관성입니다. 기술 문화는 언어마다 진영을 나눕니다. 자바는 대기업, 관료적인 프로세스, 과도한 설계와 연관된 ‘엔터프라이즈’ 언어로 분류됩니다. 이 평판은 그럴 만한 이유가 있었습니다. 2005년 무렵의 자바 개발은 그런 비판을 받아 마땅했죠. 하지만 평판은 원인이 사라진 뒤에도 수년간 살아남습니다.
JVM이라는 강점
언어의 개선도 중요하지만, 자바의 가장 큰 강점은 여전히 JVM입니다. 30년 가까운 최적화 끝에 HotSpot JVM은 지금까지 만들어진 가장 정교한 런타임 환경 중 하나가 됐습니다.
- JIT 컴파일. JVM은 실행 중인 코드를 프로파일링하고, 자주 호출되는 메서드를 최적화된 네이티브 코드로 컴파일합니다. 서버나 데이터 처리처럼 오래 도는 애플리케이션에서는 JIT가 정적 분석이 아닌 실제 실행 패턴에 맞춰 최적화할 수 있어서, JVM 성능이 C++과 대등하거나 오히려 앞서는 경우가 많습니다.
- 가비지 컬렉션. ZGC, Shenandoah 같은 최신 GC 구현체는 테라바이트급 힙에서도 밀리초 미만의 일시 정지 시간을 보장합니다. ‘GC 정지가 지연 시간을 망친다’는 비판은 이미 몇 년 전부터 사실이 아닙니다. 이 컬렉터들은 애플리케이션과 동시에 실행되거든요.
- 관측 가능성. JFR(Java Flight Recorder)은 오버헤드가 거의 없어 운영 환경에서도 안전하게 프로파일링할 수 있습니다. 애플리케이션을 느리게 만들지 않고 CPU 사용량, 메모리 할당, 락 경합, I/O 지연 시간, GC 동작을 모두 들여다볼 수 있어요.
- 생태계의 성숙도. Maven Central에는 수백만 개의 패키지가 있습니다. 빌드 도구(Gradle, Maven)는 잘 알려져 있고, 테스팅 생태계(JUnit, Mockito, Testcontainers)도 훌륭합니다. 배포 환경(컨테이너, GraalVM 네이티브 이미지)도 눈에 띄게 성숙했습니다.
GraalVM과 네이티브 이미지
자바의 가장 큰 약점은 늘 시작 시간이었습니다. JVM은 초기화하고, 클래스를 로드하고, JIT 컴파일러를 예열하는 데 수백 밀리초가 걸리죠. 서버리스 함수나 CLI 도구에게는 받아들이기 어려운 오버헤드입니다.
GraalVM Native Image는 자바 애플리케이션을 미리(AOT) 컴파일해서 독립 실행 파일로 만들어 이 문제를 해결합니다. JVM이 필요 없어요. 시작 시간은 수백 밀리초에서 한 자릿수 밀리초로 줄고, 인터프리터도 JIT 컴파일러도 클래스 메타데이터도 로드하지 않으니 메모리 사용량도 줄어듭니다.
트레이드오프도 있습니다. 네이티브 이미지는 JVM의 적응형 최적화를 잃습니다. JIT로 컴파일된 애플리케이션은 JVM이 핫 패스를 학습하면서 시간이 갈수록 빨라지지만, 네이티브 이미지는 빠르게 시작하는 대신 더 나아지지 않습니다. 수명이 짧은 프로세스(CLI 도구, 서버리스)에는 네이티브 이미지가 유리하고, 오래 돌아가는 서버에서는 결국 JVM의 JIT가 더 빠른 코드를 만들어냅니다. 런타임 특성에 맞는 도구를 고르세요.
현대 자바 vs. 대안들
Go, Kotlin, Rust 등 자바를 ‘대체’하겠다고 나선 언어들은 모두 훌륭합니다. 하지만 현대 자바는 사람들이 생각하는 것보다 훨씬 강하게 경쟁하고 있습니다.
Go의 단순함은 매력적이지만, 자바에도 이제 가상 스레드(고루틴에 대응)가 있고 훨씬 풍부한 타입 시스템도 있습니다. 타입 시스템은 대규모 코드베이스에서 중요합니다. 특히 봉인된 타입과 패턴 매칭을 갖춘 자바의 타입 시스템은 Go의 인터페이스 시스템이 놓치는 오류를 잡아냅니다.
코틀린은 자바의 문법을 개선했지만 같은 JVM 위에서 돌아갑니다. Java 8 대비 코틀린의 장점(null 안전성, 데이터 클래스, 코루틴)은 현대 자바에서 부분적으로든 완전하게든 대응되는 기능(Optional 패턴, 레코드, 가상 스레드)이 있습니다. 코틀린이 여전히 더 간결하지만, 그 격차는 상당히 좁혀졌습니다.
Rust는 가비지 컬렉터 없는 시스템 프로그래밍이라는 다른 영역에 있습니다. 자바가 그곳에서 경쟁할 이유는 없죠. 하지만 서버 애플리케이션, 데이터 처리, 엔터프라이즈 소프트웨어에서는 성능, 안정성, 생태계 성숙도를 함께 갖춘 자바를 이기기 어렵습니다.
진짜 혁신은 6개월 주기
어떤 단일 기능보다도 6개월 릴리스 주기가 자바의 궤적을 바꿨습니다. 2017년 이전의 자바 릴리스는 3~4년마다 모든 것을 한꺼번에 내놓으려는 행사였습니다. 준비되지 않은 기능은 미뤄졌고, 그러면 전체 릴리스가 밀렸으며, 다음 릴리스에 모든 걸 몰아넣어야 하는 압박이 생겼고, 그러다 그 릴리스마저 늦어졌습니다. Java 9가 3년 넘게 걸린 것도 부분적으로는 모듈 시스템의 복잡성 때문이었죠.
6개월 주기에서는 기능이 준비되면 출시됩니다. 패턴 매칭은 Java 16부터 24까지 릴리스마다 한 조각씩 점진적으로 제공됐습니다. 가상 스레드는 정식 출시 전에 두 번의 프리뷰 릴리스를 거쳤죠. 이런 점진적 접근 덕분에 자바 팀은 영구적인 API 결정을 내리기 전에 실제 사용자 피드백을 모으고 설계를 조정할 수 있습니다.
이 교훈은 자바를 넘어 적용됩니다. 작고 잦은 릴리스가 쌓이면 판도를 바꾸는 변화가 됩니다. 17 이후 자바 릴리스 하나하나는 소소해 보였지만, 모아 놓고 보면 언어를 새로 만들어 낸 셈입니다. 5년 전에 자바를 써 보고 장황함에 질렸다면, 한 번 더 시도해 보세요. 생각보다 놀랄 겁니다.


