Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
373 changes: 373 additions & 0 deletions contents/09.메모이제이션/1.memoize함수.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,373 @@
#### [back](../../README.md)    |    write by [sangcho][sangcho]

# 1. memoize 함수

> 이 글은 함수형 자바스크립트 프로그래밍을 참고하여 기록하였습니다.

<br>

메모이제이션은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여, 프로그램 실행 속도를 빠르게 하는 기술.

## 1. 메모이제이션 코드로 이해하기

#### 1.1 memoize

```typescript
function memoize(func) { // memoize는 함수를 받는 함수다.
var cache = {}; // 이 객체에 결과를 남겨둘 것이다.
return function(arg) {
if (cache[arg]) { // 이미 동일 인자에 대한 결과가 있으면 리턴
console.log('캐시로 결과 바로 리턴', arg);
return cache[arg];
}
console.log('본체 실행', arg);
return cache[arg] = func.apply(this, arguments); // 받아둔 함수를 실행하면서 결과를 cache에 남겨둠
}
}

------------------------------------------------------------
// 실행
var mult5 = memoize(function(a) {
return a * 5;
});

console.log( mult5(1) );
// 본체 실행 1
// 5

console.log( mult5(2) );
// 본체 실행 2
// 10

console.log( mult5(1) );
// 캐시로 결과 바로 리턴 1
// 5

console.log( mult5(1) );
// 캐시로 결과 바로 리턴 1
// 5
```
[실행코드](https://codesandbox.io/s/hamsuhyeongpeurogeuraeming-9jang-lzh8jz?file=/src/9.1.js)

<br/>

#### 1.2 memoize의 한계

```typescript
var add = memoize(function (a, b) {
return a + b;
});

console.log(add(3, 5));
// 본체 실행 3
// 8

console.log(add(3, 10));
// 캐시로 결과 바로 리턴 3
// 8
// 캐시가 동작했지만 3에만 의존하기 때문에 오류

var keys = memoize(function (obj) {
return _.keys(obj);
});

console.log(keys({ a: 1, b: 2 }));
// 본체 실행 Object {a: 1, b: 2}
// ["a", "b"]

console.log(keys({ a: 1, b: 2 }));
// 캐시로 결과 바로 리턴 Object {a: 1, b: 2}
// ["a", "b"]

console.log(keys({ a: 10, b: 20 }));
// 캐시로 결과 바로 리턴 Object {a: 1, b: 2}
// ["a", "b"]
// 잘 동작하는 듯 했지만 cache가 { [object Object]: ... } 이런식으로 되기 때문에 오류
```
[실행코드](https://codesandbox.io/s/hamsuhyeongpeurogeuraeming-9jang-lzh8jz?file=/src/9.1.js)

<br/>

## 2. Underscore.js의 _.memoize

#### 2.1 _.memoize

```typescript
_.memoize = function (func, hasher) {
// 본체 함수와 hasher 함수를 받음
var memoize = function (key) {
var cache = memoize.cache;
var address = "" + (hasher ? hasher.apply(this, arguments) : key);
// hasher가 있으면 hasher에게도 인자들을 넘겨 cache의 key로 사용할 address를 생성
// 없으면 첫 번째 인자를 그대로 사용
if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
// (1) 결과가 없을 때만 함수를 실행하여 cache에 담음
return cache[address]; // 결과 리턴
};
memoize.cache = {}; // (2) 클로저를 사용하지 않고 리턴할 함수 자체에 cache를 달아둠
return memoize;
};
```

- Underscore.js의 memoize는 cache의 key를 함수를 통해 만드는 것이 가능
- (1)에서 _.has를 사용한 이유는 캐시한 결과 값이 null, 0, undefined일 수 있기 때문.
- (2)에서 함수에 .cache를 쓴 이유는 개발자가 메모리 관리를 할 수 있도록 하기 위함 -> 클로저를 사용할 경우 cache에 담긴 실행 결과들이 메모리에 상주하고 있어야 하기 때문.

## 3. Partial.js의 _.memoize2

#### 3.1 _.memoize2

```typescript
var f1 = _.memoize2(function(obj) {
console.log('함수 본체에 들어옴');
return obj.a + 10;
});

var obj1 = { a: 1 };
var obj2 = { a: 2 };

console.log( f1(obj1) );
// 함수 본체에 들어옴
// {a:11}
console.log( f1(obj1) );
// {a:11} (캐시 사용)
console.log( f1(obj1) );
// {a:11} (캐시 사용)

console.log( f1(obj2) );
// 함수 본체에 들어옴
// {a:12}
console.log( f1(obj2) );
// {a:12} (캐시 사용)
```

- _.memoize가 캐시를 함수에 기록, _.memoize2는 캐시를 인자에 기록
- _.memoize2는 불변 객체 컨셉과 함께 사용하기 위해 만든 함수
- 각 함수들에 대한 결과값을 인자로 사용한 사용된 객체에 담아두므로 한 번 사용하고 버리는 객체 -> 메모리에서 비워짐
- 사용한 인자에 결과 캐시가 쌓이므로 값의 유무에 따라 자동으로 메모리가 관리

#### 3.2 mutable

```typescript
var evens = _.memoize2(function(list) {
console.log('함수 본체에 들어와서 loop 실행');
return _.filter(list, function(num) {
return num % 2 == 0;
})
});

var list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log( evens(list) );
// 함수 본체에 들어와서 loop 실행
// [2, 4, 6, 8, 10]
console.log( evens(list) );
// [2, 4, 6, 8, 10] (캐시를 사용하여 loop를 돌지 않음)

list.push(11); // mutable
list.push(12); // mutable
console.log( list );
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

console.log( evens(list) );
// [2, 4, 6, 8, 10] (캐시가 사용되어 10이 그대로 남음)
```

#### 3.3 immutable

```typescript
var list2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log( evens(list2) );
// 함수 본체에 들어와서 loop 실행
// [2, 4, 6, 8, 10]
console.log( evens(list2) );
// [2, 4, 6, 8, 10] (캐시를 사용하여 loop를 돌지 않음)

list2 = list2.concat(11, 12); // immutable
console.log( list2 );
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

console.log( evens(list2) );
// 함수 본체에 들어와서 loop 실행
// [2, 4, 6, 8, 10, 12]

console.log( evens(list2) );
// [2, 4, 6, 8, 10, 12] (캐시를 사용하여 loop를 돌지 않음)
```

#### 3.4 _.memoize2 with _.go

```typescript
var users = [
{ id: 1, name: "ID", age: 32, count: { review: 3, cart: 5 } },
{ id: 2, name: "HA", age: 25, count: { review: 8, cart: 4 } },
{ id: 3, name: "BJ", age: 32, count: { review: 0, cart: 0 } },
{ id: 4, name: "PJ", age: 28, count: { review: 4, cart: 5 } },
{ id: 5, name: "JE", age: 27, count: { review: 5, cart: 2 } },
{ id: 6, name: "JM", age: 32, count: { review: 4, cart: 6 } },
{ id: 7, name: "JI", age: 31, count: { review: 7, cart: 2 } }
];

var best_reviewers = _.memoize2(function(list) {
console.log('함수 본체에 들어와서 loop 실행');
return _.filter(list, function(user) {
return user.count.review > 5;
})
});

var cart_is_empty = _.memoize2(function(list) {
console.log('함수 본체에 들어와서 loop 실행');
return _.filter(list, function(user) {
return user.count.cart == 0;
})
});

// _.go: 즉시 실행되는 pipe 라인 함수

_.go(users,
best_reviewers,
_.pluck('name'),
console.log);
// 함수 본체에 들어와서 loop 실행
// ["HA", "JI"]

_.go(users,
best_reviewers,
_.pluck('name'),
console.log);
// ["HA", "JI"] (캐시 사용)

_.go(users,
cart_is_empty,
_.pluck('name'),
console.log);
// 함수 본체에 들어와서 loop 실행
// ["BJ"]

_.go(users,
cart_is_empty,
_.pluck('name'),
console.log);
// ["BJ"] (캐시 사용)
```

#### 3.5 _.memoize2 with _.im

```typescript

// _.im.set: immutable set 함수
users = _.im.set(users, '(#3)->count', {
review: 10,
cart: 1
});
// id가 3에 해당하는 객체 갱신

_.go(users,
best_reviewers,
_.pluck('name'),
console.log);
// 함수 본체에 들어와서 loop 실행
// ["HA", "BJ", "JI"]

_.go(users,
best_reviewers,
_.pluck('name'),
console.log);
// ["HA", "BJ", "JI"] (캐시 사용)

_.go(users,
cart_is_empty,
_.pluck('name'),
console.log);
// 함수 본체에 들어와서 loop 실행
// []

_.go(users,
cart_is_empty,
_.pluck('name'),
console.log);
// [] (캐시 사용)
```

- user는 중첩 구조 데이터 -> 가변으로 값을 변경하면 위 함수들이 캐시에 남은 값을 리턴하는 문제 발생
- _.im 을 이용하여 값을 변경 -> 캐시도 함께 갱신
- _.im 을 이용하여 값을 변경 -> 변경이 되지 않은 객체는 살려둠.

#### 3.6 _.memoize2 with _.im + predicate

```typescript

var predicate = _.memoize2(function(user) {
console.log('predicate 실행');
return user.count.review > 5;
});

var best_reviewers2 = _.memoize2(function(list) {
console.log('함수 본체에 들어와서 loop 실행');
return _.filter(list, predicate);
});

_.go(users,
best_reviewers2,
_.pluck('name'),
console.log);
// 함수 본체에 들어와서 loop 실행
// predicate 실행 * 7번 실행
// ["HA", "BJ", "JI"]

_.go(users,
best_reviewers2,
_.pluck('name'),
console.log);
// ["HA", "BJ", "JI"] (캐시 사용)

users = _.im.set(users, '(#3)->count->review', 2);
// BJ의 count.review를 다시 줄임

_.go(users,
best_reviewers2,
_.pluck('name'),
console.log);
// 함수 본체에 들어와서 loop 실행
// predicate 실행 * 1번 실행
// ["HA", "JI"]

```

- users를 새로운 값으로 변경 -> 내부 객체의 BJ만 변경 -> 나머지는 변경하지 않음 -> predicate의 실행 스킵 가능
- predicate or iteratee 등 반복 실행되는 함수 & 내부가 복잡한 함수에게는 성능적으로 이득

## 3. _.momoize2 내부와 JSON.stringify

```typescript

_.memoize2 = function(mid) {
return function(func) {
var memoize_id = ++mid;
var f = arguments.length == 1 ? func : __.apply(null, arguments);
return function(obj) { // 함수로 달아 놓음
return _.has(obj._memoize || (obj._memoize = function(){}), memoize_id) ?
obj._memoize[memoize_id] : (obj._memoize[memoize_id] = f(obj));
}
}
}(0);
```
- 필자가 만든 _.memoize2 함수는 인자를 한 개만 받을 수 있음 -> 캐시가 필요한 함수에서 인자가 2개 이상 필요한 경우가 거의 없다고 판단되었기 때문
- 캐시 객체를 함수로 달아둠 -> 객체에 달린 함수는 JSON.stringify에서 자동으로 지워지기 때문에.....(?)

---

<strong><참고자료></strong>

[책] [#함수형자바스크립트프로그래밍][함수형자바스크립트프로그래밍] - 유인동 지음 -
<br/>
[사이트] <https://codesandbox.io/s/hamsuhyeongjabaseukeuribteupeurogeuraeming-th30pc?file=/src/4.1.1.js>

---

##### 고차함수와 보조함수 end

[함수형자바스크립트프로그래밍]: https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=123715872
[sangcho]: https://github.com/SangchoKim
[taeHyen]: https://github.com/rlaxogus0517
[kangHyen]: https://github.com/bebekh1216
[sumin]: https://github.com/ttumzzi
Loading