패턴 매칭 소개

패턴 매칭은 아직 개발 중인 기능입니다. 이 기능의 일부 요소는 Java 언어의 최종 기능으로 출시되었고, 일부는 미리 보기 기능으로 출시되었으며, 일부는 여전히 논의 중입니다.

패턴 매칭에 대해 자세히 알아보고 피드백을 제공하려면 Amber 프로젝트 페이지를 방문하세요. Amber 프로젝트 페이지는 Java 언어의 패턴 매칭과 관련된 모든 것을 한 곳에서 볼 수 있는 원스톱 페이지입니다.

패턴 매칭을 처음 접하는 경우 가장 먼저 떠오르는 것이 정규식에서의 패턴 매칭일 수 있습니다. 그렇다면 이것이 “인스턴스 오브에 대한 패턴 일치”와 무슨 관련이 있는지 궁금하실 것입니다.

정규식은 문자열을 분석하기 위해 만들어진 패턴 매칭의 한 형태입니다. 이는 이해하기 쉬운 좋은 출발점입니다.

다음 코드를 작성해 보겠습니다.

String sonnet = "From fairest creatures we desire increase,\n" +
        "That thereby beauty's rose might never die,\n" +
        "But as the riper should by time decease\n" +
        "His tender heir might bear his memory:\n" +
        "But thou, contracted to thine own bright eyes,\n" +
        "Feed'st thy light's flame with self-substantial fuel,\n" +
        "Making a famine where abundance lies,\n" +
        "Thyself thy foe, to thy sweet self too cruel.\n" +
        "Thou that art now the world's fresh ornament,\n" +
        "And only herald to the gaudy spring,\n" +
        "Within thine own bud buriest thy content,\n" +
        "And, tender churl, mak'st waste in niggardly.\n" +
        "Pity the world, or else this glutton be,\n" +
        "To eat the world's due, by the grave and thee.";
 
Pattern pattern = Pattern.compile("\\bflame\\b");
Matcher matcher = pattern.matcher(sonnet);
while (matcher.find()) {
    String group = matcher.group();
    int start = matcher.start();
    int end = matcher.end();
    System.out.println(group + " " + start + " " + end);
}

이 코드는 셰익스피어의 첫 번째 소네트를 텍스트로 사용합니다. 이 텍스트는 정규식 \bflame\b로 분석됩니다. 이 정규식은 \b로 시작하고 끝납니다. 이 이스케이프 문자는 정규식에서는 단어의 시작 또는 끝을 나타내는 특별한 의미를 갖습니다. 이 예에서는 이 패턴이 flame이라는 단어와 일치한다는 의미입니다.

정규 표현식으로 훨씬 더 많은 일을 할 수 있습니다. 이 튜토리얼의 범위를 벗어납니다. 정규 표현식에 대해 자세히 알아보려면 정규 표현식 페이지를 참조하세요.

이 코드를 실행하면 다음과 같이 출력됩니다:

flame 233 238

이 결과는 소네트에서 인덱스 233과 인덱스 238 사이에 flame이 한 번 등장한다는 것을 알려줍니다.

정규식을 사용한 패턴 일치는 다음과 같은 방식으로 작동합니다:

  1. 주어진 pattern 과 일치시킵니다. flame이 이 예이며 텍스트와 일치시킵니다.
  2. 그런 다음 패턴이 일치된 위치에 대한 정보를 제공합니다.

이 튜토리얼의 나머지 부분에서 염두에 두어야 할 세 가지 개념이 있습니다:

  1. 일치시켜야 하는 대상; 이를 matched target 이라고 합니다. 여기에는 소네트가 있습니다.
  2. 일치시킬 대상; 이를 pattern 이라고 합니다. 여기서는 정규식 flame입니다.
  3. 매칭의 결과, 여기서는 시작 인덱스와 끝 인덱스입니다.

이 세 가지 요소는 패턴 매칭의 기본 요소입니다.

 

Instanceof에 대한 패턴 매칭

모든 객체를 Instanceof가 있는 타입에 일치시키기

패턴 일치를 확장하는 방법에는 여러 가지가 있습니다. 첫 번째로 다루는 것은 Pattern matching for instanceof 라고 하는 것으로, Java SE 16의 최종 기능으로 출시되었습니다.

이전 섹션의 예제를 Instanceof 사용 사례로 확장해 보겠습니다. 이를 위해 다음 예제를 살펴보겠습니다.

