Redis - 사용, 실습

희운·2025년 7월 24일

Redis

목록 보기
1/2

Redis 단일 쓰레드에서 실행이 되기 때문에 동시성 문제에 대해 고민할 필요 조차 없다.

  • 레디스 주요 특징
    • key-value로 구성된 단순화된 데이터 구조로 sql 쿼리 사용 불필요
    • 빠른 성능
      • 인메모리 NoSQL 데이터베이스로서 빠른 성능
        • rdb는 기본적으로 disk에 저장이고 필요시에 메모리에 캐싱하는 것이므로, rdb보다 훨씬 빠른 성능
        • redis의 메모리상의 데이터는 주기적으로 스냅샷 disk에 저장
      • key-value는 구조적으로 해시 테이블을 사용함으로서 매우 빠른 속도로 데이터 검색 가능
    • Single Thread 구조로 동시성 이슈 발생X
    • 윈도우 서버에서는 지원하지 않고, linux서버 및 macOS등에서 사용 가능

레디스 명령어를 직접 이용할 일은 없지만 한번 쯤 사용해 보자

# redis설치(linux)
sudo apt-get install redis-server
# redis접속
redis-cli

# redis도커설치(윈도우, mac)
docker run --name redis-container -d -p 6379:6379 redis
# docker 컨네이너 조회
docker ps
# redis도커 접속
docker exec -it <containerID> redis-cli

# redis는 0~15번까지의 database로 구성(default는 0번 db)
# 데이터베이스 선택
select db번호

# 데이터베이스내 모든 키 조회
keys *

# 일반적인 String 자료구조

# set을 통해 key:value 세팅.
set user:email:1 hong1@naver.com
set user:email:2 "hong2@naver.com"
# nx : 이미존재하면 pass, 없으면 set 
set user:email:1 hong1@naver.com nx
# ex : 만료시간(초단위) - ttl(time to live)
set user:email:1 hong1@naver.com ex 10
# redis활용 : refresh토큰등 사용자 인증정보 저장
set user:1:refresh_token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ex 10000
# get을 통해 value값 얻기
get user:1:refresh_token

# 특정 key삭제
del user:email:1
# 현재 DB내 모든 key삭제
flushdb

# redis활용 : 좋아요기능 구현
set likes:posting:1 0
incr likes:posting:1 #특정 key값의 value를 1만큼 증가
decr likes:posting:1 #특정 key값의 value를 1만큼 감소
get likes:posting:1
# redis활용 : 재고관리(동시성이슈 해결)
set stocks:product:1 100
decr stocks:product:1
get stocks:product:1

# redis활용 : 캐싱 기능 구현
# 1번 member 회원 정보 조회
# select name, email, age from member where id=1;
# 위 데이터의 결과값을 redis로 캐싱 -> json형식으로 저장 {"name":"hong", "email":"hong@daum.net", "age":30}
set member:info:1 "{\"name\":\"hong\", \"email\":\"hong@daum.net\", \"age\":30}" ex 20

# list자료구조
# redis의 list는 deque와 같은 자료구조, 즉 doueble-ended queue구조

# lpush : 데이터를 왼쪽에 삽입
# rpush : 데이터를 오른쪽에 삽입
# lpop : 데이터를 왼쪽에서 꺼내기
# rpop : 데이터를 오른쪽에서 꺼내기
lpush hongildongs hong1
lpush hongildongs hong2
rpush hongildongs hong3
rpop hongildongs
lpop hongildongs

# list조회
# -1은 리스트의 끝자리를 의미. -2는 끝에서 2번째를 의미.
lrange hongildongs 0 0 #첫번째값
lrange hongildongs -1 -1 #마지막값
lrange hongildongs 0 -1 #처음부터마지막
lrange hongildongs -3 -1 #마지막3번째부터 마지막까지
lrange hongildongs 0 2

# 데이터 개수 조회
llen hongildongs
# ttl 적용
expire hongildongs 20
# ttl 조회
ttl hongildongs
# pop과 push를 동시에
# A리스트에서 POP하여 B리스트로 PUSH
rpoplpush A리스트 B리스트

# redis활용 : 최근 방문한 페이지
# 5개정도 데이터 push
# 최근방문한 페이지 3개만 보여주는
rpush mypages www.naver.com
rpush mypages www.google.com
rpush mypages www.daum.net
rpush mypages www.chatgpt.com
rpush mypages www.daum.net
lrange mypages -3 -1

# set자료구조 : 중복없음. 순서없음.
sadd memberlist member1
sadd memberlist member2
sadd memberlist member1

# set 조회
smembers memberlist
# set멤버 개수 조회
scard memberlist 
# set에서 멤버 삭제
srem memberlist member2
# 특정 멤버가 set안에 있는지 존재여부 확인
sismember memberlist member1

# redis활용 : 좋아요 구현
sadd likes:posting:1 member1
sadd likes:posting:1 member2
sadd likes:posting:1 member1
scard likes:posting:1
sismember likes:posting:1 member1

# zset : sorted set
# 사이에 숫자는 score라고 불리고, score를 기준으로 정렬
zadd memberlist 3 member1
zadd memberlist 4 member2
zadd memberlist 1 member3
zadd memberlist 2 member4

# 조회방법
# score기준 오름차순 정렬
zrange memberlist 0 -1
# score기준 내림차순 정렬
zrevrange memberlist 0 -1

