본문 바로가기
프로그래밍/Java

[Java] Java와 Lazy Evaluation

by 사바라다 2021. 3. 26.

Lazy evaluation은 실제로 필요로 해지는 경우에 연산을 시작하는 것입니다. 이 반대로는 eager evaluation이 있으며 이는 할당되자마다 연산을 시작합니다. 기본적인 Java의 기조는 eager evaluation을 기본으로합니다. 하지만 일부 Lazy Evaluation이 있었으며 Java 8이 나오면서 Java에는 Lazy Evaluation을 좀 더 유연하게 사용할 수 있게되었습니다.

오늘은 Lazy Evaluation의 예제와 Java 8에서 어떻게 사용할 수 있을지에 대해서 알아보는 시간을 가져보도록 하겠습니다.

예제

오늩 테스트를 위해 사용하는 메서드는 아래와 같습니다. 해당 메서드를 실행하게되면 1초간 sleep을 하고 입력받은 파라미터에 a가 포함되어있는지 여부로 true / false를 판단합니다.

static boolean compute(String str) {
    System.out.println("executing...");
    try {
        Thread.sleep(1000);
    } catch (InterruptedException ignore) {
    }
    return str.contains("a");
}

아래가 대표적인 Java에서의 Lazy evaluation 이해를 위한 기본적인 매커니즘을 나타낸 코드입니다. 단순한 3항 연산자로 보입니다. 왜 아래 문장은 Lazy evaluation의 기본적인 매커니즘 코드일까요 ? b2 조건문은 b1 조건문이 false라면 실행하지 않고 b1이 true라면 b2 조건문을 판단하기 때문입니다. 즉 b2는 b1의 연산결과에 따라서 조건의 실행 여부를 판단하기 때문에 Lazy evaluation이라고 할 수 있습니다. 뒤에 나올 Lazy Evaluation은 이를 응용한 것임을 잘 알아두시기 바랍니다.

static String eagerMatch(boolean b1, boolean b2) {
    return b1 && b2 ? "match" : "incompatible!";
}   

Eager evaluation

먼저 Lazy Evaluation 하지 않은 일반적인 Eager Evaluation에 대해서 알아보도록 하겠습니다.

아래 코드를 봐주시기 바랍니다. eagerMatch 메서드는 b1과 b2가 모두 true일 경우에 "match"라는 String을 반환하며 그렇지 않을 경우는 "incompatible!" String을 반환합니다.

그리고 해당 메서드를 사용하는 메서드를 보도록 하겠습니다. b1과 b2라는 변수를 파라미터로 제공하고 있습니다. 여기서 b1과 b2에는 각각 compute를 실행 시킨 결과 값이 할당되어 있습니다. 이 때 b1과 b2에 해당하는 computeeagerMatch에 관계없이 모두 실행되는 것을 로직적으로 알 수 있습니다.

@Test
public void solution_1() {
    boolean b1 = compute("Hello_1");
    boolean b2 = compute("Hello_2");
    eagerMatch(b1, b2);
}

static String eagerMatch(boolean b1, boolean b2) {
    return b1 && b2 ? "match" : "incompatible!";
}   

결과적으로 compute 2번이 항상 실행되기 때문에 위 테스트는 항상 2초 이상의 시간이 걸린다는 것을 알 수 있었습니다.

Lazy evaluation

이제는 Lazy evaluation이 발생할 수 있도록 코딩해보도록 하겠습니다.

자바 7 이전

Java 7 이전에서는 조건문에 직접 명시하는 방법밖에 방법이 없습니다. 이렇게 하면 결과값을 변수로받은 후 재활용이 안되는 단점이 있습니다. compute의 결과값을 재활용 하는 게 불가능 해지는 것입니다.

아래와 같이 사용한다면 Lazy evaluation에 대해서 알 수 있습니다. 아래 코드는 && 논리연산자로 묶여 있습니다. 이 경우 첫번째 compute("Hello_1")에 대한 평가가 먼저 이루어집니다. 그리고 true이면 compute("Hello")에 대한 평가를 하기 시작합니다. 만약 false라면 그대로 종료됩니다. 이렇게 코드를 작성했을 경우 1초의 지연만으로 "incompatible!" 이라는 결과를 얻을 수 있습니다.

@Test
public void solution_3() {
    String value = compute("Hello_1") && compute("Hello_2") ? "match" : "incompatible!";
}

자바 8 이후

Java 8에서는 다양한 Funtional 함수들이 추가 되었습니다. 그중에 Supplier라는 함수가 있습니다. 스펙은 아래와 같습니다. 즉 파라미터를 하나도 받지 않고 T라는 객체를 생성하는 인터페이스입니다.

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

이를 이용하여 Lazy evaluation을 구현해보도록 하겠습니다. 코드는 아래와 같습니다. 이렇게 하면 기존의 Lazy Evaluation의 한계였던 결과값을 이용하는 것이 가능하게 됩니다. 이 결과값에는 어떠한 객체도 들어갈 수 있기때문에 다양한 활용이 가능하게 됩니다.

@Test
public void solution_2() {
    Supplier<Boolean> a = () -> compute("Hello_1");
    Supplier<Boolean> b = () -> compute("Hello_2"); 
    lazyMatch(a, b);
}

static String lazyMatch(Supplier<Boolean> a, Supplier<Boolean> b) {
    return a.get() && b.get() ? "match" : "incompatible!";
}

장점

오늘은 이렇게 Lazy Evaluation에 대해서 알아보는 시간을 가져보았습니다. Lazy Evaluation의 최대장점은 필요하지 않은 연산은 하지 않는것이 가능하게 된다는 점입니다. 필요하지 않는 연산을 하지 않는다는 것은 결국 성능적으로 좋은 결과를 낳을 수 있습니다. 우리가 위에서 보았던 예제처럼 말이죠.

이러한 Lazy Evaluation 방식은 Java 8 Stream에서 기본이됩니다.

마무리

Java 8 Stream에서는 각 단계에 모든 원소에 대해서 연산을 하지 않고 필요로 하는 부분만 연산을 처리합니다.

마지막에 collect 또는 findFirst 등 이 부분이 어떻게 되는지에 따라서 연산하는 엘리멘트 수가 달라지는 것입니다. 다음 시간에는 Stream에서 Lazy evaluation이 어떻게 사용되는지 알아보도록 하겠습니다.

감사합니다.

참조

codemotion_lazy-java

slideshare_mariofusco_lazy-java

stackoverflow_does-java-have-lazy-evaluation

댓글