Java - Reactor switchIfEmpty 사용시 주의점(Lambda, 람다 Lazy Evaluation)

2020. 7. 29. 18:12프로그래밍언어/Java&Servlet

리액터의 switchIfEmpty라는 메서드를 다루기 전에 자바의 Lazy evaluation(지연 평가)에 대해 다루어보자. 자바는 논리 operation을 평가할때 lazy evaluation을 사용한다. 예제 코드를 예로 들면 아래와 같다.

 

@Test
void lazyEvaluationTest() {
    boolean isLazy = lazyEvaluation();
    if (true || isLazy) {
        System.out.println("method execute!!!");
    }
}
private boolean lazyEvaluation() {
    System.out.println("lazy evaluation");
    return true;
}

 

위 테스트에서 결과는 어떻게 될 것인가?

 

lazy evaluation
method execute!!!

 

위와 같이 사용하지 않는 boolean 변수이지만, 미리 isLazy를 만들기 위해 메서드를 호출한다. 여기서 사용하지 않는 변수라는 뜻은 자바에서는 논리 operation에서 이미 참 혹은 거짓이라고 판단이 난 논리식이면 나머지 값 자체를 참조하지 않는다. 이 예는 위와 같이 true || isLazy일때, 이미 앞의 true에서 참이 되었기에 뒤의 isLazy 자체를 참조하지 않는다. 이것을 테스트 하기 위해 아래와 같이 코드를 만들어보았다.

 

@Test
void lazyEvaluationTest() {
    if (true || lazyEvaluation()) {
        System.out.println("method execute!!!");
    }
}

private boolean lazyEvaluation() {
    System.out.println("lazy evaluation");
    return true;
}

 

위 예제코드에서는 lazyEvaluation()를 호출자체를 하지 않는다. 그렇다면 아래와 같은 코드는 결과가 어떻게 될까?

 

@Test
void lazyEvaluationTest() {
    System.out.println("before boolean operation");
    if (true && lazyEvaluation()) {
        System.out.println("method execute!!!");
    }
}

private boolean lazyEvaluation() {
    System.out.println("lazy evaluation");
    return true;
}

 

결과는 아래와 같다.

 

before boolean operation
lazy evaluation
method execute!!!

 

실제 lazyEvaluation()가 필요한 시점에 호출하게 되는 것이다. 여기까지 자바에서 논리 operation은 lazy evaluation 한다는 것은 알았고, 다른 상황에서는 어떻게 될까?

 

자바는 어떠한 지역변수에 값을 할당할때, 그리고 어떠한 메서드의 매개변수를 만들때는 eager evaluation 전략을 따른다. 그 말은 무엇이냐면 어떠한 메서드를 처리하기 전에 그 메서드의 매개변수의 값이 미리 준비가 되어 있어야한다는 것이다. 아래 예제코드를 한번 살펴보자.

 

@Test
void eagerEvaluationTest() {
    System.out.println(convertBooleanToString(eagerEvaluation(true)));
}

private String convertBooleanToString(final boolean bool) {
    System.out.println("convertBooleanToString");
    return bool ? "true" : "false";
}

private boolean eagerEvaluation(final boolean bool) {
    System.out.println("eager evaluation!!!!");
    return bool;
}

 

위 메서드의 결과는 어떻게 될까?

 

eager evaluation!!!!
convertBooleanToString
true

 

convertBooleanToString을 실행시키기전에 매개변수의 값을 미리 만들기 위해서 eagerEvaluation 메서드를 호출해 값을 만든다. 그렇다면 lazy evaluation하게 하려면 어떻게 하면 될까?

 

@Test
void lazyEvaluationTest2() {
    System.out.println(convertBooleanToString(() -> lazyEval(true)));
}

private String convertBooleanToString(final Supplier<Boolean> f) {
    System.out.println("convertBooleanToString");
    return f.get() ? "true" : "false";
}

private boolean lazyEval(final boolean bool) {
    System.out.println("lazy evaluation");
    return bool;
}

 

위와 같이 매개변수로 람다를 전달하면 된다. 람다를 전달하게 되면 해당 람다가 사용되는 시점에 메서드를 호출하기 때문에 lazy하게 프로그래밍하게 할 수 있다. 이것은 꼭 실행하지 않아도될 로직을 처리하지 않게 할 수 있고, 혹은 실행 시점을 뒤로 미룰 수도 있기 때문에 알고 있으면 아주 좋은 개념이 될것이다. 위 예제를 결과는 아래와 같다.

 

convertBooleanToString
lazy evaluation
true

 

실행 순서가 바뀐 것을 볼 수 있다. 그리고 매서드의 매개변수로 담기는 메서드의 lazy 로딩하는 것 외에 지역변수의 초기화를 지연 시킬 수 도 있다.

 

@Test
void lazyEvaluationTest3() {
    Supplier<Boolean> supplier = () -> lazyEval(true);
    System.out.println("before method");
    if (supplier.get()) {
        System.out.println("method !!");
    }
}

 

여기까지 자바의 eager, lazy evaluation을 다루어보았고, 이제 본론으로 switchIfEmpty를 사용할때 주의해야할 점을 알아보자 !

 

@Test
void switchIfEmptyTest() {
    Mono.just("str")
            .map(s -> {
                System.out.println(s);
                return s;
            })
            .switchIfEmpty(defaultStr())
            .subscribe();
}

private Mono<String> defaultStr() {
    System.out.println("defalutStr");
    return Mono.just("default");
}

<console>
defalutStr
str

 

위 코드를 보면 우리는 보통 Mono.just가 Mono.empty를 리턴하면 switchIfEmpty를 실행하겠지? 라는 생각을 하기 쉽다. 하지만, 실제 동작은 그렇지 않다. 자바에서는 보통 메서드의 매개변수의 값을 미리 결정시켜놓으려한다.(eager evaluation) 그래서 실제 map을 실행하기 전에 switchIfEmpty를 먼저 실행시켜 값을 만들어 놓는다. 만약 switchIfEmpty안에 실행되는 메서드가 비용이 크고, 실제로 Mono.just는 empty를 반환하지 않는다면, 굳이 실행되지 않아도 되는 비싼 비용의 메서드를 호출해야한다. 이럴때는 우리는 lazy evaluation 전략을 사용하여 switchIfEmpty안의 실행을 실제 필요시점으로 미룰 수 있게 하는 것이다. 

 

@Test
void switchIfEmptyTest2() {
    Mono.just("str")
            .map(s -> {
                System.out.println(s);
                return s;
            })
            .switchIfEmpty(Mono.defer(this::lazyDefaultStr))
            .subscribe();
}

@Test
void switchIfEmptyTest3() {
    Mono.just("str")
            .map(s -> {
                System.out.println(s);
                return s;
            })
            .switchIfEmpty(Mono.fromSupplier(() -> "defaultStr"))
            .subscribe();
}

private Mono<String> lazyDefaultStr() {
    System.out.println("defalutStr");
    return Mono.just("default");
}

<console>
str

 

위와 같이 Mono.defer 혹은 Mono.fromSupplier를 사용하면 해당 메서드의 매개변수로 Supplier를 넘기기 때문에 미리 값을 만들어 놓지 않고, 실제 호출되는 시점으로 실행을 지연시킬 수 있다. 위 코드에서는 아예 메서드 호출자체를 하지도 않는다. 필자도 지금까지는 이러한 것을 크게 인지하지 못하고 코딩을 했었는데, 이렇게 lazy programming을 조금 생각하고 코딩하게 된다면 조금이라도 성능향상을 할 수 있지 않을까 생각이 든다.