Lazy Evaluation
지연 평가는 값을 선언한 순간 바로 계산하지 않고, 정말 그 값이 필요해질 때까지 계산을 미루는 전략입니다. 표현식은 먼저 '이걸 어떻게 계산할지'만 들고 있다가, 소비 지점이 생기면 그때 비로소 실행됩니다. 덕분에 무한 시퀀스처럼 끝이 없는 값도 다룰 수 있고, 전체 중 일부만 필요할 때는 나머지 계산을 통째로 건너뛸 수 있습니다. 함수형 프로그래밍에서 계산 시점 자체를 설계 대상으로 보는 대표적인 감각입니다.
▶아키텍처 다이어그램
🔄 프로세스 다이어그램점선 애니메이션은 데이터 또는 요청의 흐름 방향을 나타냅니다
즉시 평가에서는 표현식을 쓰는 순간 계산이 시작됩니다. map, filter를 길게 이어 놓고 마지막에 앞부분 몇 개만 써도, 그 전에 중간 단계가 전부 돌고 중간 배열까지 만들어집니다. 조건문에서도 앞쪽 값만으로 답이 이미 정해졌는데 뒤쪽의 무거운 계산이 먼저 실행되면 그냥 낭비입니다. 더 극단적으로는 자연수 전체 같은 무한 시퀀스는 즉시 평가로는 아예 다룰 수조차 없습니다. '지금 꼭 필요한 만큼만 계산하자'는 다른 실행 전략이 필요한 순간이 생기고, 그 답이 지연 평가입니다.
함수형 언어 중 Haskell은 지연 평가를 기본 실행 모델로 채택해, 무한 리스트와 고수준 합성을 자연스럽게 다루도록 했습니다. 주류 언어는 대개 즉시 평가를 기본으로 하지만, generator, iterator, stream, short-circuit 연산자처럼 지연 평가의 조각들을 꾸준히 받아들였습니다. 대용량 데이터 처리와 반응형 프로그래밍이 중요해지면서, '언제 계산할 것인가'는 성능 최적화가 아니라 API 설계 문제로까지 올라왔습니다. 지연 평가는 계산량과 메모리 사용량을 소비 패턴에 맞춰 조절하려는 흐름 속에서 더 자주 논의되기 시작했습니다.
지금 실무에서 지연 평가는 언어 전체가 lazy하냐 아니냐의 문제라기보다, eager가 기본인 환경에서 일부 구간만 늦게 계산하는 국소적 도구로 보는 편이 더 정확합니다. JavaScript의 generator, iterator, short-circuit 연산, 스트림 API가 전형적인 예입니다. 그래서 요즘의 질문은 '전부 미룰까?'보다 '이 값, 지금 정말 끝까지 계산해야 하나?'에 가깝습니다. 다만 로그나 네트워크 호출처럼 부작용이 섞이면 실행 시점이 뒤로 밀리는 것 자체가 혼란을 만들 수 있어서, 보통은 순수한 계산 경계 안에서 선택적으로 쓰는 편이 안전합니다.
지연 평가는 보통 계산을 직접 실행하지 않고 thunk, iterator, generator 같은 래퍼 안에 넣어 두는 방식으로 구현됩니다. 이 래퍼는 '나중에 계산하는 방법'만 들고 있고, 실제 값은 아직 없습니다. 소비자가 next()를 호출하거나, 조건식이 오른쪽 피연산자를 정말 필요로 하거나, UI가 특정 구간 데이터를 실제로 렌더링할 때 계산이 트리거됩니다. 그때도 전체를 한 번에 계산하지 않고 필요한 부분만 평가할 수 있습니다. 어떤 구현은 한 번 계산한 결과를 캐시해 재사용하고, 어떤 구현은 매 요청마다 다시 계산합니다. 핵심은 계산 시점이 정의 시점이 아니라 소비 시점으로 밀려 있다는 점입니다.
필요한 순간에만 값을 만드는 generator
function* naturals() {
let n = 0;
while (true) {
yield n;
n += 1;
}
}
const nums = naturals();
nums.next().value; // 0
nums.next().value; // 1
nums.next().value; // 2
// 전체 자연수를 미리 만들지 않고,
// next()가 호출될 때마다 하나씩 계산합니다.generator는 모든 값을 미리 만들지 않습니다. 소비자가 next()를 호출할 때마다 그 순간 필요한 다음 값만 계산해 내보내므로, 무한 시퀀스도 다룰 수 있습니다.
지연 평가는 map, filter, reduce 같은 함수형 변환과 경쟁하는 개념이 아니라 계산 시점을 다루는 개념입니다. 같은 변환 파이프라인이라도 배열 메서드 체인은 보통 각 단계를 즉시 실행해 중간 결과를 만들고, 지연 평가는 소비자가 필요로 하는 순간까지 그 계산을 미룹니다. 이 차이는 값의 의미보다는 실행 타이밍과 비용 구조에 영향을 줍니다. 다만 지연 평가와 부작용은 긴장 관계가 있습니다. 계산 시점이 뒤로 밀리면 로그, 네트워크 호출, 예외가 언제 발생하는지 직관이 흐려질 수 있기 때문입니다. 그래서 지연 평가는 순수한 계산과 함께 쓸 때 가장 예측 가능하고, 부작용이 섞이면 실행 타이밍을 더 조심해서 다뤄야 합니다.
Gain 필요한 값만 계산하므로 큰 데이터 흐름에서 불필요한 연산과 메모리 사용을 줄일 수 있고, 무한 시퀀스 같은 표현도 가능해집니다. Cost 실제 계산이 언제 일어나는지 코드 표면만 보고는 덜 분명해질 수 있습니다. 잘못 설계하면 계산이 너무 늦게 터져 디버깅이 어려워지고, 평가를 미룬 표현식이 오래 살아남아 메모리를 붙잡는 공간 누수(space leak) 문제가 생길 수 있습니다. Decision Scale 전체 결과를 다 소비하지 않는 흐름, 스트리밍, 긴 파이프라인, 무한 또는 매우 큰 데이터 구조에서는 지연 평가의 이점이 큽니다. 반대로 데이터가 작고 실행 시점을 명확히 드러내는 것이 더 중요한 코드에서는 즉시 평가가 더 단순합니다.
지연 평가는 generator 기반 순회, iterator 파이프라인, 검색 결과 앞부분만 읽는 스트리밍 처리, 조건이 맞을 때만 무거운 대체 계산을 돌리는 로직에서 자주 보입니다. 프론트엔드에서는 긴 목록 전체를 미리 계산하지 않고, 실제로 보이는 구간만 준비하는 방식으로도 이어집니다. 실무에서는 '이 값을 정말 지금 전부 만들어야 하나?'라는 질문이 나올 때 지연 평가가 후보로 올라옵니다. 핵심은 계산량 자체보다, 소비 방식이 설계를 어떻게 바꾸는지 보는 데 있습니다.