캐시 (Cache) 

Cache : CPU 내부 작은 영역, 빈번히 접근하는 데이터를 저장해두는 임시 기억 장치

영속성- 파일시스템

빠른 활용-메모리

많이 사용(휘발성)-캐시

 

캐싱

✅ 기본적으로 본래저장된곳 아니라 다른 곳에 저장하는 행위

 

접근 빈도가 높은 데이터 - 인메모리에 저장 →조회 속도 감소

ex) 웹브라우저에서 자주 바뀌지않는 이미지 등

 

💡 캐시를 확인하는 것도 결국 약간의 지연
캐시 미스일 때 결국 다시 원본 저장 공간을 확인해야 하기 때문에 시간이 더 늘어날 수 있음

 


 

 캐싱 전략 (Caching Strategy) 

데이터가 언제든 사라질 수 있고, 너무 크지 않게 관리 되어야함

캐시를 구현하고 사용할때는 해당 데이터가 얼마나 자주 사용될 것인가를 고려해야 함

또한 언제 캐시에 데이터를 저장하고, 언제 확인하는지에 대한 전략을 세워야 함

 

캐싱 전략 목표: 캐시  적중률↑  누락율↓

 

1. Cache-aside (Lazy Loading)

 

1) 데이터 조회시 캐시를 먼저 확인함

2-1) 캐시에 데이터 있으면 캐시 데이터 (cache hit)

2-2) 없으면 원본에서 가져와 캐시에 저장 (cache miss)

 

 

  • 최초 조회는 캐시에 데이터가 없기 때문에 상대적으로 오래 걸림
    • 첫 번째 요청을 제외하고는 좋은 성능
  • 데이터가 최신이라는 보장이 없음

 

2. Write-Through

1) 데이터 추가 시 캐시에 먼저 저장

2) 그 다음 원본에 저장

3) 조회 시 캐시만 읽으면 됨

 

  • 최신 데이터 보장
  • 자주 사용하지 않는 데이터도 캐시에 저장 됨
  • 캐시에 항상 걸치기 때문에 조금 오래 걸림

 

3. Write-Behind

1) 데이터를 캐시에만 저장

2) 일정 주기로 원본 갱신

 

  • 쓰기가 잦을 시 디비 부하 줄일 수 있음
  • 원본 적용 전 문제 생기면 데이터 유실 문제 생김

 

 


 

@EnableCaching

@EnableCacing을 사용하려면 캐시를 관리하는 CacheManager의 구현체가 Bean으로 등록되어야 함

@Configuration
@EnableCaching
public class CacheConfig {

    // Redis용 캐싱 매니저
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {

        // 설정 구성
        // Redis를 이용해 Spring Cache를 사용할 때 Redis 관련 설정을 모아두는 클래스
        RedisCacheConfiguration configuration = RedisCacheConfiguration
                .defaultCacheConfig()
                .disableCachingNullValues() // null을 캐싱하는지
                .entryTtl(Duration.ofSeconds(100)) // 기본 캐시 유지 시간 (Time To Live)
                .computePrefixWith(CacheKeyPrefix.simple()) // 캐시를 구분하는 접두사 설정
                .serializeValuesWith( // 캐시에 저장할 값을 어떻게 직렬화/역직렬화 할것인지
                        RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.java())
                );

        return RedisCacheManager
                .builder(connectionFactory)
                .cacheDefaults(configuration)
                .build();
    }
}

 

 

조회 (Cache-Aside)

@Cacheable : 이 메서드의 결과는 캐싱이 가능함, 데이터를 발견할 경우(Hit) 메서드 자체를 실행하지 않음

  • cacheNames : 만들어질 캐시를 지칭하는 이름, 적용할 캐시 규칙을 지정하기 위한 이름
  • key : 캐시 데이터를 구분하기 위해 활용하는 값
    @Cacheable(cacheNames = "itemCache", key = "args[0]")
    public ItemDto readOne(Long id) {
        log.info("Read One: {}", id);
        return itemRepository.findById(id)
                .map(ItemDto::fromEntity)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }

    @Cacheable(cacheNames = "itemAllCache", key = "methodName")
    public List<ItemDto> readAll() {
        return itemRepository.findAll()
                .stream()
                .map(ItemDto::fromEntity)
                .toList();
    }

 

 

 

생성/수정 (Write Through)

@CachePut: 항상 메서드를 실행하고 결과를 캐싱함

  • key를 "#result.id" (ItemDto.id)로 반환하여 readOne 메서드도 이 캐시를 활용할 수 있음
    @CachePut(cacheNames = "itemCache", key = "#result.id")
    public ItemDto create(ItemDto itemDto) {
        Item item = Item.builder()
                .name(itemDto.getName())
                .description(itemDto.getDescription())
                .price(itemDto.getPrice())
                .build();
        itemRepository.save(item);

        return ItemDto.fromEntity(item);
    }

 

 

 

삭제 정책

@CacheEvict: 주어진 정보를 바탕으로 저장된 캐시를 지워줌

 

    @CachePut(cacheNames = "itemCache", key = "args[0]")
    // 업데이트 시 readAll에 담겨있던 캐시를 지움 - 삭제 정책
    @CacheEvict(cacheNames = "itemAllCache", allEntries = true)
    public ItemDto update(Long id, ItemDto itemDto) {
        Item item = itemRepository.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
        item.setName(itemDto.getName());
        item.setDescription(itemDto.getDescription());
        item.setPrice(itemDto.getPrice());
        itemRepository.save(item);
        return ItemDto.fromEntity(item);
    }


    @CacheEvict(cacheNames = {"itemAllCache", "itemCache"}, allEntries = true)
    public void delete(Long id) {
            itemRepository.deleteById(id);
    }

 

 


 

생성/수정 (Write-Behind)

Write-Behind는 Annotation 기반으로 구현할 수 없음

 

    public void purchase(ItemOrderDto dto) {
    
        Item item = itemRepository.findById(dto.getItemId())
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

        orderOps.rightPush("orderCache::behind", dto);
        
        rankOps.incrementScore(
                "soldRanks",
                ItemDto.fromEntity(item),
                1
        );
    }

    @Transactional
    @Scheduled(fixedRate = 20, timeUnit = TimeUnit.SECONDS)
    public void insertOrders() {
    
        boolean exists = Optional.ofNullable(orderTemplate.hasKey("orderCache::behind"))
                .orElse(false);
        if (!exists) {
            log.info("no orders in cache");
            return;
        }
        
        // 적재된 주문을 처리하기 위해 별도로 이름을 변경하기 위해
        orderTemplate.rename("orderCache::behind", "orderCache::now");
        log.info("saving {} orders to db", orderOps.size("orderCache::now"));
        
        orderRepository.saveAll(orderOps.range("orderCache::now", 0, -1).stream()
                .map(dto -> ItemOrder.builder()
                        .itemId(dto.getItemId())
                        .count(dto.getCount())
                        .build())
                .toList());
                
        orderTemplate.delete("orderCache::now");
    }

'DB > Redis' 카테고리의 다른 글

[Redis] HttpSession / Session Clustering  (0) 2025.03.07
[Redis] Spring에서 Redis  (1) 2025.03.07
[Redis] 타입  (1) 2025.03.07
[Redis] Redis 설치  (0) 2025.03.06
[Redis] Redis  (0) 2025.03.05

 HttpSession 

  • HTTP 요청에는 상태가 없음 - 각각 요청이 독립적
    • 서버에 사용자가 보낸 몇 번째 요청인지에 대한 정보 같은 걸 저장되지 않음
    • 사용자 브라우저 측에서 자신을 식별할 수 있는 정보를 서버에 요청할 때마다 알려줘야 함

브라우저에 응답 보내면서 브라우저에 특정 데이터(쿠키)를 저장하도록 전달

쿠키에 브라우저를 특정 지을 값을 보내줌

이후 사용자가 요청을 보낼 때 해당 값을 보내주면 서버에 저장 해놓은 정보를 기반으로 사용자 식별

 

✅ 세션: 상태를 저장하지 않는 HTTP 통신을 사용하면서 이전에 요청을 보낸 사용자를 기억하는 상태를 유지하는 것

 

 

JSESSIONID : Tomcat에서 만드는 쿠키

세션을 톰캣에서 관리하고 있어서, 세션을 찾아 Dispatcher Servlet한테 넘겨주면 
Dispatcher Servlet이 GetMapping에있는 HTTP세션 매개 변수의 인자로 전달

 

 

🤔 서버를 여러개 사용하여 부하를 줄였을때 (Scale-Out), A서버로 요청했다가 B 서버로 요청한다면?

세션 유지X

여러 서버를 가동하게 되면 세션 정보를 서버 내부에서 관리하기 어려움

 

해결방법

1. Sticky Session - 로드밸런서가 요청을 한쪽으로만 보내줌

2. Session Clustering


 

 Session Clustering 

여러 서버들이 하나의 저장소를 공유, 해당 저장소에 세션에 대한 정보를 저장

→ 요청이 어느 서버로 전달되든 세션 정보 유지

 

