[GO] GO with REDIS

타키탸키·2022년 11월 29일
1

GO-WEB

목록 보기
8/11
post-thumbnail

Redis

개요

Redis(Remote Dictionary Server)는 key-value 구조의 비정형 데이터를 메모리에 로드해 처리하는 메모리 기반 비관계형 데이터베이스 관리 시스템(DBMS)이다. 다양한 데이터 구조(string, list, set, sorted set, hashes)를 지원한다.

Redis는 Caching과 Message Broker로써 사용된다. cache는 컴퓨터의 성능을 향상시키기 위해 사용되는 메모리 영역으로, REDIS는 이 임시 메모리 내에 key-value 형식의 데이터를 기존 data storage에 저장된 데이터보다 더 빠르고 간단하게 저장하고 접근할 수 있도록 돕는다. 간단한 SET, GET 명령을 통해 사용이 가능하다.

Message Broker는 송신자의 메시지 프로토콜 형식의 메시지를 수신자의 메시지 프로토콜 형식으로 변환하는 중간 컴퓨터 프로그램 모듈로, 간단히 말해 응용 소프트웨어 간에 메시지 교환을 위한 기능이라고 정의할 수 있다. REDIS는 Message Broker의 기능으로, Message Queue, Pub/Sub, Stream을 지원한다.

Redis 설치 및 실행

❗ Redis는 공식적으로 Windows를 지원하지 않는다. 따라서, 통상 Windows 개발 환경에서는 MS Open Tech 그룹에서 포팅한 Windows용 Redis를 사용한다. (이 또한, 3.0 버전 이후로 더 이상 업데이트 되고 있지 않다) 현재 계획하고 있는 프로젝트가 Windows에서 이뤄져야 하므로 Windows를 대상으로 실습을 진행하고자 한다.

Windows에서의 Redis 설치와 관련된 내용은 아래 링크를 확인하면 된다.

https://bit.ly/3ilt1rT

Redis가 설치되고 나면, redis-cli 명령을 통해 REDIS 서버에 접속해본다.

redis-cli

서버에 접속이 되면 ip 주소와 6379라는 Redis 서버 전용 포트 번호가 뜨며, 명령어를 입력할 수 있다.

❗ 만약, Connection refused라는 문구가 뜨면, 다음 포스팅을 참고해보자. 이 경우, 이미 Redis가 설치되어 있을 가능성이 높다. WSL2에서 Redis를 설치하지 않았는지 되짚어보자.

Redis 사용해보기

SELECT

SELECT는 DB를 선택할 수 있는 명령어이다. SELECT의 인자는 index로 DB의 인덱스 번호를 가리키며, 설정하지 않는 경우에는 0번째 DB가 선택된다. 총 16개의 DB를 제공하며, 0에서 15까지의 인덱스 번호를 가진다.

> SELECT 16 // (error)
> SELECT 10 // OK

127.0.0.1:6379[10] >

위 사례에서처럼 유효하지 않은 인덱스 번호를 입력할 경우, invalid DB index 에러가 발생한다. DB를 선택하고 나면, REDIS 포트 번호 옆에 DB의 인덱스가 뜬다.

SET/GET

REDIS는 key-value로 데이터를 저장한다. SET은 key와 value를 인자로 받아 key에 value를 저장하고 GET은 key를 인자로 받아 해당 key에 저장된 value를 가져온다.

  • SET key value
    • key에 value 저장하기
  • GET key
    • key에 해당하는 value 가져오기

기존에 존재하는 key에 value를 저장하면 새로운 value가 기존 value를 덮어 쓴다.

APPEND

APPEND를 통해 이미 저장된 key에 추가로 value를 넣을 수 있다. 이때, 새로 넣은 value는 기존 value의 바로 뒤에 저장된다. key가 없는 경우에는 SET과 동일하게 동작한다. 이 때, 반환되는 정수는 데이터의 길이를 의미한다.

  • APPEND key value
    • 기존 key에 value 추가 저장하기

