패턴 매칭 — instanceof, switch, sealed class와의 조합
if (obj instanceof String)다음에(String) obj로 캐스팅하는 코드, 쓸 때마다 중복이 느껴졌는데 Java가 드디어 이걸 해결했습니다.
이게 뭔가요?
패턴 매칭 은 객체의 타입을 체크하고 동시에 해당 타입의 변수로 바인딩하는 기능입니다. Java 16부터 instanceof, Java 21부터 switch에서 사용할 수 있습니다.
instanceof 패턴 매칭 (Java 16)
// Before
if (obj instanceof String) {
String s = (String) obj; // 명시적 캐스팅 필요
System.out.println(s.length());
}
// After — 캐스팅 자동
if (obj instanceof String s) {
System.out.println(s.length()); // s가 이미 String 타입
}
논리 연산자와 함께:
// && 뒤에서 바인딩 변수 사용 가능
if (obj instanceof String s && s.length() > 5) {
process(s);
}
// || 에서는 불가 — s가 바인딩되지 않을 수 있으므로
// if (obj instanceof String s || s.length() > 5) { } // 컴파일 에러
switch 패턴 매칭 (Java 21)
// Before
String describe(Object obj) {
if (obj instanceof Integer i) return "정수: " + i;
else if (obj instanceof String s) return "문자열: " + s;
else if (obj instanceof List<?> l) return "리스트 크기: " + l.size();
else return "알 수 없음";
}
// After
String describe(Object obj) {
return switch (obj) {
case Integer i -> "정수: " + i;
case String s -> "문자열: " + s;
case List<?> l -> "리스트 크기: " + l.size();
case null -> "null";
default -> "알 수 없음";
};
}
Guarded 패턴 — when 절
String classify(Object obj) {
return switch (obj) {
case Integer i when i < 0 -> "음수";
case Integer i when i == 0 -> "영";
case Integer i -> "양수";
case String s when s.isEmpty() -> "빈 문자열";
case String s -> "문자열: " + s;
default -> "기타";
};
}
when 절로 추가 조건을 걸 수 있습니다. 순서가 중요합니다 — 더 구체적인 패턴이 먼저 와야 합니다.
Record 패턴 (Java 21)
Record의 구성 요소를 직접 분해(destructure)합니다.
record Point(int x, int y) { }
// Record 패턴 — 필드를 바로 추출
void print(Object obj) {
if (obj instanceof Point(int x, int y)) {
System.out.println("x=" + x + ", y=" + y);
}
}
switch에서:
record Circle(double radius) { }
record Rectangle(double w, double h) { }
double area(Object shape) {
return switch (shape) {
case Circle(var r) -> Math.PI * r * r;
case Rectangle(var w, var h) -> w * h;
default -> 0;
};
}
중첩 Record 패턴
record Address(String city, String street) { }
record Customer(String name, Address address) { }
String getCity(Object obj) {
return switch (obj) {
case Customer(var name, Address(var city, _)) -> city;
default -> "unknown";
};
}
_(언더스코어)는 사용하지 않는 바인딩 변수를 나타냅니다 (Java 22+).
Sealed Class와의 조합
Sealed class와 패턴 매칭을 함께 사용하면 default 없이 모든 경우를 처리할 수 있습니다.
sealed interface PaymentMethod
permits CreditCard, BankTransfer, DigitalWallet { }
record CreditCard(String number, String cvv) implements PaymentMethod { }
record BankTransfer(String accountNo) implements PaymentMethod { }
record DigitalWallet(String provider) implements PaymentMethod { }
String processPayment(PaymentMethod method) {
return switch (method) {
case CreditCard(var number, _) -> "카드 결제: " + maskCard(number);
case BankTransfer(var account) -> "계좌 이체: " + account;
case DigitalWallet(var provider) -> provider + " 결제";
// default 불필요 — sealed class이므로 모든 경우가 커버됨
};
}
새 하위 클래스를 추가하면 switch에서 컴파일 에러가 발생하여 누락을 방지합니다.
null 처리
String process(Object obj) {
return switch (obj) {
case null -> "null 입력"; // null을 명시적으로 처리
case String s -> s.toUpperCase();
default -> obj.toString();
};
}
Java 21 이전에는 switch에 null을 넣으면 NullPointerException이 발생했습니다.
실전 예시: 이벤트 처리
sealed interface DomainEvent permits OrderCreated, OrderCancelled, PaymentReceived { }
record OrderCreated(Long orderId, BigDecimal amount) implements DomainEvent { }
record OrderCancelled(Long orderId, String reason) implements DomainEvent { }
record PaymentReceived(Long orderId, BigDecimal paid) implements DomainEvent { }
void handle(DomainEvent event) {
switch (event) {
case OrderCreated(var id, var amount) -> {
log.info("주문 생성: {} ({}원)", id, amount);
notifyWarehouse(id);
}
case OrderCancelled(var id, var reason) -> {
log.info("주문 취소: {} ({})", id, reason);
refundPayment(id);
}
case PaymentReceived(var id, var paid) -> {
log.info("결제 수신: {} ({}원)", id, paid);
updateOrderStatus(id);
}
}
}
자주 헷갈리는 포인트
- 패턴 순서: switch에서 부모 타입이 자식 타입보다 먼저 오면 컴파일 에러입니다. 구체적인 패턴이 먼저 와야 합니다.
- 변수 스코프:
case String s→s는 해당 case 블록 안에서만 유효합니다. - exhaustiveness: sealed class/enum의 switch는 모든 경우를 커버하면
default가 불필요합니다. 빠뜨리면 컴파일 에러가 납니다. - 성능: 패턴 매칭 switch는 if-else 체인과 비슷한 성능입니다. 컴파일러가 최적화합니다.
정리
| 기능 | 도입 버전 | 설명 |
|---|---|---|
| instanceof 패턴 | Java 16 | 타입 체크 + 변수 바인딩 |
| switch 패턴 | Java 21 | 타입별 분기 + 바인딩 |
| Record 패턴 | Java 21 | Record 필드 분해 |
| Guarded 패턴 | Java 21 | when 절로 추가 조건 |
| Sealed + switch | Java 21 | 모든 경우 커버 보장 (default 불필요) |