// Tomcat 세션 기능을 사용하지 않고 Redis에 별도로 세션을 저장
implementation 'org.springframework.session:spring-session-data-redis'

 

 

더이상 Tomcat을 사용하지 않기 때문에 SESSION 이라는 새로운 쿠키 사용

 

JSEESIONID - Tomcat

SESSION - Spring Data Session 프로젝트에서 관리 (의존성추가한거)

 

 

 

 

✅ 서버 외부에 세션을 저장하는 것이므로 관리 포인트가 늘어나게 되며, 통신 과정에서 지연이 발생함

그래서 지연이 적은 Redis와 같은 인메모리 데이터베이스가 많이 사용 됨

'DB > Redis' 카테고리의 다른 글

[Redis] 캐싱  (0) 2025.03.07
[Redis] Spring에서 Redis  (1) 2025.03.07
[Redis] 타입  (1) 2025.03.07
[Redis] Redis 설치  (0) 2025.03.06
[Redis] Redis  (0) 2025.03.05

 1. Spring Data가 가지고 있는 Repository Interface 사용 

JPA와 유사함

@Entity →  @RedisHash 

기본적인 CRUD 작업

 

Repository - CrudRepository 상속 

Redis에 Hash 자료형으로 데이터 저장

    @Test
    public void createTest() {
        // Item 객체 생성
        Item item = Item.builder()
                .name("keyboard100")
                .description("완전 좋은 키보드")
                .price(12000)
                .build();
        // save 호출
        itemRepository.save(item);
    }

 

Id를 String으로 쓰면 UUID 자동 배정

 

 


 

 2. Redis Template 

Key 직접 설정, 자료형 직접 선택 등 더 정교한 작업

 

 

1) StringRedisTemplate

Java의 문자열만 다루는 경우

Key와 Value를 전부 Java의 문자열이라고 가정

 

각 자료형에 대응하는 Operations 인터페이스 구현체를 반환할 수 있는 메서드들을 가지고 있음

 

opsForValue()

→ ValueOperations<String, String> 반환

  • Redis의 문자열 조작 명령어를 메서드 형태로 보관
// 문자열 조작을 위한 클래스
ValueOperations<String, String> ops
         // 지금 RedisTemplate에 설정된 타입을 바탕으로 Redis 문자열 조작을 할 거다
         = stringRedisTemplate.opsForValue();

ops.set("simplekey", "simplevalue"); // SET
System.out.println(ops.get("simplekey")); // GET

 

 

opsForSet()

→ SetOperations<String, String> 반환

  • Redis의 Set 조작 명령어를 메서드 형태로 보관
SetOperations<String, String> setOps
       = stringRedisTemplate.opsForSet();

setOps.add("hoboies", "games");
setOps.add("hoboies", "coding", "alcohol"); // ADD

System.out.println(setOps.members("hoboies")); // SMEMBERS / [games, coding, alcohol]

 

 

🤔 공용명령어는?
StringRedisTemplate 자체 정의
stringRedisTemplate.expire("hoboies", 10, TimeUnit.SECONDS);

stringRedisTemplate.delete("simplekey");​

 

 

 

 

2) @Configutaion에서 RedisTemplate 정의

StringRedisTemplate 자체가 RedisTemplate<String, String>을 상속받는 클래스

직접 <K, V>의 타입 파라미터를 정할 수 있음

Configutaion에서 빈 객체로 만들어서 등록해 원하는 곳에서 주입해 사용할 수 있음

 

 

<String, ItemDto>

// ItemDto
@Getter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ItemDto implements Serializable {
    private String name;
    private String description;
    private Integer price;
}

 

 

