Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-mail'

implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'com.fasterxml.jackson.core:jackson-databind'

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.blockguard.server.global.exception.BusinessExceptionHandler;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
Expand All @@ -24,6 +25,7 @@
public class NewsService {
private final NewsRepository newsRepository;

@Cacheable(value = "news:list:v2", key = "'list:' + #page + ':' + #size + ':' + #sort + ':' + #category")
public NewsPageResponse getNewsList(int page, int size, String sort, String category) {
Comment on lines +28 to 29
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

상대시간 필드가 포함된 DTO를 장시간 캐시하면 UI가 stale 됩니다 — 시간 버킷 포함 또는 TTL 단축 권장

현재 DTO(NewsArticleResponse.publishedAt)가 “x분 전” 식으로 포맷된 문자열이라 서비스 레벨 캐시(요약상 24h)와 충돌합니다. 두 가지 경로 중 하나를 선택하세요.

  • 빠른 적용: 캐시 키에 10분 버킷을 포함해 상대시간이 주기적으로 갱신되도록 함.
  • 근본 해결: 캐시는 엔티티/절대시간만 저장하고 DTO 변환은 캐시 hit 이후에 수행(또는 상대시간은 클라이언트 계산).

빠른 적용용 키 변경 예:

-    @Cacheable(value = "news:list:v2", key = "'list:' + #page + ':' + #size + ':' + #sort + ':' + #category")
+    // 10분 단위 버킷 추가로 상대시간 표시 stale 완화
+    @Cacheable(
+        value = "news:list:v2",
+        key = "'list:' + #page + ':' + #size + ':' + #sort + ':' + #category + ':' + (T(java.time.Instant).now().getEpochSecond() / 600)"
+    )

추가로 빈 결과는 캐시하지 않도록 할 수 있습니다(선택):

-    @Cacheable( ... )
+    @Cacheable( ..., unless = "#result == null || #result.news == null || #result.news.isEmpty()" )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Cacheable(value = "news:list:v2", key = "'list:' + #page + ':' + #size + ':' + #sort + ':' + #category")
public NewsPageResponse getNewsList(int page, int size, String sort, String category) {
// 10분 단위 버킷 추가로 상대시간 표시 stale 완화
@Cacheable(
value = "news:list:v2",
key = "'list:' + #page + ':' + #size + ':' + #sort + ':' + #category + ':' + (T(java.time.Instant).now().getEpochSecond() / 600)"
)
public NewsPageResponse getNewsList(int page, int size, String sort, String category) {
// … existing implementation …
}

Pageable pageable = PageRequest.of(page - 1, size, getSort(sort));
Page<NewsArticle> newsPage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.Duration;
import java.time.LocalDateTime;

@Getter
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public class NewsArticleResponse {
private Long id;
private String title;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class NewsPageResponse {
private List<NewsArticleResponse> news;
private PageableInfo pageableInfo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PageableInfo {
private int page;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.blockguard.server.infra.crawler.DaumNewsCrawler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
Expand Down Expand Up @@ -37,7 +38,7 @@ public class NewsSaveScheduler {
Map.entry(Category.ETC, List.of("몸캠"))
);

@Async
@CacheEvict(value = "news:list:v2", allEntries = true)
public void crawlingForAdmin() {
crawlByCategories(EnumSet.of(
Category.VOICE_PHISHING,
Expand All @@ -46,7 +47,7 @@ public void crawlingForAdmin() {
));
}

@Async
@CacheEvict(value = "news:list:v2", allEntries = true)
// @Scheduled(cron = "0 0 4 * * *")
public void saveNewsArticles() {
crawlAll();
Expand Down
53 changes: 53 additions & 0 deletions src/main/java/com/blockguard/server/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.blockguard.server.global.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@EnableCaching
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;

@Value("${spring.data.redis.port}")
private int port;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}

Comment on lines +23 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

직접 정의한 RedisConnectionFactory가 비밀번호/DB/SSL/타임아웃을 무시합니다

Spring Boot의 자동 설정을 덮어써서 spring.data.redis.password, database, ssl, sentinel/cluster, timeout 등 프로퍼티가 적용되지 않습니다. 운영에서 인증/DB 분리 시 연결 실패·데이터 오염 위험이 큽니다. 자동 설정으로 제공되는 RedisConnectionFactory를 그대로 주입받도록 이 빈을 제거하세요.

적용 diff:

-    @Value("${spring.data.redis.host}")
-    private String host;
-
-    @Value("${spring.data.redis.port}")
-    private int port;
-
-    @Bean
-    public RedisConnectionFactory redisConnectionFactory() {
-        return new LettuceConnectionFactory(host, port);
-    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
🤖 Prompt for AI Agents
In src/main/java/com/blockguard/server/global/config/RedisConfig.java around
lines 23-33, the explicitly defined RedisConnectionFactory using only host/port
overrides Spring Boot auto-configuration and ignores properties like
spring.data.redis.password, database, ssl, sentinel/cluster and timeout; remove
the custom @Bean method (and the host/port @Value fields if they are only used
for this) so Spring Boot can provide the auto-configured RedisConnectionFactory,
or if you must customize, construct the LettuceConnectionFactory via Spring's
RedisProperties and LettuceClientConfiguration so all standard properties are
honored.

@Bean
public CacheManager cacheManager(RedisConnectionFactory cf) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofMinutes(30));

Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("news:list:v2", defaultConfig.entryTtl(Duration.ofHours(24))); // 뉴스는 하루 TTL


return RedisCacheManager.
RedisCacheManagerBuilder.
fromConnectionFactory(cf)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
.build();

}
}