Tiny Star

프로젝트/반려동물

[반려동물 프로젝트] 백엔드 수정 (SpringCache)

하얀지 2025. 4. 25. 23:28
  • 백엔드 수정 사항
    1. region 캐시
    2. species 캐시
    3. dog/cat List 말고 다른 방식 구현
    4. 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만 넘기고 프론트에서 렌더링 로직을 다 처리했겠지만

머스테치는 백엔드에서 프론트까지 고려해야하니 점점 코드가 복잡해지는게 보인다.

 

프론트는 분리하는게 최고인 것 같다..!

top