IntelliJ IDEA Java

Java에서의 패턴 일치 – 바쁜 개발자를 위한 5가지 예시

Read this post in other languages:

바쁜 개발자가 새로운 기능을 따라잡고 어디서 어떻게 활용할 수 있을지 깊이 있게 이해하기란 어렵습니다.

이 블로그 글에서는 세부적인 내용을 살펴보지 않고도 Java에서 패턴 일치를 사용할 수 있는 5가지 경우를 알려 드립니다. 더 자세한 내용을 살펴볼 여유가 생기시면 이 글에 포함된 링크를 확인하세요.

그럼 시작하겠습니다!

1. 긴 if-else 구문을 switch로 변환하여 코드 가독성 향상

먼저 가장 중요한 질문을 해결해 보겠습니다. 이 변환을 주목하는 이유는 무엇일까요?

주된 이점 중 하나는 코드가 더 간결하고 읽고 이해하기 쉽다는 것입니다. 긴 if-else 문은 일반적으로 한 화면에 다 들어가지 않아 세로 스크롤이 생길 수 있어, 전체 if 비교문에 대해 실행되는 코드를 이해하기가 어렵습니다. 또한 각 if 조건에 다른 조건 집합이 있을 수 있으므로 if 조건의 구문이 명확하지 않을 수 있습니다.

코드 베이스를 탐색할 때 종종 아래 표시된 코드와 유사한 코드를 보게 됩니다. 긴 if-else 구문의 경우 조건부로 값을 지역 변수에 대입합니다. 아래 코드를 살펴보세요. 특정 섹션을 강조 표시하여 여러분이 코드를 탐색할 수 있도록 도와 드리겠습니다.

    
private static String getValueText(Object value) {
    final String newExpression;
    if (value instanceof String) {
        final String string = (String)value;
        newExpression = '"' + StringUtil.escapeStringCharacters(string) + '"';
    }
    else if (value instanceof Character) {
        newExpression = ''' + StringUtil.escapeStringCharacters(value.toString()) + ''';
    }
    else if (value instanceof Long) {
        newExpression = value.toString() + 'L';
    }
    else if (value instanceof Double) {
        final double v = (Double)value;
        if (Double.isNaN(v)) {
            newExpression = "java.lang.Double.NaN";
        }
        else if (Double.isInfinite(v)) {
            if (v > 0.0) {
                newExpression = "java.lang.Double.POSITIVE_INFINITY";
            }
            else {
                newExpression = "java.lang.Double.NEGATIVE_INFINITY";
            }
        }
        else {
            newExpression = Double.toString(v);
        }
    }
    else if (value instanceof Float) {
        final float v = (Float) value;
        if (Float.isNaN(v)) {
            newExpression = "java.lang.Float.NaN";
        }
        else if (Float.isInfinite(v)) {
            if (v > 0.0F) {
                newExpression = "java.lang.Float.POSITIVE_INFINITY";
            }
            else {
                newExpression = "java.lang.Float.NEGATIVE_INFINITY";
            }
        }
        else {
            newExpression = Float.toString(v) + 'f';
        }
    }
    else if (value == null) {
        newExpression = "null";
    }
    else {
        newExpression = String.valueOf(value);
    }
    return newExpression;
}

중점을 둘 코드를 강조 표시해 보겠습니다. 다음 이미지에서 getValueText 메서드는 value 변수의 값이 String, Character, Long, Double과 같은 특정 데이터 유형인지 여부를 비교합니다.

이 if-else 구조의 다른 부분을 이해하기 위해 변수 newExpression에 초점을 맞춰 보겠습니다. 이 변수에는 변수 value의 가능한 모든 값에 대해 값이 대입되고 있음에 주목하세요.

흥미롭게도 if 조건에 해당하는 코드 블록 중 두 개를 제외하고 모든 블록이 일반적으로 한 줄 코드인 다른 if 블록보다 깁니다.

이 두 개의 긴 코드 블록을 추출하여 메서드를 분리한 다음 if-else 문을 switch로 변환해 보겠습니다.

