Contents
see ListJava 21에서 정식 도입된 패턴 매칭(Pattern Matching)과 Record Patterns는 Java의 타입 검사와 데이터 추출 방식을 근본적으로 바꾸었다. instanceof 체크 후 캐스팅하던 장황한 코드가 간결하고 안전한 패턴 표현으로 대체된다. 이 글에서는 Java 21~23의 패턴 매칭 기능을 체계적으로 정리한다.
1. switch 패턴 매칭 (Java 21 정식)
switch문에서 타입 패턴을 사용하여 instanceof 검사와 변수 바인딩을 동시에 수행할 수 있다.
// 기존 방식 - 장황한 instanceof + 캐스팅
public String formatValue(Object obj) {
if (obj instanceof Integer i) {
return String.format("정수: %,d", i);
} else if (obj instanceof Double d) {
return String.format("실수: %.2f", d);
} else if (obj instanceof String s) {
return String.format("문자열: \"%s\" (길이: %d)", s, s.length());
} else if (obj instanceof List<?> list) {
return String.format("리스트: %d개 항목", list.size());
} else if (obj == null) {
return "null";
} else {
return obj.toString();
}
}
// Java 21 - switch 패턴 매칭
public String formatValue(Object obj) {
return switch (obj) {
case Integer i -> String.format("정수: %,d", i);
case Double d -> String.format("실수: %.2f", d);
case String s -> String.format("문자열: \"%s\" (길이: %d)", s, s.length());
case List<?> list -> String.format("리스트: %d개 항목", list.size());
case null -> "null"; // null도 패턴으로 처리
default -> obj.toString();
};
}
2. Guarded Patterns (when 절)
패턴에 조건을 추가하여 더 세밀한 분기가 가능하다.
public String classifyAge(Object obj) {
return switch (obj) {
case Integer age when age < 0 -> "잘못된 나이";
case Integer age when age < 13 -> "어린이";
case Integer age when age < 20 -> "청소년";
case Integer age when age < 65 -> "성인";
case Integer age -> "시니어";
case String s when s.matches("\\d+") -> classifyAge(Integer.parseInt(s));
case null, default -> "알 수 없음";
};
}
// HTTP 응답 처리
public String handleResponse(HttpResponse<?> response) {
return switch (response.statusCode()) {
case int code when code >= 200 && code < 300 -> "성공: " + response.body();
case int code when code == 301 || code == 302 -> "리다이렉트: " + response.headers().firstValue("Location").orElse("");
case int code when code == 404 -> "페이지 없음";
case int code when code >= 500 -> "서버 오류 (" + code + ")";
default -> "기타 응답: " + response.statusCode();
};
}
3. Record Patterns (레코드 패턴)
Record의 컴포넌트를 패턴 매칭으로 직접 분해(destructuring)할 수 있다.
// Record 정의
record Point(int x, int y) {}
record Circle(Point center, double radius) {}
record Rectangle(Point topLeft, Point bottomRight) {}
sealed interface Shape permits Circle, Rectangle {}
// Record Pattern으로 중첩 분해
public double calculateArea(Shape shape) {
return switch (shape) {
case Circle(Point(var cx, var cy), var r) -> {
System.out.printf("원: 중심(%d, %d), 반지름=%.1f%n", cx, cy, r);
yield Math.PI * r * r;
}
case Rectangle(
Point(var x1, var y1),
Point(var x2, var y2)
) -> {
double width = Math.abs(x2 - x1);
double height = Math.abs(y2 - y1);
System.out.printf("사각형: (%d,%d)-(%d,%d), %n", x1, y1, x2, y2);
yield width * height;
}
}; // sealed interface이므로 default 불필요
}
// 사용
Shape circle = new Circle(new Point(0, 0), 5.0);
System.out.println("면적: " + calculateArea(circle));
// 원: 중심(0, 0), 반지름=5.0
// 면적: 78.53981633974483
4. Sealed Classes + 패턴 매칭 조합
sealed class/interface와 패턴 매칭을 결합하면 컴파일러가 모든 경우를 검증해준다.
// 결제 도메인 모델
sealed interface PaymentMethod permits CreditCard, BankTransfer, DigitalWallet {}
record CreditCard(String number, String expiry, String cvv) implements PaymentMethod {}
record BankTransfer(String bankCode, String accountNumber) implements PaymentMethod {}
record DigitalWallet(String provider, String email) implements PaymentMethod {}
sealed interface PaymentResult permits Success, Failure, Pending {}
record Success(String transactionId, Instant timestamp) implements PaymentResult {}
record Failure(String errorCode, String message) implements PaymentResult {}
record Pending(String referenceId, Duration estimatedTime) implements PaymentResult {}
// 패턴 매칭으로 모든 경우 처리
public String processPayment(PaymentMethod method, BigDecimal amount) {
// 결제 수단별 처리
String gateway = switch (method) {
case CreditCard(var num, var exp, _) -> {
// 카드번호 마스킹
String masked = "****-****-****-" + num.substring(num.length() - 4);
System.out.println("카드 결제: " + masked + " (만료: " + exp + ")");
yield "card-gateway";
}
case BankTransfer(var bank, var account) -> {
System.out.println("계좌이체: " + bank + " " + account);
yield "bank-gateway";
}
case DigitalWallet(var provider, var email) -> {
System.out.println(provider + " 결제: " + email);
yield provider.toLowerCase() + "-gateway";
}
}; // sealed이므로 모든 케이스가 처리됨을 컴파일러가 보장
return gateway;
}
// 결과 처리
public void handleResult(PaymentResult result) {
switch (result) {
case Success(var txId, var ts) ->
System.out.printf("결제 성공: %s (%s)%n", txId, ts);
case Failure(var code, var msg) when code.startsWith("TEMP_") ->
System.out.printf("일시 오류(%s): %s - 재시도 예정%n", code, msg);
case Failure(var code, var msg) ->
System.out.printf("결제 실패(%s): %s%n", code, msg);
case Pending(var refId, var eta) ->
System.out.printf("처리 중: %s (예상 %d초)%n", refId, eta.toSeconds());
}
}
5. Unnamed Patterns과 Variables (Java 22)
사용하지 않는 변수를 밑줄(_)로 표시하여 의도를 명확히 할 수 있다.
// _ (언더스코어)로 사용하지 않는 변수 표시
record Order(long id, String product, int quantity, BigDecimal price) {}
// 제품명만 필요한 경우
public List<String> getProductNames(List<Order> orders) {
return orders.stream()
.map(order -> switch (order) {
case Order(_, var product, _, _) -> product;
})
.distinct()
.toList();
}
// try-catch에서 사용하지 않는 예외 변수
try {
processData();
} catch (IllegalArgumentException _) {
// 예외 객체를 사용하지 않음을 명시
System.out.println("잘못된 인자");
} catch (Exception _) {
System.out.println("기타 오류");
}
// 람다에서 사용하지 않는 파라미터
map.forEach((_, value) -> System.out.println(value));
6. 실전 활용: JSON 파서
sealed interface JsonValue permits JsonString, JsonNumber, JsonBool, JsonNull, JsonArray, JsonObject {}
record JsonString(String value) implements JsonValue {}
record JsonNumber(double value) implements JsonValue {}
record JsonBool(boolean value) implements JsonValue {}
record JsonNull() implements JsonValue {}
record JsonArray(List<JsonValue> elements) implements JsonValue {}
record JsonObject(Map<String, JsonValue> fields) implements JsonValue {}
public String toJsonString(JsonValue value) {
return switch (value) {
case JsonString(var s) -> "\"" + escapeJson(s) + "\"";
case JsonNumber(var n) -> Double.toString(n);
case JsonBool(var b) -> Boolean.toString(b);
case JsonNull() -> "null";
case JsonArray(var elems) -> "[" + elems.stream()
.map(this::toJsonString)
.collect(Collectors.joining(", ")) + "]";
case JsonObject(var fields) -> "{" + fields.entrySet().stream()
.map(e -> "\"" + e.getKey() + "\": " + toJsonString(e.getValue()))
.collect(Collectors.joining(", ")) + "}";
};
}
패턴 매칭과 Record Patterns는 Java를 더 표현력 있고 안전한 언어로 만들었다. 특히 sealed interface와 결합하면 컴파일 타임에 모든 경우를 빠짐없이 처리하는지 검증할 수 있어, 런타임 오류를 크게 줄일 수 있다. 새 프로젝트에서는 적극적으로 활용할 것을 권장한다.