웹 개발 메모장

[자바] Lazy Evaluation 이란? 본문

옛날../자바

[자바] Lazy Evaluation 이란?

도로롱주 2019. 1. 25. 14:22







Lazy Evaluation


직역하면 "게으른 연산"인데, 연산을 불필요한 연산을 피하기 위해 연산을 지연시키는 것을 말합니다.



예를 들어 보겠습니다.


아래의 코드는 1~10까지의 정수를 갖는 List에서 6보다 작고짝수인 요소를 찾아 10배 시킨 리스트를 출력하는 코드입니다.


1
2
3
4
5
6
7
8
9
final List<Integer> list = Arrays.asList(12345678910);
 
System.out.println(
    list.stream()
        .filter(i -> i<6)
        .filter(i -> i%2==0)
        .map(i -> i*10)
        .collect(Collectors.toList())
);
cs


당연히 아래와 같은 결과를 출력합니다.


[20, 40]
cs



함수형 프로그래밍을 모르는 사람이 처음 위의 코드를 접했을 때,


위 코드의 동작 방식을


1. 1~10까지 요소들 중 6보다 작은 값들을 구하고

2. 1번에서 구한 요소들 중 짝수인 값들을 구하고

3. 2번에서 구한 요소들에 10을 곱해준다.


라고 생각할 수 있지만 그렇지 않습니다.

[그러한 방식은 Eager Evaluation(조급한 연산)]



실제로 어떤 방식으로 동작하는지 테스트 해보기 위해 아래와 같이 코드를 조금 수정해서 실행시켜 보면,


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final List<Integer> list = Arrays.asList(12345678910);
 
System.out.println(
    list.stream()
        .filter(i -> {
            System.out.println("i < 6");
            return i<6;
        })
        .filter(i -> {
            System.out.println("i%2 == 0");
            return i%2==0;
        })
        .map(i -> {
            System.out.println("i = i*10");
            return i*10;
        })
        .collect(Collectors.toList())
);
cs


아래와 같은 결과가 출력됩니다.


i < 6
i%2 == 0
i < 6
i%2 == 0
i = i*10
i < 6
i%2 == 0
i < 6
i%2 == 0
i = i*10
i < 6
i%2 == 0
i < 6
i < 6
i < 6
i < 6
i < 6
[20, 40]
cs


결과를 보면 아시겠지만, 각 요소들에 대해 순차적으로 아래 1~3번 을 진행합니다.


1. 6보다 작은지 검사한다. ( false 이면 2, 3번 과정 패스하고 다음 요소 진행 )

2. 짝수인지 검사한다. ( false 이면 3번 과정 패스하고 다음 요소 진행 )

3. 요소에 10을 곱해준다.


이렇듯 Lazy 방식은 당장에 해결해야할 문제들이 차례로 주어지더라도 마지막 문제를 제공받을 때 까지 게으르게 기다리다가 마지막 문제를 알게되면, 그때 연산을 시작함으로써 결과를 얻기 위해 필요하지 않은 연산은 수행하지 않게 됩니다.

( 만능이라는 이야기가 아니라 연산 방식(순서)에 관한 이야기 )



그렇다면, Lazy Evaluation이 말하는


"불필요한 연산을 피한다."


의 장점을 잘 보여주도록 코드를 조금 수정해 보겠습니다.


아래의 코드는 1~10까지의 정수를 갖는 List에서 6보다 작고짝수인 요소를 찾아 10을 곱한 리스트 중 첫 번째 요소를 출력하는 코드입니다.


1
2
3
4
5
6
7
8
9
10
final List<Integer> list = Arrays.asList(12345678910);
 
System.out.println(
    list.stream()
        .filter(i -> i<6)
        .filter(i -> i%2==0)
        .map(i -> i*10)
        .findFirst()
        .get()
);
cs


당연히 아래와 같은 결과를 출력합니다.


20
cs


위의 코드가 만약 Eager Evaluation 방식으로 동작한다면,


1. 모든 요소들에 대해 6보다 작은지 체크해야하고,

2. 1번에서 구한 모든 요소들에 대해 짝수인지 체크해야하고,

3. 2번에서 구한 모든 요소들에 10을 곱해주고,

4. 3번의 요소들 중 첫 번째 요소를 반환해야 합니다.


