
@Cacheable이란?springframework의 어노테이션으로 메서드에 target은 메서드이고
메서드의 리턴 값을 편리하게 캐시해주는 것
이때, @cacheable은 특정 캐시(Redis) 등에 종속되지 않은 추상화된 기능이기에 캐시를 변경하여도 애플리케이션 코드에 영향을 주지 않는다
Spring AOP 방식으로 프록시 패턴을 통해 메서드의 반환 값을 자동으로 캐시해주는 방식이다.
간단하게 Spring AOP와 프록시 패턴에 대해 알아보자
Aspect-Oriented Programming (관점 지향 프로그래밍)의 줄임말
관점을 바탕으로 모듈별(메서드별)로 중복해서 나오는 횡단 관심사(cross concern)을 메서드로 걷어내고, 해당 관심사를 필요로 하는 곳에 해당 메서드를 주입하는 것이다
즉, AOP는 횡단 관심사 로직들을 주입해주는 것이다
서비스 전반에서 현재 로그인한 유저의 정보를 가져오고 싶을 때, 서비스 계층 등의 각 메서드에서 현재 로그인한 유저의 정보를 조회하는 로직이 다 들어가야 된다
이러한 애플리케이션 전반에 걸친 횡단 관심사를 걷어내 메서드로 만들어 놓고, 각자 해당 로직이 필요할 때 간단하게 어노테이션 등을 통해 로직을 주입 받을 수 있다.
@Slf4j
@Component
@Aspect
public class LoginAspect {
@Pointcut("execution(* *(.., @com.outstagram.outstagram.common.annotation.Login (*), ..))")
public void loginRequired() {
}
@Around("loginRequired()")
public Object checkSession(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("AOP - @Login Check Started");
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
HttpSession session = request.getSession(false);
if (session == null) {
log.info("AOP - @Login Check Result - session empty");
throw new ApiException(ErrorCode.UNAUTHORIZED_USER);
}
UserDTO user = (UserDTO) session.getAttribute(LOGIN_USER);
if (user == null) {
log.info("AOP - @Login Check Result - user empty");
throw new ApiException(ErrorCode.UNAUTHORIZED_USER);
}
/*
joinPoint 안는 현재 실행중인 메서드에 대한 정보들을 담고 있음(메소드 이름, 타입, 파라미터)
getArgs() : 현재 메서드에 전달된 파라미터들을 객체 배열 형태로 리턴
이 파라미터들 중에서 타입이 UserDTO인 것을 찾아 현재 세션의 유저 정보를 넣어줄 것임
*/
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof UserDTO) {
args[i] = user;
}
}
/*
@LoginSession UserDto user -> 이 user에 현재 session에 있는 유저를 넣어준다
위에서 변경한 파라미터를 적용하려면 파라미터 배열을 proceed() 메서드에 전달해야 함
*/
return joinPoint.proceed(args);
}
}
/**
* 로그인한 유저 정보 세션에서 찾아오는 애노테이션
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
@PostMapping
public ResponseEntity<ApiResponse> createPost(
@ModelAttribute @Valid CreatePostReq createPostReq, @Login UserDTO user) {
postService.insertPost(createPostReq, user.getId());
return ResponseEntity.ok(
ApiResponse.builder().isSuccess(true).httpStatus(HttpStatus.OK).message("게시물을 저장했습니다.")
.build());
}
@Login 어노테이션을 호출하면 LoginAspect 의 코드가 실행되면서 현재 세션에 있는 로그인된 유저의 정보를 가져와서 파라미터인 UserDTO user에 넣어준다클라이언트에 의해 컨트롤러의 createPost 메서드를 호출하면 실제로는 프록시 객체가 이 호출을 받는다.
프록시 객체가 @Login을 보고 LoginAspect 로직을 실행해 파라미터 user에 현재 세션에 로그인된 유저의 정보를 넣어준다.
이렇게 수정된 파라미터들을 가지고 실제 컨트롤러 객체의 createPost 메서드를 실행한다.
createPost 메서드 실행하기 직전에 파라미터를 수정하고, 수정된 파라미터들을 넣어서 실제 객체의 createPost 메서드를 실행한다프록시 패턴을 통해 AOP를 수행하기에 실제 메서드를 호출하기 전, 후 즈음에만 로직을 집어 넣을 수 있고, 실제 메서드 안에 로직을 집어넣을 수는 없다.
그래서 메서드 실행 직전, 직후, 파라미터 등에만 로직을 집어넣을 수 있는 것이 프록시 패턴을 통해 AOP를 구현하기 때문이다.

대리자(Proxy) 객체를 통해서 실제 객체의 참조를 숨기며, 대리자는 실제 객체와 같은 인터페이스를 구현한다
클라이언트는 대리자를 통해 실제 객체를 사용할 수 있다
실제 객체의 리턴 값을 수정하지 않는다!!! 그저 실제 객체의 메서드 실행 전 후로 제어 흐름을 조정할 수 있을 뿐,,,,
OCP, DIP가 적용된 설계 패턴
로깅, 모니터링 작업 등에서 유용하게 이용된다
지연 로딩에도 활용된다
같은 PostService 내부에서 @Cacheable이 적용된 메서드를 호출하면 @Cacheable이 적용되지 않고 메서드가 호출된다
아래 예시를 통해 확인해보쟈
@Slf4j
@Service
@RequiredArgsConstructor
public class PostService {
...
@Cacheable(value = POST, key = "#postId")
public PostDTO getPost(Long postId) {
return postMapper.findById(postId);
}
...
public PostDetailsDTO getPostDetails(Long postId, Long userId) {
PostDTO post = getPost(postId);
...
}
}
getPost()
postId를 통해 DB에서 post 데이터 가져오기@Cacheable을 통해 캐시에 없으면 findById()를 통해 DB에서 가져온 후 캐시에 넣고 캐시에 있으면 캐시의 데이터를 반환해줌getPostDetails()
getPost()를 통해 게시물 데이터가 캐시에 있다면 캐시의 데이터를, 캐시에 없다면 DB의 데이터를 가져오고 싶다PostDTO post = getPost(postId); 로 getPost() 메서드를 호출하면 캐시에 데이터가 있든 없든 무조건 DB에서 조회해온다...@Cacheable이 안 적용될까??왜냐하면 @Cacheable은 위에서 말했다시피 Spring AOP를 통해 적용된다.
Spring AOP는 프록시 객체를 통해 동작하는데, 프록시 객체는 외부에서 호출될 때만 프록시 객체를 거쳐서 메서드를 호출한다.
즉, @Cacheable을 적용하려면 프록시 객체를 통해 메서드가 호출되어야 된다
하지만 클래스 내부에서 메서드를 호출하면 프록시 객체를 거치지 않고 직접 호출되기 때문에 AOP가 적용되지 않는다
@Cacheable 동작 원리프록시를 통해 해당 메서드가 호출 될 때, 실제 메서드 실행 직전에 캐시를 검사하고, 캐시에 없으면 실제 메서드를 호출하여 결과를 캐시에 저장한다.
같은 클래스 내에서 메서드를 호출하면 프록시가 동작하지 않기 때문에 캐시를 확인하지 않고 바로 메서드를 실행해버린다
그래서 getPostDetails()에서 getPost()를 호출했을 때 캐시를 확인하지 않고 무조건 DB에서 조회해오는 것임
AopContext.currentProxy() 를 통해 현재 실행 중인 AOP 프록시 객체를 반환받을 수 있다.
이를 통해서 클래스 내부에서도 자기 자신의 메서드를 프록시를 통해 호출할 수 있다
public PostDetailsDTO getPostDetails(Long postId, Long userId) {
PostService proxy = (PostService) AopContext.currentProxy();
PostDTO post = proxy.getPost(postId);
...
}
getPost()를 호출하면 캐시에 있다면 캐시의 데이터를, 캐시에 없다면 DB에서 조회한 데이터를 반환해준다.AopContext.currentProxy() 사용을 위한 몇 가지 세팅@EnableAspectJAutoProxy(exposeProxy = true) 어노테이션 달기 ( + 캐싱을 위해 @EnableCaching도 달기)@EnableAspectJAutoProxy(exposeProxy = true)
@EnableCaching
@SpringBootApplication
public class OutstagramApplication {
public static void main(String[] args) {
SpringApplication.run(OutstagramApplication.class, args);
}
}
spring.aop.proxy-target-class=true
spring.aop.expose-proxy=true
@Cacheable은 Spring AOP를 통해 동작함
프록시 객체가 @Cacheable 달려 있는 메서드 수행을 받아서 실제 메서드 수행 전에 캐시에 데이터 있는지 확인
있으면 캐시 데이터 바로 리턴(실제 메서드 수행 안됨)
없으면 실제 메서드 수행하고 결과를 캐시에 저장
프록시 객체를 통해서 메서드를 수행해야지 캐시 조회 및 저장이 가능하다
그래서 같은 클래스 내의 메서드를 호출하면 @Cacheable이 적용되지 않을 수 있다
그럴 땐, AopContext.currentProxy()를 통해 현재 실행 중인 AOP 프록시 객체를 반환받아서 메서드를 실행하자!