코드를 다른 메서드로 추출하려면 코드를 선택하고 Alt+Enter(또는 macOS의 경우 Option+Enter)를 사용하여 컨텍스트 액션을 호출한 다음, ‘extract method’ 옵션을 선택합니다. 제안된 새 메서드 이름 중에서 하나를 선택하거나 원하는 이름을 입력할 수 있습니다. 코드의 논리적 부분을 선택하기 위해 제가 즐겨 사용하는 단축키는 Ctrl+W(또는 Ctrl+Shift+W로 선택 항목 축소)입니다. 메서드 추출 후, 노란색 배경의 키워드를 확인하여 IntelliJ IDEA의 지침을 따라 컨텍스트 액션(Alt+enter)을 호출합니다. 이 if-else를 switch로 변환하기 위해 ‘if’에서 컨텍스트 액션을 호출하고 ‘if’를 ‘switch’로 바꿉니다.

간결하고 이해하기 쉬운 getValueText 메서드의 switch 문은 다음과 같습니다.

private static String getValueText(Object value) {
    final String newExpression = switch (value) {
        case String string -> '"' + StringUtil.escapeStringCharacters(string) + '"';
        case Character character -> ''' + StringUtil.escapeStringCharacters(value.toString()) + ''';
        case Long aLong -> value.toString() + 'L';
        case Double aDouble -> getNewExpression(aDouble);
        case Float aFloat -> getNewExpression(aFloat);
        case null -> "null";
        default -> String.valueOf(value);
    };
    return newExpression;
}

이렇게 간단한데 그동안 어째서 여러분이 코드에서 if-else 문만큼 자주 switch 식을 사용하지 않았는지 의아하신가요? 여기에는 몇 가지 이유가 있습니다. switch 문은 최근 Java 릴리스에서 향상되었습니다. 값(switch 식)을 반환할 수 있고, 더 이상 제한된 기본 데이터 유형, 래퍼 클래스, String 또는 열거형 등의 기타 항목의 값 비교에만 국한되지 않습니다. 또한 case 라벨에 패턴과 조건을 포함할 수 있습니다.

패턴 일치와 switch를 사용하면 null을 case 라벨로 사용하여 null 값을 처리할 수도 있습니다. 또한 각 case 라벨은 해당 코드 블록에서 사용되는지 여부와 관계없이 패턴 변수를 선언합니다. 누락된 break 라벨이 우려되는 경우, switch와 함께 화살표 스타일을 사용할 때 라벨이 필요하지 않습니다.

그러나 이 기능은 아직 테스트 단계에 있으므로 향후 Java 버전에서 변경될 수 있어 프로덕션 코드에 사용해서는 안 됩니다. 이러한 내용에 익숙하지 않은 경우 이 링크를 따라 구성을 확인하세요.

모든 if-else 문을 switch 문으로 변환할 수 있는 것은 아닙니다. if-else 문을 사용하여 변수, 상수 또는 메서드 호출을 함께 사용할 수 있는 복잡한 조건을 정의할 수 있습니다. 이러한 복잡한 비교는 아직 switch 문에서 지원되지 않습니다. 

패턴 일치에 대한 자세한 내용은 이 블로그 글을 확인하세요.

코드 베이스에서 if를 switch로 바꿀 수 있는지 여부 검사 실행

코드에서 if-else 문을 찾고 switch로 대체할 수 있는지 확인하려면 시간이 많이 걸릴 수 있습니다. 이 블로그 글에서 다룬 것처럼 코드 베이스 또는 그 일부의 모든 클래스에서 ‘if can be replaced with switch'(if를 switch로 바꿀 수 있는지 여부) 검사를 실행할 수 있습니다.

Java의 프로덕션 기능인 instanceof에 대해 패턴 일치를 사용하는 다음 예시를 살펴보겠습니다.

2. instanceof에 패턴 일치를 사용하여 간결한 코드 작성

instanceof 연산자에 대한 패턴 일치 사용은 Java 버전 16부터 프로덕션 기능으로 제공되고 있어 이를 프로덕션 코드에 사용할 수 있습니다.

IntelliJ IDEA의 지침을 따라 노란색 배경으로 강조 표시된 if 키워드에서 컨텍스트 액션을 호출하기만 하면 이 기능을 사용할 수 있습니다.