SET ... EX

key에 만료 시간을 줄 수 있다. EX 뒤에는 초 단위의 시간이 오며, 그 시간만큼만 key에 value가 저장된다. 시간이 만료되면, 해당 key에 대해 nil을 반환한다.

  • SET key value EX time(s)
    • key의 만료 시간을 초 단위로 설정하기

KEYS *

DB에 존재하는 모든 key를 불러온다.

  • KEYS *
    • DB의 모든 KEY 불러오기

GO with Redis

다음 내용은 ALEX EDWARDS의 포스팅에 기반하여 작성되었음을 알린다

Redigo 설치하기

Redigo는 Redis 데이터베이스를 위한 Go 클라이언트 패키지이다. 다음 명령어를 통해 redigo를 설치하자.

go get github.com/gomodule/redigo/redis

Go with Redis 시작하기

음반을 파는 온라인 쇼핑몰을 떠올려 보자. 이 쇼핑몰은 redis를 활용해 앨범에 대한 정보를 저장하려고 한다.

각각의 앨범은 타이틀, 아티스트, 가격과 좋아요 수 등의 필드를 가진 hash 형태로 정의할 수 있다. (참고로 hash는 key와 value로 이루어진 데이터 구조를 말한다)

또한, 각각의 앨범에 대한 key는 album:{id}과 같은 형태로 나타낼 수 있는데 이때, id는 고유한 정수값이어야 한다.

Redis db에 data 저장하기

Redis CLI를 통해 새로운 앨범을 저장하려면 다음과 같이 입력하면 된다.

HMSET album:1 title "Electric Ladyland" artist "Jimi Hendrix" price 4.95 likes 8

HMSET은 key에 저장된 hash 필드를 여러 개 설정할 때 사용하는 Redis 명령어이다.

우리는 이 명령어가 수행하는 작업을 Go 코드를 통해 똑같이 실현해보고자 한다. 그러기 위해서는 다음과 같은 redigo 패키지의 함수들이 필요하다.

  1. Dial()
    Redis 서버와 연결하기 위한 함수이다. 새로운 Connection을 반환한다.
  1. Do()
    Connection을 통해 Redis 서버에 명령어를 전송하는 함수이다. Redis로부터 받은 응답을 반환하며, 이때 응답의 타입은 interface{}이다. 에러를 받을 수도 있다.

이제 코드를 직접 보며, 사용법을 알아보자.

<main.go>

package main

import (
	"fmt"
	"log"

	// redigo/redis package 불러오기
	"github.com/gomodule/redigo/redis"
)

func main() {
	// localhost의 6379 포트에서 redis server와의 connection 생성(listen)
    // 6379는 redis용 포트로, 설정을 변경하지 않았다면 기본적으로 이 포트를 사용하면 된다
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		log.Fatal(err)
	}
	// main 함수가 종료되기 전에 connection이 종료될 수 있도록 지연한다
	defer conn.Close()

	// connection으로 명령어를 전송한다
    // Do의 첫번째 인자는 항상 Reids 명령어가 와야 한다(이 예시에서는 HMSET)
    // 나머지 인자로는 key와 hash로 이루어진 value가 온다
    // (key - "album:1" / value(hash) - {"title": "Electric Ladyland"})
	_, err = conn.Do("HMSET", "album:1", "title", "Electric Ladyland",
    "artist", "Jimi Hendrix", "price", 4.95, "likes", 8)
	if err != nil {
		log.Fatal(err)
	}

	// 위 작업이 성공적으로 진행되면, 아래 문구를 출력한다
	fmt.Println("Electric Ladyland added!")
}

일반적으로, Redis CLI에서 HMSET 명령어가 성공하면 OK라는 문자열만 돌아온다. 위 코드에서는 그러한 응답에 대한 처리를 하지 않았다. Do 함수가 성공할 때 출력되는 문구로 성공 여부를 알 수 있을 뿐이다.