실제로 어떻게 동작하는지 살펴보기 위해 코드를 수정해서 실행시켜 봅시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
final List<Integer> list = Arrays.asList(12345678910);
 
System.out.println(
    list.stream()
        .filter(i -> {
            System.out.println("i < 6");
            return i<6;
        })
        .filter(i -> {
            System.out.println("i%2 == 0");
            return i%2==0;
        })
        .map(i -> {
            System.out.println("i = i*10");
            return i*10;
        })
        .findFirst()
        .get()
);
cs


위 코드에 해당하는 출력 결과는 아래와 같습니다.


i < 6
i%2 == 0
i < 6
i%2 == 0
i = i*10
20
cs


순차적으로 연산을 진행하다가 원하는 값인 첫 번째 요소의 값을 구하면 나머지 연산을 피하는 것을 볼 수 있습니다.



이해하기 쉬운 이미지 파일이 있어서 첨부했습니다.

( 이미지 출처: https://edykim.com/ko/post/introduction-to-lodashs-delay-evaluation-by-filip-zawada/ )


[Eager Evaluation]



[Lazy Evaluation]


같은 동작을 하는 반복문을 이용한 코딩과 비교해보면,


1
2
3
4
5
6
7
8
9
10
11
12
13
final List<Integer> list = Arrays.asList(12345678910);
 
int resultInt=0;
for (Integer i : list) {
    if(i<6) {
        if(i%2==0) {
            i*=10;
            resultInt = i;
            break;
        }
    }
}
System.out.println(resultInt);
cs


함수형 프로그래밍은 위의 반복문을 통한 코딩에 비해 가독성이 좋고, 실수할 여지가 줄어드는 장점이 있습니다.





Lazy Evaluation 을 통해 효율적인 동작을 이끌어낼 수 있는 다른 예


파라미터로 받아온 value를 출력하는 다음과 같은 메소드가 있습니다.


1
2
3
4
5
6
private static void getValueUsingMethodResult(boolean valid, String value) {
    if(valid)
        System.out.println("Success: The value is "+value);
    else
        System.out.println("Failed: Invalid action");
}
cs


위 메소드를 호출할 때, 아래의 코드와 같이 매개 변수 value실행시간이 1초 이상 걸리는 메소드의 결과 넘겨준다고 한다면,


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
    long startTime = System.currentTimeMillis();
 
    getValueUsingMethodResult(true, getExpensiveValue());
    getValueUsingMethodResult(false, getExpensiveValue());
    getValueUsingMethodResult(false, getExpensiveValue());
 
    System.out.println("passed Time: "+ (System.currentTimeMillis()-startTime)/1000+"sec" );
}
 
private static String getExpensiveValue() {
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
    return "Hello World";
}
cs


boolean valid 의 값에 관계 없이, getExpensiveValue() 메소드가 호출되기 때문에 아래 결과를 얻기까지 3초가 경과하게 됩니다.


Success: The value is Hello World
Failed: Invalid action
Failed: Invalid action
passed Time: 3sec
cs



getValueUsingMethodResult()를 조금 수정해서, String value 대신 Functional Interface 인 Supplier<T> 를 매개 변수로 전달하는 getValueUsingSupplier() 메소드를 생성해 보았습니다.


1
2
3
4
5
6
private static void getValueUsingSupplier(boolean valid, Supplier<String> valueSupplier) {
    if(valid)
        System.out.println("Success: The value is "+valueSupplier.get());
    else
        System.out.println("Failed: Invalid action");
}
cs


이 경우에는 메소드의 반환 값이 아닌 함수가 전달되었기 때문에, valueSupplier.get() 을 호출할 때만,

getExpensiveValue() 메소드가 호출됩니다.


main 메소드에서의 호출부 역시 수정해 주면 아래와 같은 결과가 출력됩니다.


1
2
3
getValueUsingSupplier(true, () -> getExpensiveValue());
getValueUsingSupplier(false, () -> getExpensiveValue());
getValueUsingSupplier(false, () -> getExpensiveValue());
cs


Success: The value is Hello World
Failed: Invalid action
Failed: Invalid action
passed Time: 1sec
cs


따라서 위와 같이 1초만에 같은 결과를 얻을 수 있게 됩니다.




Comments