* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
(자바 버전 여행 ⑮)
자바 17 LTS의 얼굴 두 개가 이번 편이다. 하나는 캐스팅을 없애는 instanceof 패턴 매칭, 다른 하나는 상속을 통제하는 sealed 클래스. 따로도 쓸모 있지만, 둘이 만나면 자바가 함수형 언어처럼 데이터를 다루기 시작한다.
instanceof — 검사하고 또 캐스팅하던 번거로움
// Before — 검사한 걸 또 캐스팅
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
// After — 검사와 동시에 s로 바인딩
if (obj instanceof String s) {
System.out.println(s.length()); // 바로 s 사용
}
instanceof 패턴 — 스코프까지 이해하기
if (o instanceof String s)면 그 블록 안에서 s를 바로 쓴다. 흥미로운 건 조건과의 결합이다 — &&로 이어 쓰면 바인딩이 그 뒤로도 유효하다. 반대로 || 뒤에선 못 쓴다(매치가 안 됐을 수 있으니 당연하다).
// && 로 이어 쓰기 — s가 뒤까지 유효
if (o instanceof String s && s.length() > 3) {
System.out.println(s.toUpperCase());
}
sealed — 아무나 상속하지 못하게
보통 interface나 추상 클래스는 누구든 구현·상속할 수 있다. sealed는 permits로 허락한 타입만 하위가 되게 막는다. “이 타입의 종류는 이게 전부다”를 컴파일러에게 약속하는 것이다.
public sealed interface Shape permits Circle, Rectangle { }
public record Circle(double r) implements Shape { }
public record Rectangle(double w, double h) implements Shape { }
// 다른 클래스가 Shape를 implements 하려 하면 컴파일 에러
sealed — permits로 상속을 명시
앞 절에서 permits로 상속을 못박는 걸 봤다. 한 가지 규칙이 더 있다 — 허용된 하위 타입은 final(더 못 잇게), sealed(다시 통제하며 잇게), 또는 non-sealed(여기부터는 열어두게) 중 하나여야 한다. non-sealed는 언제 쓰냐면 — 코어 경우들은 닫아 관리하되 한 갈래만 플러그인처럼 확장을 허용하고 싶을 때다. 예: sealed Payment permits Card, Cash, non-sealed ExternalPay — 결제 종류는 통제하지만 외부 연동 결제는 서드파티가 상속해 붙일 수 있게 여는 식이다. 이로써 ‘이 계층의 경우의 수는 이게 전부’임을 컴파일러와 사람 모두에게 선언한다.
sealed interface Payment permits Success, Failure, Pending {}
record Success(String txId) implements Payment {}
record Failure(String reason) implements Payment {}
record Pending() implements Payment {}
둘이 만나면 — 컴파일러가 빠진 경우를 잡는다
sealed로 경우를 닫고, switch 패턴(18편)으로 각 타입을 다루면, 컴파일러가 ‘빠진 하위 타입‘을 검사할 수 있다. 새 하위 타입을 추가하면 그걸 안 다룬 코드가 컴파일 에러가 난다 — 런타임 버그가 컴파일 타임으로 앞당겨진다. 이게 ‘자바가 데이터를 함수형처럼 다룬다’는 말의 실체다.
직접 해보기 — 컴파일러가 빠진 경우를 잡는 순간
이 조합의 진짜 맛은 직접 깨뜨려봐야 안다. 두 파일이면 된다.
- 위의
Shapesealed 계층과 default 없는 switch를 그대로 작성해 컴파일한다 — 통과. record Triangle(double b, double h) implements Shape {}를 추가하고 permits에 Triangle을 넣는다.- 다시 컴파일 — switch 자리에서
the switch expression does not cover all possible input values에러.
✅ 확인 — 에러가 switch를 쓰는 파일에서 난다는 걸 눈여겨보자. 타입을 추가한 사람이 아니라 그 타입을 다루는 모든 코드가 컴파일 타임에 소환된다 — 런타임에 조용히 빠지던 else 사다리와의 결정적 차이다.
언제 쓰나 — 닫힌 계층
sealed는 ‘경우의 수가 정해진‘ 도메인에 맞는다 — 결제 상태(성공·실패·대기), 도형(원·사각·삼각), 파싱 결과(성공·에러). 반대로 무한히 확장될 계층엔 안 맞는다(그건 그냥 열린 인터페이스). ‘이 타입의 자식은 이게 전부여야 한다’가 참이면 sealed가 답이다. 이 두 기능이 record 패턴과 만나 자바 21의 데이터 중심 문법으로 완성된다.
이제 자바 17이 담은 큰 조각들을 다 봤다. 다음 편에서 자바 17을 통째로 정리하며, 왜 17이 오늘날 ‘새 기준’이 됐는지 짚는다.
자주 묻는 질문 (FAQ)
instanceof 패턴이 뭐가 좋나요?
타입 검사와 동시에 변수로 바인딩해 별도 캐스팅이 필요 없습니다. if (o instanceof String s)처럼 바로 s를 씁니다.
sealed 클래스는 왜 쓰나요?
permits로 허락한 타입만 상속·구현하게 제한합니다. 종류가 정해져 있어 switch 완전성 검사가 가능해집니다.

답글 남기기