Monitor와 같은 클래스가 있다고 가정해 보겠습니다. 다음은 equals 메서드를 구현하기 위해 코드 베이스에서 찾을 수 있는 일반적인 예 중 하나입니다.

public class Monitor {
    String model;
    double price;

    @Override
    public boolean equals(Object object) {
        if (object instanceof Monitor) {
            Monitor other = (Monitor) object;
            return model.equals(other.model) && price == other.price;
        }
        return false;
    }
}

다음 gif는 노란색 배경으로 강조 표시된 other라는 변수에 컨텍스트 액션을 호출한 다음 ‘replace ‘other’ with pattern variable'(‘other’를 패턴 변수로 바꾸기) 옵션을 선택하는 식으로 패턴 일치를 사용하는 방법을 보여줍니다. if 문에서 컨텍스트 액션을 호출하여 결과 코드를 리팩터링하면 이 코드를 더욱 간결하게 만들 수 있습니다. 최종 코드는 읽고 이해하기가 더 쉬우며, 언급된 세 가지 조건이 모두 true면 true를 반환합니다.

일반 클래스 대신 Record의 인스턴스로 작업하는 경우 어떻게 될까요? Record의 경우, instanceof에 대한 패턴 일치로 Record 구성 요소에 대한 패턴 변수를 정의하여 Record 인스턴스를 분해할 수 있습니다. 다음 예에서 instanceof 연산자와 함께 사용되는 Citizen(String name, int age)이 Record 패턴입니다.

앞의 두 코드와 같은 간단한 코드 예시로 시작할 때는 이러한 기능의 강력함을 놓치기 쉽습니다. instanceof 연산자와 함께 패턴 일치를 사용하는 또 다른 예를 간단히 살펴보겠습니다. 여기서는 다른 리팩터링 또는 개선 가능성을 위해 지역 변수 선언을 제거합니다. 간단히 말해, 이 기능을 다른 리팩터링 또는 코드 개선 기술과 결합하면 더 나은 코드를 작성하는 데 도움이 될 수 있습니다(IntelliJ IDEA의 지침을 따르세요!).

3. 의미가 없는 상태 무시

if-else 문은 열거형 또는 sealed 클래스의 하위 타입과 같이 전체 값 세트가 있는 타입의 값을 반복하는 데 최선의 선택이 아닐 수 있습니다. 예를 들어, 다음과 같이 고정 값 세트를 정의하는 열거형이 있다고 가정하겠습니다.

enum SingleUsePlastic {
    BOTTLE, SPOON, CARRY_BAG;
}

SingleUsePlastic 타입의 인스턴스가 BOTTLE, SPOONCARRY_BAG의 세 가지 값 중 하나를 가질 수 있음을 알고 있더라도, 다음 코드는 최종 지역 변수 replacement에 대해 컴파일링하지 않습니다.

public class Citizen {
    String getReplacements(SingleUsePlastic plastic) {
        final String replacement;
        if (plastic == SingleUsePlastic.BOTTLE) {
            replacement = "Booth 4: Pick up a glass bottle";
        } else if (plastic == SingleUsePlastic.SPOON) {
            replacement = "Pantry: Pick up a steel spoon";
        } else if (plastic == SingleUsePlastic.CARRY_BAG) {
            replacement = "Booth 5: Pick up a cloth bag";
        } 
        return replacement;
    }
}

컴파일링하려면 끝에 else 절을 추가해야 하지만 이는 말이 되지 않습니다.

public class Citizen {
    String getReplacements(SingleUsePlastic plastic) {
        final String replacement;
        if (plastic == SingleUsePlastic.BOTTLE) {
            replacement = "Booth 4: Pick up a glass bottle";
        } else if (plastic == SingleUsePlastic.SPOON) {
            replacement = "Pantry: Pick up a steel spoon";
        } else if (plastic == SingleUsePlastic.CARRY_BAG) {
            replacement = "Booth 5: Pick up a cloth bag";
        } else {
            replacement = "";
        }
        return replacement;
    }
}

switch 문을 사용하면 존재하지 않는 값에 대해 default 부분을 코딩할 필요가 없습니다.

