1. 라이브러리 설정하기

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

2. RedisConfig 생성하기

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean // RedisTemplate<String, Object>를 빈으로 등록하여 애플리케이션에서 사용할 수 있게 함
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        template.setKeySerializer(new StringRedisSerializer()); // key 값을 String으로 저장
        template.setValueSerializer(new GenericToStringSerializer<>(String.class)); // value 값을 String으로 저장
        return template;
    }

    @Bean
    public CacheManager productCacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 응답 타입 Object 직렬화
        RedisCacheConfiguration objectCacheConfig = RedisCacheConfiguration
                .defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                        new Jackson2JsonRedisSerializer<>(Object.class)))
                .entryTtl(Duration.ofMinutes(30L));

        // 응답 타입 Response 직렬화
        RedisCacheConfiguration responseCacheConfig = RedisCacheConfiguration
                .defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                        new Jackson2JsonRedisSerializer<>(Response.class)));

        // 캐시 이름별로 직렬화 방식 지정
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        cacheConfigurations.put("rank", objectCacheConfig);
        cacheConfigurations.put("search", responseCacheConfig);

        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .withInitialCacheConfigurations(cacheConfigurations)
                .build();
    }
}

 

3. yml로 포트 설정해주기

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/freship
    username: root
    password: 
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        use_sql_comments: true
        dialect: org.hibernate.dialect.MySQLDialect
  data:
    redis:
      host: localhost
      port: 6379

 

4. redisTemplate을 사용하여 Redis에서 데이터 입출력하기

@Component
@RequiredArgsConstructor
public class ProductRedisUtils {

    private final RedisTemplate<String, Object> redisTemplate;

    // 사용자 검색 기록 저장 (동일한 검색어 중복 방지)
    public void saveSearchHistory(Long userId, String searchKeyword) {
        String searchHistoryKey = "user:" + userId + ":search:" + searchKeyword;
        if (Boolean.FALSE.equals(redisTemplate.hasKey(searchHistoryKey))) {
            redisTemplate.opsForValue().set(searchHistoryKey, "searched");
            incrementSearchRank(searchKeyword);
        }
    }

    // 검색어 순위 증가
    private void incrementSearchRank(String searchKeyword) {
        redisTemplate.opsForZSet().incrementScore("rank", searchKeyword, 1);
    }

    // 인기 검색어 조회 (상위 10개)
    public List<String> getTopSearchKeywords() {
        Set<String> popularSearch = redisTemplate.opsForZSet()
                .reverseRange("rank", 0, 9)
                .stream()
                .map(Object::toString)
                .collect(Collectors.toSet());
        return popularSearch != null ? new ArrayList<>(popularSearch) : new ArrayList<>();
    }

    public void setReadCount(Long id){
        redisTemplate.opsForZSet().add("product:readCount", "product:" + id, 1);
    }

    public Long getReadCount(Long productId){
        return redisTemplate.opsForZSet().score("product:readCount", "product:" + productId).longValue();
    }

    public Long addReadCount(Long productId){
        return redisTemplate.opsForZSet().incrementScore("product:readCount", "product:" + productId, 1).longValue();
    }

    public boolean notExistsReadCount(Long productId){
        // 값이 존재하지 않는다면 null 반환
        Double currentReadCount = redisTemplate.opsForZSet().score("product:readCount", "product:" + productId);
        return currentReadCount == null;
    }

    // 조회수 상위 10개의 상품 id 리스트 조회
    public List<Long> findProductIds() {
        Set<Object> objectSet = redisTemplate.opsForZSet().reverseRange("product:readCount",0, 9);
        if (objectSet == null) {
            return Collections.emptyList();
        }

        //"product:7" -> ["product", "7"] -> ["7"]로 변환
        List<Long> productIdList = new ArrayList<>();
        for (Object o : objectSet) {
            String value = (String) o;
            String[] parts = value.split(":");
            productIdList.add(Long.parseLong(parts[1]));
        }
        return productIdList;
    }

