· 개인

Redis Local Hands-on (feat. redis key 문법)

도커로 레디스를 띄워 문자열·리스트·해시·TTL 명령을 직접 쳐보고, 실무에서 쓸 키 네이밍 규칙(계층 구조, 환경 구분, 임시 키)까지 정리했다.

작성날짜: 24.11.25

레디스를 로컬에서 테스트해보자

Docker 환경 내 준비사항

  • redis

docker-compose setting

version: '3.8'

services:
  redis:
    container_name: local-redis
    image: redis:latest
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    restart: always

volumes:
  redis_data:

간단한 Redis 테스트 위한 단계별 가이드

0. 기본적인 도커 명령어

- it 명령어: 
  - -i(interactive(대화형)): 대화형 모드로 표준 입력을 열어둠
  - -t(terminal): 터미널 할당하여 bash 셸을 올바르게 표시하고 사용
  이 2개의 옵션 사용 시, 컨테이너의 셸을 마치 로컬 터미널처럼  사용 가능!

1. Redis 컨테이너 접속

docker exec -it local-redis bash

2. redis-cli 실행

redis

3. 테스트

문자열 테스트:

# 값 설정
SET name "Hong Gil Dong"
# 값 가져오기
GET name

# 값이 존재하는지 확인
EXISTS name

리스트 테스트:
# 리스트에 값 추가
LPUSH fruits apple
LPUSH fruits banana
RPUSH fruits orange

# 리스트 전체 조회
LRANGE fruits 0 -1

# 리스트 길이 확인
LLEN fruits

해시 테스트:
# 해시에 필드와 값 설정
HSET user:1 username "hong" email "hong@example.com" age "30"

# 해시의 모든 필드와 값 조회
HGETALL user:1

# 특정 필드 값 조회
HGET user:1 username

만료시간 테스트:
# 10초 후 만료되는 키 설정
SET temporary "will expire" EX 10

# 남은 만료 시간 확인
TTL temporary

# 10초 후에 다시 조회하면 키가 없어짐
GET temporary

👨‍🌾 Redis Key 문법을 알아보자

키 네이밍 기본 규칙

  • 최대 크기: 512MB (일반적으로 짧은 키 권장)
  • 이진 안전(binary-safe): 모든 문자열 가능
  • 가독성을 위해 일반적으로 ASCII 문자 사용
  • 대소문자 구분
# 1. 객체 타입:엔티티ID
user:1000
product:2000
order:3000

# 2. 객체 타입:엔티티ID:속성
user:1000:profile
user:1000:settings
product:2000:reviews

# 3. 객체 타입:엔티티ID:집합
user:1000:followers
post:1000:likes
product:2000:tags

# 4. 기능:세부사항
cache:categories
search:recent
config:api

# 5. 시간 기반
events:2024-03-20
logs:2024:03:20:13
stats:daily:2024-03-20

# 6. 카운터
count:users
count:views:post:1000
seq:order

일반적인 키 네이밍 컨벤션 (4)

image

키 네이밍 Best Practice

# 1. 계층적 구조 사용
company:1:department:2:employee:3

# 2. 검색을 위한 패턴
product:*          # 모든 제품
user:1000:*        # 특정 사용자의 모든 관련 키
order:2024-03-*    # 특정 월의 모든 주문

# 3. 임시 키 (TTL 적용)
temp:session:abc123
temp:verification:xyz789

# 4. 환경 구분
dev:users:1000
prod:users:1000
test:users:1000

키 네이밍 실제 사용 예시

# 사용자 관련
SET user:1000:profile "{name:'John',age:30}"
HSET user:1000:settings theme "dark" language "ko"
SADD user:1000:followers "user:1001" "user:1002"

# 상품 관련
SET product:2000 "{name:'Phone',price:1000}"
LPUSH product:2000:reviews "{text:'Good',rating:5}"
SADD product:2000:categories "electronics" "mobile"

