지도개발팀에서 근무하는 제이지는 지도에서 도시 이름을 검색하면 해당 도시와 관련된 맛집 게시물들을 데이터베이스에서 읽어 보여주는 서비스를 개발하고 있다.
이 프로그램의 테스팅 업무를 담당하고 있는 어피치는 서비스를 오픈하기 전 각 로직에 대한 성능 측정을 수행하였는데, 제이지가 작성한 부분 중 데이터베이스에서 게시물을 가져오는 부분의 실행시간이 너무 오래 걸린다는 것을 알게 되었다.
어피치는 제이지에게 해당 로직을 개선하라고 닦달하기 시작하였고, 제이지는 DB 캐시를 적용하여 성능 개선을 시도하고 있지만 캐시 크기를 얼마로 해야 효율적인지 몰라 난감한 상황이다.
어피치에게 시달리는 제이지를 도와, DB 캐시를 적용할 때 캐시 크기에 따른 실행시간 측정 프로그램을 작성하시오.
캐시크기(cacheSize) | 도시이름(cities) | 실행시간 |
---|---|---|
3 | ["Jeju", "Pangyo", "Seoul", "NewYork", "LA", "Jeju", "Pangyo", "Seoul", "NewYork", "LA"] | 50 |
3 | ["Jeju", "Pangyo", "Seoul", "Jeju", "Pangyo", "Seoul", "Jeju", "Pangyo", "Seoul"] | 21 |
2 | ["Jeju", "Pangyo", "Seoul", "NewYork", "LA", "SanFrancisco", "Seoul", "Rome", "Paris", "Jeju", "NewYork", "Rome"] | 60 |
5 | ["Jeju", "Pangyo", "Seoul", "NewYork", "LA", "SanFrancisco", "Seoul", "Rome", "Paris", "Jeju", "NewYork", "Rome"] | 52 |
2 | ["Jeju", "Pangyo", "NewYork", "newyork"] | 16 |
0 | ["Jeju", "Pangyo", "Seoul", "NewYork", "LA"] | 25 |
LRU(Least Recently Used)
: 캐시는 제한된 용량을 가지고 있으므로, 캐시 메모리에 새로운 데이터를 저장하기 전에 가장 오래된 데이터를 제거하여 새로운 데이터를 저장할 공간을 확보하는 방식
- cache hit 캐시에 해당 데이터가 있는 경우 → 캐시에서 데이터를 가져옴
- cache miss : 캐시에 해당 데이터가 없는 경우 → 캐시에 저장
먼저 캐시를 연결리스트로 구현하기 위해
연결리스트의 노드를 나타내는 Node
클래스와 연결리스트 자체를 구현하는 LinkedList
클래스를 만들었다.
LinkedList
는 속성으로 head
, tail
, size
, capacity
를 가지고, find()
, insert()
, remove()
메소드를 가진다.
// 연결리스트의 노드
class Node {
constructor(value) {
this.value = value;
this.next = null;
}
}
// 연결리스트
class LinkedList {
constructor(capacity) {
this.head = null;
this.tail = null;
this.size = 0;
this.capacity = capacity;
}
find(value) {
let currentNode = this.head;
while(currentNode !== null) { // tail까지 갈때까지
if (currentNode.value === value) {
return true;
}
currentNode = currentNode.next; // 현재 노드가 찾는 값이 아니라면 다음 노드로 이동
}
return false;
}
insert(value) {
const newNode = new Node(value);
if (this.head === null) {
this.head = newNode;
this.tail = newNode;
} else {
this.tail.next = newNode;
this.tail = newNode;
}
this.size++;
if (this.size > this.capacity) {
// 가장 오래된 노드 삭제
this.head = this.head.next;
this.size--;
}
}
remove(value) {
let currentNode = this.head; // head부터 시작
let prevNode = null;
while (currentNode !== null) { // currentNode가 null이 될때까지 (연결리스트를 끝까지 순회)
if (currentNode.value === value) { // 삭제할 노드를 찾았을 때
if (prevNode === null) { // currentNode가 맨 앞인 경우
this.head = currentNode.next;
} else if (currentNode.next === null) { // currentNode가 맨 끝에 있는 경우
prevNode.next = null;
this.tail = prevNode;
} else { // currentNode가 중간에 끼어있는 경우
prevNode.next = currentNode.next; // 삭제할 노드의 이전 노드가 다음 노드를 가르키도록
}
this.size--;
return true;
}
prevNode = currentNode;
currentNode = currentNode.next; // 못찾았다면 다음 노드로 이동
}
return false;
}
}
find(value)
: 연결리스트에서 value
값이 존재하는지 찾는 메소드
head
부터 시작하여 마지막 노드까지 차례대로 value
를 비교하여 값을 찾으면 true를 반환하고, insert(value)
: 연결리스트에 새로운 노드를 삽입하는 메소드
remove(value)
: 연결리스트에서 value
값을 가진 노드를 삭제하는 메소드
find()
와 마찬가지로 head
부터 시작하여 마지막 노드까지 순회하면서 삭제할 노드를 찾는다.이렇게 구현한 LinkedList
클래스를 이용해, 캐시 객체를 만든다.
cities
배열을 순회하면서, 만약 캐시에 해당 도시가 있는 경우에는 캐시에서 해당 도시를 삭제하고 맨 뒤에 다시 삽입하여 최근에 사용된 것으로 처리한다. 그리고 cache hit이므로 시간을 +1 해준다.
만약 캐시에 해당 도시가 없는 경우에는 캐시에 해당 도시를 삽입한다. 그리고 cache miss이므로 시간을 +5해준다.
cities
를 모두 순회한 후에는 최종적으로 계산된 실행 시간 time
변수를 리턴해주면 된다.
function solution(cacheSize, cities) {
let time = 0;
const cache = new LinkedList(cacheSize);
for (let city of cities) {
city = city.toUpperCase();
if (cache.find(city)) {
// cache hit
cache.remove(city); // cache에서 city 잘라서 맨 뒤에 붙이기
cache.insert(city);
time += 1;
} else {
// cache miss
cache.insert(city);
time += 5;
}
}
return time;
}
실제 캐시도 이런식으로 이루어지지 않을까 해서 ㅎㅎ 신기한거 같습니다 고생하셨어요~