- 백엔드 수정 사항
- region 캐시
- species 캐시
- dog/cat List 말고 다른 방식 구현
- QueryRepository = Service 역할 분리
region + sepecies 합쳐도 50개도 안되기 때문에 Spring Cache를 사용하려고 한다.
대용량이거나 이벤트성이었으면 Redis와 Kafka를 고려했을텐데, 전혀 아니니..
다음엔 다른 성향의 프로젝트로 위 캐시를 써봐야겠다.
캐시
@EnableCaching // 추가
@SpringBootApplication
public class PetstatsApplication {
public static void main(String[] args) {
SpringApplication.run(PetstatsApplication.class, args);
}
}
@EnableCaching 어노테이션 추가만하면 캐시를 사용할 수 있다.
@Slf4j
@RequiredArgsConstructor
@Service
public class FilterCacheService {
private final RegionRepository regionRepository;
private final SpeciesRepository speciesRepository;
private final Long CACHE_EXPIRATION = (long) (60 * 60 * 24);
@Cacheable(value = "regions")
public List<Region> getRegions() {
log.info("[Cacheable] regions");
return regionRepository.findAll();
}
@Scheduled(fixedDelay = CACHE_EXPIRATION)
@CacheEvict(value = "regions", allEntries = true)
public void evictRegions() {
log.info("[CacheEvict] regions");
}
...
..
필터에서는 단순 조회만 사용하기 때문에 QueryDSL 대신 JpaRepository 를 사용했다.
각 요소마다 CacheService 를 만들면 쓸데없이 파일만 많아지기 때문에
FilterCacheService 에 모아서 한번에 처리할거다.
캐시는 실행일로부터 24시간 뒤 초기화하기로 했다.
만약 추후 API 호출해서 가져온다면, 스케줄이 아닌 그때 초기화하는 방식으로 가도 될 것 같다.
@RequiredArgsConstructor
@Service
public class FilterService {
private final FilterCacheService filterCacheService;
public List<RegionResponse> getRegions(Integer regionId) {
List<Region> regions = filterCacheService.getRegions();
return regions.stream()
.map(region -> new RegionResponse(
region.getId(),
region.getProvince(),
Objects.equals(region.getId(), regionId)
))
.toList();
}
}
그리고 기존에 Controller에서 데이터 가공을 했는데,
Controller는 데이터 전달하고 반환하는 역할만 하기 위해, Service 로 옮겼다.
여기서 selected 조건이 없었다면, 가공하는 과정이 들어간 CacheService를 만드는게 더 효율적이다.
자세한 코드는 [여기]로
Dog/Cat 리스트 표현 변경
public List<Map<String, Object>> getTopAnimalTypes(RegionTopAnimalTypeRequest request) {
List<Map<String, Object>> response = new ArrayList<>();
int count = 0;
for (Species species : filterService.getSpecies()) {
int finalCount = count;
response.add(new HashMap<>() {{
put("index", finalCount);
put("name", species.getName());
put("data", animalStatsQueryRepository.getTopBreedsBySpecies(request.getRegionId(), species.getId()));
}});
count++;
}
return response;
}
기존에는 강제로 1, 2 숫자로 데이터를 보냈는데, Species 를 가져와서 자동으로 들어가도록 수정했다.
반환 데이터도 바꼈는데, List<Response> 에서 List<Map<String, Object>> 로 변경했다.
처음엔 Map 의 key를 species.getName()으로 했다.
하다보니 프론트에 index라는 요소가 필요했는데,
머스테치에는 인덱스를 알 수 있는 방법이 없어서 서버에서 넘겨줘야 했기 때문에 변경했다.
window.addEventListener('load', () => {
{{#dataList}}
new Chart(document.getElementById('chart-{{index}}'), {
type: 'bar',
data: {
labels: [{{#data}}"{{type}}"{{^last}},{{/last}}{{/data}}],
datasets: [{
label: '{{name}} 품종',
data: [{{#data}}{{count}}{{^last}},{{/last}}{{/data}}],
backgroundColor: colorList[{{index}}]
}]
},
options: getOptions('{{name}}'),
plugins: [ChartDataLabels]
});
{{/dataList}}
});
window.addEventListener('DOMContentLoaded', () => {
const firstTab = document.querySelector('.tab-content');
if (firstTab) {
firstTab.style.display = 'block';
}
const firstButton = document.querySelector('.tab-button');
if (firstButton) {
firstButton.classList.add('active');
}
});
저렇게 구조를 바꿔서 프론트에서도 '강아지', '고양이' 이렇게 지정하지 않고도
동적으로 들어갈 수 있게 되었다.
여기서 문제는 최초에 모든 요소를 display: none을 하게 되는데,
머스테치는 첫 번째 요소를 인지할 수가 없어서 자바스크립트로 첫번째 요소일 경우 block, active 처리하도록 해야했다.
그게 아니면 서버에서 첫번째 요소에 대한 데이터를 넘겨야하는데,
그 하나의 데이터를 늘리는 것보단 자바스크립트로 처리하는게 효율적이라고 생각해서 저렇게했다.
하지만... 고민해보니 렌더링 후에 DOM 을 조작하는거다보니까 머스테치를 이용하는 의미가 없어보인다.
저 부분은 좀 더 고민해보기로...
관련된 전체 수정 코드는 [여기]로

머스테치는 단순 템플릿에 데이터바인딩만 하기 때문에 렌더링 속도가 빠르다.
하지만 그만큼 기능들이 없어서 서버에서 데이터를 일일이 가공해서 넘겨줘야한다는 단점이 보인다.
리액트를 썼다면 JSON만 넘기고 프론트에서 렌더링 로직을 다 처리했겠지만
머스테치는 백엔드에서 프론트까지 고려해야하니 점점 코드가 복잡해지는게 보인다.
프론트는 분리하는게 최고인 것 같다..!
'프로젝트 > 반려동물' 카테고리의 다른 글
| [반려동물 프로젝트] 마무리 (1) | 2025.04.26 |
|---|---|
| [반려동물 프로젝트] 프론트 구현 (Mustache) (0) | 2025.04.25 |
| [반려동물 프로젝트] 백엔드 구현 (QueryDSL) (3) | 2025.04.24 |
| [반려동물 프로젝트] DB 구성 (JPA, Python) (0) | 2025.04.24 |
| [반려동물 프로젝트] 데이터 활용 고민 (0) | 2025.04.24 |