public void print(Object o) {
    if (o instanceof String s){
        System.out.println("This is a String of length " + s.length());
    } else {
        System.out.println("This is not a String");
    }
}

앞서 제시한 세 가지 요소에 대해 설명하겠습니다.

matched target 은 모든 타입의 객체입니다. Instanceof 연산자의 왼쪽 피연산자인 o입니다.

pattern 은 변수 선언이 뒤따르는 타입입니다. Instanceof의 오른쪽 피연산자입니다. 타입은 클래스, 추상 클래스 또는 인터페이스일 수 있습니다. 이 경우에는 그냥 String s입니다.

일치의 결과는 matched target 에 대한 새로운 참조입니다. 이 참조는 패턴의 일부로 선언된 변수(이 예에서는 s)에 넣습니다. 이 변수는 matched targetpattern 과 일치하면 생성됩니다. 이 변수에는 일치시킨 타입이 있습니다. 이 s 변수를 패턴의 pattern variable 라고 합니다. 일부 패턴에는 pattern variable 가 두 개 이상 있을 수 있습니다.

이 예제에서 o 변수는 일치시켜야 하는 요소로, matched target 입니다. 패턴_은 String s 선언입니다. 매칭의 결과는 String 타입과 함께 선언된 변수 s입니다. 이 변수는 o가 타입이 String인 경우에만 생성됩니다.

Instanceof로 선언된 타입과 함께 변수를 정의할 수 있는 이 특수 구문은 Java SE 16에 추가된 새로운 구문입니다.

일치하는 대상의 타입을 검사하기 때문에 String s 패턴을 type pattern 이라고 합니다. 타입 String은 타입 CharSequence를 확장하기 때문에 다음과 같은 패턴이 일치합니다:

public void print(Object o) {
    if (o instanceof CharSequence cs) {
        System.out.println("This is a CharSequence of length " + cs.length());
    }
}

패턴 변수 사용

컴파일러는 s 변수를 사용하는 것이 합당한 곳에 사용할 수 있도록 허용합니다. 가장 먼저 떠오르는 범위는 if 브랜치입니다. 이 변수는 if 문의 일부에서도 사용할 수 있습니다.

다음 코드는 objectString 클래스의 인스턴스인지, 비어 있지 않은 문자열인지 확인합니다. 부울 표현식에서 && 뒤에 s 변수를 사용하고 있음을 알 수 있습니다. 부울 표현식의 첫 번째 부분이 true인 경우에만 이 부분을 평가하기 때문에 완벽하게 이해가 됩니다. 이 경우 s 변수가 생성됩니다.

public void print(Object o) {
    if (o instanceof String s && !s.isEmpty()) {
        int length = s.length();
        System.out.println("This object is a non-empty string of length " + length);
    } else {
        System.out.println("This object is not a string.");
    }
}

코드에서 변수의 실제 타입을 확인하고 이 타입이 예상한 타입이 아닌 경우 나머지 코드를 건너뛰는 경우가 있습니다. 다음 예시를 살펴보세요.

public void print(Object o) {
    if (!(o instanceof String)) {
        return;
    }
    String s = (String)s;
    // do something with s
}

Java SE 16부터는 instanceof에 대한 패턴 매칭을 활용하여 이러한 방식으로 이 코드를 작성할 수 있습니다:

public void print(Object o) {
    if (!(o instanceof String s)) {
        return;
    }
 
    System.out.println("This is a String of length " + s.length());
}

return을 사용하거나 예외를 던져 if 브랜치에서 메서드를 종료하는 경우 s 패턴 변수는 if 문 외부에서 사용할 수 있습니다. 코드가 if 분기를 실행할 수 있고 나머지 메서드와 함께 전달할 수 있는 경우 패턴 변수는 생성되지 않습니다.

컴파일러가 매칭이 실패하는지 알 수 있는 경우도 있습니다. 다음 예시를 살펴보겠습니다:

Double pi = Math.PI;
if (pi instanceof String s) {
    // this will never be true!
}

컴파일러는 String 클래스가 최종적이라는 것을 알고 있습니다. 따라서 변수 piString 타입일 수 있는 방법은 없습니다. 컴파일러는 이 코드에서 오류를 발생시킵니다.

Instanceof에 대한 패턴 매칭으로 더 깔끔한 코드 작성하기

이 기능을 사용하면 코드 가독성을 훨씬 더 높일 수 있는 곳이 많이 있습니다.

