-
Notifications
You must be signed in to change notification settings - Fork 1
RedisTemplate 설정 + 키 스키마 정의 #163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ca4af75
1717c2f
e044cec
26ff969
394dbd0
7756f23
c110257
06ba621
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<String, ?>` (직렬화 전략 선택: `StringRedisSerializer`, `GenericJackson2JsonRedisSerializer` 등) | ||||||||||||||||||
| 3. 필요 시 `ObjectMapper` 등 공용 Bean을 주입하거나 별도 설정을 만듭니다. | ||||||||||||||||||
|
Comment on lines
+12
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major RedisConnectionFactory 수동 등록은 피하고, 오토컨피그를 활용하세요. 수동 팩토리 빈을 정의하면 Boot의 수정 예시(가이드 문구): -2. `@Configuration` 클래스를 만들고 다음 빈을 등록합니다.
- - `RedisConnectionFactory` (Lettuce 클라이언트 사용 권장)
- - `RedisTemplate<String, ?>` (직렬화 전략 선택: `StringRedisSerializer`, `GenericJackson2JsonRedisSerializer` 등)
+2. `@Configuration` 클래스에서는 `RedisTemplate<String, ?>`만 커스터마이즈합니다.
+ - 연결 팩토리(`RedisConnectionFactory`)는 Spring Boot 오토컨피그에 맡깁니다(프로퍼티: `spring.redis.*`).
+ - 직렬화 전략만 명시적으로 설정(`StringRedisSerializer`, `GenericJackson2JsonRedisSerializer` 등). 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| ## 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과 키 스키마를 준비한 뒤, 게시글 상세/목록 캐시에 재사용할 수 있습니다. | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| 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.core.RedisTemplate; | ||
| import org.springframework.data.redis.serializer.StringRedisSerializer; | ||
|
|
||
| @Configuration | ||
| public class RedisConfig { | ||
| @Bean | ||
| RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) { | ||
| RedisTemplate<String, String> 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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String, String> redisTemplate) { | ||
| @Test | ||
| void RedisTemplate_설정_확인() { | ||
| assertThat(redisTemplate).isNotNull(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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:"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
프로퍼티 prefix가 잘못되었습니다.
spring.redis.*사용이 맞습니다.Spring Boot 오토컨피그는
spring.redis.host|port|password를 사용합니다. 문서 예시를 수정해 주세요.수정 예시:
🤖 Prompt for AI Agents