    public Boolean isNotViewed (Long productId, Long userId) {
        Boolean isNotViewed = redisTemplate.opsForValue().setIfAbsent("product:" + productId + ":user:" + userId, "viewed");
        return isNotViewed;
    }

    // 자정에 캐시 리셋
    @Scheduled(cron = "0 0 0 * * ?")
    public void clearCache() {

        Set<String> cacheKeys = redisTemplate.keys("search*");
        if (cacheKeys != null && !cacheKeys.isEmpty()) {
            redisTemplate.delete(cacheKeys);
        }

        // 유저 검색 기록 삭제
        Set<String> keysForDelete = redisTemplate.keys("user:*");
        if (keysForDelete != null && !keysForDelete.isEmpty()) {
            redisTemplate.delete(keysForDelete);
        }

        // 검색 조회수 삭제
        if (Boolean.TRUE.equals(redisTemplate.hasKey("rank"))) {
            redisTemplate.opsForZSet().removeRange("rank", 0, -1);
        }

        Set<String> visitorKeys = redisTemplate.keys("product*");
        if (visitorKeys != null && !visitorKeys.isEmpty()) {
            redisTemplate.delete(visitorKeys);
        }

        if (Boolean.TRUE.equals(redisTemplate.hasKey("product:readCount"))) {
            redisTemplate.opsForZSet().removeRange("product:readCount", 0, -1);
        }
    }

}

 

5. 캐싱하고 싶은 데이터에 어노테이션 붙이기

@Cacheable(cacheNames = "rank", key = "'products:rank'", cacheManager = "productCacheManager")

'Redis' 카테고리의 다른 글

Redis 캐싱 전략과 조회 성능 비교  (0) 2025.03.25
Redis의 정의와 기본 명령어  (1) 2025.03.25

Redis 네이밍 컨벤션

  • 콜론(:)을 활용해 계층적으로 의미를 구분해서 사용한다!

users:200:profile ->  users 중에서 pk가 200인 user의 profile

 

캐시 (Cache)

  • 원본 저장소보다 빠르게 데이터를 가져올 수 있는 임시 데이터 저장소
  • 임시 저장소를 의미한다고 보면 됨

 

캐싱 (Caching)

  • 캐시에 저장해서 데이터를 빠르게 가져오는 방식
  • Cache Hit : 데이터를 요청했을 때 캐시에 데이터가 있는 경우
  • Cache Miss : 데이터를 요청했을 때 캐시에 데이터가 없는 경우

 

캐싱전략

Cache Aside : 조회시에 캐시를 먼저 찌르고, 없으면 DB에서 조회하는 방식

데이터를 조회할 때 캐시에서 먼저 조회하고, 없으면 데이터베이스를 통해서 조회해오는 방식

 

1. 캐시에 데이터가 있을 경우 (= Cache Hit)

 

2. 캐시에 데이터가 없을 경우 (= Cache Miss)

 

White Around : 쓰기 작업(저장, 수정, 삭제)을 캐시에는 반영하지 않고, DB에만 반영

데이터를 저장, 수정, 삭제시에는 데이터베이스에만 수행

조회시에 Redis에 데이터가 없으면 데이터베이스로부터 데이터를 조회해와서 Redis에 저장시켜줌

 

Cache Aside, Write Around 전략의 한계점

  • 캐시된 데이터와 DB 데이터가 일치하지 않음 → 데이터의 일관성을 보장할 수 없음 → 데이터를 수정할때 데이터베이스의 값만 업데이트하기 때문
  • 캐시에 저장할 수 있는 공간이 적음

 

Cache Aside, White Around 의 한계점 극복 방법 : TTL 설정

  • 데이터를 업데이트할 때마다 레디스도 업데이트하면 성능적으로 느려진다 → 데이터 조회 성능 개선 목적으로 적용한 레디스가 의미없어짐
  • 그렇기에 1. 자주 조회되고 2. 잘 변하지 않고 3. 정확하게 실시간으로 일치하지 않아도 되는 데이터를 기준으로 캐시 전략을 사용
  • 하지만 장시간동안 동기화되지 않으면 문제 생김 → TTL(만료 시간 설정 기능)으로 극복 → 시간이 지나면 레디스에서 데이터가 사라지므로 디비에서 업데이트 된 데이터 조회하고 레디스에 넣어서 해결!
  • 저장할 수 있는 공간이 적은 캐시를 → TTL로 인해서 비워주기 떄문에 효율적으로 공간을 사용할 수 있음

 

Throughput 비교를 통한 캐싱 적용 전후의 성능 비교

Throughput : 1초당 처리한 트랜잭션(API 요청)의 수

→ 내 서비스가 1초당 100개의 API요청을 처리할 수 있으면 이 서비스의 Throughput은 100 TPS

 

30명이 10초동안 요청을 계속 보낸다고 가정하자

 k6 run --vus 30 --duration 10s script.js

 

캐시 적용 전

이 서비스가 1초에 최대 요청할 수 있는 요청 수는 17.6개의 요청이다

-> Throughput = 17.6개

 

캐시 적용 후

이 서비스가 1초에 최대 요청할 수 있는 요청 수는 3753개의 요청이다

-> Throughput = 3753개

 

결론 : 213배 빨라졌다!

'Redis' 카테고리의 다른 글

Spring에 Redis적용하기  (0) 2025.03.31
Redis의 정의와 기본 명령어  (1) 2025.03.25

간단한 컴퓨터 구조 지식

RAM

  • 컴퓨터가 작업할 때 임시로 사용하는 공간
  • 컴퓨터가 실행중에 값을 임시로 저장하는 공간 → 램이 크면 클수록 작업을 빠르게 할 수 있음
  • 휘발성
  • 실행 속도를 좌우함

ROM

  • 시스템이 부팅될 때 필요한 기본적인 데이터를 저장
  • 비휘발성
  • 부팅할 때 꼭 필요한 정보가 저장되어 있는 공간 (몇 MB~GB)

디스크 (SSD/HDD)

  • 데이터를 저장하는 저장 장치
  • 운영체제, 프로그램 사용자 데이터를 저장
  • 비휘발성
  • 저장용량이 매우 큼(GB~TB 단위)

NoSQL

키-벨류 형태로 저장되는 데이터베이스

Redis

데이터 처리 속도가 매우 빠른 NoSQL 데이터베이스

데이터를 일정 시간이 지나면 삭제되게 할 수 있음 : 만료시간 (TTL)

 

Redis의 데이터 처리 속도가 빠른 이유

RMDBS는 디스크에 데이터를 저장함 → 속도 느림

Redis는 in-memory 에 데이터를 저장함 → 데이터 처리 속도가 매우 빠름

→ 그래서 Redis가 빠른 것이다!

 


Redis 기본 명령어

Background로 실행하기

// redis background로 실행
brew services start redis

 

재실행

// redis background로 재실행
brew services restart redis

 

Background로 레디스 종료하기

// redis background로 중지
brew services stop redis

 

redis-cli 접속

redis-cli

 

redis-cli 나가기

ctrl + c

 

데이터 저장하기 명령어

# set [key 이름] [value]
$ set jaeseong:name "jaeseong park" # 띄워쓰기 해서 저장하려면 쌍따옴표로 묶어주면 됨
$ set jaeseong:hobby soccer

 

데이터 조회하기

# get [key 이름]
get jaeseong:hobby

get pjs:name # 없는 데이터를 조회할 경우 (nil)이라고 출력됨

 

저장된 모든 key 조회하기

keys *

 

key로 데이터 삭제하기

del key이름

 

데이터 저장 시 만료시간 정하기

# set [key 이름] [value] ex [만료 시간(초)]
set yena:pet dog ex 30

 

만료시간(TTL) 확인하기

ttl jaeseong:pet

 

모든 데이터 삭제하기

flushall

'Redis' 카테고리의 다른 글

Spring에 Redis적용하기  (0) 2025.03.31
Redis 캐싱 전략과 조회 성능 비교  (0) 2025.03.25

+ Recent posts