equals() 메서드가 있는 다음 Point 클래스를 만들어 보겠습니다. 여기서는 hashCode() 메서드는 생략합니다.

public class Point {
    private int x;
    private int y;
 
    public boolean equals(Object o) {
        if (!(o instanceof Point)) {
            return false;
        }
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }
 
    // constructor, hashCode method and accessors have been omitted
}

이것은 equals() 메서드를 작성하는 고전적인 방법이며, IDE에서 생성했을 수도 있습니다.

equals() 메서드를 다음 코드로 재작성하면 instanceof 패턴 일치 기능을 활용하여 훨씬 더 읽기 쉬운 코드가 됩니다.

public boolean equals(Object o) {
    return o instanceof Point point &&
            x == point.x &&
            y == point.y;
}

 

Switch용 패턴 매칭

대소문자 레이블에 타입 패턴을 사용하도록 Switch 표현식 확장하기

Switch용 패턴 일치는 JDK의 최종 기능이 아닙니다. Java SE 17, 18, 19 및 20에서 미리보기 기능으로 제공됩니다. 여기에서는 마지막 버전에 대해 설명합니다.

Switch용 패턴 일치는 switch 문 또는 표현식을 사용합니다. 이를 통해 한 번에 matched target 을 여러 patterns 에 일치시킬 수 있습니다. 여기서 패턴Instanceof에 대한 패턴 일치에서와 마찬가지로 _type pattern 입니다.

이 경우 _matched target_은 스위치의 선택자 표현식입니다. 이러한 기능에는 여러 개의 _패턴_이 있으며, Switch 표현식의 각 경우는 그 자체로 이전 섹션에서 설명한 구문을 따르는 타입 패턴입니다.

다음 코드를 살펴봅시다.

Object o = ...; // any object
String formatted = null;
if (o instanceof Integer i) {
    formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
    formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
    formatted = String.format("double %f", d);
} else {
    formatted = String.format("Object %s", o.toString());
}

각 if 문마다 하나씩 세 개의 type patterns 이 포함되어 있음을 알 수 있습니다. Switch에 대한 패턴 일치를 사용하면 다음과 같은 방식으로 이 코드를 작성할 수 있습니다.

Object o = ...; // any object
String formatter = switch(o) {
    case Integer i -> String.format("int %d", i);
    case Long l    -> String.format("long %d", l);
    case Double d  -> String.format("double %f", d);
    case Object o  -> String.format("Object %s", o.toString());
}

Switch에 대한 패턴 일치는 코드의 가독성을 높여줄 뿐만 아니라 성능도 향상시킵니다. if-else-if 문을 평가하는 시간은 이 문에 포함된 분기 수에 비례하며, 분기 수를 두 배로 늘리면 평가 시간도 두 배로 늘어납니다. Switch를 평가하는 것은 케이스 수에 의존하지 않습니다. 따라서 if 문의 시간 복잡도는 O(n) 이고 스위치 문의 시간 복잡도는 O(1) 이라고 할 수 있습니다.

지금까지는 패턴 매칭 자체의 확장이 아니라 타입 패턴을 케이스 레이블로 받아들이는 Switch의 새로운 기능입니다.

현재 버전에서 Switch 표현식은 대/소문자 레이블에 대해 다음을 허용합니다:

  1. 숫자 타입은 다음과 같습니다.: byteshortchar, int (long 은 허용되지 않엄)
  2. 래퍼 타입은 다음과 같습니다.: ByteShortCharacter, Integer
  3. the type String
  4. enumerated types.

Switch의 패턴 일치는 케이스 라벨에 타입 패턴을 사용할 수 있는 기능을 추가합니다.

보호된 패턴 사용

Instanceof에 대한 패턴 매칭의 경우, 다음 예제와 같이 매칭된 대상이 패턴과 일치하면 생성되는 패턴 변수를 instanceof가 포함된 부울 표현식에서 사용할 수 있다는 것을 이미 알고 계실 것입니다.

Object object = ...; // any object
if (object instanceof String s && !s.isEmpty()) {
    int length = s.length();
    System.out.println("This object is a non-empty string of length " + length);
}

이는 문장의 인수가 부울 타입이므로 if 문에서 잘 작동합니다. Switch 표현식에서 case 레이블은 부울이 될 수 없습니다. 따라서 다음과 같이 작성할 수 없습니다:

