-
Notifications
You must be signed in to change notification settings - Fork 2
perf: 캘린더 캐시 조회 추가 #252
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
base: develop
Are you sure you want to change the base?
perf: 캘린더 캐시 조회 추가 #252
Conversation
📝 WalkthroughWalkthroughSpring Cache와 Redis 기반 캐시 구성이 추가되었고, CalendarService의 월별 조회에 캐시 적용 및 생성/삭제 시 관련 월 범위의 캐시 무효화 로직이 도입되었다. DTO들에 Jackson 역직렬화용 애너테이션이 추가되었으며, 빌드에 spring-boot-starter-cache 의존성이 포함되었다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant Svc as CalendarService
participant Cache as Redis Cache
participant Repo as Repository
Client->>Svc: getMonth(year, month)
alt 캐시 히트
Svc->>Cache: GET month:YYYY-MM
Cache-->>Svc: CalendarMonthResponse
Svc-->>Client: return cached
else 캐시 미스
Svc->>Cache: GET month:YYYY-MM (miss)
Svc->>Repo: fetchMonth(year, month)
Repo-->>Svc: CalendarMonthResponse
Svc->>Cache: PUT month:YYYY-MM -> response (TTL 30m)
Svc-->>Client: return response
end
sequenceDiagram
autonumber
actor Client
participant Svc as CalendarService
participant Cache as Redis Cache
participant Repo as Repository
rect
note right of Svc: Create flow
Client->>Svc: create(event)
Svc->>Repo: save(event)
Repo-->>Svc: savedEntity(startDate,endDate)
Svc->>Cache: FOR each YearMonth in [start,end]: EVICT month:YYYY-MM
Svc-->>Client: return saved
end
rect
note right of Svc: Delete flow
Client->>Svc: delete(eventId)
Svc->>Repo: softDelete(eventId)
Repo-->>Svc: startDate,endDate
Svc->>Cache: FOR each YearMonth in [start,end]: EVICT month:YYYY-MM
Svc-->>Client: return ok
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
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.
Actionable comments posted: 2
🧹 Nitpick comments (8)
src/main/java/inu/codin/codin/common/config/CacheConfig.java (1)
20-31: RedisCache 설정에 몇 가지 안전장치와 운영 편의 옵션 추가 제안
- null 캐시 방지, 트랜잭션 인지, 키 프리픽스 지정 등을 권장합니다.
다음과 같이 보완을 고려해 주세요:
@Bean public CacheManager cacheManager(RedisConnectionFactory cf) { RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) - .entryTtl(Duration.ofMinutes(30)); + .entryTtl(Duration.ofMinutes(30)) + .disableCachingNullValues() + .computePrefixWith(cacheName -> "codin:" + cacheName + ":"); return RedisCacheManager.RedisCacheManagerBuilder .fromConnectionFactory(cf) .cacheDefaults(redisCacheConfiguration) + .transactionAware() .build(); }src/main/java/inu/codin/codin/domain/calendar/dto/EventDto.java (1)
32-37: 불필요한 삼항 연산자 제거 (미세 정리)null 전달 그대로 허용되므로 삼항 연산자 없이 간결화 가능합니다.
return EventDto.builder() .eventId(ObjectIdUtil.toString(calendarEntity.getId())) .content(calendarEntity.getContent()) - .department(calendarEntity.getDepartment() != null ? calendarEntity.getDepartment() : null) + .department(calendarEntity.getDepartment()) .build();src/main/java/inu/codin/codin/domain/calendar/service/CalendarService.java (3)
31-37: 주석 용어 수정: Write-Through → Read-Through
@Cacheable은 조회 시 캐시 미스에 한해 저장하는 Read-Through 패턴에 해당합니다. 주석을 정정해 주세요.- * + 조회 캐시 Write-Through 전략 사용 + * + 조회 캐시 Read-Through 전략 사용
40-41: 로그 레벨 완화 제안고빈도 조회 엔드포인트면 INFO는 과도할 수 있습니다. DEBUG 전환 검토를 권장합니다.
- log.info("Getting month for year {} and month {}", year, month); + log.debug("Getting month for year {} and month {}", year, month);
132-144: 키 생성 일관성 보장 유틸 제안(선택)캐시 키 포맷이 여러 곳에서 생성됩니다. 정적 유틸로 중앙집중화하면 포맷 변경 시 리스크를 줄일 수 있습니다. (어노테이션 SpEL에서는
T(…Utils).method(..)로 호출)예시:
public final class CacheKeys { private CacheKeys() {} public static String calendarMonth(int year, int month) { return String.format("month:%04d-%02d", year, month); } public static String calendarMonth(YearMonth ym) { return String.format("month:%04d-%02d", ym.getYear(), ym.getMonthValue()); } }사용:
@Cacheable(..., key = "T(inu.codin.codin.common.cache.CacheKeys).calendarMonth(#year, #month)")String key = CacheKeys.calendarMonth(ym);src/main/java/inu/codin/codin/domain/calendar/dto/CalendarDayResponse.java (3)
3-9: JSR-310 (LocalDate) 직렬화 관련 import/의존성 최소화 제안Spring Boot의 Jackson Auto-Configuration이 JavaTimeModule을 기본 등록하므로, 필드 단 @JsonSerialize/@JsonDeserialize 지정 없이도 @jsonformat만으로 일관된 포맷을 유지할 수 있습니다. DTO에 Jackson 구현 세부사항을 덜어 커플링을 줄이는 방향을 권장합니다. Redis 캐시용 ObjectMapper에도 JavaTimeModule이 등록되어 있는지 CacheConfig에서 한 번만 확인해 주세요.
적용 예(불필요한 import 제거):
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; -import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
21-22: 필드 레벨 직렬화/역직렬화 어노테이션 제거 제안@jsonformat(pattern = "yyyy-MM-dd")만 유지해도 충분합니다. 중복 지정은 유지보수 비용을 높일 수 있습니다.
- @JsonSerialize(using = LocalDateSerializer.class) - @JsonDeserialize(using = LocalDateDeserializer.class)
31-36: 생성자 기반 역직렬화는 적절. 컬렉션 불변/널 안전성 및 값 검증 보강 제안
- null 방지: date는 null 불가.
- 값 검증: totalCont 음수 방지.
- 불변성: items를 불변 복사(List.copyOf)하고 null을 빈 리스트로 대체.
- 선택: @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)로 의도를 명시.
- 네이밍 확인: "totalCont"가 오탈자 아닌지(의도대로면 OK). API/문서/프론트 사용 처 확인 부탁드립니다.
- public CalendarDayResponse(@JsonProperty("date") LocalDate date, @JsonProperty("totalCont") int totalCont, @JsonProperty("items") List<EventDto> items) { - this.date = date; - this.totalCont = totalCont; - this.items = items; - } + @JsonCreator(/* 명시적 의도 표현 */ mode = JsonCreator.Mode.PROPERTIES) + public CalendarDayResponse(@JsonProperty("date") LocalDate date, + @JsonProperty("totalCont") int totalCont, + @JsonProperty("items") List<EventDto> items) { + this.date = java.util.Objects.requireNonNull(date, "date must not be null"); + if (totalCont < 0) throw new IllegalArgumentException("totalCont must be >= 0"); + this.totalCont = totalCont; + this.items = (items == null) ? java.util.List.of() : java.util.List.copyOf(items); + }(외부 변경 필요: java.util.Objects가 없다면 import 추가)
import java.util.Objects;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
build.gradle(1 hunks)src/main/java/inu/codin/codin/common/config/CacheConfig.java(1 hunks)src/main/java/inu/codin/codin/domain/calendar/dto/CalendarDayResponse.java(3 hunks)src/main/java/inu/codin/codin/domain/calendar/dto/CalendarMonthResponse.java(2 hunks)src/main/java/inu/codin/codin/domain/calendar/dto/EventDto.java(2 hunks)src/main/java/inu/codin/codin/domain/calendar/service/CalendarService.java(3 hunks)
🔇 Additional comments (3)
build.gradle (1)
75-76: 캐시 스타터 추가 LGTMspring-boot-starter-cache 추가가 Redis 캐시 구성과 일치합니다. 별다른 이슈 없어 보입니다.
src/main/java/inu/codin/codin/domain/calendar/dto/EventDto.java (1)
25-26: Jackson 생성자 주입 추가 LGTMRedis 캐시 역직렬화를 고려한 @JsonCreator/@JsonProperty 적용이 적절합니다.
src/main/java/inu/codin/codin/domain/calendar/dto/CalendarMonthResponse.java (1)
24-26: Jackson 생성자 주입 추가 LGTMDTO 캐시 역직렬화 호환성 측면에서 적합합니다.
| private final String CACHE_NAME = "calendar"; | ||
| private final CacheManager cacheManager; | ||
|
|
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.
@Cacheable에서 사용하는 캐시 이름은 컴파일 타임 상수여야 합니다
현재 private final String CACHE_NAME는 주석 위치에서 컴파일 타임 상수가 아니라서 어노테이션 인자로 사용할 수 없어 컴파일 오류가 납니다. static final로 승격하거나 문자열 리터럴을 직접 사용하세요.
- private final String CACHE_NAME = "calendar";
+ private static final String CACHE_NAME = "calendar";📝 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.
| private final String CACHE_NAME = "calendar"; | |
| private final CacheManager cacheManager; | |
| private static final String CACHE_NAME = "calendar"; | |
| private final CacheManager cacheManager; |
🤖 Prompt for AI Agents
In src/main/java/inu/codin/codin/domain/calendar/service/CalendarService.java
around lines 28 to 30, the CACHE_NAME field is declared as a non-compile-time
constant (private final String CACHE_NAME) but is used in @Cacheable which
requires a compile-time constant; change the declaration to a compile-time
constant by promoting it to private static final String CACHE_NAME = "calendar"
(or replace usages in annotations with the string literal "calendar") so the
annotation parameter becomes a constant that compiles.
| @Cacheable(value = CACHE_NAME, key = "'month:' + T(java.time.YearMonth).of(#year, #month)") | ||
| public CalendarMonthResponse getMonth(int year, int month) { | ||
| log.info("Getting month for year {} and month {}", year, month); | ||
| if (month < 1 || month > 12) { |
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.
키 SpEL에서 YearMonth.of 사용 시 유효성 검사 이전에 예외 발생 가능 + value 속성 사용법
- 현재 키:
T(java.time.YearMonth).of(#year, #month)는 메서드 본문 유효성 검사(1~12) 이전에 SpEL 평가 중DateTimeException을 유발할 수 있습니다. - 또, 어노테이션 속성은 상수여야 하므로 위의
CACHE_NAME수정과 함께cacheNames속성 사용을 권장합니다.
안전한 키 문자열 포맷을 사용하세요.
- @Cacheable(value = CACHE_NAME, key = "'month:' + T(java.time.YearMonth).of(#year, #month)")
+ @Cacheable(cacheNames = CACHE_NAME, key = "T(java.lang.String).format('month:%04d-%02d', #year, #month)")
- public CalendarMonthResponse getMonth(int year, int month) {
- log.info("Getting month for year {} and month {}", year, month);
+ public CalendarMonthResponse getMonth(int year, int month) {
+ log.info("Getting month for year {} and month {}", year, month);설명:
- 키는
month:YYYY-MM형태로 패딩되어evictMonthBetween의"month:" + ym와 동일 포맷을 유지합니다.
📝 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.
| @Cacheable(value = CACHE_NAME, key = "'month:' + T(java.time.YearMonth).of(#year, #month)") | |
| public CalendarMonthResponse getMonth(int year, int month) { | |
| log.info("Getting month for year {} and month {}", year, month); | |
| if (month < 1 || month > 12) { | |
| @Cacheable(cacheNames = CACHE_NAME, key = "T(java.lang.String).format('month:%04d-%02d', #year, #month)") | |
| public CalendarMonthResponse getMonth(int year, int month) { | |
| log.info("Getting month for year {} and month {}", year, month); | |
| if (month < 1 || month > 12) { |
#️⃣ 연관된 이슈
#253
📝 작업 내용
spring-boot-starter-cache와 redis를 이용해 Calendar 부분에 캐시를 추가했습니다.
Read 비율이 매우 높은 API이기 때문에 30분 TTL으로 구현
CacheConfig를 생성해 캐시 설정 내용을 저장
DB 데이터가 워낙 작기 때문에 유의미한 성능 향상은 스트레스 테스트에서 23퍼센트로 보였습니다.
스크린샷 (선택)
캐싱 도입 이전
캐싱 도입 이후
💬 리뷰 요구사항(선택)
@coderabbitai review
Summary by CodeRabbit