// RedisConfig
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, ItemDto> itemRedisTemplate(
    		// Redis와의 연결 담당 (yml 내용을 바탕으로 만들어 Bean 객체 등록)
            RedisConnectionFactory connectionFactory 
    ) {
        RedisTemplate<String, ItemDto> template
                = new RedisTemplate<>();

        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(RedisSerializer.string()); // Key: 문자열로 직렬화,역직렬화
        template.setValueSerializer(RedisSerializer.json()); // Value: JSON으로 직렬화

        return template;
    }
}

 

 

 

 // Test
 
    @Autowired
    private RedisTemplate<String, ItemDto> itemRedisTemplate;

    @Test
    public void itemRedisTemplateTest() {
        ValueOperations<String, ItemDto> ops
                = itemRedisTemplate.opsForValue();

        ops.set("my:keyboard", ItemDto.builder()
                .name("시끄러운키보드")
                .description("엄청 시끄러워서 못쓰겠음")
                .price(20000)
                .build());

        System.out.println(ops.get("my:keyboard"));
        // ItemDto(name=시끄러운키보드, description=엄청 시끄러워서 못쓰겠음, price=20000)
   
   
 // Redis - my:keyboard       
 // {"@class":"com.example.redis.ItemDto","name":"시끄러운키보드","description":"엄청 시끄러워서 못쓰겠음","price":20000}

 

 

 

✅ 저장하고 싶은 데이터의 형태를 생각해 적당한 RedisTemplate을 만들어쓰자

'DB > Redis' 카테고리의 다른 글

[Redis] 캐싱  (0) 2025.03.07
[Redis] HttpSession / Session Clustering  (0) 2025.03.07
[Redis] 타입  (1) 2025.03.07
[Redis] Redis 설치  (0) 2025.03.06
[Redis] Redis  (0) 2025.03.05
💡 Redis는 자료형에 따라 명령어가 정해져 있다
     Key에 저장된 자료형이 다를 경우 오류 발생

 

 

Java의 Map<String, String> 처럼 동작한다고 접근

 


 

 String 

이미지, 음성, 영상, 파일 등 보관 가능 - 문자열도 결국 바이트 배열이기 때문

저장할 수 있는 최대 크기 512MB

<명령어> <key> <value>
-- 데이터 입력, 조회
SET user:email alex@example.com
GET user:email

-- 정수가 문자열로 저장된 경우 INCR(++), DECR(--)
SET user:count 1
INCR user:count  # 2
DECR user:count  # 1

-- ex) 조회수
-- 여러 데이터 한번에 입력, 조회
MSET user:name brad user:email brad@example.com
MGET user:name user:email

 

 

 


 

 List 

 

Linked List 형태로 보관

Stack, Queue처럼 사용 가능 - PUSH / POP을 왼쪽(L), 오른쪽(R)을 조합하여 사용

LPUSH user:list alex  # [alex]
LPUSH user:list bred  # [bred, alex]

RPUSH user:list chad  # [bred, alex, chad]
RPUSH user:list dave  # [bred, alex, chad, dave]

LPOP user:list  # bred
RPOP user:list  # dave
-- 리스트의 크기
LLEN user:list  # 2 [alex, chad]
-- 인덱스로 원소 보기
LRANGE user:list 0 1  # [alex, chad]

-- 모든 원소 보기
-- 1) 충분히 큰 숫자 씀 : out of range x, 인덱스 범위가 벗어나도 범위 내에 있는 요소 나옴
LRANGE user:list 0 100000  # [alex, chad]

-- 2) 0 -1
LRANGE user:list 0 -1 # [alex, chad]

-- end < start : 빈리스트
LRANGE user:list 1 0
-- ex)
-- 소셜네트워크 타임라인 (트위터)
-- Worker Queue 구성 (여러 Wocker Application에게 일을 분배하기 위해 사용하는 큐)

 

 

 



 Set 

중복허용X

-- 데이터 입력, 삭제
SADD user:java alex  # [alex]
SADD user:java brad  # [alex, brad]
SADD user:java char  # [alex, brad, char]

SREM user:java char  # [alex, brad]
-- key에 저장된 모든 원소 반환
SMEMBERS user:java  # [alex, brad]

-- key에 저장된 집합에 value가 존재하는지
SISMEMBER user:java char  # fasle
SISMEMBER user:java alex  # true

-- 집합의 크기
SCARD user:java  # 2
--교집합 합집합
SADD user:python alex
SADD user:python dave

SINTER user:java user:python  # [alex]
SUNION user:java user:python  # [alex, brad, dave]

-- n개의 key에 저장된 집합들의 교집합 크기
SINTERCARD 2 user:java user:python  # 1
-- ex)
-- 중복을 허용하지 않는 방문자 수 세기: URL을 키로 만들고 사용자 ID를 넣어줌
-- 인증 토큰 블랙리스트

-- SISMEMBER 시간복잡도: (O)1

 

 

 



 Hash 

Field-Value Pair

Map<String, Map<String, String>

-- 데이터 입력
HSET user:alex name alex age 25
HSET user:alex major CSE

-- 데이터 조회
HGET user:alex name
HGET user:alex age

-- 복수의 필드 value 조회
HMGET user:alex age major  # [25, CSE]

-- key에 저장된 Hash에 저장된 field-value 전부 반환
HGETALL user:alex

-- key에 저장된 Hash에 저장된 field 전부 반환
HKEYS user:alex  # [name, major, age]

-- key에 저장된 Hash에 저장된 field 갯수 반환
HLEN user:alex  # 3
-- ex)
-- 장바구니, 세션 정보

 

 

 


 Sorted Set 

