모듈과 모던 자바 — 언어는 계속 진화한다
큰 코드를 어떻게 경계 지어 관리하고, 자바가 어떻게 더 안전하고 간결해졌는지. 모듈 시스템에서 시작해 record·sealed·패턴 매칭, 그리고 Java 21/25의 새 무기까지 — 왜 이런 기능이 생겼는지와 함께 본다.
1. 경계 긋기 — 모듈 시스템(JPMS)
public이면 어디서든 쓸 수 있다는 게 큰 시스템에선 약점이다. 내부 구현이 의도치 않게 외부에 새어 결합이 생긴다. 그래서 Java 9는 패키지 위에 모듈이라는 더 큰 경계를 뒀다.
// module-info.java
module com.shop.order {
requires com.shop.common; // 무엇에 의존하는가
exports com.shop.order.api; // 무엇을 공개하는가 (이것 외엔 전부 숨김)
opens com.shop.order.entity;// 리플렉션(JPA·Jackson)에만 연다
}
핵심은 강한 캡슐화 — exports하지 않은 패키지는 public이어도 모듈 밖에서 아예 안 보인다. JVM은 시작 시 requires를 따라 모듈 그래프를 해석해 경계를 강제한다.
실무에서는 — 꼭 모듈로 안 나눠도,
jlink로 앱이 실제 쓰는 모듈만 묶어 경량 런타임을 만들면 컨테이너 이미지가 확 줄어든다. Java 25의import module java.base;는 모듈이 공개한 패키지를 한 줄로 임포트하게 해 준다.
2. 데이터는 record로 — 보일러플레이트의 끝
값을 담는 클래스마다 생성자·접근자·equals·hashCode·toString을 손으로 쓰는 건 실수의 온상이었다(01의 hashCode 누락 버그처럼). record가 이걸 자동화한다.
record Range(int lo, int hi) {
Range { if (lo > hi) throw new IllegalArgumentException(); } // 컴팩트 생성자로 검증
}
컴파일러가 private final 필드 + 표준 생성자 + 접근자 + equals/hashCode/toString을 만들어 준다. 클래스파일에 Record 속성이 남아 리플렉션·패턴 매칭이 이를 활용한다.
실무에서는 — DTO, API 요청/응답, 다중 반환값, 맵 키 같은 값 객체는 record가 사실상 표준이다. 불변이라 스레드 안전·키 안전도 공짜로 따라온다.
3. 닫힌 계층 — sealed + 패턴 매칭
타입의 가능한 경우가 정해져 있다면(예: 도형은 원/사각형뿐), 그걸 컴파일러에게 알려 주는 게 sealed다.
sealed interface Shape permits Circle, Rectangle {}
record Circle(double r) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
permits로 하위 타입을 못 박으면(클래스파일 PermittedSubclasses 속성), switch가 모든 경우를 다뤘는지 검사해 준다 — 새 타입을 빠뜨리면 컴파일 오류가 나니, 누락 버그가 사라진다.
String describe(Shape s) {
return switch (s) {
case Circle(double r) when r > 10 -> "큰 원"; // 가드
case Circle(double r) -> "원 r=" + r; // record 디컨스트럭션
case Rectangle(double w, double h)-> "사각형 " + w*h;
}; // sealed라 망라 → default 불필요
}
실무에서는 — sealed + record + switch 조합은 상태/이벤트/결과를 모델링하는 강력한 도구다. 예:
sealed interface Result permits Ok, Err로 성공/실패를 타입으로 표현하면, 예외 대신 값으로 다루며 컴파일러가 두 경우를 다 처리했는지 보장한다.
4. 더 안전하고 간결해진 문법
String sql = """
SELECT id FROM users
WHERE active = true
"""; // text block: 여러 줄 문자열, 들여쓰기 자동 정리
var list = new ArrayList<String>(); // var: 우변으로 타입 추론(지역변수만)
int code = switch (status) { // switch 식: 값을 반환, 폴스루 없음(안전)
case ACTIVE, PENDING -> 200;
case CLOSED -> 410;
};
이들은 전부 실수 줄이기와 읽기 쉬움을 향한다 — switch 식은 폴스루 버그가 없고, text block은 문자열 이어붙이기 실수를 없앤다.
5. Java 21 / 25의 새 무기
| 기능 (JEP) | 한 줄 · 어디에 |
|---|---|
| Virtual Threads (444, 21) | 블로킹 코드로 대규모 동시성 → I/O 많은 서버(동시성 페이지) |
| Sequenced Collections (431, 21) | getFirst/getLast/reversed 통일 |
| Scoped Values (506, 25) | ThreadLocal의 불변·자동 해제 대체 → 요청 컨텍스트 전파 |
| Stable Values (502, 25) | 지연 1회 초기화 상수 → 무거운 싱글톤·캐시 |
| Module Import (511, 25) | import module … 일괄 임포트 |
| Compact Source + Instance main (512, 25) | 클래스 없이 void main() → 입문·스크립트 |
| Compact Object Headers (519, 25) | 객체 헤더 12→8B → 힙 절약(JVM 페이지) |
// Stable Values (JEP 502): final 불변성 + 지연 초기화 — 홀더/이중검사잠금을 대체
private final StableValue<Config> cfg = StableValue.of();
Config cfg() { return cfg.orElseSet(this::loadConfig); } // 최초 1회만 load
실무에서는 — 새 LTS(21·25)의 무게중심은 동시성(가상 스레드·구조적 동시성·Scoped Values)과 메모리/기동(Compact Headers·AOT)이다. 블로킹 I/O 위주 서버라면 가상 스레드 전환이 가장 체감이 크고, 컨테이너 밀도를 높이려면 Compact Object Headers가 조용히 힙을 아껴 준다.
한눈에 정리
- 모듈(JPMS): 패키지 위의 경계.
exports안 하면 public이어도 숨김.jlink로 경량 런타임. - record: 값 객체의 보일러플레이트 자동화 → DTO·키·다중반환. 불변.
- sealed + 패턴 매칭: 닫힌 계층 → switch 망라 검사. 상태/결과 모델링(
Result타입). - 간결 문법: text block·var·switch 식 — 실수 줄이고 읽기 쉽게.
- 21/25: 동시성(가상스레드·ScopedValues)·메모리(StableValues·Compact Headers)가 핵심.