Java 14~21에서의 Switch Case

Letsdev·2023년 6월 1일
12
post-thumbnail
post-custom-banner

서론: 몸에 좋은 Switch Case

C-like 스타일의 switch문은 다소 불편하더라도 기능에는 충실했습니다. 언어 특성에 따라 다르지만, 선택지가 많아지면 switch case의 접근 방식 덕분에, 과도한 if~else if~... 나열에서 생기는 분기 인스트럭션의 영향을 switch case로 짜면 덜 받는 언어들이 있고, 최적화에서 유리하거나 적어도 동등한 입지를 지키는 목적으로 쓰이기도 합니다.
(다만 컴파일러에 의한 최적화가 있기 때문에, 이런 신택스 응용 레벨 최적화는 프로덕션 개발에서 측정 없이 성능에 대한 보장이 될 수 없습니다.)

다만 기능에는 충실한 이 switch 문은 개발자에게 억지로 몇 가지 '습관'을 요구했습니다.
여러 케이스를 묶는 과정에서도 불필요하게 타이핑이 많고, 각 구간에 break를 적지 않으면 의도와 달라지는 경우가 많았습니다. 실제로 break 없이 이어갈 코드는 구태여 만들지 않는 분들이 많고, 정 쓰더라도 주석으로 이것이 의도된 non-break임을 명시하게 됩니다.

switch (foo) {
    case 0:
    case 1:
    case 2:
        System.out.println("0, 1, 2일 때 실행");
        // fall through
    case 3:
        System.out.println("0, 1, 2, 3일 때 실행");
        break;
    default:
        System.out.println("그 외 경우에만 실행");
        break;
}

