diff --git a/keys b/keys new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardViewService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardViewService.java index 10ded65..b0f4d1f 100644 --- a/src/main/java/com/example/gamemate/domain/board/service/BoardViewService.java +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardViewService.java @@ -9,7 +9,9 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,16 +24,23 @@ import java.util.stream.Collectors; @Service -@RequiredArgsConstructor @Slf4j public class BoardViewService { private final BoardRepository boardRepository; private final String VIEW_COUNT_KEY = "board:view:"; private final String VIEW_RANKING_KEY = "board:ranking:"; - private final RedisTemplate redisTemplate; + private final StringRedisTemplate redisTemplate; private final HttpServletRequest request; + public BoardViewService( + BoardRepository boardRepository, + @Qualifier("viewCountRedisTemplate")StringRedisTemplate redisTemplate, + HttpServletRequest request) { + this.boardRepository = boardRepository; + this.redisTemplate = redisTemplate; + this.request = request; + } /** * 조회수 높은 게시글 조회 하는 메서드입니다. diff --git a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java index 8cb9c80..6d90825 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java @@ -12,6 +12,7 @@ import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,16 +23,21 @@ @Service @Transactional -@RequiredArgsConstructor public class CouponService { + + private static final String COUPON_STOCK_KEY = "coupon:%d:stock"; + private final CouponRepository couponRepository; private final UserCouponRepository userCouponRepository; private final StringRedisTemplate redisTemplate; - private static final String COUPON_STOCK_KEY = "coupon:%d:stock"; - - private String getCouponStockKey(Long couponId) { - return String.format(COUPON_STOCK_KEY, couponId); + public CouponService( + CouponRepository couponRepository, + UserCouponRepository userCouponRepository, + @Qualifier("couponRedisTemplate") StringRedisTemplate redisTemplate) { + this.couponRepository = couponRepository; + this.userCouponRepository = userCouponRepository; + this.redisTemplate = redisTemplate; } public CouponCreateResponseDto createCoupon(CouponCreateRequestDto requestDto, User loginUser) { @@ -59,6 +65,12 @@ public CouponCreateResponseDto createCoupon(CouponCreateRequestDto requestDto, U return new CouponCreateResponseDto(savedCoupon); } + private void validateCouponDates(LocalDateTime startAt, LocalDateTime expiredAt) { + if (startAt.isAfter(expiredAt)) { + throw new ApiException(ErrorCode.INVALID_COUPON_DATE); + } + } + public CouponIssueResponseDto issueCoupon(Long couponId, User loginUser) { Coupon coupon = couponRepository.findById(couponId) .orElseThrow(() -> new ApiException(ErrorCode.COUPON_NOT_FOUND)); @@ -131,9 +143,7 @@ public void useCoupon(Long userCouponId, User loginUser) { userCoupon.updateUsedAt(); } - private void validateCouponDates(LocalDateTime startAt, LocalDateTime expiredAt) { - if (startAt.isAfter(expiredAt)) { - throw new ApiException(ErrorCode.INVALID_COUPON_DATE); - } + private String getCouponStockKey(Long couponId) { + return String.format(COUPON_STOCK_KEY, couponId); } } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index 725967b..91fee35 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -10,8 +10,8 @@ import com.example.gamemate.global.exception.ApiException; import jakarta.annotation.PostConstruct; import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.connection.stream.StreamInfo; import org.springframework.data.redis.connection.stream.StreamRecords; import org.springframework.data.redis.core.RedisTemplate; @@ -25,17 +25,28 @@ * 알림을 처리하는 서비스 클래스입니다. */ @Service -@RequiredArgsConstructor @Slf4j public class NotificationService { + private static final String STREAM_KEY = "notification_stream"; + private static final String GROUP_NAME = "notification-group"; + private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; + private final NotificationRepository notificationRepository; private final EmitterRepository emitterRepository; private final RedisStreamService redisStreamService; private final RedisTemplate redisTemplate; - private static final String STREAM_KEY = "notification_stream"; - private static final String GROUP_NAME = "notification-group"; - private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; + + public NotificationService( + NotificationRepository notificationRepository, + EmitterRepository emitterRepository, + RedisStreamService redisStreamService, + @Qualifier("notificationRedisTemplate") RedisTemplate redisTemplate) { + this.notificationRepository = notificationRepository; + this.emitterRepository = emitterRepository; + this.redisStreamService = redisStreamService; + this.redisTemplate = redisTemplate; + } /** * 레디스 스트림의 스트림그룹을 생성합니다. diff --git a/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java b/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java index f272e9d..d48b630 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java @@ -3,8 +3,8 @@ import com.example.gamemate.domain.notification.dto.NotificationResponseDto; import com.example.gamemate.domain.notification.repository.EmitterRepository; import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.connection.stream.*; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; @@ -16,19 +16,27 @@ import java.util.*; @Service -@RequiredArgsConstructor @Slf4j public class RedisStreamService { - private final RedisTemplate redisTemplate; - private final EmitterRepository emitterRepository; private static final String STREAM_KEY = "notification_stream"; private static final String GROUP_NAME = "notification-group"; + private static final String CONSUMER_PREFIX = "consumer"; private static final int BATCH_SIZE = 100; private static final Duration POLL_TIMEOUT = Duration.ofMillis(100); private static final int MAX_STREAM_LENGTH = 1000; + private final RedisTemplate redisTemplate; + private final EmitterRepository emitterRepository; + + public RedisStreamService( + @Qualifier("notificationRedisTemplate") RedisTemplate redisTemplate, + EmitterRepository emitterRepository) { + this.redisTemplate = redisTemplate; + this.emitterRepository = emitterRepository; + } + @PostConstruct public void init() { createStreamGroup(); diff --git a/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java b/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java index 2e670ec..290dda0 100644 --- a/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java +++ b/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java @@ -4,6 +4,7 @@ import com.example.gamemate.domain.coupon.repository.CouponRepository; import com.example.gamemate.domain.coupon.repository.UserCouponRepository; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; import org.springframework.data.redis.core.StringRedisTemplate; @@ -12,16 +13,21 @@ import java.util.List; @Component -@RequiredArgsConstructor public class CouponDataSynchronizer { + + private static final String COUPON_STOCK_KEY = "coupon:%d:stock"; + private final CouponRepository couponRepository; private final UserCouponRepository userCouponRepository; private final StringRedisTemplate redisTemplate; - private static final String COUPON_STOCK_KEY = "coupon:%d:stock"; - - private String getCouponStockKey(Long couponId) { - return String.format(COUPON_STOCK_KEY, couponId); + public CouponDataSynchronizer( + CouponRepository couponRepository, + UserCouponRepository userCouponRepository, + @Qualifier("couponRedisTemplate") StringRedisTemplate redisTemplate) { + this.couponRepository = couponRepository; + this.userCouponRepository = userCouponRepository; + this.redisTemplate = redisTemplate; } @EventListener(ApplicationReadyEvent.class) @@ -34,4 +40,9 @@ public void syncCouponStock() { String.valueOf(remainingStock)); } } + + + private String getCouponStockKey(Long couponId) { + return String.format(COUPON_STOCK_KEY, couponId); + } } \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/global/config/RedisConfig.java b/src/main/java/com/example/gamemate/global/config/RedisConfig.java index 59f3729..1c914a4 100644 --- a/src/main/java/com/example/gamemate/global/config/RedisConfig.java +++ b/src/main/java/com/example/gamemate/global/config/RedisConfig.java @@ -2,17 +2,85 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; 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.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { + // 기본 RedisConnectionFactory @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + @Primary + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setDatabase(0); + return new LettuceConnectionFactory(config); + } + + // 기본 RedisTemplate + @Bean + @Primary + public RedisTemplate redisTemplate() { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory()); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); + return template; + } + + // DB 1: 알림 + @Bean + public RedisConnectionFactory notificationConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setDatabase(1); + return new LettuceConnectionFactory(config); + } + + // DB 2: 조회수 + @Bean + public RedisConnectionFactory viewCountConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setDatabase(2); + return new LettuceConnectionFactory(config); + } + + // DB 3: 리프레시 토큰 + @Bean + public RedisConnectionFactory refreshTokenConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setDatabase(3); + return new LettuceConnectionFactory(config); + } + + // DB 4: 토큰 블랙리스트 + @Bean + public RedisConnectionFactory blacklistConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setDatabase(4); + return new LettuceConnectionFactory(config); + } + + // DB 5: 쿠폰 + @Bean + public RedisConnectionFactory couponConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setDatabase(5); + return new LettuceConnectionFactory(config); + } + + // 알림 RedisTemplate (DB 1) + @Bean + public RedisTemplate notificationRedisTemplate() { RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setConnectionFactory(notificationConnectionFactory()); // String 타입을 위한 직렬화 설정 redisTemplate.setKeySerializer(new StringRedisSerializer()); @@ -22,4 +90,28 @@ public RedisTemplate redisTemplate(RedisConnectionFactory connec return redisTemplate; } + + // 조회수 RedisTemplate (DB 2) + @Bean + public StringRedisTemplate viewCountRedisTemplate() { + return new StringRedisTemplate(viewCountConnectionFactory()); + } + + // 리프레시 토큰 RedisTemplate (DB 3) + @Bean + public StringRedisTemplate refreshTokenRedisTemplate() { + return new StringRedisTemplate(refreshTokenConnectionFactory()); + } + + // 토큰 블랙리스트 RedisTemplate (DB 4) + @Bean + public StringRedisTemplate blacklistRedisTemplate() { + return new StringRedisTemplate(blacklistConnectionFactory()); + } + + // 쿠폰 RedisTemplate (DB 5) + @Bean + public StringRedisTemplate couponRedisTemplate() { + return new StringRedisTemplate(couponConnectionFactory()); + } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 52c35fc..8857447 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -61,11 +61,13 @@ spring.mail.properties.mail.smtp.starttls.enable=true # DEBUG logging.level.org.springframework.security=DEBUG +logging.level.org.springframework.security.oauth2=TRACE logging.level.org.springframework.web=DEBUG logging.level.org.springframework.web.servlet=DEBUG -logging.level.org.springframework.security.oauth2=TRACE -logging.level.org.springframework.web.client=TRACE +logging.level.org.springframework.web.client=INFO logging.level.com.example.gamemate=DEBUG +logging.level.org.springframework.data.redis=INFO +logging.level.com.example.gamemate.domain.notification.service.RedisStreamService=INFO # Gemini gemini.api.url=${GEMINI_URL} @@ -87,4 +89,4 @@ spring.jpa.properties.jakarta.persistence.lock.timeout=3000 # Redis spring.data.redis.host=${REDIS_HOST} -spring.data.redis.port=6379 +spring.data.redis.port=6379 \ No newline at end of file