코딩/알고리즘 공부하면서

LRU 캐시 알고리즘

americanoallday 2025. 5. 31. 17:06

🔍 LRU (Least Recently Used) 캐시란?

“가장 오래전에 사용된 데이터를 가장 먼저 제거하는 캐시 알고리즘”

 

 

🤔 왜 이렇게 하냐면?

  • 자주 쓰이는 데이터는 앞으로도 쓸 가능성이 높고
  • 오랫동안 안 쓰인 데이터는 앞으로도 안 쓸 확률이 높음
  • → **“최근 사용된 데이터일수록 더 오래 남겨두자”**는 전략

🎯 작동 방식 (직관적으로)

  1. 새로운 데이터가 들어오면
    • 캐시에 있으면 → Hit → 최근 사용으로 갱신 (뒤로 보냄)
    • 없으면 → Miss → 캐시에 추가 (꽉 찼다면 가장 오래된 데이터 제거)
  2. 항상 최근에 사용한 순서로 정렬(최근에 사용된건 맨 뒤로 이동)되어 있어야 함

예시 (cacheSize = 3)

도시 요청 순서: [A, B, C, A, D, B]

Step 1: A (MISS) → [A]      → +5
Step 2: B (MISS) → [A, B]   → +5
Step 3: C (MISS) → [A, B, C]→ +5
Step 4: A (HIT)  → [B, C, A]→ +1
Step 5: D (MISS) → [C, A, D]→ +5 (B 제거)
Step 6: B (MISS) → [A, D, B]→ +5 (C 제거)

총 시간 = 5 + 5 + 5 + 1 + 5 + 5 = **26**

 


🔧 실제 어디에 쓰냐면?

분야 설명
OS 페이지 교체 메모리 페이지 캐시 (→ LRU로 오래된 페이지부터 제거)
웹 브라우저 자주 방문한 페이지 캐시
DB 쿼리 캐시 자주 조회하는 데이터 보관
CDN, Redis, Memcached 캐시 만료 정책으로 LRU 도입 가능

 


📌 요약 정리

항목 내용
Least Recently Used = 가장 덜 사용된 것을 제거
장점 구현 간단 + 성능 준수
단점 사용 순서를 기억해야 해서 약간의 오버헤드
오늘날 사용? Yes — 여전히 널리 쓰임 (특히 메모리 제한 있는 곳)

 


프로그래머스 문제 : https://school.programmers.co.kr/learn/courses/30/lessons/17680

캐시

지도개발팀에서 근무하는 제이지는 지도에서 도시 이름을 검색하면 해당 도시와 관련된 맛집 게시물들을 데이터베이스에서 읽어 보여주는 서비스를 개발하고 있다.
이 프로그램의 테스팅 업무를 담당하고 있는 어피치는 서비스를 오픈하기 전 각 로직에 대한 성능 측정을 수행하였는데, 제이지가 작성한 부분 중 데이터베이스에서 게시물을 가져오는 부분의 실행시간이 너무 오래 걸린다는 것을 알게 되었다.
어피치는 제이지에게 해당 로직을 개선하라고 닦달하기 시작하였고, 제이지는 DB 캐시를 적용하여 성능 개선을 시도하고 있지만 캐시 크기를 얼마로 해야 효율적인지 몰라 난감한 상황이다.
어피치에게 시달리는 제이지를 도와, DB 캐시를 적용할 때 캐시 크기에 따른 실행시간 측정 프로그램을 작성하시오.

 

입력 형식

  • 캐시 크기(cacheSize)와 도시이름 배열(cities)을 입력받는다.
  • cacheSize는 정수이며, 범위는 0 ≦ cacheSize ≦ 30 이다.
  • cities는 도시 이름으로 이뤄진 문자열 배열로, 최대 도시 수는 100,000개이다.
  • 각 도시 이름은 공백, 숫자, 특수문자 등이 없는 영문자로 구성되며, 대소문자 구분을 하지 않는다. 도시 이름은 최대 20자로 이루어져 있다.

출력 형식

  • 입력된 도시이름 배열을 순서대로 처리할 때, "총 실행시간"을 출력한다.

조건 

  • 캐시 교체 알고리즘은 LRU(Least Recently Used)를 사용한다.
  • cache hit일 경우 실행시간은 1이다.
  • cache miss일 경우 실행시간은 5이다.