그렇다면, 이 코드가 정상적으로 동작했는지 어떻게 알 수 있을까? 직접 Redis CLI에서 확인해보면 된다.


위와 같이 KEYS * 명령을 통해 현재 db에 등록된 key를 볼 수 있다. Go 프로그램 실행 전과 실행 후에 이 명령어를 실행해보자. 그럼 프로그램 실행 후, "album:1"이라는 key가 등록되었다는 사실을 알 수 있다.

Redis db로부터 data 가져오기

Redis로부터 받는 응답의 타입은 interface{}이다. 따라서, 이 응답을 받아 처리하려면 Go에서 작업하기 편한 형태의 Go type으로 변환시켜야 한다. 이를 위해 redigo 패키지의 다음과 같은 함수들이 필요하다.

  • redis.Bool() : 단일 응답을 bool로 변환
  • redis.Bytes() : 단일 응답을 byte 슬라이스([]byte)로 변환
  • redis.Float64(): 단일 응답을 float64로 변환
  • redis.int(): 단일 응답을 int로 변환
  • redis.String(): 단일 응답을 string으로 변환

  • redis.Values(): 배열 응답을 slice로 변환
  • redis.Strings(): 배열 응답을 string slice([]string)로 변환
  • redis.ByteSlices(): 배열 응답을 byte slice의 slice([][]byte)로 변환

  • redis.StringMap()
    • key와 value를 대체하는 문자열 배열을 map[string]string으로 변환
    • HGETALL 등에 유용하다

이를 참고하여, HGET 명령어를 통해 하나의 앨범 hash 정보를 가져오는 예제를 살펴보자.

package main

import (
	"fmt"
	"log"

	"github.com/gomodule/redigo/redis"
)

func main() {
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	// HGET 명령어로 앨범의 타이틀을 가져온다
    // String 헬퍼 함수를 통해 응답을 string으로 변환시킨다
	title, err := redis.String(conn.Do("HGET", "album:1", "title"))
	if err != nil {
		log.Fatal(err)
	}

	// 마찬가지로, string으로 변환시켜 아티스트를 가져온다
	artist, err := redis.String(conn.Do("HGET", "album:1", "artist"))
	if err != nil {
		log.Fatal(err)
	}

	// 가격은 float64로 가져온다
	price, err := redis.Float64(conn.Do("HGET", "album:1", "price"))
	if err != nil {
		log.Fatal(err)
	}

	// 좋아요 수는 int로 가져온다
	likes, err := redis.Int(conn.Do("HGET", "album:1", "likes"))
	if err != nil {
		log.Fatal(err)
	}

	// 모든 작업이 성공하면, 타이틀, 아티스트, 가격, 좋아요 수 정보를 출력한다
	fmt.Printf("%s by %s: £%.2f [%d likes]\n", title, artist, price, likes)
}

위 코드를 실행하면, 콘솔에 다음과 같이 Redis db에 저장한 앨범 정보가 출력된다.

$ go run main.go

Electric Layland by Jimi Hendrix: £4.95 [8 likes]

redigo의 helper 함수를 사용할 때, 반환되는 에러에는 두 가지 유형이 있다. 하나는 명령어 실행이 실패했다는 것이고, 다른 하나는 응답이 기대했던 결과 type과 맞지 않는 type으로 변환되었다는 것이다("Jimi Hendirx"를 불러오는데 float64 type으로 변환하는 helper 함수를 사용하는 경우를 떠올려보자). 에러 메시지를 확인하지 않는 한, 두 사례 중 어떤 에러가 발생했는지 알 수 없다.

Redis data를 Go struct에 저장하기

더 복잡한 예시로 넘어가보자. 다음은 HGETALL 명령어를 사용하여 Redis db로부터 앨범 hash의 모든 필드를 가져와서 Go의 struct에 저장하는 코드이다.

package main

import (
	"fmt"
	"log"
	"strconv"

	"github.com/gomodule/redigo/redis"
)

