-
Notifications
You must be signed in to change notification settings - Fork 8
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
Changes from 6 commits
4a6ebf0
2a2f784
070cb84
f52187c
115c9eb
218aad3
81ec948
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,23 @@ | ||
package com.festago.common.cache; | ||
|
||
import java.util.Optional; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.cache.CacheManager; | ||
import org.springframework.stereotype.Component; | ||
|
||
@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)); | ||
} | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.festago.common.cache; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.cache.Cache; | ||
import org.springframework.cache.CacheManager; | ||
import org.springframework.cache.caffeine.CaffeineCache; | ||
import org.springframework.context.event.ContextClosedEvent; | ||
import org.springframework.context.event.EventListener; | ||
import org.springframework.stereotype.Component; | ||
|
||
@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()); | ||
} | ||
} | ||
} | ||
} | ||
Comment on lines
+14
to
+30
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. 어플리케이션 종료 시점에 캐시 분석을 위해 로그를 남기도록 했습니다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package com.festago.config; | ||
|
||
import java.util.List; | ||
import org.springframework.cache.Cache; | ||
import org.springframework.cache.CacheManager; | ||
import org.springframework.cache.annotation.EnableCaching; | ||
import org.springframework.cache.support.SimpleCacheManager; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
|
||
@Configuration | ||
@EnableCaching | ||
public class CacheConfig { | ||
|
||
@Bean | ||
public CacheManager cacheManager(List<Cache> caches) { | ||
SimpleCacheManager cacheManager = new SimpleCacheManager(); | ||
cacheManager.setCaches(caches); | ||
return cacheManager; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package com.festago.school.application.v1; | ||
|
||
import com.festago.common.cache.CacheInvalidator; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.scheduling.annotation.Scheduled; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class SchoolFestivalsV1CacheInvalidateScheduler { | ||
|
||
private final CacheInvalidator cacheInvalidator; | ||
|
||
// 매일 정각마다 캐시 초기화 | ||
@Scheduled(cron = "0 0 0 * * *") | ||
public void invalidate() { | ||
cacheInvalidator.invalidate(SchoolFestivalsV1QueryService.SCHOOL_FESTIVALS_V1_CACHE_NAME); | ||
cacheInvalidator.invalidate(SchoolFestivalsV1QueryService.PAST_SCHOOL_FESTIVALS_V1_CACHE_NAME); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package com.festago.school.application.v1; | ||
|
||
import com.festago.school.dto.v1.SchoolFestivalV1Response; | ||
import com.festago.school.repository.v1.SchoolFestivalsV1QueryDslRepository; | ||
import java.time.Clock; | ||
import java.time.LocalDate; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.cache.annotation.Cacheable; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@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); | ||
} | ||
} | ||
Comment on lines
+13
to
+35
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.
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package com.festago.school.infrastructure; | ||
|
||
import com.festago.school.application.v1.SchoolFestivalsV1QueryService; | ||
import com.github.benmanes.caffeine.cache.Caffeine; | ||
import java.util.concurrent.TimeUnit; | ||
import org.springframework.cache.Cache; | ||
import org.springframework.cache.caffeine.CaffeineCache; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
|
||
@Configuration | ||
public class SchoolFestivalsV1CacheConfig { | ||
|
||
private static final long EXPIRED_AFTER_WRITE = 30; | ||
private static final long MAXIMUM_SIZE = 1_000; | ||
|
||
@Bean | ||
public Cache schoolFestivalsV1Cache() { | ||
return new CaffeineCache(SchoolFestivalsV1QueryService.SCHOOL_FESTIVALS_V1_CACHE_NAME, | ||
Caffeine.newBuilder() | ||
.recordStats() | ||
.expireAfterWrite(EXPIRED_AFTER_WRITE, TimeUnit.MINUTES) | ||
.maximumSize(MAXIMUM_SIZE) | ||
.build() | ||
); | ||
} | ||
|
||
@Bean | ||
public Cache pastSchoolFestivalsV1Cache() { | ||
return new CaffeineCache(SchoolFestivalsV1QueryService.PAST_SCHOOL_FESTIVALS_V1_CACHE_NAME, | ||
Caffeine.newBuilder() | ||
.recordStats() | ||
.expireAfterWrite(EXPIRED_AFTER_WRITE, TimeUnit.MINUTES) | ||
.maximumSize(MAXIMUM_SIZE) | ||
.build() | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package com.festago.school.repository.v1; | ||
|
||
import static com.festago.festival.domain.QFestival.festival; | ||
import static com.festago.festival.domain.QFestivalQueryInfo.festivalQueryInfo; | ||
|
||
import com.festago.common.querydsl.QueryDslHelper; | ||
import com.festago.school.dto.v1.QSchoolFestivalV1Response; | ||
import com.festago.school.dto.v1.SchoolFestivalV1Response; | ||
import java.time.LocalDate; | ||
import java.util.Comparator; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Repository; | ||
|
||
@Repository | ||
@RequiredArgsConstructor | ||
public class SchoolFestivalsV1QueryDslRepository { | ||
|
||
private final QueryDslHelper queryDslHelper; | ||
|
||
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(); | ||
} | ||
Comment on lines
+21
to
+41
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. DB를 통해 정렬을 수행하지 않고, 어플리케이션 레벨에서 정렬을 수행하도록 했습니다. 해당 인덱스가 다른 쿼리에서 사용중이라, 제거가 힘들겠네요 😂 |
||
|
||
public List<SchoolFestivalV1Response> findPastFestivalsBySchoolId( | ||
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.lt(today))) | ||
.stream() | ||
.sorted(Comparator.comparing(SchoolFestivalV1Response::endDate).reversed()) | ||
.toList(); | ||
} | ||
} |
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.
캐시 초기화를 담당하는 컴포넌트 입니다.
축제 캐싱이 30분 동안 유지되므로, 그 사이 새로운 축제가 추가되면 사용자가 축제 정보를 확인할 수 없는 문제가 생깁니다.
축제는 관리자가 추가하기에 큰 문제는 아니지만, 가끔 즉시 초기화가 필요한 시점이 있을 수 있기에 별도의 컴포넌트로 분리했습니다.
만약, 수동으로 초기화가 필요하다면 관리자 API를 열어서 직접 초기화하면 될 것 같습니다.