[TIL] 2023/09/14

yongkini ·2023년 9월 14일
0

Today I Learned

목록 보기
149/173
post-thumbnail

Today I Learned

Lodash 를 통한 함수 체이닝과 지연 평가의 효율성

지연 평가 정의 및 특징

: 이터러블과 관련해서 공부를 하다가 나온 지연 평가라는 말이 있다. 지연 평가란 일반적으로 loop를 돌 때 배열을 생성해놓고, 이를 순회하는 경우가 많은데, 이 경우처럼 데이터를 미리 메모리에 확보하는 게 아니라 데이터가 필요한 시점에 데이터를 생성한다는 의미라고 할 수 있다. 혹은 데이터 처리 작업을 실제로 필요할 때까지 미루고, 필요한 결과만 계산하는 기술이라고도 할 수 있다.

이러한 지연 평가는 함수형 프로그래밍의 일부이다. 지연 평가의 장점은

  • 메모리 절약 효과가 있다(일반적으로).
  • 시간 복잡도를 줄여준다(일반적으로).
  • 무한 수열을 다룰 수 있음.

이다.

파이프 라이닝

시간 복잡도 부분은 파이프 라이닝이라고도 한다. 예를 들어, map(...).filter(...) 식의 로직이 있을 때 본래는 각각 n번을 실행하기에 2n의 시간복잡도를 갖는다. 하지만 지연 평가를 활용하면(이터레이터 기법을 활용하면) n번의 순회로 처리할 수 있다.

코드로 설명하면

const source = Array.from({length: 200000},() => 0);
const func1 = (v) => v+1;
const func2 = (v) => v+1;
const func3 = (v) => v+1;

let result = [], temp1 = [], temp2 = [], temp3 = [];
console.time('test');
for(let i = 0; i < source.length; i++) {
   temp1[i] = func1(source[i]);
}

for(let i = 0; i < source.length; i++) {
   temp2[i] = func2(temp1[i]);
}

for(let i = 0; i < source.length; i++) {
   temp3[i] = func3(temp2[i]);
}

console.timeEnd('test'); // test: 9.310791015625 ms
result = temp3;

위와 같은 코드가 앞서 말한 2n의 시간 복잡도를 갖는(예시에선 3n) 예시이고, 즉, 엄격한 평가로 인한 비효율성을 안고 가는 코드이고,

const source = Array.from({length: 200000},() => 0);
const func1 = (v) => v+1;
const func2 = (v) => v+1;
const func3 = (v) => v+1;

let result = [];
console.time('test');

for(var i = 0; i < source.length; i++) {
   result[i] = func3(func2(func1(source[i])));
}
console.timeEnd('test'); // 4.152099609375 ms

결과 : 9.310791015625 ms(엄격한 평가 방식) > 4.152099609375 ms(지연 평가 방식)

위의 코드가 지연 평가를 통한(물론 실제로 지연 평가를 쓰진 않는다) 효율성을 갖고가는 코드와 유사하다. 즉, 좀 더 효율적인 코드이다. 지연 평가로 돼있는 코드 또한 위와 유사한 방식으로 동작한다.

이렇게 임시 배열이 존재하지 않는 경우 극적인 성능 향상이 가능하다. 특히 배열이 위의 예시에는 요소 개수가 20만이었는데, 10만, 1억 등으로 늘어날 경우 메모리 접근이 비싸지게 되는데, 이런 경우에는 더욱 효율적일 것이다.

하지만, 이러한 장점이 있다고 할 때, 그럼 무조건 지연 평가를 쓰는게 좋은걸까? 라고 하면 대답은 NO다. 만약 여러개의 반복 로직이 복잡하게 엮여있는 경우라면 성능이 좋은 것이 명확하지만, 단순 반복의 경우 map, filter 등이 더 좋다(이유는 map, filter 등은 언어 자체에서 지원하는 기능이기에 그 로직이 더 효율적으로 짜여져있고, 제너레이터를 이용하는 경우, 즉, 지연 평가를 이용하는 경우에는 next() 등의 메서드 실행을 통해 다음 순회로 넘어가는데, JS의 경우 명령어 처리 효율이 앞서 말한 map, filter 등의 로우 레벨 언어로 구현된 것보다 안좋기 때문에 그허다고 한다).

결론적으로 언제 지연 평가를 쓰면 좋을까?
: 순회 로직이 여러개 엮여있을 때 쓰면 좋다.
ex) [...someArray].map(...).filter(...).map(...)

Lodash로 지연 평가를 구현한다?

UI 상으로 뭔가 보여주려고 code sandbox를 이용한건 아니기에 console창과 mjs 코드를 보면 될 것 같다. 포인트는 lodash의 chain
** chain() 메서드를 호출하면 원래 배열이나 컬렉션을 Lodash 체인으로 래핑하고, 그 이후에 체인 내부에서 호출되는 다양한 메서드들은 실제 계산을 미루고 중간 결과를 저장하지 않습니다.
메서드와 take(iterator.prototype.take), 마지막으로 value() 라는 lodash의 메서드를 활용해서 지연 평가를 구현한 방법과 일반적인 함수형 프로그래밍 방식의 메서드를 사용해서 1억개의 요소를 갖는 배열을 바탕으로 같은 작업을 시켜봤다. 그 결과는 위와 같았다. test2가 일반적인 로직을 쓴거고(엄격한 평가) test1이 지연평가를 쓴 것이다(lodash로). ms 단위이기 때문에 사실상 그렇게 큰 차이가 느껴지는지는 알 수 없다. 하지만 이를 연산 회수(즉, 시간 복잡도) 측면에서 보면 차이가 꽤나 크다.

const result1 = _
  .chain(arr)
  .map((num) => {
    return num + 1
  })
  .filter((num) => {
    return num % 2
  })
  .take(100000) 
  .value();

lodash를 써서 구현한 코드인데, 이 코드는 위에서 보여준 예시처럼

const source = Array.from({length: 200000},() => 0);
const func1 = (v) => v+1;
const func2 = (v) => v+1;
const func3 = (v) => v+1;

let result = [];
console.time('test');

for(var i = 0; i < source.length; i++) {
   result[i] = func3(func2(func1(source[i])));
}
console.timeEnd('test'); // 4.152099609375 ms

이런식으로 동작한다고 보면 된다(chain이 그걸 가능하게 해주고, value로 마지막에 래핑된걸 풀어주는 역할을 한다). 따라서, lodash를 이용한 구현에는 arr의 길이가 1억이라도 로직상으로 홀수인 숫자를 100000개(앞에서부터) 찾을 때까지만 연산을 수행하게 되므로 대략 200,000번의 연산을 수행한다. 하지만, test2의 경우 1억개의 요소를 전부 map을 통해 순회하고, 새로운 배열(중간 저장과정)을 리턴하고, 그 배열을 바탕으로 또다시 1억번의 filter 메서드 순회를 하고, 거기서는 홀수만 걸러졌을 것이기에 홀수 개수에 대해서 slice를 진행하는데 slice 자체도 O(n)의 시간복잡도를 갖기에 여기에 100000 정도가 추가될 것이다(slice의 연산 회수는 정확히는 모르겠다). 이에 따라 대략적으로 test2의 연산 회수는 1억 + 1억 + 10만 = 200,100,000 정도가 된다. 앞서 test1의 연산 회수가 대략 20만번이라고 했었는데 거의 1000배가 차이난다. 위에서 ms 단위로는 큰 차이를 못느꼈지만 이게 배열의 크기가 더 커졌다고 했을 때는 연산 회수로 치면 처리 속도에서도 확연한 차이가 날 것이라고 본다.

lodash는 cloneDeep 등의 메서드를 제공하면서 개발자들에게 편의를 제공함과 동시에 최대한의 효율을 제공하려고 노력하는 편인 것 같다. 오늘 배운 지연 평가 부분도 분명히 나중에 써먹을 곳이 있을 것 같다.

회고

지연 평가, 엄격한 평가, 이터러블, 메모리 및 속도 효율성 관련해서 좀 더 공부를 해보면 재밌을 것 같다.

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글