중복x + 점수

Key Score Value

-- 데이터 입력 + 점수
ZADD user:ranks 10 alex
ZADD user:ranks 9 brad 11 chad
ZADD user:ranks 8 dave 9.5 eric

-- 점수 증가
ZINCRBY user:ranks 2 alex

 

-- member 순위
ZRANK user:ranks alex  # 4
ZRANK user:ranks dave  # 0

-- 내림차순 기준
ZREVRANK user:ranks alex
-- start부터 stop순위까지 
ZRANGE user:ranks 0 3

-- start부터 stop순위까지 내림차순
ZREVRANGE user:ranks 0 3
-- ex)
-- 순위표(리더보드)
-- Rate Limiter 기능 (과도한 요청 거부)

 

 

 


 

공통 명령

-- 키 삭제
DEL user:count
DEL user:list
DEL user:python
-- 만료 시간 설정 (초)
SET expirekey "to be expired"
EXPIRE expirekey 50

--반환 만료될 시간을 UNIX Timestamp로 반환
EXPIRETIME expirekey

-- https://www.unixtimestamp.com/
-- 저장된 모든 Key
KEYS *


-- 모든 Key 제거
FLUSHDB

'DB > Redis' 카테고리의 다른 글

[Redis] 캐싱  (0) 2025.03.07
[Redis] HttpSession / Session Clustering  (0) 2025.03.07
[Redis] Spring에서 Redis  (1) 2025.03.07
[Redis] Redis 설치  (0) 2025.03.06
[Redis] Redis  (0) 2025.03.05

1. 네이티브로 내 컴퓨터에 직접 설치 (비권장)

MacOS - homebrew

Windows - 네이티브 설치 지원 버전3에서 끝남, WSL도입+Linux 설치 → APT

Linux - APT

 

2.  Docker (권장)

Docker compose 사용

Docker에 설치하려면 Redis Stack 을 설치해야함.

redis/redis-stack 이미지를 설치하면 Redis Insight라는 UI도 같이 따라옴 (Redis 전용 IDE라고 보면 됨)

redis/redis-stack-server 이미지는 Redis Insight는 없고 Redis Stack 서버만 있음

 

 

docker-compose.yml

services:
  redis-stack:
    image: redis/redis-stack
    container_name: redis-stack-compose
    restart: always
    environment:
      REDIS_ARGS: "--requirepass systempass" # requirepass : 
    ports:
      - 6379:6379
      - 8001:8001

--requirepass : 사용자이름 default

systempass : 비밀번호

 

 

연결 성공 👍

 

 

 

Redis Insight
localhost:8001

 

'DB > Redis' 카테고리의 다른 글

[Redis] 캐싱  (0) 2025.03.07
[Redis] HttpSession / Session Clustering  (0) 2025.03.07
[Redis] Spring에서 Redis  (1) 2025.03.07
[Redis] 타입  (1) 2025.03.07
[Redis] Redis  (0) 2025.03.05

 

관계형 데이터베이스 (테이블 형태)

 

MySQL, Maria, Oracle

영속성 / 일관성 ➡ 데이터가 사라지지 않도록 파일시스템(SSD, HDD)에 저장
파일 시스템의 데이터를 변경하는 건 상대적으로 느림

 

 

서비스를 만들다보면 때때로 일시적인 데이터를 저장해야 하는 상황 발생
이런 상황에서 느린 파일시스템에 저장하기보다 메모리를 사용해 일시적으로 보관

 

 

 

 Redis 

 

Key-Value 형태의 NoSQL Database

메모리, 즉 RAM에 데이터를 저장 (In-memory) 하기 때문에 복잡한 입출력 과정 불필요

그래서 관계형 데이터베이스에 비해 빠르게 동작

또한 RAM은 휘발성이기 때문에 언제든 사라질 수 있는 데이터를 다룸

 

데이터 수정이 빈번하게 발생하는 기능 : 세션정보, 장바구니, 조회수 등

 

 

 

NoSQL
관계를 기준으로 데이터를 다루지 않기 때문에 스키마를 많들지 않아 비정형 대규모 데이터를 빠르게 다룰 수 있음

 

 

 

 

 

 

 

'DB > Redis' 카테고리의 다른 글

[Redis] 캐싱  (0) 2025.03.07
[Redis] HttpSession / Session Clustering  (0) 2025.03.07
[Redis] Spring에서 Redis  (1) 2025.03.07
[Redis] 타입  (1) 2025.03.07
[Redis] Redis 설치  (0) 2025.03.06

+ Recent posts