// Album data를 담기 위한 struct를 선언한다
type Album struct {
	Title  string
	Artist string
	Price  float64
	Likes  int
}

func main() {
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	// HGETALL 명령어로 모든 album 필드를 가져온다
    // HGETALL이 배열 형태의 응답을 반환하고 Redis에 저장된 data가 hash 형태이므로,
    // Map() helper 함수를 사용하여 map[string][string] 형태로 변환해야 한다
	reply, err := redis.StringMap(conn.Do("HGETALL", "album:1"))
	if err != nil {
		log.Fatal(err)
	}

	// 아래 populateAlbum 함수를 사용하여 map[string][string]으로부터 
    // 새로운 앨범 객체를 생성한다
	album, err := populateAlbum(reply)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v", album)
}

// map[string][string]의 data를 활용하여 새로운 Album struct의 포인터를 생성하고 반환한다
func populateAlbum(reply map[string]string) (*Album, error) {
	var err error
    
	album := new(Album)
	album.Title = reply["title"]
	album.Artist = reply["artist"]
    
    // strconv 패키지를 사용해서 문자열 형태의 price 값을 float64로 변환해야 한다 
	album.Price, err = strconv.ParseFloat(reply["price"], 64)
	if err != nil {
		return nil, err
	}
	// 마찬가지로, 문자열 형태의 likes의 값을 정수로 변환해야 한다
	album.Likes, err = strconv.Atoi(reply["likes"])
	if err != nil {
		return nil, err
	}
    
	return album, nil
}

위 코드를 실행하면, 다음과 같은 결과가 출력된다.

$ go run main.go

&{Title:Electric Ladyland Artist:Jimi Hendrix Price:4.95 Likes:8}

다음은 redis.Values()redis.ScanStruct()를 활용한 예제이다. 이 함수들을 사용하면, data를 자동으로 적절한 Album struct field에 할당할 수 있다. 이 과정에서 형 변환 코드가 생략되어 위 예제보다 더 간결한 코드를 작성할 수 있다.

package main

import (
	"fmt"
	"log"

	"github.com/gomodule/redigo/redis"
)

// struct tag가 보이는가?
// 이들은 redigo에 응답 data를 어떤 방식으로 struct에 할당해야 하는지를 명시해준다
type Album struct {
	Title  string  `redis:"title"`
	Artist string  `redis:"artist"`
	Price  float64 `redis:"price"`
	Likes  int     `redis:"likes"`
}

func main() {
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	// HGETALL 명령어에 대한 실행 함수를 redis.Values()로 감싸서
    // redis.ScanStruct()에 전달할 []interface{} 형태로 type을 변환한다.
	values, err := redis.Values(conn.Do("HGETALL", "album:1"))
	if err != nil {
		log.Fatal(err)
	}

	// Album struct의 인스턴스를 생성하고 redis.ScanStruct()를 통해
    // 자동으로 data를 분리하여 struct field로 넣어준다
    // struct tag를 사용해서 어떤 data가 어떤 struct field에 매핑되어야 하는지를 결정한다
	var album Album
    
	err = redis.ScanStruct(values, &album)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v", album)
}

❗ 참고
redis.ScanStruct() 함수는 Redis가 반환하는 data를 알맞은 Go type으로 변환하기 위해 Go의 strconv 패키지를 사용한다. 기본적으로, 이 함수는 정수, 실수, boolean, 문자열 그리고 []byte 필드를 지원한다. 필요에 따라, redis.Scanner() 인터페이스를 직접 구현하면 custom type을 자동으로 스캔할 수 있다.


지금까지 redigo 패키지를 활용한 GO와 Redis의 기본적인 활용법을 알아봤다. 이 내용들을 바탕으로 다음 포스팅에서 Go 웹 애플리케이션에서 Redis를 활용하는 방법에 대해 더 알아보고자 한다.

profile
There's Only One Thing To Do: Learn All We Can

0개의 댓글