Skip to content

Conversation

@doma17
Copy link
Contributor

@doma17 doma17 commented Sep 5, 2025

#️⃣ 연관된 이슈

ex) #이슈번호, #이슈번호

#253

📝 작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능)

  • spring-boot-starter-cache와 redis를 이용해 Calendar 부분에 캐시를 추가했습니다.

  • Read 비율이 매우 높은 API이기 때문에 30분 TTL으로 구현

  • CacheConfig를 생성해 캐시 설정 내용을 저장

  • DB 데이터가 워낙 작기 때문에 유의미한 성능 향상은 스트레스 테스트에서 23퍼센트로 보였습니다.

    • Jmeter: 200스레드, Infitie로 부하 테스트

스크린샷 (선택)

캐싱 도입 이전

스크린샷 2025-09-05 17 16 26 스크린샷 2025-09-05 17 16 16 스크린샷 2025-09-05 17 16 21

캐싱 도입 이후

스크린샷 2025-09-05 17 12 53 스크린샷 2025-09-05 17 12 46 스크린샷 2025-09-05 17 12 38

💬 리뷰 요구사항(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

@coderabbitai review

Summary by CodeRabbit

  • 신규 기능
    • 캘린더 월별 조회 결과를 캐시하여 반복 요청 시 응답 속도를 크게 향상했습니다. 일정 생성/삭제 시 관련 월 캐시가 자동으로 갱신되며, 기본 캐시 유효기간은 30분입니다.
    • API 응답의 날짜와 캘린더 관련 DTO에 대한 JSON 직렬화/역직렬화 설정을 강화해 클라이언트와의 호환성과 데이터 일관성을 개선했습니다.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 5, 2025

📝 Walkthrough

Walkthrough

Spring Cache와 Redis 기반 캐시 구성이 추가되었고, CalendarService의 월별 조회에 캐시 적용 및 생성/삭제 시 관련 월 범위의 캐시 무효화 로직이 도입되었다. DTO들에 Jackson 역직렬화용 애너테이션이 추가되었으며, 빌드에 spring-boot-starter-cache 의존성이 포함되었다.

Changes

Cohort / File(s) Change Summary
빌드 설정
build.gradle
Spring Cache 의존성 org.springframework.boot:spring-boot-starter-cache 추가.
캐시 구성
src/main/java/.../common/config/CacheConfig.java
@EnableCaching 활성화. RedisCacheManager 빈 등록: 키 StringRedisSerializer, 값 GenericJackson2JsonRedisSerializer, 기본 TTL 30분.
DTO Jackson 역직렬화 지원
src/main/java/.../domain/calendar/dto/CalendarDayResponse.java, .../CalendarMonthResponse.java, .../EventDto.java
생성자에 @JsonCreator 및 파라미터 @JsonProperty 추가. CalendarDayResponse.date@JsonSerialize(LocalDateSerializer)/@JsonDeserialize(LocalDateDeserializer) 적용.
서비스 캐시 적용 및 무효화
src/main/java/.../domain/calendar/service/CalendarService.java
getMonth@Cacheable(cache = "calendar", key = "...'month:' + YearMonth.of(year, month)") 추가. 생성/삭제 후 기간 내 모든 YearMonth 키에 대해 캐시 제거 메서드 evictMonthBetween로 무효화. 로깅 추가. 공개 메서드 시그니처 변화 없음.

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
Loading
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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

캐시에 당근 한 봉지 숨겨 두고
달력의 달을 콕 집어 톡! 꺼내보오
새 일정 오면 폴짝폴짝 지워내고
지운 자리에 신선한 달빛 채우고
깡충, 깡충—빠른 응답, 따끈한 캐시 🥕✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/calendar-cache

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@doma17
Copy link
Contributor Author

doma17 commented Sep 5, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 5, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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.

📥 Commits

Reviewing files that changed from the base of the PR and between f736ade and 4dbb3b9.

📒 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: 캐시 스타터 추가 LGTM

spring-boot-starter-cache 추가가 Redis 캐시 구성과 일치합니다. 별다른 이슈 없어 보입니다.

src/main/java/inu/codin/codin/domain/calendar/dto/EventDto.java (1)

25-26: Jackson 생성자 주입 추가 LGTM

Redis 캐시 역직렬화를 고려한 @JsonCreator/@JsonProperty 적용이 적절합니다.

src/main/java/inu/codin/codin/domain/calendar/dto/CalendarMonthResponse.java (1)

24-26: Jackson 생성자 주입 추가 LGTM

DTO 캐시 역직렬화 호환성 측면에서 적합합니다.

Comment on lines +28 to +30
private final String CACHE_NAME = "calendar";
private final CacheManager cacheManager;

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

@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.

Suggested change
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.

Comment on lines +38 to 41
@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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

키 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.

Suggested change
@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) {

@doma17 doma17 self-assigned this Sep 6, 2025
@doma17 doma17 added ✨ Feature 기능 개발 Priority: Low 우선순위 하 Status: In Progress 현재 처리 중인 이슈 labels Sep 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발 Priority: Low 우선순위 하 Status: In Progress 현재 처리 중인 이슈

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants