From ca4af75891061d016ccb898d3f3a36659a0a8f98 Mon Sep 17 00:00:00 2001 From: Park JeongHyun Date: Tue, 21 Oct 2025 17:03:57 +0900 Subject: [PATCH 1/8] feat(gradle): add Redis dependency for caching layer implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Redis 의존성 추가로 캐시 계층 구현 지원 - 이후 DB 및 Elasticsearch 부하를 감소시키는 단계적 캐시 도입을 위한 기반 마련 - TTL 및 키 설계와 같은 캐시 정책 적용을 위한 사전 작업 --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle.kts b/build.gradle.kts index e80d36b..e9d9436 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-elasticsearch") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-data-redis") implementation("org.springframework.security:spring-security-core") implementation("org.springframework.kafka:spring-kafka") implementation("org.springframework.retry:spring-retry") From 1717c2f7e0d7221599a96610350cefa25334031b Mon Sep 17 00:00:00 2001 From: Park JeongHyun Date: Tue, 21 Oct 2025 20:11:39 +0900 Subject: [PATCH 2/8] feat(docker): add Redis service configuration in docker-compose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Docker Compose에 Redis 서비스 추가하여 로컬 개발 환경에서도 캐시 계층 테스트 가능 - Redis 컨테이너 healthcheck 설정으로 서비스 상태 점검 및 안정성 강화 - 데이터 지속성을 위해 redis_data 볼륨 마운트 설정 - Redis 설정에 appendonly 모드 활성화하여 데이터 보존 보장 --- docker-compose.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 67f3ffa..f18d65f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -97,6 +97,22 @@ services: timeout: 5s retries: 10 + redis: + image: redis:7-alpine + container_name: see-redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: ["redis-server", "--appendonly", "yes"] + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 10 + volumes: mysql_data: es_data: + redis_data: \ No newline at end of file From e044cec6e4f256f0a404dae9b18cbcc560392cf5 Mon Sep 17 00:00:00 2001 From: Park JeongHyun Date: Tue, 21 Oct 2025 20:11:49 +0900 Subject: [PATCH 3/8] feat(config): add Redis integration configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Spring 기반 Redis 설정 클래스 추가 - LettuceConnectionFactory 및 RedisTemplate 구성하여 Redis 연결 지원 - RedisSerializer 설정으로 키/값 직렬화 방식 정의 - 캐시 계층 도입을 위한 기반 작업으로, 이후 성능 최적화 및 부하 분산 목표 --- .../integration/cache/config/RedisConfig.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/java/dooya/see/adapter/integration/cache/config/RedisConfig.java diff --git a/src/main/java/dooya/see/adapter/integration/cache/config/RedisConfig.java b/src/main/java/dooya/see/adapter/integration/cache/config/RedisConfig.java new file mode 100644 index 0000000..440fbdd --- /dev/null +++ b/src/main/java/dooya/see/adapter/integration/cache/config/RedisConfig.java @@ -0,0 +1,30 @@ +package dooya.see.adapter.integration.cache.config; + +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 +public class RedisConfig { + @Bean + RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + return new LettuceConnectionFactory(config); + } + + @Bean + RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new StringRedisSerializer()); + template.afterPropertiesSet(); + return template; + } +} From 26ff9691efd7f3a596866ba0a998da53091a4232 Mon Sep 17 00:00:00 2001 From: Park JeongHyun Date: Tue, 21 Oct 2025 20:21:52 +0900 Subject: [PATCH 4/8] feat(test): add Redis configuration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RedisTemplate 설정 테스트 작성하여 Redis 연동 구성의 유효성 검증 - Spring Boot 기반 통합 테스트로 RedisTemplate 빈 등록 여부 확인 - 이후 캐시 계층 테스트 자동화 및 안정성 강화 목표 --- .../integration/cache/RedisConfigTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/test/java/dooya/see/adapter/integration/cache/RedisConfigTest.java diff --git a/src/test/java/dooya/see/adapter/integration/cache/RedisConfigTest.java b/src/test/java/dooya/see/adapter/integration/cache/RedisConfigTest.java new file mode 100644 index 0000000..fa4487a --- /dev/null +++ b/src/test/java/dooya/see/adapter/integration/cache/RedisConfigTest.java @@ -0,0 +1,15 @@ +package dooya.see.adapter.integration.cache; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +public record RedisConfigTest(RedisTemplate redisTemplate) { + @Test + void RedisTemplate_설정_확인() { + assertThat(redisTemplate).isNotNull(); + } +} From 394dbd0ce0bb6aec5cc6f164133950f40faf393e Mon Sep 17 00:00:00 2001 From: Park JeongHyun Date: Tue, 21 Oct 2025 21:04:07 +0900 Subject: [PATCH 5/8] feat(cache): add PostCacheKey utility for cache key generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 게시글 상세, 목록, 검색, 통계와 관련된 캐시 키 생성 유틸리티 추가 - 안정적이고 일관된 키 생성 방식 제공하여 캐시 조회와 무효화 로직 간소화 - 키 정규화, 유효성 검증, 해시 알고리즘(MD5) 적용으로 예외 처리와 확장성 고려 - Redis 캐시 계층 설계의 기반 코드로, 이후 성능 최적화 및 테스트 강화 목표 test(cache): add tests for PostCacheKey utility - PostCacheKey 클래스의 상세, 목록, 검색, 통계 키 생성 방식을 검증하는 테스트 추가 - 유효하지 않은 입력값에 대한 예외 처리 동작 확인 - 정상적인 키 형식 및 정규화 로직 검증으로 키 생성의 신뢰성 확보 --- .../integration/cache/key/PostCacheKey.java | 64 +++++++++++++++++++ .../cache/key/PostCacheKeyTest.java | 48 ++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 src/main/java/dooya/see/adapter/integration/cache/key/PostCacheKey.java create mode 100644 src/test/java/dooya/see/adapter/integration/cache/key/PostCacheKeyTest.java diff --git a/src/main/java/dooya/see/adapter/integration/cache/key/PostCacheKey.java b/src/main/java/dooya/see/adapter/integration/cache/key/PostCacheKey.java new file mode 100644 index 0000000..108d348 --- /dev/null +++ b/src/main/java/dooya/see/adapter/integration/cache/key/PostCacheKey.java @@ -0,0 +1,64 @@ +package dooya.see.adapter.integration.cache.key; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Objects; + +public final class PostCacheKey { + private static final String PREFIX = "post"; + private static final HexFormat HEX = HexFormat.of(); + private static final String HASH_ALGORITHM = "MD5"; + + private PostCacheKey() {} + + public static String detail(Long postId) { + return PREFIX + ":detail:" + requirePositive(postId); + } + + public static String publicList(int page, int size) { + return PREFIX + ":list:public:" + page + ":" + size; + } + + public static String categoryList(String category, int page, int size) { + return PREFIX + ":list:category:" + normalize(category) + ":" + page + ":" + size; + } + + public static String search(String normalizedQuery, int page, int size) { + String hash = hashToHex(normalizedQuery); + return PREFIX + ":search:" + hash + ":" + page + ":" + size; + } + + public static String stats(Long postId) { + return PREFIX + ":stats:" + requirePositive(postId); + } + + private static String normalize(String value) { + if (value == null) { + return "null"; + } + return value.trim().toUpperCase(); + } + + private static long requirePositive(Long id) { + Objects.requireNonNull(id, "postId must not be null"); + if (id <= 0) { + throw new IllegalArgumentException("postId must be positive"); + } + return id; + } + + private static String hashToHex(String input) { + if (input == null || input.isBlank()) { + return "empty"; + } + try { + MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM); + byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8)); + return HEX.formatHex(digest); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("MD5 algorithm not available", e); + } + } +} diff --git a/src/test/java/dooya/see/adapter/integration/cache/key/PostCacheKeyTest.java b/src/test/java/dooya/see/adapter/integration/cache/key/PostCacheKeyTest.java new file mode 100644 index 0000000..6143e2c --- /dev/null +++ b/src/test/java/dooya/see/adapter/integration/cache/key/PostCacheKeyTest.java @@ -0,0 +1,48 @@ +package dooya.see.adapter.integration.cache.key; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PostCacheKeyTest { + @Test + void 게시글_상세_키는_post_detail_id_형태로_생성된다() { + assertThat(PostCacheKey.detail(42L)).isEqualTo("post:detail:42"); + } + + @Test + void 검색_키는_해시값과_페이지_정보를_포함한다() { + String key = PostCacheKey.search("keyword=hello", 0, 20); + assertThat(key).startsWith("post:search:"); + assertThat(key).endsWith(":0:20"); + } + + @Test + void 카테고리_목록_키는_대문자_카테고리와_페이지_정보를_포함한다() { + String key = PostCacheKey.categoryList("tech", 1, 10); + assertThat(key).isEqualTo("post:list:category:TECH:1:10"); + } + + @Test + void 공개_목록_키는_페이지와_사이즈를_그대로_붙인다() { + assertThat(PostCacheKey.publicList(2, 50)).isEqualTo("post:list:public:2:50"); + } + + @Test + void 통계_키는_post_stats_id_형태로_생성된다() { + assertThat(PostCacheKey.stats(7L)).isEqualTo("post:stats:7"); + } + + @Test + void 음수_ID가_들어오면_예외를_던진다() { + assertThatThrownBy(() -> PostCacheKey.detail(-1L)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 검색어가_null_or_blank이면_empty_해시로_처리한다() { + assertThat(PostCacheKey.search(null, 0, 10)).contains("post:search:empty:"); + assertThat(PostCacheKey.search(" ", 0, 10)).contains("post:search:empty:"); + } +} From 7756f23620e9a097c682a78e9d23b445c8ca438c Mon Sep 17 00:00:00 2001 From: Park JeongHyun Date: Tue, 21 Oct 2025 21:04:21 +0900 Subject: [PATCH 6/8] docs(cache): add RedisTemplate setup guide and key schema documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RedisTemplate 설정 및 키 스키마 정의에 관한 문서 추가 - 환경 설정, Redis 설정 클래스, 키 스키마 유틸리티 구현 절차를 상세히 기술 - TTL/설정 상수 관리 및 테스트/검증 방법 포함 - 캐시 계층 도입의 첫 번째 단계로, 이후 개발팀의 일관된 설계와 구현 지원 목적 --- .docs/cache-strategy.md | 2 +- .docs/redis-template-setup.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 .docs/redis-template-setup.md diff --git a/.docs/cache-strategy.md b/.docs/cache-strategy.md index b9b3382..ccba750 100644 --- a/.docs/cache-strategy.md +++ b/.docs/cache-strategy.md @@ -57,7 +57,7 @@ post:stats:{postId} - 향후 Kafka 메시지를 이용한 비동기 갱신을 1차 릴리스에 포함할지, 후속 단계로 분리할지 결정. ## 8. 다음 단계 제안 -1. 위 정책을 근거로 Redis 인프라 구성 및 Spring Cache 설정 +1. 위 정책을 근거로 Redis 연결 설정과 템플릿 구성 2. 게시글 상세 캐시 구현 & 이벤트 무효화 PoC 3. Kafka 연동 확장 및 모니터링 지표 추가 4. 인기글/검색 캐시 도입 및 TTL 튜닝 diff --git a/.docs/redis-template-setup.md b/.docs/redis-template-setup.md new file mode 100644 index 0000000..3cc5735 --- /dev/null +++ b/.docs/redis-template-setup.md @@ -0,0 +1,34 @@ +# RedisTemplate 설정 & 키 스키마 정의 절차 + +캐시 1단계 구현(“RedisTemplate 설정 + 키 스키마 정의”)을 진행할 때 따라야 할 단계별 가이드입니다. + +## 1. 환경 설정 값 정의 +1. Redis 접속 정보를 `application.yml` 또는 프로파일별 설정에 추가합니다. + - 예: `spring.data.redis.host`, `spring.data.redis.port`, `spring.data.redis.password`. +2. 로컬·운영 환경에서 값을 어떻게 주입할지(환경 변수, docker-compose 등) 사전에 정리합니다. + +## 2. Redis 설정 클래스 작성 +1. 패키지 예시: `dooya.see.adapter.integration.cache`. +2. `@Configuration` 클래스를 만들고 다음 빈을 등록합니다. + - `RedisConnectionFactory` (Lettuce 클라이언트 사용 권장) + - `RedisTemplate` (직렬화 전략 선택: `StringRedisSerializer`, `GenericJackson2JsonRedisSerializer` 등) +3. 필요 시 `ObjectMapper` 등 공용 Bean을 주입하거나 별도 설정을 만듭니다. + +## 3. 키 스키마 유틸리티 구현 +1. 패키지 예시: `dooya.see.adapter.integration.cache.key`. +2. `PostCacheKey`와 같은 유틸 클래스를 만들고 정적 메서드로 키를 생성합니다. + - `detail(postId)`, `publicList(page,size)`, `search(hash,page,size)` 등. +3. 검색 조건 해시가 필요하면 `MessageDigest` 혹은 외부 라이브러리를 사용합니다. +4. 키 규칙: 소문자 + 콜론(`:`) 구분, null/빈 값은 normalize 후 처리. + +## 4. TTL/설정 상수 관리 +1. `@ConfigurationProperties("see.cache")` 같은 설정 클래스를 만들어 TTL을 외부화합니다. +2. 게시글 상세/목록/검색/통계별 기본 TTL 값을 명시하고 `application.yml`에 설정합니다. +3. 추후 모니터링 결과에 따라 조정할 수 있도록 기본값과 설명을 문서화합니다. + +## 5. 테스트 및 검증 메모 +1. 키 생성 유틸의 단위 테스트를 작성해 입력→키 문자열을 검증합니다. +2. RedisTemplate이 Bean으로 정상 등록되는지 `@SpringBootTest` 혹은 슬라이스 테스트로 확인합니다. +3. 향후 Kafka 이벤트와 연동할 때 사용할 `CacheService` 인터페이스/구현체 초안을 생각해둡니다. + +위 순서를 순차적으로 따라가면 RedisTemplate과 키 스키마를 준비한 뒤, 게시글 상세/목록 캐시에 재사용할 수 있습니다. From c11025742d98a770b3b6b2a17d32de3a35f88f62 Mon Sep 17 00:00:00 2001 From: Park JeongHyun Date: Wed, 22 Oct 2025 20:05:19 +0900 Subject: [PATCH 7/8] refactor(cache): remove explicit LettuceConnectionFactory setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Spring Boot의 자동 구성 기능 활용으로 LettuceConnectionFactory 제거 - `spring.data.redis.*` 설정을 기반으로 Redis 연결 자동 구성 지원 - 코드 단순화 및 중복 제거로 유지보수성 향상 --- .../adapter/integration/cache/config/RedisConfig.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/dooya/see/adapter/integration/cache/config/RedisConfig.java b/src/main/java/dooya/see/adapter/integration/cache/config/RedisConfig.java index 440fbdd..8f477fb 100644 --- a/src/main/java/dooya/see/adapter/integration/cache/config/RedisConfig.java +++ b/src/main/java/dooya/see/adapter/integration/cache/config/RedisConfig.java @@ -3,22 +3,15 @@ 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 public class RedisConfig { - @Bean - RedisConnectionFactory redisConnectionFactory() { - RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); - return new LettuceConnectionFactory(config); - } - @Bean RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate template = new RedisTemplate<>(); + // Spring Boot auto-configures LettuceConnectionFactory based on spring.data.redis.* properties. template.setConnectionFactory(connectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new StringRedisSerializer()); From 06ba6215756c8106e9f5541718b7729aaebfa07b Mon Sep 17 00:00:00 2001 From: Park JeongHyun Date: Wed, 22 Oct 2025 22:05:02 +0900 Subject: [PATCH 8/8] refactor(config): remove redundant comment in Redis configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Spring Boot 자동 구성을 통해 LettuceConnectionFactory가 자동으로 설정됨을 기술한 주석 제거 - 코드 가독성 향상 및 불필요한 주석 관리 비용 감소 - Redis 설정 클래스 간결화로 유지보수성 개선 --- .../dooya/see/adapter/integration/cache/config/RedisConfig.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/dooya/see/adapter/integration/cache/config/RedisConfig.java b/src/main/java/dooya/see/adapter/integration/cache/config/RedisConfig.java index 8f477fb..5893e3e 100644 --- a/src/main/java/dooya/see/adapter/integration/cache/config/RedisConfig.java +++ b/src/main/java/dooya/see/adapter/integration/cache/config/RedisConfig.java @@ -11,7 +11,6 @@ public class RedisConfig { @Bean RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate template = new RedisTemplate<>(); - // Spring Boot auto-configures LettuceConnectionFactory based on spring.data.redis.* properties. template.setConnectionFactory(connectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new StringRedisSerializer());