최근에 나온 언어들로 갈수록 이런 불편함을 굳이 남겨 두지 않는 것으로 보입니다. 사용하다 보니 break는 당연히 넣어야 할 때가 많고, 각 구간에 공통으로 실행할 부분은 굳이 남의 case로 갈(// fall through) 필요 없이 적절한 단계에서 리팩토링을 해서 취급하면 됩니다. 또 저기서 0, 1, 2에 대한 case도 묶어서 하나지, 각각의 케이스라고 생각하지 않아도 됩니다.

그 관점을 자바도 담아냈습니다.

switch (foo) {
    case 0, 1, 2 -> // ... not finalized with `break`

개선된 Switch문은 JDK 12 때부터(preview였어서) 미리 눈여겨 봐 두었던 구문인데, 결국 젊은 언어들의 트렌드를 자바도 써 먹는 것 같습니다. 그만큼 편해졌다는 이야기고, 초보자라면 더더욱 눈여겨 봐 두시는 게 도움이 될 것 같습니다.

Arrow 사용과 인라인

기존 스위치문 사용 시에는, 케이스를 이렇게 나열했습니다.
규칙은 단순하지만, 작성은 번거로웠습니다.

before

// 위에서 선언
int lastDate;

// 선언 이후 스위치문
switch (month) {
    case 1:
    case 3:
    case 5:
    case 7:
    case 8:
    case 10:
    case 12:
        lastDate = 31;
        break;
    case 2:
        lastDate = !isLeapYear ? 28 : 29;
        break;
    case 4:
    case 6:
    case 9:
    case 11:
        lastDate = 30;
        break;
    default:
        throw ... ;
}

System.out.println(lastDate);

개선된 스위치문은 이렇습니다. break를 생략하고, 반환 결과를 바로 담을 수도 있습니다.

after

// 인라인으로 작성 가능(대입연산자와 함께 쓰인 스위치문)
int lastDate = switch (month) {
    case 1, 3, 5, 7, 8, 10, 12 -> 31;
    case 2 -> !isLeapYear ? 28 : 29;
    case 4, 6, 9, 11 -> 30;
    default -> throw ... ;
};

System.out.println(lastDate);

Yield 키워드

Statements Block(중괄호)을 사용해야 한다면 yield를 통해 결과를 반환할 수 있습니다.
(예시에서는 case 2에서 isLeapYear ? 29 : 28을 반환하고 있습니다.)

int lastDate = switch (month) {
    case 1, 3, 5, 7, 8, 10, 12 -> 31;
    case 2 -> {
        // 올해가 윤년인가(true or false)
        boolean isLeapYear = 
            (year % 4 == 0)
            == (year % 100 == 0)
            == (year % 400 == 0);
        
        // 예시에서는 삼항 연산자의 결과를 반환합니다.
        yield !isLeapYear ? 28 : 29; // yield 28; 또는 yield 29;
    }
    case 4, 6, 9, 11 -> 30;
    default -> throw ... ;
};

System.out.println(lastDate);

패턴 매칭

switch문에 대한 패턴 매칭은 JDK 21(17 Preview) 스펙입니다.

타입을 통해 케이스를 태울 수 있습니다.

JDK 17~20을 사용하고 있다면 컴파일 시 파라미터로 --enable-preview를 제공해 주어야 합니다.
(IDE, 빌드도구 등에서는 VM Option, VM Arguments 또는 그러한 용어로 표현되는 것을 제공합니다.)

패턴 매칭을 쓰지 않을 때, 오버라이딩과 별개로 '타입'에 따른 선택 구조가 필요하다면 이렇게 작성했습니다.

before

if (obj == null) {
    
} else if (obj instanceof SubType1) { // [1] type 체크
    SubType1 s1 = (SubType1) obj; // [2] 캐스팅
    // ... [3] 실행문 작성
} else if (obj instanceof SubType2) {
    SubType2 s2 = (SubType2) obj;
    // ...
} else if (obj instanceof SubType3) {
    // ...
}  else if (obj instanceof SubType3) {
    // ...
} else {
    // ...
}

패턴 매칭이 생기고 난 후 이 과정은 이렇게 축약됩니다.

after

switch (obj) {
    case SubType1 s1 -> [실행문]; // s1을 사용할 수 있음.
    case SubType2 s2 -> [실행문]; // s2를 사용할 수 있음.
    case SubType3 ignore -> [실행문]; // 굳이 캐스팅한 변수를 사용하지 않을 때 ignore로 명명
    case SubType4 ignore -> [실행문]; // 굳이 캐스팅한 변수를 사용하지 않을 때 ignore로 명명
    case null -> [실행문];
    default -> [실행문];
}

예시에 사용한 타입 참고

쓰임새 있는 예시를 만들기는 빡세서 대강 에러코드 중 최소한의 기능만 표현했습니다.
하나의 ErrorCode 인터페이스와, 이를 구현받은 여러 enum 클래스를 예시로 작성했습니다.

public interface ErrorCode {
    String name();
    String defaultMessage();
}
public enum FooErrorCode implements ErrorCode {
    FOO_NOT_FOUND(" ... "),
    FOO_ALREADY_EXISTS(" ... "),
    DEFAULT(" ... ");

    final String message;
    
    FooErrorCode(String message) {
        this.message = message;
    }
    
    @Override
    public String defaultMessage() {
        return message;
    }
}
public enum BarErrorCode implements ErrorCode {
    BAR_NOT_FOUND(" ... "),
    BAR_ALREADY_EXISTS(" ... "),
    DEFAULT(" ... ");

    final String message;
    
    FooErrorCode(String message) {
        this.message = message;
    }
    
    @Override
    public String defaultMessage() {
        return message;
    }
}
public enum SampleErrorCode implements ErrorCode {
    SAMPLE_NOT_FOUND(" ... "),
    SAMPLE_ALREADY_EXISTS(" ... "),
    DEFAULT(" ... ");

    final String message;
    
    FooErrorCode(String message) {
        this.message = message;
    }
    
    @Override
    public String defaultMessage() {
        return message;
    }
}

스위치문에 대한 패턴 매칭

다음 코드에서 e1, e2, e3는 캐스팅이 된 채로 사용할 수 있는 애들입니다만, 캐스팅된 인스턴스를 활용할 필요 없이 그냥 타입만 확인하고 싶을 때도 변수명은 써야 합니다.
(아마 자바 버전이 조금만 오르면 곧 _ 키워드가 불필요 변수 선언을 대체할 수 있게 해 줄 것으로 보입니다.)

실제로 써 보면 e1, e2, e3의 하위 기능을 직접 사용할 일이 많지는 않습니다.
각 클래스에서 공통되는 기능은 어차피 상위 인터페이스에 선언해 두니까, 스위치케이스를 쓰지 않고 오버라이딩으로 다 수행 가능합니다.

public final class ErrorMaps {
    private final Map<String, Integer> currentErrorMap = new HashMap<>();

    // NOTE 자료 구조에 타입별 다른 인덱스/키 등을 사용하는 예시. 무슨 기능인지는 무시하기 바람.
    public void updateCurrentErrorMap(ErrorCode errorCode) {
        String key = switch (errorCode) {
            case FooErrorCode ignore -> "currentFooErrorCode";
            case BarErrorCode ignore -> "currentBarErrorCode";
            case SampleErrorCode ignore -> "currentSampleErrorCode";
            case null -> throw new NullPointerException("에러 코드를 넣긴 해야지;;");
            default ->
                assert (false) : "에러 코드 클래스를 더 뒀는데 여기 안 썼구나";
        };
        
        this.currentErrorMap.put(key, errorCode.code());
    }
    
    // ...
}
profile
아 성장판 쑤셔 (블로그 이전) https://letsdev.hashnode.dev
post-custom-banner

1개의 댓글