게시글 조회 로직에서 스프링 캐시(Spring Cache)를 사용하였는데 제대로 동작하지 않았습니다.
package com.evertrip.post.service;
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class PostService {
private final MemberRepository memberRepository;
private final PostRepository postRepository;
private final PostDetailRepository postDetailRepository;
private final RedisForSetService redisForSetService;
private final CacheManager cacheManager;
public PostResponseDto getPostDetailV2(Long postId, Long memberId) {
// 레디스에 해당 post가 존재할 시 레디스 정보를 넘겨주고 없을 시 실제 DB 조회 후 레디스에 저장
PostResponseDto postDetail = getPostDetailUsingCachable(postId);
// 조회수(제일 최신)는 Redis에서 조회해서 postDetail의 조회수에 넣어줍니다.
Long views = getViews(postId).longValue();
// 방문자 리스트에 해당 사용자가 존재하지 않을 시 방문자 리스트에 추가해주고 조회수 1 증가 시켜주기
if (!redisForSetService.isMember(ConstantPool.CacheName.VIEWERS + ":" + postId, memberId.toString())) {
// Redis에 방문자 명단 추가
redisForSetService.addToset(ConstantPool.CacheName.VIEWERS + ":" + postId, memberId.toString());
// 수동으로 cacheManager를 통해 redis에 조회수 +1 증가 시켜주기
String viewsCacheKey = postId.toString();
Cache viewsCache = cacheManager.getCache(ConstantPool.CacheName.VIEWS);
viewsCache.put(viewsCacheKey, views+1);
views = views+1;
}
postDetail.setView(views);
return postDetail;
}
@Cacheable(value = ConstantPool.CacheName.POST, key = "#postId")
public PostResponseDto getPostDetailUsingCachable(Long postId) {
PostResponseDto postDetail = postRepository.getPostDetail(postId).orElseThrow(() -> new ApplicationException(ErrorCode.POST_NOT_FOUND));
return postDetail;
}
@Cacheable(value = ConstantPool.CacheName.VIEWS, key = "#postId")
public Long getViews(Long postId) {
Long views = postRepository.getViews(postId).orElseThrow(() -> new ApplicationException(ErrorCode.POST_NOT_FOUND));
return views;
}
}
기존 코드는 @Cacheable
을 사용하여 Redis 저장소에 해당 key 값이 존재할 시 value에 저장된 값을 그대로 반환하고 key 값이 존재하지 않을 시에 @Cacheable
이 선언된 메서드가 실제 호출되어 원격 DB에서 게시글 정보를 읽어오도록 구현했습니다.
해당 getPostDetailV2
메서드를 실행시켜주는 URL에 GET 요청을 해봤습니다.
위와 같이 백앤드 서버에서 실제 DB로 게시글 조회 쿼리가 나가는 것을 확인할 수 있습니다. Redis에 해당 게시글이 저장되어 있지 않기 때문에 이는 정상적인 동작입니다.
하지만 Redis 저장소에는 DB에서 조회해온 게시글에 대한 정보가 저장되지 않았습니다. 그리고 재요청 했을 때도 똑같이 백앤드 실제 DB로 게시글 조회 쿼리가 나가고 Redis에는 해당 게시글이 저장되지 않았습니다.
실제로 게시글 조회 요청이 오면 getPostDetailV2()
이 호출되는데 내부에서 @Cacheable
이 선언된 getPostDetailUsingCachable()
메서드를 호출하는 구조입니다.
위의 오류 상황을 보면 getPostDetailUsingCachable()
메서드가 @Cacheable
을 선언했음에도 불구하고 적용이 안되고 실제 호출이 되고 있다는 게 문제입니다.
스프링 캐시(Spring Cache)는 스프링 AOP 기반으로 동작하는데 AOP 적용이 안되는 게 아닐까 생각했습니다.
토비 스프링을 공부하면서 배웠던 내용(토비 스프링 Vol 1권 - p 524쪽)으로 스프링 AOP를 사용할 때 주의해야할 점 중 하나가 바로 아래 내용입니다.
프록시 방식 AOP는 같은 타깃 오브젝트 내의 메소드를 호출할 때는 적용되지 않는다
타깃 오브젝트 내에서 타깃 오브젝트의 다른 메소드를 호출하는 경우에는 프록시를 거치지 않고 직접 타깃의 메소드가 호출되기 때문에 @Cacheable
이 적용이 안된 본래의 getPostDetailUsingCachable()
메서드가 호출되었던 것입니다.
오류 상황은 간단하게 해결할 수 있었습니다.
@Cacheable
이 적용된 메서드를 별도의 서비스로 분리하여, 프록시 메커니즘이 올바르게 작동하고 캐시 기능이 제대로 동작하도록 하면 됩니다.
@Service
@RequiredArgsConstructor
public class PostCacheService {
private final PostRepository postRepository;
@Cacheable(value = ConstantPool.CacheName.POST, key = "#postId")
public PostResponseDto getPostDetailUsingCachable(Long postId) {
PostResponseDto postDetail = postRepository.getPostDetail(postId).orElseThrow(() -> new ApplicationException(ErrorCode.POST_NOT_FOUND));
return postDetail;
}
@Cacheable(value = ConstantPool.CacheName.VIEWS, key = "#postId")
public Long getViews(Long postId) {
Long views = postRepository.getViews(postId).orElseThrow(() -> new ApplicationException(ErrorCode.POST_NOT_FOUND));
return views;
}
}
package com.evertrip.post.service;
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class PostService {
private final MemberRepository memberRepository;
private final PostRepository postRepository;
private final PostDetailRepository postDetailRepository;
private final RedisForSetService redisForSetService;
private final CacheManager cacheManager;
private final PostCacheService postCacheService;
public PostResponseDto getPostDetailV2(Long postId, Long memberId) {
// 레디스에 해당 post가 존재할 시 레디스 정보를 넘겨주고 없을 시 실제 DB 조회 후 레디스에 저장
PostResponseDto postDetail = postCacheService.getPostDetailUsingCachable(postId); ----> 수정
// 조회수(제일 최신)는 Redis에서 조회해서 postDetail의 조회수에 넣어줍니다.
Long views = postCacheService.getViews(postId).longValue(); ----> 수정
// 방문자 리스트에 해당 사용자가 존재하지 않을 시 방문자 리스트에 추가해주고 조회수 1 증가 시켜주기
if (!redisForSetService.isMember(ConstantPool.CacheName.VIEWERS + ":" + postId, memberId.toString())) {
// Redis에 방문자 명단 추가
redisForSetService.addToset(ConstantPool.CacheName.VIEWERS + ":" + postId, memberId.toString());
// 수동으로 cacheManager를 통해 redis에 조회수 +1 증가 시켜주기
String viewsCacheKey = postId.toString();
Cache viewsCache = cacheManager.getCache(ConstantPool.CacheName.VIEWS);
viewsCache.put(viewsCacheKey, views+1);
views = views+1;
}
postDetail.setView(views);
return postDetail;
}
}
게시글 조회 요청을 보냈습니다. 게시글 데이터는 제대로 나오는 것을 확인할 수 있습니다.
조회된 게시글이 Redis에 제대로 저장된 것을 확인할 수 있었습니다.
이제 한번 더 요청을 보내고 실제 쿼리가 나가는 지 확인해보도록 하겠습니다.
요청에 대한 정상적인 응답이 온 것을 확인할 수 있습니다.
백앤드 서버의 로그를 확인해보겠습니다.
처음 시큐리티 설정에 의한 회원 조회 쿼리 외에 실제 DB에 전송된 쿼리가 없는 것을 확인할 수 있습니다.