diff --git a/build.gradle b/build.gradle index 14846dd..ff78305 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,15 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-aws-messaging:2.2.4.RELEASE' implementation 'org.springframework.kafka:spring-kafka' implementation 'software.amazon.awssdk:sns:2.21.37' - implementation 'io.github.lotteon-maven:blooming-blooms-utils:202401080033' + implementation 'io.github.lotteon-maven:blooming-blooms-utils:202401081357' + // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop + implementation 'org.springframework.boot:spring-boot-starter-aop:2.7.17' + + + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation("it.ozimov:embedded-redis:0.7.2") + implementation 'org.redisson:redisson-spring-boot-starter:3.17.0' testImplementation 'org.mockito:mockito-core:4.8.0' @@ -80,7 +88,7 @@ jacocoTestCoverageVerification { value = 'COVEREDRATIO' minimum = 0.80 } - excludes = ["*.mapper*", "**.emitter.**", "**.infrastructure.**"] + excludes = ["*.mapper*", "**.emitter.**", "**.infrastructure.**"] } } } diff --git a/src/main/java/kr/bb/notification/NotificationServiceApplication.java b/src/main/java/kr/bb/notification/NotificationServiceApplication.java index 9bd0c8e..a3fc96f 100644 --- a/src/main/java/kr/bb/notification/NotificationServiceApplication.java +++ b/src/main/java/kr/bb/notification/NotificationServiceApplication.java @@ -3,11 +3,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; +import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication @EnableEurekaClient @EnableJpaAuditing +@EnableAspectJAutoProxy public class NotificationServiceApplication { public static void main(String[] args) { diff --git a/src/main/java/kr/bb/notification/common/annotation/DuplicateEventHandleAnnotation.java b/src/main/java/kr/bb/notification/common/annotation/DuplicateEventHandleAnnotation.java new file mode 100644 index 0000000..be516fa --- /dev/null +++ b/src/main/java/kr/bb/notification/common/annotation/DuplicateEventHandleAnnotation.java @@ -0,0 +1,16 @@ +package kr.bb.notification.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface DuplicateEventHandleAnnotation { + String eventId(); // Specify eventId for constructing the cache key + + String userId(); // Specify userId for constructing the cache key + + long ttl() default 180; // Default TTL is 3 minutes +} diff --git a/src/main/java/kr/bb/notification/common/aop/DuplicateEventHandlerAop.java b/src/main/java/kr/bb/notification/common/aop/DuplicateEventHandlerAop.java new file mode 100644 index 0000000..2b1b15b --- /dev/null +++ b/src/main/java/kr/bb/notification/common/aop/DuplicateEventHandlerAop.java @@ -0,0 +1,48 @@ +package kr.bb.notification.common.aop; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Aspect +@Slf4j +@Component +@RequiredArgsConstructor +public class DuplicateEventHandlerAop { + private final RedisTemplate redisTemplate; + // https://alwayspr.tistory.com/34 + + + @Pointcut("@annotation(kr.bb.notification.common.annotation.DuplicateEventHandleAnnotation)") + public void duplicateEvent() {} + + @Around("duplicateEvent()") + public Object duplicateEventHandlerAop(ProceedingJoinPoint joinPoint) throws Throwable { + String eventId = null; + Object[] args = joinPoint.getArgs(); + + // Check if data exists in Redis + Object cachedData = redisTemplate.opsForValue().get(eventId); + + Object result = joinPoint.proceed(); // run method + + if (cachedData != null) { + // Data exists in cache, return it without executing the method + return cachedData; + } + + // Data doesn't exist in cache, proceed to execute the method + Object result = joinPoint.proceed(); + + // Save the result in Redis with the specified TTL + redisTemplate.opsForValue().set(key, result, duplicateEventHandlerAop.ttl()); + + return result; + } +} diff --git a/src/main/java/kr/bb/notification/config/RedisConfiguration.java b/src/main/java/kr/bb/notification/config/RedisConfiguration.java new file mode 100644 index 0000000..ac9d333 --- /dev/null +++ b/src/main/java/kr/bb/notification/config/RedisConfiguration.java @@ -0,0 +1,43 @@ +package kr.bb.notification.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@RequiredArgsConstructor +public class RedisConfiguration { + @Value("${spring.redis.host}") + private String host; + + @Value("${spring.redis.port}") + private int port; + + @Value("${spring.redis.password}") + private String password; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); + redisStandaloneConfiguration.setHostName(host); + redisStandaloneConfiguration.setPort(port); + redisStandaloneConfiguration.setPassword(password); + return new LettuceConnectionFactory(redisStandaloneConfiguration); + } + + @Bean + public RedisTemplate redisTemplate( + RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory); + return redisTemplate; + } +} diff --git a/src/main/java/kr/bb/notification/domain/notification/application/NotificationCommandService.java b/src/main/java/kr/bb/notification/domain/notification/application/NotificationCommandService.java index 36b6703..3d158a5 100644 --- a/src/main/java/kr/bb/notification/domain/notification/application/NotificationCommandService.java +++ b/src/main/java/kr/bb/notification/domain/notification/application/NotificationCommandService.java @@ -8,7 +8,6 @@ import kr.bb.notification.domain.notification.entity.MemberNotificationCommand; import kr.bb.notification.domain.notification.entity.Notification; import kr.bb.notification.domain.notification.entity.NotificationCommand; -import kr.bb.notification.domain.notification.repository.MemberNotificationJpaRepository; import kr.bb.notification.domain.notification.repository.NotificationJpaRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service;