public class Citizen {
    String getReplacements(SingleUsePlastic plastic) {
        final String replacement = switch (plastic) {
            case BOTTLE -> "Booth 4: Pick up a glass bottle";
            case SPOON -> "Pantry: Pick up a steel spoon";
            case CARRY_BAG -> "Booth 5: Pick up a cloth bag";
        };
        return replacement;
    }
}

마찬가지로, sealed 클래스를 정의하는 경우 switch 문을 사용하여 default 절을 정의하지 않고 전체 하위 클래스를 반복 처리할 수 있습니다.

sealed interface Lego {}
final class SquareLego implements Lego {}
non-sealed class RectangleLogo implements Lego {}
sealed class CharacterLego implements Lego permits PandaLego {}
final class PandaLego extends CharacterLego {}

public class MyLegoGame {
    int processLego(Lego lego) {
        return switch (lego) {
            case SquareLego squareLego -> 100;
            case RectangleLego rectangleLego-> 200;
            case CharacterLego characterLego -> 300;
        };
    }
}

sealed 클래스에 익숙하지 않고 이 주제에 대해 자세히 알아보려면 이 블로그 글을 읽어보세요.

4. 강력하고 간결한 데이터 처리

레코드 패턴, switch 식 및 sealed 클래스를 함께 사용하여 데이터를 처리하는 강력하면서도 간결하고 표현력이 풍부한 코드를 생성할 수 있습니다. 다음은 Record Point, Line, TriangleSquare에 의해 구현되는 sealed 인터페이스 TwoDimensional의 예입니다.

sealed interface TwoDimensional {}
record Point    (int x, 
                 int y) implements TwoDimensional { }
record Line     (Point start, 
                 Point end) implements TwoDimensional { }
record Triangle (Point pointA, 
                 Point pointB, 
				 Point PointC) implements TwoDimensional { }
record Square   (Point pointA, 
                 Point pointB, 
			     Point PointC, 
			     Point pointD) implements TwoDimensional { }

다음 메서드는 switch 문을 사용하여 Line, Triangle 또는 Square와 같은 2차원 형상에서 모든 포인트의 x 및 y 좌표 합계를 반환하는 재귀 메서드 프로세스를 정의합니다.

static int process(TwoDimensional twoDim) {
    return switch (twoDim) {
        case Point(int x, int y) -> x + y;
        case Line(Point a, Point b) -> process(a) + process(b);
        case Triangle(Point a, Point b, Point c) -> process(a) + process(b) + process(c);
        case Square(Point a, Point b, Point c, Point d) -> process(a) + process(b) + process(c) + process(d);
    };
}

IntelliJ IDEA는 또한 이 메서드의 여백에 재귀 호출 아이콘을 표시합니다.

5. 계산과 부수 효과의 분리

동일한 코드 블록에서 계산과 부수 효과(예: 콘솔에 출력)를 결합하는 코드를 자주 볼 수 있습니다. 예를 들어, 다음 코드는 if-else 블록과 instanceof 연산자를 사용하여 변수 타입을 결정하고 각 if 코드 블록에서 조건부로 값을 출력합니다.

void printObject(Object obj) {
    if (obj instanceof String s) {
        System.out.println("String: "" + s + """);
    } else if (obj instanceof Collection<?> c) {
        System.out.println("Collection (size = " + c.size() + ")");
    } else {
        System.out.println("Other object: " + obj);
    }
}

다음 gif는 이 if-else 블록을 switch 문으로 변환한 다음 switch에서 새 검사를 사용하는 방법을 보여줍니다. ‘switch’ 식에 대해 푸시다운한 다음 변수를 추출하여 계산과 그 부수 효과를 분리합니다.

요약

이 블로그 글에서는 바쁜 개발자를 위해 Java에서 패턴 일치를 사용할 수 있는 5가지 경우를 살펴보았습니다. 이러한 기능에 대해 자세히 알아보거나 IntelliJ IDEA가 이러한 기능을 사용하는 데 어떤 도움을 주는지 알고 싶다면 제가 예시를 소개하면서 첨부한 링크를 참조하세요.

다음 블로그 글에서 다루었으면 하는 다른 주제가 있다면 알려주세요.

다시 만날 때까지 즐겁게 코딩하세요!

게시물 원문 작성자

Jessie Cho

Mala Gupta

image description

Discover more