메뉴

캐시

작동 가능한 상태로 코드를 개발했다고 하면, 성능은 캐시(Cache)로 달성한다. 보통 memcacheredis와 같은 인-메모리 데이터베이스를 사용하한다. 이들로도 충분하긴 하지만 때때로 프로세스 내에서 캐시를 유지해야 할 때도 있다.

애플리케이션 레벨에서는 주로 데이터베이스 조회 결과와 네트워크 조회 결과를 캐시할 것이다.

아이디어

캐시는 key, value를 저장하는 기능과 key에 대한 만료시간 설정 기능이면 구현이 가능하다. 대략 아래와 같이 구현 할 수 있을 것이다. 의사코드에 가까운 수준이니 참고만 하자.
type Item struct {
	Object interface{}
	Expiration int64
}

type Cache struct {
	items  map[string]Item
}

func (c *Cache) Set(key string, value interface{}, expire time.Duration) {
	c.items[key] = Item{Object: value, Expiration: time.Duration(....)}
}

func (c *Cache) Get(key string) (interface{}, error) {
	// Key is deleted after the expiration time 
	// find object
	if val, ok := c.items[key]; ok {
		return val, nil
	} else {
		return nil, errors.New("Item not Found")
	}
}

go-cache

직접 코드를 만드는 대신 이미 만들어진 코드를 사용하기로 했다. 몇 개 코드가 있었는데 go-cache가 가장 좋아보여서 사용하기로 했다.

Install
go get github.com/patrickmn/go-cache

테스트 코드를 만들어봤다. interface를 리턴하기 때문에 type casting에 신경을 좀 써야 하는 것을 제외하면, 쓰는데 문제는 없었다.
package main

import (
	"fmt"
	"time"

	"github.com/patrickmn/go-cache"
)

type User struct {
	Name     string
	Email    string
	HomePage string
	TagCount map[string]int
}

func main() {
	c := cache.New(10*time.Second, 20*time.Second)
	yundream := User{Name: "yundream",
		Email:    "yundream@gmail.com",
		HomePage: "https://www.joinc.co.kr",
		TagCount: map[string]int{"Linux": 30, "DevOps": 45},
	}
	c.Add("user-001", yundream, cache.DefaultExpiration)

	for {
		u := User{}
		if value, found := c.Get("user-001"); found {
			u = value.(User)
			fmt.Printf("%v\n", u)
		} else {
			fmt.Println("user-001 not found")
			c.Add("user-001", yundream, cache.DefaultExpiration)
		}
		time.Sleep(2 * time.Second)
	}
}

경쟁조건

go-cache는 하나 이상의 고루틴이 접근해서 사용 할 수 있다. 여러 고루틴이 경합을 벌이면서 동시에 데이터에 경쟁조건(Race condition)이 발생할 수 있다. 이런 경쟁조건은 뮤텍스(mutex)를 이용해서 제거 할 수 있다.

뮤텍스 메커니즘은 단순하다.
  1. 보호하려는 자원이 있다.
  2. 자원에 접근하기 전에 "잠금"을 건다. 이 잠금은 배타적이다. 즉 한 번에 하나의 고루틴만 잠금을 얻을 수 있다.
  3. 잠금을 얻지 못하는 고루틴은 잠금을 풀어주기를 기다린다.
  4. 잠금을 가지고 보호 영역에 들어간 고루틴은 데이터를 읽거나 쓰는 작업을 한다.
  5. 작업을 끝마치면 잠금을 되돌려준다.
  6. 자원에 접금하기를 기다리던 고루틴 중 하나가 잠금을 얻고 자원에 접근한다.
이러한 배타적 잠금을 이용해서 자원을 안전하게 보호 한다. 예를 들어 Get이라면 아래와 같이 코드를 작성하면 된다.
func (c *Cache) Get(k string) (interface{}, bool) {
	c.mu.RLock() // Mutex Lock
	item, found := c.items[k]
	if !found {
		c.mu.RUnlock() // Mutex UnLock
		return nil, false
	}
	if items.Expiration > 0 {
		return nil.false
	}
	c.mu.RUnlock()
	return item.Object. true
}

사용

내 사이트에 적용해서 사용하고(혹은 사용하려 할 계획에) 있다.

사이트의 문서는 위키(마크다운과 비슷한)문서로 데이터베이스에 저장돼있다. 한번 저장 되면 변경되지 않는 데이터를 매번 데이터베이스에서 읽어서 전송한다. 데이터베이스와 네트워크는 값비싼 자원으로 전송을 느리게 만들고, 서비스에 부하를 준다.

문서의 이름을 Key, 내용을 Value로 해서 go-cache에 저장하기로 했다. 메모리에 있는 데이터 사본을 읽기 때문에 빠르게 응답하며, 네트워크 문제/데이터베이스 문제로 부터 좀 더 자유로워진다.(애플리케이션 문제의 대부분은 데이터베이스와 네트워크에서 발생한다.)