Post

[Golang] Cache 2편 - In-Memory Cache 구현

In-Memory Cache 구현을 알아보겠습니다.

In-Memory Cache란?

  • In-memory cache는 데이터를 메모리(RAM)에 저장하여 빠르게 접근할 수 있게 하는 데이터 저장소이며 일반적으로 디스크 기반 저장소나 데이터베이스에 비해 훨씬 빠른 읽기 및 쓰기 속도를 제공

주요 특징

  • 속도
    • RAM은 디스크보다 훨씬 빠르기 때문에, 데이터를 메모리에 저장하면 접근 속도가 매우 빨라짐
    • 자주 접근해야 하는 데이터에 유용
  • 일시성
    • 시스템 재부팅 또는 애플리케이션 재시작 시 데이터가 소실됨
    • 영구 저장이 필요한 데이터는 적합하지 않음
  • 용량 제한
    • RAM은 디스크에 비해 용량이 제한적
    • 인메모리 캐시는 보통 용량 제한이 있으며, 용량 초과 시 가장 오래된 항목을 삭제하는 방식(LRU, Least Recently Used) 등을 사용
  • 데이터 만료
    • 각 항목에 TTL(Time-To-Live)을 설정하여 일정 시간이 지나면 데이터가 자동으로 삭제되도록 할 수 있음

사용 사례

  • 자주 참조되는 데이터 저장 (예: 사용자 세션 정보, 설정 데이터 등)
  • 데이터베이스 쿼리 결과 캐싱
  • 계산 비용이 높은 작업의 결과 캐싱 (예: 이미지 변환 결과, 복잡한 계산 결과 등)

[Golang] In-Memory Cache 구현

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
package cache

import (
	"context"
	"sync"
	"time"
)

type cacheItem struct {
	value  any
	expiry time.Time
}

type InMemoryCache struct {
	items map[string]*cacheItem
	mtx   sync.RWMutex
}

func NewInMemoryCache() *InMemoryCache {
	return &InMemoryCache{
		items: make(map[string]*cacheItem),
	}
}

func (c *InMemoryCache) Get(_ context.Context, key string) (any, error) {
	c.mtx.RLock()
	item, exists := c.items[key]
	if !exists {
		c.mtx.RUnlock()
		return nil, ErrKeyNotFound
	}

	if item.expiry.Before(time.Now()) {
		c.mtx.RUnlock()
		c.mtx.Lock()
		delete(c.items, key)
		c.mtx.Unlock()
		return nil, ErrKeyExpired
	}
	c.mtx.RUnlock()
	return item.value, nil
}

func (c *InMemoryCache) Set(_ context.Context, key string, value any, ttl time.Duration) error {

	expiry := time.Now().Add(ttl)
	item := &cacheItem{
		value:  value,
		expiry: expiry,
	}
	c.mtx.Lock()
	c.items[key] = item
	c.mtx.Unlock()
	return nil
}

func (c *InMemoryCache) Delete(_ context.Context, key string) error {
	c.mtx.Lock()
	defer c.mtx.Unlock()

	delete(c.items, key)
	return nil
}

func (c *InMemoryCache) SetTTL(_ context.Context, key string, ttl time.Duration) error {
	c.mtx.Lock()
	defer c.mtx.Unlock()

	item, exists := c.items[key]
	if !exists {
		return ErrKeyNotFound
	}
	item.expiry = time.Now().Add(ttl)
	return nil
}

func (c *InMemoryCache) GetTTL(_ context.Context, key string) (time.Duration, error) {
	c.mtx.RLock()
	defer c.mtx.RUnlock()

	item, exists := c.items[key]
	if !exists {
		return 0, ErrKeyNotFound
	}
	ttl := time.Until(item.expiry)
	return ttl, nil
}

func (c *InMemoryCache) Exists(_ context.Context, key string) (bool, error) {
	c.mtx.RLock()
	defer c.mtx.RUnlock()

	_, exists := c.items[key]
	return exists, nil
}

func (c *InMemoryCache) Clear(_ context.Context) error {
	c.mtx.Lock()
	defer c.mtx.Unlock()

	c.items = make(map[string]*cacheItem)
	return nil
}

func (c *InMemoryCache) Close() error { return nil }

func (c *InMemoryCache) Description() string {
	return "InMemoryCache: A simple in-memory cache implementation"
}

설명

  • 예제는 Mutex 를 직접 제어하였으나, 여러 고루틴이 동시에 읽고 쓸 수 있도록 설계되어 있어 동기화 문제를 자동으로 처리해주는 sync.Map 를 활용 할 수도 있습니다.
1
2
3
4
type cacheItem struct {
	value  any
	expiry time.Time
}
  • cacheItem은 캐시에 저장되는 항목의 구조체로, value는 실제 저장된 데이터, expiry는 항목의 만료 시간을 나타냄
1
2
3
4
5
type InMemoryCache struct {
	items map[string]*cacheItem
	mtx   sync.RWMutex
}

  • InMemoryCache는 캐시의 구현체로, items 맵은 캐시된 항목을 저장하며, mtx는 동시 접근을 위한 읽기-쓰기 뮤텍스를 사용하여 데이터의 안전성을 보장
1
2
3
4
5
func NewInMemoryCache() *InMemoryCache {
	return &InMemoryCache{
		items: make(map[string]*cacheItem),
	}
}
  • InMemoryCache 인스턴스를 생성하며 items 맵을 초기화하여 캐시 항목을 저장할 준비
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (c *InMemoryCache) Get(_ context.Context, key string) (any, error) {
	c.mtx.RLock()
	item, exists := c.items[key]
	if !exists {
		c.mtx.RUnlock()
		return nil, ErrKeyNotFound
	}

	if item.expiry.Before(time.Now()) {
		c.mtx.RUnlock()
		c.mtx.Lock()
		delete(c.items, key)
		c.mtx.Unlock()
		return nil, ErrKeyExpired
	}
	c.mtx.RUnlock()
	return item.value, nil
}
  • 주어진 key에 대한 캐시 항목을 조회
  • 읽기 & 쓰기 잠금을 사용하여 데이터 무결성을 유지
  • 항목이 존재하지 않으면 ErrKeyNotFound를 반환
  • 만료되지 않았다면 해당 value를 반환하며 만료된 데이터는 삭제
  • 지연 삭제 (Lazy Deletion) 방식
  • 삭제 메커니즘
    • 지연 삭제 (Lazy Deletion) 방식 : 위 코드는 get(key) 을 한 경우 만료된 key에 대한 삭제 작업이 이루어짐 (get이 없다면 계속 삭제되지 못하고 데이터가 저장되어 있음)
    • 주기적 정리 (Periodic Cleanup): 일정한 주기로 만료된 키를 정리하는 작업을 수행하는 별도의 고루틴을 실행
    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
    
      type InMemoryCache struct {
          data        map[string]cacheItem
          mu          sync.RWMutex
          stopCleanup chan struct{}
      }
    
      func NewMemoryCache() *InMemoryCache {
          cache := &InMemoryCache{
              data:        make(map[string]cacheItem),
              stopCleanup: make(chan struct{}),
          }
          go cache.startCleanup()
          return cache
      }
    
      func (c *InMemoryCache) startCleanup() {
          ticker := time.NewTicker(5 * time.Minute)
          defer ticker.Stop()
          for {
              select {
              case <-ticker.C:
                  c.cleanup()
              case <-c.stopCleanup:
                  return
              }
          }
      }
    
    • 주기적 정리는 백그라운드에서 자동으로 수행되고, 지연 삭제 는 키를 액세스할 때마다 수행되어 즉각적인 관리를 보장합니다. 이렇게 하면 캐시의 메모리 사용을 효율적으로 유지할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
func (c *InMemoryCache) Set(_ context.Context, key string, value any, ttl time.Duration) error {

	expiry := time.Now().Add(ttl)
	item := &cacheItem{
		value:  value,
		expiry: expiry,
	}
	c.mtx.Lock()
	c.items[key] = item
	c.mtx.Unlock()
	return nil
}
  • 주어진 keyvalue를 설정하고, ttl에 따라 만료 시간을 지정
  • 쓰기 잠금을 사용하여 안전하게 데이터를 수정
  • 만료 시간을 현재 시간에 ttl을 더한 값으로 설정하여 cacheItem을 생성하고 items 맵에 항목을 추가
1
2
3
4
5
6
7
func (c *InMemoryCache) Delete(_ context.Context, key string) error {
	c.mtx.Lock()
	defer c.mtx.Unlock()

	delete(c.items, key)
	return nil
}
  • 주어진 key의 캐시 항목을 삭제
  • 쓰기 잠금을 사용하여 안전하게 items맵에서 항목을 삭제
1
2
3
4
5
6
7
8
9
10
11
func (c *InMemoryCache) SetTTL(_ context.Context, key string, ttl time.Duration) error {
	c.mtx.Lock()
	defer c.mtx.Unlock()

	item, exists := c.items[key]
	if !exists {
		return ErrKeyNotFound
	}
	item.expiry = time.Now().Add(ttl)
	return nil
}
  • 주어진 key의 TTL(Time-to-Live)을 설정
  • 쓰기 잠금을 사용하여 안전하게 항목의 만료 시간을 수정
  • 항목이 존재하지 않으면 ErrKeyNotFound를 반환
  • 존재하는 경우, 만료 시간을 현재 시간에 ttl을 더한 값으로 업데이트
1
2
3
4
5
6
7
8
9
10
11
func (c *InMemoryCache) GetTTL(_ context.Context, key string) (time.Duration, error) {
	c.mtx.RLock()
	defer c.mtx.RUnlock()

	item, exists := c.items[key]
	if !exists {
		return 0, ErrKeyNotFound
	}
	ttl := time.Until(item.expiry)
	return ttl, nil
}
  • 주어진 key의 남은 TTL을 조회
  • 읽기 잠금을 사용하여 데이터 무결성을 유지
  • 항목이 존재하지 않으면 ErrKeyNotFound를 반환하고, 존재한다면 현재 시간과 만료 시간의 차이를 계산하여 남은 TTL 을 반환
1
2
3
4
5
6
7
func (c *InMemoryCache) Exists(_ context.Context, key string) (bool, error) {
	c.mtx.RLock()
	defer c.mtx.RUnlock()

	_, exists := c.items[key]
	return exists, nil
}
  • 주어진 key가 캐시에 존재하는지 확인
  • 읽기 잠금을 사용하여 데이터 무결성을 유지
  • 항목이 존재하면 true, 그렇지 않으면 false
1
2
3
4
5
6
7
func (c *InMemoryCache) Clear(_ context.Context) error {
	c.mtx.Lock()
	defer c.mtx.Unlock()

	c.items = make(map[string]*cacheItem)
	return nil
}
  • map 을 초기화 시킴
1
func (c *InMemoryCache) Close() {}
  • Interface 에 정의된 메서드이나, In-Memory Cache 의 구현체 에서는 선택적 요소 (사용 예 : {c.items = nil} 과 같이 사용해도 되지만, 단, items 가 nil 일 때의 예외처리를 다른 메서드에 추가할 것)
1
2
3
func (c *InMemoryCache) Description() string {
	return "InMemoryCache: A simple in-memory cache implementation"
}
  • 인스턴스의 기본 설명을 문자열로 반환
This post is licensed under CC BY 4.0 by the author.