if (obj instanceof String) 다음에 (String) obj로 캐스팅하는 코드, 쓸 때마다 중복이 느껴졌는데 Java가 드디어 이걸 해결했습니다.

이게 뭔가요?

패턴 매칭 은 객체의 타입을 체크하고 동시에 해당 타입의 변수로 바인딩하는 기능입니다. Java 16부터 instanceof, Java 21부터 switch에서 사용할 수 있습니다.

instanceof 패턴 매칭 (Java 16)

JAVA
// 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 타입
}

논리 연산자와 함께:

JAVA
// && 뒤에서 바인딩 변수 사용 가능
if (obj instanceof String s && s.length() > 5) {
    process(s);
}

// || 에서는 불가 — s가 바인딩되지 않을 수 있으므로
// if (obj instanceof String s || s.length() > 5) { } // 컴파일 에러

switch 패턴 매칭 (Java 21)

JAVA
// 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 절

JAVA
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)합니다.

JAVA
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에서:

JAVA
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 패턴

JAVA
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 없이 모든 경우를 처리할 수 있습니다.

JAVA
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 처리

JAVA
String process(Object obj) {
    return switch (obj) {
        case null        -> "null 입력"; // null을 명시적으로 처리
        case String s    -> s.toUpperCase();
        default          -> obj.toString();
    };
}

Java 21 이전에는 switch에 null을 넣으면 NullPointerException이 발생했습니다.

실전 예시: 이벤트 처리

JAVA
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 ss는 해당 case 블록 안에서만 유효합니다.
  • exhaustiveness: sealed class/enum의 switch는 모든 경우를 커버하면 default가 불필요합니다. 빠뜨리면 컴파일 에러가 납니다.
  • 성능: 패턴 매칭 switch는 if-else 체인과 비슷한 성능입니다. 컴파일러가 최적화합니다.

정리

기능도입 버전설명
instanceof 패턴Java 16타입 체크 + 변수 바인딩
switch 패턴Java 21타입별 분기 + 바인딩
Record 패턴Java 21Record 필드 분해
Guarded 패턴Java 21when 절로 추가 조건
Sealed + switchJava 21모든 경우 커버 보장 (default 불필요)

References

댓글 로딩 중...