Skip to content
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

[BE] feat: SchoolFestivalsV1QueryService 추가 및 Spring Cache 적용 (#862) #864

Closed
wants to merge 7 commits into from

Conversation

seokjin8678
Copy link
Collaborator

@seokjin8678 seokjin8678 commented Apr 16, 2024

📌 관련 이슈

✨ PR 세부 내용

이슈 내용 그대로, Spring Cache를 사용하여 학교 식별자로 축제를 조회하는 기능을 추가했습니다.

캐시는 조회한 시점에서 30분 뒤에 초기화가 되도록 했습니다.

또한 날짜가 바뀌는 0시 기준, 스케줄링을 통한 캐시 초기화를 사용하여, 캐시로 인해 기간이 지난 축제를 조회할 수 있는 문제를 해결했습니다.

그 외 구현했던 의도는 코드에 커멘트를 통해 남기겠습니다!

아직 API는 구현하지 않았는데, 응답은 SchoolFestivalV1Response을 그대로 사용하기에 기존에 사용하던 /api/v1/schools//{schoolId}/festivals API를 그대로 대체할 수 있습니다.

다만, 클라이언트 측에서 SliceResponse<> 타입의 응답을 받고 있으므로, 사용하려면 new SliceResponse<>(true, content)와 같이 변환해야 합니다.

@seokjin8678 seokjin8678 added BE 백엔드에 관련된 작업 🙋‍♀️ 제안 제안에 관한 작업 🏗️ 기능 기능 추가에 관한 작업 labels Apr 16, 2024
@seokjin8678 seokjin8678 self-assigned this Apr 16, 2024
@github-actions github-actions bot requested review from BGuga, carsago and xxeol2 April 16, 2024 16:34
Copy link

github-actions bot commented Apr 16, 2024

Test Results

198 files  198 suites   31s ⏱️
668 tests 668 ✅ 0 💤 0 ❌
681 runs  681 ✅ 0 💤 0 ❌

Results for commit 81ec948.

♻️ This comment has been updated with latest results.

Comment on lines +9 to +23
@Component
@RequiredArgsConstructor
@Slf4j
public class CacheInvalidator {

private final CacheManager cacheManager;

public void invalidate(String cacheName) {
Optional.ofNullable(cacheManager.getCache(cacheName))
.ifPresentOrElse(cache -> {
cache.invalidate();
log.info("{} 캐시를 초기화 했습니다.", cacheName);
}, () -> log.error("{} 캐시를 찾을 수 없습니다.", cacheName));
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

캐시 초기화를 담당하는 컴포넌트 입니다.
축제 캐싱이 30분 동안 유지되므로, 그 사이 새로운 축제가 추가되면 사용자가 축제 정보를 확인할 수 없는 문제가 생깁니다.
축제는 관리자가 추가하기에 큰 문제는 아니지만, 가끔 즉시 초기화가 필요한 시점이 있을 수 있기에 별도의 컴포넌트로 분리했습니다.
만약, 수동으로 초기화가 필요하다면 관리자 API를 열어서 직접 초기화하면 될 것 같습니다.

Comment on lines +13 to +35
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class SchoolFestivalsV1QueryService {

public static final String SCHOOL_FESTIVALS_V1_CACHE_NAME = "schoolFestivalsV1";
public static final String PAST_SCHOOL_FESTIVALS_V1_CACHE_NAME = "pastSchoolFestivalsV1";

private final SchoolFestivalsV1QueryDslRepository schoolFestivalsV1QueryDslRepository;
private final Clock clock;

@Cacheable(cacheNames = SCHOOL_FESTIVALS_V1_CACHE_NAME, key = "#schoolId")
public List<SchoolFestivalV1Response> findFestivalsBySchoolId(Long schoolId) {
LocalDate now = LocalDate.now(clock);
return schoolFestivalsV1QueryDslRepository.findFestivalsBySchoolId(schoolId, now);
}

@Cacheable(cacheNames = PAST_SCHOOL_FESTIVALS_V1_CACHE_NAME, key = "#schoolId")
public List<SchoolFestivalV1Response> findPastFestivalsBySchoolId(Long schoolId) {
LocalDate now = LocalDate.now(clock);
return schoolFestivalsV1QueryDslRepository.findPastFestivalsBySchoolId(schoolId, now);
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

SchoolFestivalsV1QueryService에서 CacheName을 가지고 있습니다.
이유는 누군가 CacheName을 관리해야 하는데, 캐시 구현체인 SchoolFestivalsV1CacheConfig에서 관리하기엔 application 레이어인 Service에서 infrastructure에 대한 의존이 발생하더군요. 😂
따라서 캐시의 직접적인 사용자인 SchoolFestivalsV1QueryService에서 CacheName을 가지고 있도록 하였습니다.
CacheName이 문자열 + 불변하므로, public으로 노출되더라도 큰 문제는 없을 것 같다고 판단됩니다.

Comment on lines +21 to +41
public List<SchoolFestivalV1Response> findFestivalsBySchoolId(
Long schoolId,
LocalDate today
) {
return queryDslHelper.select(
new QSchoolFestivalV1Response(
festival.id,
festival.name,
festival.startDate,
festival.endDate,
festival.thumbnail,
festivalQueryInfo.artistInfo
)
)
.from(festival)
.leftJoin(festivalQueryInfo).on(festivalQueryInfo.festivalId.eq(festival.id))
.where(festival.school.id.eq(schoolId).and(festival.endDate.goe(today)))
.stream()
.sorted(Comparator.comparing(SchoolFestivalV1Response::startDate))
.toList();
}
Copy link
Collaborator Author

@seokjin8678 seokjin8678 Apr 16, 2024

Choose a reason for hiding this comment

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

DB를 통해 정렬을 수행하지 않고, 어플리케이션 레벨에서 정렬을 수행하도록 했습니다.
이유는 조회 시 가져오는 축제의 개수가 매우 적기 때문에, 어플리케이션 레벨에서 정렬하는게 효율적이라 판단하였습니다.
실제 성능 비교를 해보면, 정말 미미한 차이겠지만.. 해당 구현으로 FESTIVAL 테이블에 index_festival_end_date_desc, index_festival_start_date 인덱스를 제거할 수 있습니다.


해당 인덱스가 다른 쿼리에서 사용중이라, 제거가 힘들겠네요 😂
ms 단위라, 성능 비교에도 큰 차이는 없을 것 같은데.. DB 부담을 덜기에도 캐싱이 적용된터라 큰 차이도 없을 것 같네요.

Comment on lines +9 to +22
public class CacheClearTestExecutionListener implements TestExecutionListener {

@Override
public void beforeTestMethod(TestContext testContext) {
ApplicationContext applicationContext = testContext.getApplicationContext();
CacheManager cacheManager = applicationContext.getBean(CacheManager.class);
for (String cacheName : cacheManager.getCacheNames()) {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
cache.invalidate();
}
}
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

CacheConfig@Profile("!test") 어노테이션을 사용할 수도 있었지만, 캐싱이 제대로 작동하는지 테스트로 검증할 필요도 있고, CacheInvalidator에도 @Profile("!test") 어노테이션을 붙여야 하기 때문에, 해당 TestExecutionListener를 구현하였습니다.

@seokjin8678 seokjin8678 changed the title [BE] feat: SchoolFestivalsV2QueryService 추가 및 Spring Cache 적용 (#862) [BE] feat: SchoolFestivalsV1QueryService 추가 및 Spring Cache 적용 (#862) Apr 16, 2024
- 어플리케이션 종료 시점에 캐시 스탯 로깅
Comment on lines +12 to +28
@Slf4j
@Component
@RequiredArgsConstructor
public class CacheStatsLogger {

private final CacheManager cacheManager;

@EventListener(ContextClosedEvent.class)
public void logCacheStats() {
for (String cacheName : cacheManager.getCacheNames()) {
Cache cache = cacheManager.getCache(cacheName);
if (cache instanceof CaffeineCache caffeineCache) {
log.info("CacheName={} CacheStats={}", cacheName, caffeineCache.getNativeCache().stats());
}
}
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

어플리케이션 종료 시점에 캐시 분석을 위해 로그를 남기도록 했습니다.
마찬가지로 어드민 API를 열어서, 특정 시점에 남길 수 있도록 해도 좋을 것 같네요.

- 로그 목적이므로 테스트에서 제외
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BE 백엔드에 관련된 작업 🏗️ 기능 기능 추가에 관한 작업 🙋‍♀️ 제안 제안에 관한 작업
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant