Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

Factory method pattern

팩토리는 공장이라는 뜻을 내포하고 있다. 우리가 공장에 어떤 제품을 요청하면, 공장에서 그에 맞는 제품을 내놓는다. 이때 개발자는 공장의 내부를 알고 있을 필요가 없다. 그냥 공장에서 제공하는 제품의 목록과 이름을 알고 있으면 된다. 소프트웨어 공학에서 팩토리 메서드는 메서드의 구현을 서브 클래스에 위임하는 개발 방법이다. 개발자는 필요에 따라서 다양한 구현을 할 수 있으며, 이름으로 객체를 만들어서 쓸 수 있다.

데이터베이스에 접근해서 질의를 수행하는 애플리케이션을 개발한다고 가정해보자. 환경에 따라서 MySQL, Postgresql, Oracle, SqLite등 다양한 데이터베이스를 사용 할 수 있을 것이다. 그러면 추상 클래스를 만들고, 각 데이터베이스를 위한 서브클래스를 만들어서 메서드를 구현하면 된다. 이렇게 만든 데이터베이스 클래스는 아래와 같이 호출 하면 된다.
  • sql.Open("mysql") : mysql 데이터베이스 객체를 리턴한다.
  • sql.Open("postgresql") : postgresql 데이터베이스 객체를 리턴한다.
아래는 일반적으로 묘사한 그림이다.

 Factory method pattern
  • Product
  • Concrete Product : Product 인터페이스의 구현.
  • Creator : 팩토리 메서드의 선언과 기본적인 구현 제공.
  • Concrete Creator : Concrete Product를 반환하는 메서드의 구현.

예제

REST 기반의 웹 애플리케이션을 개발하고 있다. 이 애플리케이션은 세션을 관리하는데, 환경에 따라서 다양한 세션저장 방법을 제공해야 했다.
  • MySQL에 있는 세션 정보를 직접 읽어서 처리
  • MySQL앞에 REDIS를 둔다. 세션정보는 MySQL과 REDIS에 동시에 저장한다. 평소에는 REDIS에서 세션정보를 읽는다. 데이터베이스의 부하를 줄이고, 읽기 속도를 높이기 위한 방안이다.
  • JWT를 사용한다. CPU를 좀 더 사용하긴 하겠으나 데이터베이스 없이 사용 할 수 있다는 장점이 있다.
  • 메모리에서 처리 할 수도 있다.
  • 때에 따라서는 MongoDB를 사용해야 할 수도 있다.
나는 팩토리 패턴을 이용해서 위 기능을 구현하기로 했다.

먼저 세션데이터를 관리하기 위한 Session interface를 만들었다. 이 interface는 Session 관리를 위한 4개의 메서드를 가질 것이다.
  • Create() : 세션을 만든다.
  • Get() : 세션을 가져온다.
  • Update() : 세션을 업데이트 한다.
  • Delete() : 세션을 삭제한다.
기타 몇개 에러까지 정의해서 아래와 같은 코드를 만들었다.
var (
    SessionFoundError = errors.New("Session not found")
    letterRunes       = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
)

func RandStringRunes(n int) string {
    rand.Seed(time.Now().UnixNano())
    b := make([]rune, n)
    for i := range b {
        b[i] = letterRunes[rand.Intn(len(letterRunes))]
    }
    return string(b)
}

type Session interface {
    Create(value string) string
    Get(id string) (string, error)
    Delete(id string) error
}
Update까지 구현하려니 귀찮아서 뺐다. 세션값을 랜덤하게 만들기 위해서 RandStringRunes 함수를 추가했다. Create 메서드는 RandStringRunes를 이용해서 랜덤한 세션아이디를 만든다.

세션 데이터는 key-value 형태로 저장한다. id는 세션 아이디로 key가 되며, 세션에 저장할 값이 value가 된다. 실제 구현시에는 value를 interface 타입을 사용하겠으나, 변환하기 귀찮아서 그냥 string을 사용했다.

메모리 기반 세션 메니저

이제 첫번째 구현을 만들어보자. 일단 가장 간단한 메모리 처리버전을 만들어보기로 했다. 이 코드는 세션아이디와 값을 golang의 map 에 저장한다.
type MemSession struct {
    DB map[string]string
}

func MemSessionNew() *MemSession {
    sess := &MemSession{DB: make(map[string]string)}
    return sess
}
func (m *MemSession) Create(v string) string {
    randStr := RandStringRunes(32)
    m.DB[randStr] = v
    return randStr
}
func (m *MemSession) Get(id string) (string, error) {
    if v, ok := m.DB[id]; ok {
        return v, nil
    }
    return "", SessionFoundError
}

func (m *MemSession) Delete(id string) error {
    if _, ok := m.DB[id]; ok {
        delete(m.DB, id)
        return nil
    }
    return SessionFoundError
}
main 함수에서 호출해 봤다.
func main() {
    sess := Session(MemSessionNew())
    sid := sess.Create("hello world")
    v, _ := sess.Get(sid)
    fmt.Println(sid, "->", v)
}

REDIS 기반 세션 메니저