Object o = ...; // any object
String formatter = switch(o) {
    // !!! THIS DOES NOT COMPILE !!!
    case String s && !s.isEmpty() -> String.format("Non-empty string %s", s);
    case Object o                 -> String.format("Object %s", o.toString());
}

스위치에 대한 pattern matching for switch 가 타입 패턴 뒤에 부울 식을 추가할 수 있도록 확장되었습니다. 이 부울 표현식을 _guard_라고 하고 결과 대/소문자 레이블을 _guarded case label_이라고 합니다. 이 부울 표현식은 다음 구문을 사용하여 when 절에 추가할 수 있습니다.

Object o = ...; // any object
String formatter = switch(o) {
    case String s when !s.isEmpty() -> String.format("Non-empty string %s", s);
    case Object o                   -> String.format("Object %s", o.toString());
}

이 확장된 대소문자 레이블을 _guarded case label_이라고 합니다. 표현식 String s when !s.isEmpty()는 이러한 가드 케이스 레이블입니다. 타입 패턴과 부울 표현식으로 구성됩니다.

 

레코드 패턴

record 는 Java SE 16에 도입된 특수한 타입의 불변(immutable ) 클래스입니다. 이 기능에 대한 자세한 내용은 레코드를 사용하여 불변 데이터 모델링하기를 참조하세요.

레코드 패턴은 Java SE 19 및 20에서 미리보기 기능으로 제공되는 특수한 종류의 패턴입니다. 레코드는 레코드 선언의 일부로 선언되는 컴포넌트를 기반으로 구축됩니다. 다음 예제에서 Point 레코드에는 두 개의 컴포넌트가 있습니다: xy입니다.

public record Point(int x, int y) {}

이 정보를 통해 레코드 패턴 매칭에 사용되는 _레코드 해체_라는 개념을 사용할 수 있습니다. 다음 코드는 _record pattern_을 사용한 첫 번째 예시입니다.

Object o = ...; // any object
if (o instanceof Point(int x, int y)) {
    // do something with x and y
}

대상 피연산자 는 여전히 o 참조입니다. 이 피연산자는 Point(int x, int y)라는 record pattern 과 일치합니다. 이 패턴은 두 개의 _패턴 변수, 즉 xy를 선언합니다. o가 실제로 Point 타입이면, 이 두 바인딩 변수는 Point 레코드의 해당 접근자를 호출하여 생성되고 초기화됩니다.

다음 구문을 사용하여 포인트 자체를 다른 바인딩 변수에 바인딩할 수도 있습니다.

Object o = ...; // any object
if (o instanceof Point(int x, int y) point) {
    // do something with x, y, and point
}

레코드 패턴은 이 예제에서 Point라는 레코드 이름과 해당 레코드의 구성 요소당 하나의 타입 패턴으로 구축됩니다. 따라서 o Instanceof Point(int x, int y)를 작성할 때 int xint y는 타입 패턴으로, Point 레코드의 첫 번째와 두 번째 구성 요소를 일치시키는 데 사용됩니다. 이 경우 원시 타입으로 타입 패턴을 정의할 수 있다는 점에 유의하세요. Instanceof의 경우에는 그렇지 않습니다.

결과적으로 레코드 패턴은 레코드의 표준 생성자와 동일한 모델에 구축됩니다. 주어진 레코드에서 표준 생성자가 아닌 다른 생성자를 생성하더라도 해당 레코드의 레코드 패턴은 항상 표준 생성자의 구문을 따릅니다. 따라서 다음 코드는 컴파일되지 않습니다.

record Point(int x, int y) {
    Point(int x) {
        this(x, 0);
    }
}
 
Object o = ...; // any object
// !!! THIS DOES NOT COMPILE !!!
if (o intanceof Point(int x)) {
 
}

레코드 패턴은 타입 추론을 지원합니다. 패턴을 작성하는 데 사용하는 구성 요소의 타입은 var로 유추할 수도 있고, 레코드에 선언된 실제 타입의 확장일 수도 있습니다.

각 구성 요소의 매칭이 실제로는 타입 패턴이므로 구성 요소의 실제 타입의 확장인 타입을 매칭할 수 있습니다. 패턴에 레코드 컴포넌트의 실제 타입의 확장이 될 수 없는 타입을 사용하면 컴파일러 오류가 발생합니다.

다음은 컴파일러가 바인딩 변수의 실제 타입을 유추하도록 요청할 수 있는 첫 번째 예시입니다.

