반응형
Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
Tags
- c
- 부하 테스트
- 코드 트리
- 연습문제
- AOP
- 디프만
- depromeet
- Kotlin
- 디프만16기
- pub.dev
- Oidc
- 운영체제
- flutter
- java
- C언어
- OAuth
- Redis
- Spring
- Scaffold
- 코드트리
- Kafka
- 코딩
- kakao
- Sharding
- 자료구조
- 코딩테스트
- dip
- nGrinder
- 코딩 테스트
- exception
Archives
- Today
- Total
Nick Dev
[Outstagram] 왜 같은 클래스에서 @Cacheable 달린 메서드 호출하면 씹힐까... 본문
반응형
@Cacheable
이란?
- springframework의 어노테이션으로 메서드에 target은 메서드이고
- 메서드의 리턴 값을 편리하게 캐시해주는 것
- 이때,
@cacheable
은 특정 캐시(Redis) 등에 종속되지 않은 추상화된 기능이기에 캐시를 변경하여도 애플리케이션 코드에 영향을 주지 않는다 - 동작 방식
- Spring AOP 방식으로 프록시 패턴을 통해 메서드의 반환 값을 자동으로 캐시해주는 방식이다.
- 간단하게 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);
}
}
```java
/**
* 로그인한 유저 정보 세션에서 찾아오는 애노테이션
*/
@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
에 넣어준다
⭐ 그럼 어떻게 이렇게 작동되는가...?
- 프록시 패턴을 통해서 AOP를 적용해준다
- 클라이언트에 의해 컨트롤러의
createPost
메서드를 호출하면 실제로는 프록시 객체가 이 호출을 받는다. - 프록시 객체가
@Login
을 보고LoginAspect
로직을 실행해 파라미터user
에 현재 세션에 로그인된 유저의 정보를 넣어준다. - 이렇게 수정된 파라미터들을 가지고 실제 컨트롤러 객체의
createPost
메서드를 실행한다.
- 즉, 프록시 객체가 실제 객체의
createPost
메서드 실행하기 직전에 파라미터를 수정하고, 수정된 파라미터들을 넣어서 실제 객체의createPost
메서드를 실행한다
✅ Spring AOP의 단점..?!
- 프록시 패턴을 통해 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()
사용을 위한 몇 가지 세팅
- 메인 Application 클래스에
@EnableAspectJAutoProxy(exposeProxy = true)
어노테이션 달기 ( + 캐싱을 위해@EnableCaching
도 달기) @EnableAspectJAutoProxy(exposeProxy = true) @EnableCaching @SpringBootApplication public class OutstagramApplication { public static void main(String[] args) { SpringApplication.run(OutstagramApplication.class, args); }
}
2. application.properties or application.yml에서 설정해주기
```yml
spring.aop.proxy-target-class=true
spring.aop.expose-proxy=true
⭐ 정리
@Cacheable
은 Spring AOP를 통해 동작함- 프록시 객체가
@Cacheable
달려 있는 메서드 수행을 받아서 실제 메서드 수행 전에 캐시에 데이터 있는지 확인 - 있으면 캐시 데이터 바로 리턴(실제 메서드 수행 안됨)
- 없으면 실제 메서드 수행하고 결과를 캐시에 저장
- 프록시 객체를 통해서 메서드를 수행해야지 캐시 조회 및 저장이 가능하다
- 그래서 같은 클래스 내의 메서드를 호출하면
@Cacheable
이 적용되지 않을 수 있다 - 그럴 땐,
AopContext.currentProxy()
를 통해 현재 실행 중인 AOP 프록시 객체를 반환받아서 메서드를 실행하자!
반응형
'Outstagram' 카테고리의 다른 글
[Outstagram] Redis에서도 동시성 이슈가 발생한다고...? (lua script 적용기) (0) | 2024.12.12 |
---|---|
[Outstagram] 템플릿 메서드 패턴을 실제 프로젝트에 적용해보기 (0) | 2024.12.12 |
[Outstagram] kafka를 활용한 피드 push model 구현 과정 (0) | 2024.12.12 |
[Outstagram] 무한 스크롤 구현하려다 Snowflake ID 도입한 이야기 (0) | 2024.12.12 |
[Outstagram] 좋아요 동시성 문제 해결 (2) | 2024.12.11 |