# 시스템 관련
INCR count:daily:visitors
ZADD ranking:products:daily 100 "product:2000"
SET session:token:abc123 "user:1000" EX 3600

데이터 유형별 사용 사례

[!NOTE] 각 자료형마다 적합한 사용 예시 존재

image

🔥 키 네이밍 주의사항 & 팁

  • 아래와 같이 네이밍 규칙의 장점
    • 데이터 구조를 쉽게 이해할 수 있음
    • 키 충돌을 방지할 수 있음
    • 효율적인 키 검색이 가능함
    • 시스템 유지보수가 용이함

🤣 키 길이

너무 긴 키는 메모리 낭비 적절한 길이로 의미 전달

# 좋은 예
user:1000:profile

# 나쁜 예
this:is:very:long:key:name:for:user:profile:data:1000

🤣 일관성

같은 타입의 데이터는 같은 패턴 사용

# 좋은 예
user:1000:profile
user:1001:profile

# 나쁜 예
user:1000:profile
user_1001_profile

🤣 검색 가능성

와일드카드로 검색 가능하게 설계

# 특정 사용자의 모든 데이터 검색
KEYS user:1000:*


# 특정 날짜의 모든 로그 검색
KEYS logs:2024-03-20:*

🤣 특수문자 사용

: - 네임스페이스 구분 . - 상세 속성 구분

    • 가독성을 위한 구분
app:user:1000        # 네임스페이스
user.profile.image   # 속성

🔥 Redis Key 연결 및 조합

  • 키 조합을 통해?
    • 복잡한 데이터 관계 표현 가능
    • 효율적인 데이터 검색과 조회 가능
    • 실시간 데이터 처리와 분석 가능
    • 다양한 비즈니스 로직 구현 가능
  • 주의사항
    • 키 조합 연산은 메모리를 많이 사용할 수 있음
    • 복잡한 조합은 성능에 영향을 줄 수 있음
    • 임시 키 사용 시 적절한 삭제 필요
    • 데이터 일관성 유지 필요

1. 파이프라인(Pipeline)을 사용한 다중 키 조회:

# 여러 키를 한 번에 조회
MGET user:1000 user:1000:profile user:1000:settings

# 파이프라인 예시
PIPELINE
GET user:1000
SMEMBERS user:1000:followers
LRANGE user:1000:posts 0 10
EXECUTE

2. Set 연산을 통한 키 조합:

# 두 사용자의 공통 팔로워 찾기
SINTER user:1000:followers user:1001:followers

# 사용자 A의 팔로워 중 사용자 B의 팔로워가 아닌 사람들
SDIFF user:1000:followers user:1001:followers

# 두 사용자의 모든 팔로워 통합
SUNION user:1000:followers user:1001:followers

3. Sorted Set 연산 예시:

# 여러 제품 카테고리의 인기 상품 통합 랭킹
ZUNIONSTORE popular:products 2 popular:electronics popular:clothing WEIGHTS 1 1

# 특정 기간의 사용자 활동 점수 합산
ZUNIONSTORE user:1000:activity:total 3 
    user:1000:posts:score 
    user:1000:comments:score 
    user:1000:likes:score

그렇다면 키 조합하고 일반적인 RDBMS의 JOIN하고 비슷한거 아닌가?

image

키 조합에서 나온 Redis Pipeline 이란

[!NOTE] 파이프라인은 여러 Redis 명령어를 한 번에 묶어서 실행하는 방식

sequenceDiagram
    participant C as Client
    participant R as Redis Server
    
    Note over C,R: 일반 실행 방식
    C->>R: GET user:1
    R-->>C: 응답
    C->>R: GET user:2
    R-->>C: 응답
    C->>R: GET user:3
    R-->>C: 응답
    
    Note over C,R: 파이프라인 방식
    C->>R: GET user:1\nGET user:2\nGET user:3
    R-->>C: 모든 응답 한 번에

