@Service
@Transactional
@RequiredArgsConstructor
public class ChattingService {
private final UserRepository userRepository;
private MessageResponse getMessageResponse(final MessageRequest request) {
User user = userRepository.findById(request.getUserId()).orElseThrow(IllegalArgumentException::new);
return new MessageResponse(user.getId(),user.getName(), request.getContent()); //보낼 메세지 객체를 리턴
}
}
위의 코드처럼 ChattingService.getMessageResponse()
메소드가 있다. 이 메소드는 매우 자주 호출되는 메소드로, repository
를 통해 DB에서 데이터를 조회하는 로직이 포함되어 있어 캐시를 이용할 필요가 있다고 느꼈다. 그래서 맨 처음 리팩토링한 코드는 다음과 같았다.
@Service
@Transactional
@RequiredArgsConstructor
public class ChattingService {
private final UserRepository userRepository;
@Cacheable(value = "UserInfo", key = "#userId", cacheManager = "redisCacheManager")
public UserDto.UserInfo getUserInfoById(UUID userId) {
User findUser = userRepository.findById(userId).orElseThrow(IllegalArgumentException::new);
return UserDto.UserInfo.of(findUser);
}
private MessageResponse getMessageResponse(final MessageRequest request) {
UserDto.UserInfo userInfo = getUserInfoById(request.getUserId()).orElseThrow(IllegalArgumentException::new);
return new MessageResponse(userInfo.getId(), userInfo.getName(), request.getContent());
}
}
User
엔티티를 조회하는 로직을 다른 메소드로 분리했다. 분리된 UserDto.UserInfo getUserInfoById(UUID userId)
메소드는 조회한 엔티티를 DTO로 변환하여 리턴하는 메소드로, 만약 캐시에 해당 값이 있다면 DB에 직접 조회하는 것이 아닌 캐시의 값을 이용하는 로직이다. 캐시 이용은 Spring Cache의 @Cacheable
어노테이션을 이용했다.
실행하고 로그를 보니 처음 조회 시만 DB 쿼리 로그가 나가고 그 다음부터는 로그가 뜨면 안 되는데 계속해서 로그가 뜨고 있었다. 마찬가지로 캐시에도 아무런 값이 들어가 있지 않았다.
다른 클래스에서 @Cacheable
을 사용할 때는 캐싱이 잘 됐던 것이 이상해서, 찾아보니 Spring Cache는 AOP를 기반으로 동작한다고 한다.
AOP에서 가장 중요한 키워드 중 하나를 고르라고 한다면 프록시 패턴일 것이다.
코드상으로만 보면 흐름은 다음과 같다.
그러나 실제로는 다음과 같이 동작한다.
ChattingService.getMessageResponse()
호출은 프록시의 메소드를 호출하는 것이다. 이후 프록시의 부가기능이 실행되면 실제 Target의 getMessageResponse()
를 호출한다. 그 다음 getUserInfoById()
를 호출하는데, 이때 부가기능이 적용된 프록시의 메소드가 아닌 this
의 메소드를 호출한다. 따라서 당연하게도 캐시 부가기능은 적용되지 않는다.
AOP가 적용된 로직은 같은 클래스 내에서 호출하면 안 된다.
가장 자주 흔하게 쓰이지만 AOP가 적용되었다는 점을 모르고 쓰는 것이 바로 @Transactional
어노테이션이다.
메소드를 아예 다른 클래스로 분리하여 해결했다.
UserService.java
@Service
@Transactional
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Cacheable(value = "UserInfo", key = "#userId", cacheManager = "redisCacheManager")
public UserDto.UserInfo getUserInfoById(UUID userId) {
User findUser = userRepository.findById(userId).orElseThrow(IllegalArgumentException::new);
return UserDto.UserInfo.of(findUser);
}
}
ChattingService.java
@Service
@Transactional
@RequiredArgsConstructor
public class ChattingService {
private final UserService userService;
private MessageResponse getMessageResponse(final MessageRequest request) {
UserDto.UserInfo userInfo = userService.getUserInfoById(request.getUserId()).orElseThrow(IllegalArgumentException::new);
return new MessageResponse(userInfo.getId(), userInfo.getName(), request.getContent());
}
}
실행해 보니 캐싱이 잘 작동하는 것을 확인했다.