@Cacheable, RedisTemplate, TTL 설정까지 한 번에 잡기
📁 controller
└── PostController
📁 domain
└── Post
📁 service
├── PostService // @Cacheable, @CacheEvict
└── ViewCountService // RedisTemplate
📁 config
├── RedisConfig
└── CacheConfig
📁 repository
└── PostRepository
@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);
}
}
@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;
}
}
@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);
}
}
@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;
}
}
@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()));
}
}
Q. 조회수도 @Cacheable로 할 수 있지 않나요?
A. 증가 연산이 포함되어 있기 때문에 @Cacheable보다 RedisTemplate이 더 적합합니다.
Q. 조회수는 DB에 저장 안 해도 되나요?
A. 대부분의 서비스에서는 실시간 조회수는 Redis에 두고, 주기적으로 비동기 처리로 DB에 flush합니다.
이 예제는 Redis를 통해 다음 두 가지를 간단히 구현해봤습니다:
https://github.com/KangJiSseok/spring-labs
RedisExample폴더