
SOFTMOA TECHNOLOGY
Java 21-23 패턴 매칭과 Record Patterns: switch문의 혁명적 진화
소프트모아가 정리한 기술 기록입니다.
Contents
작성일 2026. 04. 03.
Java 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와 결합하면 컴파일 타임에 모든 경우를 빠짐없이 처리하는지 검증할 수 있어, 런타임 오류를 크게 줄일 수 있다. 새 프로젝트에서는 적극적으로 활용할 것을 권장한다.
Share this
Java
Showing 1 to 10 of 68 entries