import java.util.*;

public class LRUCache {
    public static int solution(int cacheSize, String[] cities) {
        if (cacheSize == 0) return cities.length * 5;

        int time = 0;
        LinkedHashMap<String, Integer> cache = new LinkedHashMap<>(cacheSize, 0.75f, true);

        for (String city : cities) {
            city = city.toLowerCase();

            if (cache.containsKey(city)) {
                // 캐시 HIT
                time += 1;
                cache.get(city); // access to move it to recent
            } else {
                // 캐시 MISS
                time += 5;
                if (cache.size() >= cacheSize) {
                    // 가장 오래된 항목 제거
                    String oldest = cache.keySet().iterator().next();
                    cache.remove(oldest);
                }
                cache.put(city, 0);
            }
        }

        return time;
    }

    public static void main(String[] args) {
        int result = solution(3, new String[]{
            "Jeju", "Pangyo", "Seoul", "NewYork", "LA",
            "Jeju", "Pangyo", "Seoul", "NewYork", "LA"
        });
        System.out.println(result); // 출력: 50
    }
}

 

 

✅ 1. if (cacheSize == 0) return cities.length * 5;

🔍 왜 5를 곱해서 리턴하나?

  • 문제 조건: 캐시 Miss 시 실행시간은 5
  • cacheSize == 0이면 캐시 기능이 꺼져 있는 상태 → 모든 요청은 무조건 Miss
  • 따라서 도시 수만큼 Miss 발생
return cities.length * 5;

📌 즉, 캐시 없이 전부 DB에서 불러오는 경우의 총 시간 계산

 


✅ 2. Map, HashMap, LinkedHashMap의 개념 및 차이

인터페이스/클래스 설명
Map Key-Value 쌍을 저장하는 인터페이스. 구현체로 HashMap, LinkedHashMap 등이 있음
HashMap 순서를 보장하지 않는 Map. 빠른 검색 속도 제공 (O(1) 평균)
LinkedHashMap 삽입 순서 또는 접근 순서 유지 가능. LRU 캐시 구현에 자주 사용

 

🔑 주요 차이점

항목 HashMap LinkedHashMap
순서 보장 ❌ 없음 ⭕ 있음 (삽입 순 or 접근 순)
성능 빠름 조금 더 느림 (순서 관리 때문)
사용 예 빠른 검색 LRU 캐시처럼 순서 중요한 경우

 


 

✅ 3. new LinkedHashMap<>(cacheSize, 0.75f, true);

new LinkedHashMap<>(initialCapacity, loadFactor, accessOrder)
파라미터 의미
cacheSize 초기 용량 (버킷 개수)
0.75f load factor (기본값) → 얼마나 채워졌을 때 리사이징 할지 결정 (0.75는 널리 쓰이는 값)
true accessOrder = true → 접근 순서 유지 (LRU)  false일 경우 삽입 순서 유지 (FIFO 비슷)

 

🔍 핵심: true면 최근에 get()한 키를 가장 뒤로 이동시킴

→ LRU 캐시의 핵심 동작


✅ 4. String oldest = cache.keySet().iterator().next();

이건 LRU 캐시에서 가장 오래된 항목을 제거하는 부분.

하나씩 해석

🔹 cache.keySet()

  • Map의 모든 key를 Set 형태로 반환
Set<String> keys = cache.keySet(); // 예: [A, B, C]

 

🔹 .iterator()

  • Set을 순회할 수 있는 Iterator 객체를 반환
Iterator<String> it = keys.iterator();

 

🔹 .next()

  • Iterator에서 가장 처음 요소 반환
  • LinkedHashMap에서는 이게 가장 오래된 key
String oldest = it.next(); // 예: "A"
🔁 그래서 cache.remove(oldest)로 가장 오래된 항목 제거 가능

 


📦 최종 요약

코드 설명
cacheSize == 0 캐시 비활성화 → 모든 요청 Miss → 시간 = 도시 수 × 5
Map Key-Value 저장 인터페이스
HashMap 순서 없음, 빠름
LinkedHashMap 순서 있음, LRU 구현 가능
new LinkedHashMap<>(cap, load, true) 접근 순서 유지하는 LRU 캐시 구현
keySet().iterator().next() 가장 오래된 key 꺼내기 (제거 대상)