AOP
AOP는 관점 지향 프로그래밍의 약자로, 핵심 비즈니스 로직과 부가적인 기능을 분리해서 코드를 더 깔끔하고 유지보수하기 좋게 만드는 프로그래밍 기법이다. XML설정 또는 어노테이션 기반으로 부가기능을 손쉽게 선언 가능하고, 공통 기능을 한 곳에서 관리하므로 변경이 필요할 때도 그 곳만 수정하면 된다.
1.관점(Aspect)
공통된 기능을 하나의 모듈로 정의한 것. 로깅, 트랜잭션, 보안 등과 같은 기능은 여러 클래스나 메서드에 중복적으로 나타나는데, 이를 한 곳에서 정의하고 관리하는 것이 Aspect이다.
2.타겟(Target)
AOP가 적용될 대상 클래스 또는 메서드다. @service 또는 @repository클래스의 특정 메서드가 AOP의 대상이 될 수 있다.
3.조인 포인트(Join Point)
Aspect가 적용될 수 있는 지점. 메서드 호출, 객체 생성, 예외 처리 등 다양한 지점이 될 수 있지만, 스프링 AOP에서는 메서드 실행 지점만 지원한다.
4.포인트컷(Pointcut)
조인포인트 중에서 실제로 Advice를 적용할 지점을 선별하는 표현식. 예를들어, excution(* com. (..))은 특정 패키지의 모든 메서드를 포인트컷으로 지정하는 표현이다.
5.어드바이스(Advice)
실제 실행될 부가 기능이다. Advice는 타겟 메서드의 실행 전후 또는 예외 발생 시점 등에 실행된다.
before:실행 전
after returning: 실행 후 결과 반환 시
after throwing: 예외가 발생했을 때
around: 메서드 실행 전후를 모두 감싸서 실행
6.위빙(Weaving)
Aspect와 타겟 객체를 결합하는 과정. 스프링 AOP에서는 런타임에 프록시 객체를 생성해 위빙을 수행한다.
원래 이벤트 기반으로 cache를 사용하다가 구현하기도 너무 복잡하고 내 서비스에는 투 머치라는 생각이 들어서 캐싱로직을 aop로 바꾸었다.
aop가 이벤트 기반에 비해 지니는 가장 큰 장점은 일단 간단하다는 것이다. 메서드 실행 시에 바로 적용되기 때문에 오버헤드가 적고 단일 클래스에서 관리가 가능하다.
또한, 새로운 캐싱 로직의 추가가 편해 확장성이 좋다.
하지만 이벤트리스너는 좀 더 복잡한 대신에 다른 시스템과의 연동(게시글이 생성되었을 때 알림 전송, 비동기 처리 등)이 가능하다.
aop는 동일 어플리케이션 내에서만 동작한다.
따라서 일단 단순한 캐싱 작업은 aop로 하고, 이벤트가 필요한 작업은 이벤트리스너로 분리하면 된다. 메세지 브로커 역할을 하는 kafka도 사용해보고싶다. websocket과 kafka를 혼용해서 실시간 메세지 서비스도 만들어봐야겠다.
내 코드에 적용된 spring aop기반 캐싱
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomCacheable {
String key();
long ttl() default 300;
}
커스텀 어노테이션 CustomCacheable이다. 메서드의 결과를 redis에 캐싱하며 key를 설정하고 캐시 유지시간을 지정할 수 있다.
@Aspect
@Component
@RequiredArgsConstructor
public class CacheAspect {
private final RedisTemplate<String, Object> redisTemplate;
private final ExpressionParser parser = new SpelExpressionParser();
@Around("@annotation(customCacheable)")
public Object handleCacheable(
ProceedingJoinPoint joinPoint,
CustomCacheable customCacheable) throws Throwable {
//캐시 키 설정
String cacheKey = getKey(customCacheable.key(), joinPoint);
//ttl 값 설정
long ttl = customCacheable.ttl();
//redis 에서 캐시 확인
Object cacheValue = redisTemplate.opsForValue().get(cacheKey);
//캐시에 값이 있으면 반환
if(cacheValue != null){
return cacheValue;
}
//캐시 없으면 메서드 실행
Object result = joinPoint.proceed();
redisTemplate.opsForValue().set(cacheKey, result, Duration.ofSeconds(ttl));
return result;
}
private String getKey(
String customCache,
ProceedingJoinPoint joinPoint) {
EvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
String[] paramNames =
((MethodSignature) joinPoint.getSignature()).getParameterNames();
for (int i = 0; i < paramNames.length; i++) {
//파라미터 이름과 값 설정
context.setVariable(paramNames[i], args[i]);
}
//SpEL을 통해 키 생성
return parser.parseExpression(customCache).getValue(context, String.class);
}
}
@Around : @CustomCacheable이 붙은 모든 메서드 호출 전후에 AOP advice를 적용한다.
->
redis에서 캐시 키로 값을 조회하고, 값이 존재하면 즉시 반환하고 proceed()메서드가 실행되지 않는다.
->
캐시에 값이 없으면 메서드를 실행하고 결과를 redis에 저장한다. ttl을 사용하여 캐시 만료 시간도 설정한다.
->
SpEL을 활용해 메서드의 파라미터 값으로 캐시 키를 동적으로 생성한다. 파라미터 값을 기반으로 키를 유연하게 설정 가능하다.
@CustomCacheable(key = "'post::' + #id", ttl = 600)
public final BoardResponseDto updatePost(BoardRequestDto dto, Object userInfo, Long id){
validateUser(userInfo);
return executeUpdatePost(dto, userInfo, id);
}
service단에서 이렇게 메서드를 호출하면 된다.
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
// key -> string, value -> object
RedisTemplate<String, Object> template = new RedisTemplate<>();
// Redis 와 연결 관리
template.setConnectionFactory(redisConnectionFactory);
// key 를 string 으로 직렬화
template.setKeySerializer(new StringRedisSerializer());
// value 를 json 으로 직렬화 / 역직렬화
template.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper));
// bean 으로 등록
return template;
}
redisTemplate 설정을 통해 redis의 key를 문자열로 직렬화한다.
또한, value를 json형식으로 직렬화/역직렬화를 한다. 이를 통해 java객체가 json으로 변환되어서 redis에 저장되며, 가져올때 json을 다시 java로 변환할 수 있다.
'개인 공부용 프로젝트 > crud.jpa.api' 카테고리의 다른 글
Redis Insight (0) | 2024.12.16 |
---|---|
템플릿 메서드 패턴 (0) | 2024.12.15 |
DDD(Domain-Driven Design) (1) | 2024.12.13 |