Spring Boot에서 Redis 캐시 전략과 조회수 캐싱 구현하기

ramong2·2025년 7월 9일

@Cacheable, RedisTemplate, TTL 설정까지 한 번에 잡기

✨ 목표

  • 게시글 조회 시 DB 호출을 줄이기 위해 Redis 캐시 적용
  • 게시글 조회수(View Count)는 별도로 Redis에 저장 및 증가
  • 캐시 무효화, TTL(Time-To-Live) 설정 등 실전에서 사용하는 캐싱 전략 학습

🧱 사용 기술 스택

  • Spring Boot 3.5.3
  • Spring Data JPA
  • Spring Cache
  • Redis
  • RedisTemplate

프로젝트 구조 요약

📁 controller
   └── PostController
📁 domain
   └── Post
📁 service
   ├── PostService  // @Cacheable, @CacheEvict
   └── ViewCountService // RedisTemplate
📁 config
   ├── RedisConfig
   └── CacheConfig
📁 repository
   └── PostRepository

✅기능별 코드 설명

1. 게시글 저장/조회/수정 컨트롤러

@RestController
@RequestMapping("/posts")
@RequiredArgsConstructor
public class PostController {
    private final PostService postService;
    private final ViewCountService viewCountService;

    @PostMapping
    public Post create(@RequestBody Post post) {
        return postService.save(post);
    }

    @GetMapping("/{id}")
    public Post get(@PathVariable Long id) {
        viewCountService.increaseViewCount(id);  // 조회수 증가
        return postService.getPost(id);          // 캐시 활용
    }

    @PutMapping("/{id}")
    public void update(@PathVariable Long id, @RequestBody Post updated) {
        postService.updatePost(id, updated);     // 캐시 무효화
    }

    @GetMapping("/{id}/views")
    public int getViews(@PathVariable Long id) {
        return viewCountService.getViewCount(id);
    }
}

2. 게시글 Entity

@Entity
@Getter @Setter
public class Post implements Serializable {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String content;

    protected Post() {}
    public Post(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

3. PostService - 캐시 적용

@Service
@Slf4j
public class PostService {

    private final PostRepository postRepository;

    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    public Post save(Post post) {
        return postRepository.save(post);
    }

    @Cacheable(value = "post", key = "#postId")
    public Post getPost(Long postId) {
        log.info("📦 DB에서 조회합니다.");
        return postRepository.findById(postId)
            .orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없습니다."));
    }

    @CacheEvict(value = "post", key = "#postId")
    public void updatePost(Long postId, Post updated) {
        Post post = postRepository.findById(postId).orElseThrow();
        post.setTitle(updated.getTitle());
        post.setContent(updated.getContent());
        postRepository.save(post);
    }
}
  • @Cacheable: Redis에 post::{id} 형태로 캐시 저장
  • @CacheEvict: 게시글 수정 시 캐시 무효화 처리

4. ViewCountService - RedisTemplate 사용

@Service
public class ViewCountService {
    private final RedisTemplate<String, Integer> redisTemplate;

    public ViewCountService(RedisTemplate<String, Integer> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void increaseViewCount(Long postId) {
        String key = "view:post:" + postId;
        redisTemplate.opsForValue().increment(key);
    }

    public int getViewCount(Long postId) {
        String key = "view:post:" + postId;
        Integer count = redisTemplate.opsForValue().get(key);
        return count != null ? count : 0;
    }
}
  • RedisTemplate을 직접 사용하여 가볍게 조회수 관리
    → 별도 RDB에 쿼리하지 않아도 되므로 성능 효율성 매우 높음

5. Redis 설정: RedisTemplate + TTL

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Integer> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Integer> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
        return template;
    }
}
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10)) // TTL 10분
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }
}

📊 결과 시나리오

  • /posts/1 호출
    → 조회수 +1
    → Redis 캐시 없으면 DB에서 조회 후 캐싱
  • /posts/1 재호출
    → 조회수 +1
    → 캐시된 Post 반환 (DB hit 없음)
  • /posts/1 수정 시
    → 캐시 무효화됨
    → 다음 조회 시 다시 DB 접근

❓ FAQ

Q. 조회수도 @Cacheable로 할 수 있지 않나요?
A. 증가 연산이 포함되어 있기 때문에 @Cacheable보다 RedisTemplate이 더 적합합니다.

Q. 조회수는 DB에 저장 안 해도 되나요?
A. 대부분의 서비스에서는 실시간 조회수는 Redis에 두고, 주기적으로 비동기 처리로 DB에 flush합니다.

🏁 마무리

이 예제는 Redis를 통해 다음 두 가지를 간단히 구현해봤습니다:

  • Post 조회 캐싱 (@Cacheable) → DB 부하 감소
  • 조회수 Redis 관리 (RedisTemplate) → 빠르고 독립적인 처리

📎 전체 소스코드

https://github.com/KangJiSseok/spring-labs

RedisExample폴더

0개의 댓글