record Point(double x, double y) {}
 
Object o == ...; // any object
if (o instanceof Point(var x, var y)) {
    // x and y are of type double
}

다음 예제에서는 Box 레코드의 컴포넌트 타입을 Switch할 수 있습니다.

record Box(Object o) {}
 
Object o = ...; // any object
switch (o) {
    case Box(String s)  -> System.out.println("Box contains the string: " + s);
    case Box(Integer i) -> System.out.println("Box contains the integer: " + i);
    default -> System.out.println("Box contains something else");
}

Instanceof의 경우와 마찬가지로 불가능한 타입을 확인할 수 없습니다. 여기서 Integer 타입은 CharSequence 타입을 확장할 수 없으므로 컴파일러 오류가 발생합니다.

record Box(CharSequence o) {}
 
Object o = ...; // any object
switch (o) {
    case Box(String s)  -> System.out.println("Box contains the string: " + s);
    // !!! THE FOLLOWING LINE DOES NOT COMPILE !!!
    case Box(Integer i) -> System.out.println("Box contains the integer: " + i);
    default -> System.out.println("Box contains something else");
}

레코드 패턴은 박싱이나 언박싱을 지원하지 않습니다. 따라서 다음 코드는 유효하지 않습니다.

record Point(Integer x, Integer y) {}
 
Object o = ...; // any object
// !!! DOES NOT COMPILE !!!
if (o instanceof Point(int x, int y)) {
}

마지막으로 레코드 패턴은 중첩을 지원하므로 다음 코드를 작성할 수 있습니다.

record Point(double x, double y) {}
record Circle(Point center, double radius) {}
 
Object o = ...; // any object
if (o instanceof Circle(Point(var x, var y), var radius)) {
    // Do something with x, y and radius
}

 

Enhanced for 문에 대한 패턴 일치

향상된 for 문 은 다음 구문을 사용하여 Iterable 객체의 요소를 반복하는 것으로 구성됩니다.

Iterable<String> iterable = ...;
for (String s: iterable) {
    // Do something with s
}

대부분의 경우 컬렉션이나 배열을 반복하게 되지만, Iterable을 직접 구현하여 이 구문을 사용할 수 있다는 점에 주목할 필요가 있습니다.

Java SE 20부터는 이 구문에서 레코드 패턴이 미리보기 기능으로 지원됩니다. 따라서 다음과 같은 방식으로 포인트 목록을 반복할 수 있습니다.

record Point(double x, double y) {}
List<Points> points = ...;
 
for (Point(double x, double y): points) {
    // Do something with x and y
}

하지만 여러 가지 제한 사항이 있습니다.

  1. 반복하는 컬렉션은 널 값을 포함할 수 없습니다. 이는 논리적으로만 가능합니다. xypoint의 각 인스턴스의 접근자를 호출하여 초기화됩니다.
  2. 사용하는 패턴이 컬렉션의 요소와 일치하지 않으면 예외가 발생합니다.

double xdouble y는 그 자체로 타입 패턴이므로 이터러블 객체의 모든 요소와 일치하지 않을 수 있는 레코드 패턴을 작성할 수 있다는 점을 염두에 두어야 합니다. 다음 예시가 이에 해당합니다. 이 예제는 있는 그대로 제공된다는 점에 유의하세요. 코드에 절대 이런 식으로 사용하지 마세요!

record Box(Object o) {}
List<Box> boxes = List.of(new Box("one"), new Box("two"), new Box(1), new Box(2));
 
for (Box(String s): boxes) {
    // this code does compile, but will throw a MatchException
    // when reaching the third element
}

두 경우 모두 생성된 예외는 이 예외의 원인으로 던져진 정확한 예외와 함께 MatchException 타입입니다.

 

더 많은 패턴

이제 Java 언어의 세 가지 요소에서 패턴 매칭이 최종 기능 또는 미리보기 기능으로 지원됩니다:

  • Instanceof` 키워드,
  • Switch` 문 및 표현식,
  • 그리고 확장된 for 루프입니다.

모두 두 가지 종류의 패턴을 지원합니다: type patternsrecord patterns.

가까운 시일 내에 더 많은 기능이 추가될 예정입니다. Java 언어의 더 많은 요소가 수정되고 더 많은 종류의 패턴이 추가될 수 있습니다. 이 페이지는 이러한 수정 사항을 반영하여 업데이트될 예정입니다.