본문 바로가기
언어와 프레임워크/Java

[Java] lazy evaluation(short circuit, loop fusion), Stream API

by Unagi_zoso 2023. 7. 26.

lazy evaluation이란 어느 조건이 이미 달성하여 생략해도 상관없는 그 다음 행위를 생략하는 일종의 최적화 기능이라 할 수 있습니다. 

 

비교 연산자에서의 short circuit이 그 예가 되겠습니다. 어느 조건에서 나머지 조건에 상관없이 그 결과가 정해졌을때 나머지 조건에 대한 연산은 실행하지 않는 것을 말합니다.

 

static boolean executeFlag = false;
public static void main(String[] args) {

    Supplier<Boolean> returnTrue = () -> true;
    Supplier<Boolean> returnFalse = () -> false;
    Supplier<Boolean> updateFlag = () -> {
        executeFlag = true;
        return true;
    };

    if (returnFalse.get() && updateFlag.get()) {

    }
    
    System.out.println(executeFlag);  // false
}

 

이 코드의 경우 if문의 두 번째 조건 updateFlag 함수에서 executeFlag를 true로 바꿔야함에도 불구하고

executeFlag의 출력결과는 false가 나왔습다. 

 

이미 returnFalse에서 False가 나오고 && 연산의 결과가 확정되었기에 updateFlag는 실행되지 않은 것 입니다.

즉, 필요하지 않은 연산은 하지 않습니다.

 


 

이러한 lazy한 특성은 Stream API에서도 엿볼 수 있습니다.

StreamAPI에는 중간 연산, 최종 연산이 있으며 0에서부터 다수 개의 중간 연산과 최후미의 최종 연산으로 만들어집니다. (파이프라인의 형태)

여기서 볼 수 있는 lazy한 요소는 다수 개의 중간 연산이 앞에 존재함에도 최종 연산을 만날 때 까지 중간 연산은 실행을 하지 않는 다는 것 입니다.

최종 연산에 달했을 때 다루게 될 전체 데이터에서 실질적으로 다루게될 데이터를 구해내어 

필요한 부분만 처리하고 필요하지 않은 부분은 처리하지 않습니다.

 

여기엔 Stream API 내부방식에 대하여 좀 알아야합니다.

먼저 현재 스트림 파이프라인의 구조를 파악하고 이후 최적화를 진행합니다.

Stream API에서는 loop fusion과 short circuit 을 볼 수 있습니다.

 

기존 파이프라인의 경우 앞 단의 행위가 다 끝난 다음 뒤의 행위가 시작될 것이라 볼 수 있습니다.

그러니 중간1 - 중간2 - 최종1 형태의 스트림 파이프라인이 있다면 중간1에서 모든 데이터를 다 적용한 후 처리된 스트림이 중간2로 넘어갈 것이라 예상할 수 있는데 이 때 중간1, 중간, 최종1 연속된 스트림에서 반복행위를 하나로 묶어도 결과가 순수하고 성능적으로도 최적화가 가능하다면 이러한 반복행위를 묶어줍니다.

이것이 loop fusion입니다.

 

그 다음으론 short circuit인데 Stream API 중 limit이라는 메서드가 있습니다. 

 

Integer[] ints = new Integer[] { 1, 2, 3, 4, 5, 6, 7 ,8 ,9, 10 };
Arrays.stream(ints)
        .filter((i) -> {
            System.out.println(i);
            return (i % 2) == 0;
        })
        .limit(3)
        .collect(Collectors.toList());

 

다음은 1 ~ 10까지의 정수에서 짝수를 세 개 골라 List로 만드는 코드입니다.

이 코드만 봤을 땐 filter 부분에서 1 ~ 10까지 접근하고 짝수인 부분만 스트림으로 전달될 것이라 예상할 수 있습니다.

 

출력의 결과는 다음과 같습니다.

 

1
2
3
4
5
6

 

limit에 의해 최초 3개의 정수를 얻기 위한 데이터만을 다루고 있습니다. 앞서 조건문에서의 short circuit과 마찬가지로 필요한 부분만을 다루고 그렇지 않은 부분은 생략하도록 최적화가 됩니다.

 

유의해야하는 부분도 존재합니다.

sorted() 같은 메서드의 경우 스트림 내 모든 요소들을 사용하기 스트림 중간에 생기게 되면 전체를 대상으로 최적화가 불가능해지고 sorted()를 기준이 되어 앞 뒤 두 부분으로 나눠져 최적화가 됩니다.

 

이러한 sorted의 성향을 봤을 때 Iterator 같은 무한스트림과 사용할 땐 더더욱 조심해야합니다. 스트림 내 모든 요소를 사용하기에 무한의 데이터를 다룰 순 없다. 꼭 limit() 등으로 데이터를 먼저 제한한 뒤에 다루어야합니다.

 

 

 

 

다음 글을 참조하였습니다.

 

모던 자바 인 액션

https://bugoverdose.github.io/development/stream-lazy-evaluation/

 

댓글