...
지연 평가란?
컴퓨터 프로그래밍에서 지연 평가(Lazy Evaluation)는 함수형 프로그래밍에서 자주 사용되는 개념이다.
지연 평가는 단어 그대로 계산이 필요한 시점까지 계산을 미루는 것을 의미한다. 즉, 지연 평가는 값을 계산하는 시점을 늦추어서 불필요한 계산을 방지하고 시스템의 성능을 향상시킬 수 있게 된다. 이를 통해, 메모리 사용량이 감소하고 프로그램의 반응성이 향상된다.
지연 평가 동작 원리 #1
지연 평가(Lazy Evaluation)와 엄격한 평가(strict evaluation)의 동작 방식과 비교를 통해 지연 평가의 동작 방식을 알아보자.
다음 코드와 같이, 0~5로 이루어진 배열에서 각 원소에 대해 10을 곱한 뒤 홀수만 고르고 숫자를 문자로 바꾸고 첫2개만 추출하는 로직을 자바스크립트 ES6와 Lodash 에서 어떤식으로 평가가 되는지 알아보자.
const arr = [0, 1, 2, 3, 4, 5]
const result = arr
.map(num => num + 10)
.filter(num => num % 2)
.slice(0, 2)
console.log(result)
ES6 엄격한 평가
엄격한 평가는 지연 평가의 반대말로, 우리가 일반적으로 생각하는 평가 방식이다. 엄격한 평가는 평가 흐름이 왼쪽에서 오른쪽으로 흐른다.
const arr = [0, 1, 2, 3, 4, 5]
const result = arr
.map(num => num + 10) // 1. 모든 배열원소에 대해 10을 더한다 → [0, 11, 12, 13, 14, 15]
.filter(num => num % 2) // 2. 모든 배열원소에 대해 홀수만 구한다 → [11, 13, 15]
.slice(0, 2) // 3. 모든 배열원소에 대해 2개만 추출한다 → [11, 13]
console.log(result) // [11, 13]
위 실행 흐름을 보면 map, filter, slice 고차함수들이 각각의 계산이 모두 종료되어야 다음 단계를 수행하는 것을 볼 수 있다. 그래서 총 계산 횟수를 구하자면 map 6번 + filter 6번 + slice 2번으로 총 14번이 된다.
코드 상으로는 문제는 없지만 생각을 환기해보자.
최종 결과물인 11과 13을 얻기 위해서 map을 통해 모든 배열 원소에 덧셈을 하고, 홀수를 구하기 위해 모든 배열 원소에 필터링을하였다. 하지만 위의 그림을 보면 11과 13을 얻기 위해 배열 원소 0 ~ 3 까지는 평가 되야 할테지만 그 뒤의 4와 5는 논리적으로 평가할 필요가 없어 보인다. 즉, 쓸데없는 연산이 들어간 것과 같다.
Lodash 지연 평가
다음은 위의 예제를 Lodash 함수를 이용한 지연 평가를 활용해 예제를 수행한 코드이다.
Loadash 라이브러리에서는 _.chain , _.value 함수를 이용해 지연 평가를 구현할 수 있다. chain 함수의 인자로 사용할 데이터를 넘기면 해당 데이터를 lodash 객체로 감싼다. 이후 제공되는 지연 평가 함수들을 이용해 원하는 결과로 가공한다. 마지막으로 value 함수를 통해 wrapping 되어 있는 결과를 실제 값으로 치환한다.
const arr = [0, 1, 2, 3, 4, 5]
const result = _.chain(arr) // 일반배열을 lodash 체인 객체로 변환한다.
.map(num => num + 10)
.filter(num => num % 2)
.take(2)
.value() // 지연 평가된 결과가 평가되어 실제값으로 변환
console.log(result) // [11, 13]
위 그림에서 볼 수 있듯이 지연 평가는 평가 흐름은 위에서 아래로 흐른다. 이러한 흐름의 장점은 배열 원소 3 까지 평가가 완료되었을 때 이미 원하는 결과가 나왔기 때문에, 4와 5에 대한 계산은 하지 않는걸 볼 수 있다. 따라서 총 계산 횟수를 구하자면 map 4번 + filter 4번 + slice 2번으로 총 10번이 된다.
정리하자면 지연 평가를 사용하면 필요한 계산만 수행하므로, 성능 향상에도 도움이 되게 된다.
지연 평가 동작 원리 #2
이번엔 좀더 시각적으로 애니메이션을 통해 지연 평가의 성능 향상 원리에 대해 살펴보자
ES6 엄격한 평가
아래 애니메이션을 보면 먼저 모든 배열 원소에 대해 10보다 작은 숫자만 따로 임시 배열에 한꺼번에 모아놓고 원소 3개만 가져가는 것을 볼 수 있다. 즉, filter를 모두 수행하고 마지막으로 slice를 수행한다.
const arr = [4, 15, 20, 7, 3, 13, 2, 20]
const result = arr
.filter(num => num < 10) // 1. 10보다 작은 숫자만 필터링 → [4, 7, 3, 2]
.slice(0, 3) // 2. 3개만 고름 → [4, 7, 3]
console.log(result); // [4, 7, 3]
Lodash 지연 평가
지연 평가가 적용된 애니메이션을 보면 배열 원소마다 각각 filter를 한번 수행하고 slice를 수행하고 다음 원소로 넘어가는 식으로 평가가 수행됨을 볼 수 있다. 그리고 남은 13, 2, 20 원소에 대해선 평가 연산을 적용하지 않아 훨씬 더 빠르게 결과값을 얻게 된다.
const arr = [4, 15, 20, 7, 3, 13, 2, 20];
const result = _.chain(arr)
.filter(num => num < 10) // 지연 평가를 시작합니다.
.take(3)
.value(); // 평가를 수행합니다.
console.log(result); // [4, 7, 3]
이처럼 지연 평가는 연산 횟수 자체를 줄여 보다 좋은 성능을 준다. 그리고 지연 평가는 대상이 크면 클수록 그 효과를 발휘한다.
다음은 위의 예제 코드에서 배열의 원소 개수를 1,000,000개로 수정한 뒤 적용한 성능 비교 결과이다. 그래프에서 보듯 지연 평가가 압도적인 성능을 자랑한다.
# 참고자료
https://lodash.com/docs/4.17.15#chain
https://blog.weirdx.io/post/16662
https://armadillo-dev.github.io/javascript/whit-is-lazy-evaluation/
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.