이제 REDIS 기반의 세션관리자를 만들어보자. gopkg.in/redis.v3 패키지를 설치하자.
$ go get gopkg.in/redis.v3
레디스 기반의 세션 메니저다.
func RedisSessionNew() *RedisSession {
    client := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })
    _, err := client.Ping().Result()
    if err != nil {
        panic(err)
    }
    return &RedisSession{cli: client}
}

type RedisSession struct {
    cli *redis.Client
}

func (r *RedisSession) Create(v string) string {
    randStr := RandStringRunes(32)
    r.cli.Set(randStr, v, 0)
    return randStr
}

func (r *RedisSession) Get(id string) (string, error) {
    v, err := r.cli.Get(id).Result()
    if err != nil {
        return "", err
    }
    return v, nil
}

func main() {
    redisSess := Session(RedisSessionNew())
    sid = redisSess.Create("myname is yundream")
    v, _ := redisSess.Get(sid)
    fmt.Println(sid, "->", v)
}

완전한 코드

package main

import (
    "errors"
    "fmt" 
    redis "gopkg.in/redis.v3"
    "math/rand"
    "time"
)

var (
    SessionFoundError = errors.New("Session not found")
    letterRunes       = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
)

// 세션아이디로 사용할 랜덤 문자열을 얻는다.
func RandStringRunes(n int) string {
    rand.Seed(time.Now().UnixNano())
    b := make([]rune, n)
    for i := range b {
        b[i] = letterRunes[rand.Intn(len(letterRunes))]
    }
    return string(b)
}

type Session interface {
    Create(value string) string
    Get(id string) (string, error)
    Delete(id string) error
}

// 메모리 기반 세션 메니저
// golang의 map 자료구조를 사용했다.
type MemSession struct {
    DB map[string]string
}

func MemSessionNew() *MemSession {
    sess := &MemSession{DB: make(map[string]string)}
    return sess
}
func (m *MemSession) Create(v string) string {
    randStr := RandStringRunes(32)
    m.DB[randStr] = v
    return randStr
}
func (m *MemSession) Get(id string) (string, error) {
    if v, ok := m.DB[id]; ok {
        return v, nil
    }
    return "", SessionFoundError
}

func (m *MemSession) Delete(id string) error {
    if _, ok := m.DB[id]; ok {
        delete(m.DB, id)
        return nil
    }
    return SessionFoundError
}

// 레디스 기반 세션 메니저
func RedisSessionNew() *RedisSession {
    client := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })
    _, err := client.Ping().Result()
    if err != nil {
        panic(err)
    }
    return &RedisSession{cli: client}
}

type RedisSession struct {
    cli *redis.Client
}

func (r *RedisSession) Create(v string) string {
    randStr := RandStringRunes(32)
    r.cli.Set(randStr, v, 0)
    return randStr
}

func (r *RedisSession) Get(id string) (string, error) {
    v, err := r.cli.Get(id).Result()
    if err != nil {
        return "", err
    }
    return v, nil
}
func (r *RedisSession) Delete(id string) error {
    return r.cli.Del(id).Err()
}

func main() {
    sess := Session(MemSessionNew())
    sid := sess.Create("hello world")
    v, _ := sess.Get(sid)
    fmt.Println(sid, "->", v)

    redisSess := Session(RedisSessionNew())
    sid = redisSess.Create("myname is yundream")
    v, err := redisSess.Get(sid)
    if err != nil {
        panic(err)
    }
    fmt.Println(sid, "->", v)
}

개선 - 세션 프로바이더 만들기

기본 원리를 설명하는데는 문제 없는 코드지만, 실제 써먹기에는 너무 지저분하다. 좀 더 깔끔하게 만들어보자. 팩토리 패턴을 사용한 이유는
  1. 다양한 종류의 세션 관리자를 등록 하고
  2. 환경에 맞는 세션 관리자를 사용하기 위함이다.
그래서 세션 관리자를 등록해서 사용 할 수 있는 세션 프로바이더를 만들기로 했다.
var SessionStore = make(map[string]Session)

// 세션을 등록한다.
func Register(name string, sess Session) error {
    if sess == nil {
        return errors.New("Session not found") 
    }
    _, ok := SessionStore[name]
    if ok {
        return errors.New("Session exists")
    }
    SessionStore[name] = sess
    return nil
}

// 이름으로 세션 프로바이더를 찾는다.
func GetSessionStore(name string) (Session, error) {
    sess, ok := SessionStore[name]  
    if !ok {
        return nil, errors.New("Session store not found :" + name)
    }
    return sess, nil
}
아래는 테스트 코드다.
func TestSessionStore(t *testing.T) {
    err := Register("mem", MemSessionNew())
    assert.Nil(t, err)
    err = Register("redis", RedisSessionNew())
    assert.Nil(t, err)

    // 존재하지 않는 프로바이더를 호출 했다.
    // 에러를 출력한다.
    _, err = GetSessionStore("mongo")
    assert.NotNil(t, err)
    sess, err := GetSessionStore("mem")
    assert.Nil(t, err)

    sid := sess.Create("hello world")
    v, err := sess.Get(sid)
    assert.Nil(t, err)
    assert.Equal(t, "hello world", v)
}   

코드는 에서 찾을 수 있다.