Redis namespace 는?

[!NOTE] 네임스페이스는 키를 논리적으로 그룹화하는 방식

redis pipeline, redis namespace 고려사항

redis pipelineredis namespace
너무 많은 명령어를 한 번에 보내지 않기
메모리 사용량 고려
에러 처리 중요
일관된 네이밍 규칙 사용
키 충돌 방지
검색 가능성 고려

redis 추가된 코틀린 예시 코드

// 사용자 데이터 모델
data class UserProfile(
    val id: String,
    val name: String,
    val email: String,
    val followers: Set<String> = emptySet(),
    val following: Set<String> = emptySet(),
    val posts: List<String> = emptyList()
)

// Redis 작업을 처리하는 Repository
class UserRedisRepository(private val redis: RedisCommands<String, String>) {
    // 네임스페이스 정의
    companion object {
        const val USER_NS = "user"
        const val PROFILE_NS = "profile"
        const val FOLLOWERS_NS = "followers"
        const val FOLLOWING_NS = "following"
        const val POSTS_NS = "posts"
    }

    // 키 생성 헬퍼 함수
    private fun createKey(vararg parts: String) = parts.joinToString(":")

    // 파이프라인을 사용한 사용자 프로필 조회
    suspend fun getUserProfile(userId: String): UserProfile {
        return coroutineScope {
            redis.async().let { async ->
                // 병렬로 여러 데이터 조회
                val basicInfo = async { 
                    async.hgetall(createKey(USER_NS, userId))
                }
                val followers = async {
                    async.smembers(createKey(USER_NS, userId, FOLLOWERS_NS))
                }
                val following = async {
                    async.smembers(createKey(USER_NS, userId, FOLLOWING_NS))
                }
                val posts = async {
                    async.lrange(createKey(USER_NS, userId, POSTS_NS), 0, 10)
                }

                // 모든 데이터 수집
                UserProfile(
                    id = userId,
                    name = basicInfo.await()["name"] ?: "",
                    email = basicInfo.await()["email"] ?: "",
                    followers = followers.await(),
                    following = following.await(),
                    posts = posts.await()
                )
            }
        }
    }

    // 팔로우 기능 구현
    suspend fun followUser(followerId: String, targetId: String) {
        redis.async().let { async ->
            async.multi().apply {
                // 팔로잉 추가
                sadd(createKey(USER_NS, followerId, FOLLOWING_NS), targetId)
                // 팔로워 추가
                sadd(createKey(USER_NS, targetId, FOLLOWERS_NS), followerId)
            }.exec()
        }
    }

    // 포스트 작성 기능
    suspend fun createPost(userId: String, content: String) {
        val postId = "post:${System.currentTimeMillis()}"
        
        redis.async().let { async ->
            async.multi().apply {
                // 포스트 내용 저장
                hset(postId, mapOf(
                    "userId" to userId,
                    "content" to content,
                    "timestamp" to System.currentTimeMillis().toString()
                ))
                
                // 사용자의 포스트 목록에 추가
                lpush(createKey(USER_NS, userId, POSTS_NS), postId)
                
                // 팔로워들의 피드에 추가
                async.smembers(createKey(USER_NS, userId, FOLLOWERS_NS)).get().forEach { followerId ->
                    lpush(createKey(USER_NS, followerId, "feed"), postId)
                }
            }.exec()
        }
    }
}

// Spring Boot에서 사용 예시
@Service
class UserService(private val userRedisRepository: UserRedisRepository) {
    
    suspend fun getUserProfile(userId: String): UserProfile {
        return userRedisRepository.getUserProfile(userId)
    }
    
    suspend fun followUser(followerId: String, targetId: String) {
        userRedisRepository.followUser(followerId, targetId)
    }
    
    suspend fun createPost(userId: String, content: String) {
        userRedisRepository.createPost(userId, content)
    }
}