# zset삭제
zrem memberlist member4

# zrank : 특정 멤버가 몇번째(index 기준) 순서인지 출력
zrank memberlist member4

# redis 활용 : 최근 본 상품목록
# zset을 활용해서 최근시간순으로 정렬
# zset도 set이므로 같은 상품을 add할 경우에 시간만 업데이트되고 중복이 제거
# 같은 상품을 더할경우 시간만 마지막에 넣은 값으로 업데이트(중복제거)
zadd recent:products 151930 pineapple
zadd recent:products 152030 banana
zadd recent:products 152130 orange
zadd recent:products 152230 apple
zadd recent:products 152330 apple
# 최근본 상품목록 3개 조회
zrevrange recent:products 0 2
zrevrange recent:products 0 2 withscores

# redis활용사례 : 주식시세저장
# 종목명: 삼성전자, 시세: 72000원, 시간: 1672527600 (유닉스 타임스탬프) -> 년월일시간을 초단위로 변환한것.(밀리초 단위도 가능능)
zadd stock:prices:samsung 1672527600 "53000"
# 종목명: LG전자, 시세: 95000원, 시간: 1672527660
zadd stock:prices:lg 1672527660 "95000"
# 종목명: 삼성전자, 시세: 72500원, 시간: 1672527720
zadd stock:prices:samsung 1672527720 "72500"
# 종목명: LG전자, 시세: 94500원, 시간: 1672527780
zadd stock:prices:lg 1672527780 "94500"
# 삼성전자의 최신 시세 조회 (최대 1개)
zrevrange stock:prices:samsung 0 0 withscores

# hashes : map형태의 자료구조(key:value key:value ... 형태의 자료구조)
hset author:info:1 name hong email hong@naver.com age 30
# 특정값 조회
hget author:info:1 name
# 모든 객체값 조회
hgetall author:info:1
# 특정 요소값 수정
hset author:info:1 name kim

# 

Redis 를 공부하고 나서 실제로 Redis 를 구현했을때 얼마나 더 빠른지 직접 확인해보자.


package com.example.redisTest.service;

import com.example.redisTest.ArticleCacheDto;
import com.example.redisTest.RequestDto;
import com.example.redisTest.entity.Article;
import com.example.redisTest.repository.ArticleRepository;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@CacheConfig(cacheNames = "article")
public class ArticleService {

    public final ArticleRepository repo;


    @Transactional(readOnly = true)
    @Cacheable(key = "'all'")
    public List<ArticleCacheDto> findAll() {
        return repo.findAll()
                .stream()
                .map(a -> new ArticleCacheDto(a.getId(), a.getTitle(), a.getBody()))
                .collect(Collectors.toList());
    }

    /** 조회: @Cacheable → 캐시에 있으면 캐시에서 가져오고 없으면 DB에서 가져옴과 동시에 캐시에 올려둔다.(만료시간) */
    @Transactional
    @Cacheable(key = "#id")
    public ArticleCacheDto getById(Long id) {

        long start = System.currentTimeMillis();
        Article article = repo.findById(id)
                .orElseThrow(() -> new RuntimeException());

        return new ArticleCacheDto(article.getId(), article.getTitle(), article.getBody());

    }

    @Transactional
    public ArticleCacheDto getById2(Long id) {

        long start = System.currentTimeMillis();
        Article article = repo.findById(id)
                .orElseThrow(() -> new RuntimeException());

        return new ArticleCacheDto(article.getId(), article.getTitle(), article.getBody());

    }

    /** DB 반영: 메서드 로직(repo.save())으로 처리
     캐시 반영: 메서드 반환값을 무조건 캐시에 저장 */
    @Transactional
    @CachePut(key = "#result.id")
    @CacheEvict(key = "'all'") // -> redis-cli : DEL article::all
    public ArticleCacheDto create(RequestDto articleDto) {

        Article article = new Article(articleDto.getTitle(), articleDto.getBody());
        repo.save(article);

        return new ArticleCacheDto(article.getId(), article.getTitle(), article.getBody());
    }


    @Transactional
    @CachePut(key = "#articleDto.id")
    @CacheEvict(key = "'all'")
    public ArticleCacheDto update(RequestDto articleDto) {
        Article article = repo.findById(articleDto.getId())
                .orElseThrow(() -> new EntityNotFoundException("not found"));
        article.setTitle(articleDto.getTitle());
        article.setBody(articleDto.getBody());
        // 변경 감지 → save 호출 안 해도 됨
        return new ArticleCacheDto(article.getId(), article.getTitle(), article.getBody());
    }

    /** 삭제: @CacheEvict → DB 삭제 후 캐시에서 해당 키 제거 */
    @Transactional
    @Caching(evict = {
            @CacheEvict(key = "#id"),       // article::{id} 삭제
            @CacheEvict(key = "'all'")      // article::all 삭제
    })
    public void delete(Long id) {
        repo.deleteById(id);
    }


}

여기서 중요한게 Spring AOP 이다.
저 서비스 빈 클래스 내부에서는 해당 @Cacheable 과 같은 내부 메서드를 호출해도 캐싱이 안된다.
이것 마치 @Transactional 과 같이 Spring AOP 때문이다.

결국 캐시관련 서비스 클래스나 따로 빈을 만들어서
도메인 서비스 빈 -> 캐시관련 빈 클래스를 호출해서 사용하자.

profile
기록하는 공간

0개의 댓글