메모이제이션 (Memoization)

메모이제이션 이란?

자바 스크립트에서 함수는 객체이기 때문에 프로퍼티를 가질 수 있습니다. 그리고 언제든지 함수에 사용자 정의 프로퍼티를 추가할 수도 있습니다. 함수에 프로퍼티를 추가하여 결과(반환 값)을 캐시하면 다음 호출 시점에 복잡한 연산을 반복하지 않을 수 있습니다. 이런 활용 방법을 **메모이제이션 패턴**이라고 합니다.

다음 코드에서는 myFunc 함수에 cache 프로퍼티를 생성합니다. 이 프로퍼티는 일반적인 프로퍼티처럼 myFunc.cache와 같은 형태로 접근할 수 있습니다. cache 프로퍼티는 함수로 전달된 param 매개변수를 키로 사용해서 계산의 결과를 값으로 가지는 객체(해시)입니다. 결과 값은 필요에 따라 복잡한 데이터 구조로 저장할 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
var myFunc = function (param) {
if (!myFunc.cache[param]) {
var result = {};
// ...
// 비용이 많이 드는 수행 후 result에 결과 저장
// ...
myFunc.cache[param] = result;
}
return myFunc.cache[param];
};

// 캐시 저장공간
myFunc.cache = {};

메모이제이션 적용하기

메모이제이션을 공부한 후 현재 진행중인 에밀리(개인 프로젝트)에 적용해 보았습니다. 어느 부분에 적용했는지 적용 전과 적용 후 얼마나 효율이 올라갔는지를 알아보겠습니다.

수정할 부분

메모이제이션 패턴을 적용할 코드가 현재 하고 있는 기능은 다음과 같습니다. 사용자의 요청(학생식당, 카페테리아, 사범대식당, 기숙사식당, 교직원식당)에 따라 미리 크롤링 후 DB에 저장되어 있는 데이터(메뉴)를 가져와 응답합니다.
카카오톡 플러스친구 자동응답 봇
메모이제이션 패턴을 사용함으로써 얻을 수 있는 이점은 비용이 많이 드는 결과를 캐싱하고 그 이후에 재사용함으로써 비용을 줄일 수 있다는 것입니다. 현재 코드에서 비용이 많이 드는 작업은 DB를 조회하는 부분입니다. DB에는 다음과 같이 미리 크롤링한 데이터가 날짜별로 저장되어 있습니다.

DB 메뉴 table

사용자의 요청에 따라 해당 날짜의 데이터를 조회한 후 사용자가 요청한 식당에 맞는 데이터를 결과로 반환합니다. 그렇기 때문에 해당 날짜의 최초 요청이 이루어진 후 그 하루 동안에는 계속해서 DB에 같은 쿼리를 통해 같은 결과를 얻게 됩니다. 이 부분이 비용이 많이 드는 작업이기 때문에 메모이제이션 패턴을 통해 개선해보았습니다.

메모이제이션 적용 전

먼저 메모이제이션 패턴을 적용하기 전 코드입니다. menuHandler 클래스의 getMenu 함수는 menuService를 통해 DB에서 해당 날짜의 식당 메뉴를 가져와 매개변수로 전달받은 식당의 이름을 사용하여 결과로 반환하는 역할을 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const dateUtil = require('util/dateUtil');
const menuService = require('services/menuService');

class menuHandler {
static getMenu(place) {
return new Promise((_s, _f) => {
const today = new Date().yyyymmdd();
let menu = '';

menuService.show(today)
.then(menuList => {
if (!menuList) {
menu = '데이터가 없습니다. 관리자에게 문의해주세요';
}

switch (place) {
case '학생식당':
menu = menuList.student.join('\n\n');
break;
case '카페테리아':
menu = menuList.cafeteria.join('\n\n');
break;
case '사범대식당':
menu = menuList.education.join('\n\n');
break;
case '기숙사식당':
menu = menuList.dormitory.join('\n\n');
break;
case '교직원식당':
menu = menuList.staff.join('\n\n');
break;
}

_s(menu);
})
.catch(err => {
_f(err);
});
});
}
}

module.exports = menuHandler;

3. 메모이제이션 적용 후

메모이제이션 패턴을 적용 한 후 코드입니다. setCache, getCache, pickMenu 함수가 추가되었고, getMenu 함수의 코드도 조금 변경되었습니다. 기존 코드의 getMenu 함수에서는 바로 DB를 조회하여 결과를 반환하였지만, 변경된 코드에서는 getCache 함수를 통해 캐시에 저장된 데이터가 있는지 확인 후 분기하여 처리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const dateUtil = require('util/dateUtil');
const menuService = require('services/menuService');

class menuHandler {
static setCache(param, menuList) {
menuHandler.cache[param] = menuList;
}

static getCache(param) {
if (menuHandler.cache[param]) {
return menuHandler.cache[param];
} else {
return null;
}
}

static pickMenu(menuList, place) {
let menu = '';

switch (place) {
case '학생식당':
menu = menuList.student.join('\n\n');
break;
case '카페테리아':
menu = menuList.cafeteria.join('\n\n');
break;
case '사범대식당':
menu = menuList.education.join('\n\n');
break;
case '기숙사식당':
menu = menuList.dormitory.join('\n\n');
break;
case '교직원식당':
menu = menuList.staff.join('\n\n');
break;
}

return menu;
}

static getMenu(place) {
return new Promise((_s, _f) => {
const today = new Date().yyyymmdd();
const cachedMenuList = this.getCache(today);
let menu = '';

if (cachedMenuList) {
menu = this.pickMenu(cachedMenuList, place);
_s(menu);
} else {
menuService.show(today)
.then(menuList => {
if (!menuList) {
menu = '데이터가 없습니다. 관리자에게 문의해주세요';
}

this.setCache(today, menuList);

menu = this.pickMenu(menuList, place);
_s(menu);
})
.catch(err => {
_f(err);
});
}
});
}
}

menuHandler.cache = {};

module.exports = menuHandler;

Line:70 에서 선언한 menuHandler의 cache Object에는 다음과 같이 데이터가 캐싱될 것입니다.

1
2
3
4
5
6
{
...
`2017-06-13`: { ... menuList ...}
`2017-06-14`: { ... menuList ...}
...
}

결과 비교

메모이제이션 패턴을 적용하기 전과 적용한 후의 성능 차이는 아래 보이는것처럼 눈에 띄게 차이가 납니다. 메모이제이션 패턴을 적용한 코드는 첫번째 요청(캐싱 하기 전)때는 메모이제이션 패턴 적용 전과 응답속도가 비슷하지만 그 이후의 응답은 캐싱된 데이터를 이용하기 때문에 비교될 정도로 빨라졌습니다.

메모이제이션 적용 전

메모이제이션 적용 후

마치며

메모이제이션을 적용하기 전과 후 모두 같은 기능을 결과를 만들어내는 코드이지만 코드를 작성하는 방법에 따라 더욱 더 빠른 효율적인 서비스를 만들 수 있다는 것을 느끼게 되었습니다.
현재 제 상황에서는 캐싱된 데이터도 하루가 지나게되면 쓰이지 않고 계속 메모리에 남아있게 되는데 그 부분에 대한 처리를 추가해야할것 같습니다. 이번에 적용한 코드 뿐만 아니라 아직 프로잭트 내에 메모이제이션 패턴을 적용할 수 있는 부분이 더 있습니다. 앞으로 디자인패턴 공부를 계속하여 새로운 패턴들을 적용시키며 리팩토링을 해야겠습니다.