내일배움캠프

[내일배움캠프] Redis 응용

munsik22 2026. 5. 8. 15:46

📚 목차

    🧩 Http Session과 Session Clustering

    HttpSession

    • HTTP 요청은 상태를 저장하지 않아(STATELESS) 누가 몇 번째로 보낸 요청인지 서버에 요청할 때 마다 알려줘야 함
    • 이렇게 사용자의 데이터를 저장하는 기술을 세션이라고 하고, 서버에서 작성해서 응답을 받은 브라우저가 저장하는 데이터를 쿠키라고 함
    • 서버는 저장한 정보를 기반으로 브라우저가 누구인지 구분할 수 있음
    • SessionController
    @RestController
    public class SessionController {
        @GetMapping("/set")
        public String set(
                @RequestParam("q")
                String q,
                HttpSession session
        ) {
            session.setAttribute("q", q);
            return "Saved: " + q;
        }
    
        @GetMapping("/get")
        public String get(
                HttpSession session
        ) {
            return String.valueOf(session.getAttribute("q"));
        }
    }
    • http://localhost:8080/set?q=password

    • http://localhost:8080/get

    • 여기서 JSESSIONID는 Spring Boot에 내장된 Tomcat 서버에서 생성한 것이다.

    서버 Scale-Out 확장에 따른 문제점

    • Spring Boot 서버를 여러 개의 포트로 바꿔서 올리면(Scale-Out) 세션 정보를 서버 내부에서 관리하기 어려워짐
    • 예: 포트 8080에서 /set을 하고 포트 8081에서 /get을 하면 찾을 수 없음

    Sticky Session

    • 특정 사람이 보낸 요청을 하나의 서버로 고정하는 방법
    • 로드 밸런서를 통해 요청을 보낸 사용자를 기록하고 해당 사용자가 다시 요청을 할 경우 최초로 요청이 전달된 서버로 요청을 전달하는 방식
      • 예: Alex가 처음에 서버 A에 세션을 만들면, 로드밸런서는 Alex의 요청을 전부 서버 A로 보냄
    • 특정 서버로 보내진 사용자만 활발하게 활동한다면 요청이 균등하게 분산되지 않는다는 문제가 있음
      • 예: A, B, C서버에 대해서 B서버의 사용자들만 활발히 서비스를 사용하다가 B서버가 다운된다면?

    Session Clustering

    • 여러 서버들이 하나의 저장소를 공유하고 해당 저장소에 세션에 대한 정보를 저장함으로서 요청이 어느 서버로 전달이 되든 세션 정보가 유지될 수 있도록 하는 방법
    • 서버가 세션을 생성하되 이 세션에 연결된 정보를 외부 저장소에 저장함
    • 서버 상태에 따라 세션이 사라질 위험은 없지만, 외부 저장소를 추가로 관리해야 하고 지연이 생길 수 있음

    Spring Session

    • Spring Boot와 Redis를 함께 쓰면 쉽게 Session Clustering을 사용가능함
    • Spring Session Data Redis를 추가하면 내장 Tomcat의 세션 기능을 사용하지 않고 Redis에 별도로 세션을 저장하게 됨
    implementation 'org.springframework.boot:spring-boot-starter-session-data-redis'
    • 포트 8080에서 만든 세션을 포트 8081에서도 확인할 수 있음 (쿠키에는 JSESSIONID 대신 SESSION이 생성됨)

    • Redis 저장소를 보면 세션이 저장된 모습을 볼 수 있음

    • RedisConfig: springSessionDefaultRedisSerializer 메서드를 추가해 읽을 수 있는 형태로 저장
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
      return RedisSerializer.json();
    }

    🧩 리더보드와 Sorted Set

    리더보드란?

    • 실시간 랭킹을 보여주는 기능
    • 예: 게임 순위, 실시간 검색 순위, 인기상품
    • 관계형 DB로 구현하려면 복잡한 JOIN과 함께 SUM, COUNT 등의 집계함수도 필요함
    SELECT i.id, SUM(o.count)
    FROM item i
        INNER JOIN orders o
              ON i.id = o.item_id
    GROUP BY i.id
    ORDER BY SUM(o.count) DESC
    LIMIT 10;
    • 이를 피하기 위해 물품 테이블에 구매횟수 컬럼을 추가한다 해도 결국 해당 칼럼을 빈번하게 수정해야 하며 데이터를 조회하는 과정에서 정렬이 필요하기 때문에 성능 저하가 일어남
    • 반면 Redis의 Sorted Set은 데이터를 추가하거나 조회하는 행위의 시간복잡도가 log에 비례하기 때문에 구현도 쉽고 편하게 진행할 수 있다.
    • RedisConfig
    @Configuration
    public class RedisConfig {
        @Bean
        public RedisTemplate<String, ItemDto> rankTemplate (
                RedisConnectionFactory redisConnectionFactory
        ) {
            RedisTemplate<String, ItemDto> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory);
            template.setKeySerializer(RedisSerializer.string());
            template.setValueSerializer(RedisSerializer.json());
            return template;
        }
    }
    • ItemService
    @Slf4j
    @Service
    @Slf4j
    @Service
    public class ItemService {
        private final ItemRepository itemRepository;
        private final OrderRepository orderRepository;
        // RedisTemplate으로 Sorted Set을 사용한다면 ZSetOperations가 필요함
        private final ZSetOperations<String, ItemDto> rankOps;
    
        public ItemService(
                ItemRepository itemRepository,
                OrderRepository orderRepository,
                RedisTemplate<String, ItemDto> rankTemplate
        ) {
            this.itemRepository = itemRepository;
            this.orderRepository = orderRepository;
            this.rankOps = rankTemplate.opsForZSet();
        }
    
        public void purchase(Long id) {
            Item item = itemRepository.findById(id)
                    .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
            orderRepository.save(ItemOrder.builder()
                    .item(item)
                    .count(1)
                    .build());
            rankOps.incrementScore(
                    "soldRanks",
                    ItemDto.fromEntity(item),
                    1
            );
        }
    
        public List<ItemDto> getMostSold() {
            Set<ItemDto> ranks = rankOps.reverseRange("soldRanks", 0, 9);
            if (ranks == null) return Collections.emptyList();
            return ranks.stream().toList();
        }
    }
    • http://localhost:8080/items/{id}/purchase로 여러 번 POST 요청을 보낸 후 Redis에 soldRank라는 Sorted Set이 생긴 것을 볼 수 있다.

    • http://localhost:8080/items/ranks로 GET 요청을 보내면 리더보드를 List 형태로 볼 수 있다.

    🧩 캐싱 개념과 캐싱 전략

    캐시란?

    • 캐시: 빈번히 접근하게 되는 데이터를 저장해두는 CPU 내부의 임시 기억 장치
      • 영속성을 위해 파일시스템(SSD)에 저장
      • 빠른 활용을 위해 메모리(RAM)에 저장
      • 빈번하게 사용되는 휘발성 데이터를 캐시에 저장
    • 캐싱: 빈번하게 접근하게 되는 DB의 데이터를 Redis 등의 In-Memory DB에 저장함으로서 데이터를 조회하는데 걸리는 시간과 자원을 감소시키는 기술
      • 예: 자주 바뀌지 않는 이미지 등을 브라우저 캐시에 저장해 페이지 로드를 줄이는 것
    • RESTful 설계 원칙 중 응답이 캐싱이 가능한지 명시해야 한다는 제약사항이 존재함

    캐싱 전략

    • 캐시에 저장한 데이터는 언제든 사라질 수 있기 때문에 너무 크지 않게 관리되어야 함
    • 캐시를 확인했을 때 필요한 데이터가 있을수도 있고 없을 수도 있음
      • 캐시 적중(Cache Hit): 캐시에 접근했을 때 찾고 있는 데이터가 있는 경우
      • 캐시 누락(Cache Miss): 캐시에 접근했을 때 찾고 있는 데이터가 없는 경우
    • 삭제 정책: 캐시에 공간이 부족할때 어떻게 공간을 확보하는지에 대한 정책
    • 캐시에 찾는 데이터가 있을지 없을지는 캐시에 접근하기 전까지는 알기 어렵기 때문에, 어떤 데이터를 얼마나 오래 캐시에 보관할지에 대한 전략을 잘 세워 적중률을 높이고 누락을 최소화해야 함

    Cache-Aside (Lazy Loading)

    • 데이터를 조회할 때 항상 캐시를 먼저 확인하는 전략
      • 캐시에 데이터가 있으면 캐시에서 데이터를 가져옴
      • 캐시에 데이터가 없으면 원본에서 데이터를 가져온 뒤 캐시에 저장함
    • 애플리케이션이 캐시와 DB를 모두 직접 제어하며 읽기 성능을 극대화하는 방식
    • 필요한 데이터만 캐시에 보관됨
    • 최초로 조회할 때 캐시를 확인하기 때문에 최초의 요청은 상대적으로 오래 걸림
    • 원본 데이터를 확인하지 않아 데이터가 최신이라는 보장이 없음

    Write-Through

    • 데이터를 작성할때 항상 캐시에 작성하고 원본에도 작성하는 전략
    • 캐시에 저장되는 데이터가 최신이 보장됨
    • 자주 사용하지 않는 데이터도 캐시에 중복해서 작성하기 때문에 시간이 오래 걸림

    Write-Behind

    • 캐시에만 데이터를 작성하고, 일정 주기로 원본을 갱신하는 방식
    • 쓰기가 잦은 상황에 DB의 부하를 줄일 수 있음
    • 캐시 서버가 다운되면 아직 DB에 동기화되지 않은 최신 데이터가 영구적으로 유실될 위험이 있음
    전략 실무 적용 사례 장점 단점
    Cache-Aside 일반적인 웹/앱 API, 상품 정보, 사용자 프로필 유연한 구조, 캐시 장애 시 방어 가능 초기 조회 속도 저하, 정합성 관리 필요
    Write-Through 결제 정보, 재고 상태 등 정합성이 필수인 도메인 완벽한 데이터 일관성 쓰기 성능 저하
    Write-Behind 조회수/좋아요 카운트, 대규모 로그 및 통계 쓰기 성능 극대화, DB 병목 해소 데이터 유실 위험, 복잡한 구현