본문 바로가기
반응형
Spring/MVC

[MVC] 웹 페이지 성능 최적화: EhCache를 활용한 데이터 캐싱 구현

by brightGarden02 2024. 5. 31.

웹 페이지를 처음 로드할 때 필요한 데이터는 매번 Contoller -> Service -> Repository를 통해 DB 조회를 하지 않고 캐싱을 통해 성능을 향상시킬 수 있습니다.

 

캐싱을 구현하는 방법 중 하나로 Interceptor를 사용하여 요청을 가로채고, 캐시된 데이터를 제공할 수 있습니다.

 

캐싱 라이브러리는 많이 사용되는 EhCache를 이용하여 메뉴 데이터를 캐싱하겠습니다.

 

앞으로 사용할 클래스 다이어그램입니다.

 

 

 

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false">
    <diskStore path="java.io.tmpdir" />

    <cache name="findAll"
           maxEntriesLocalHeap="10000"
           maxEntriesLocalDisk="1000"
           eternal="false"
           diskSpoolBufferSizeMB="20"
           timeToIdleSeconds="300" timeToLiveSeconds="600"
           memoryStoreEvictionPolicy="LFU"
           transactionalMode="off">
        <persistence strategy="localTempSwap" />
    </cache>

</ehcache>

ehcache.xml 세팅 정보입니다. 캐시의 이름, 메모리 및 디스크에 대한 구성, 캐시의 수명 및 유효시간 등을 설정할 수 있습니다.

 

 

 

spring:
  cache:
    ehcache:
      config: classpath:ehcache.xml

yml파일에 ehcache.xml 파일 경로를 기입합니다.

 

 

 

import com.example.core_license.interceptor.MainInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class WebMvConfig implements WebMvcConfigurer {

    private final MainInterceptor mainInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(mainInterceptor);
    }
}

Interceptor를 웹 어플리케이션에 적용하기 위해, WebMvcConfigurer를 구현한 WebMvcConfig 클래스에서 addInterceptors 메서드를 오버라이드하여 MainInterceptor를 등록합니다. 

 

 

 

import com.example.core_license.dto.response.MenuResponseDto;
import com.example.core_license.service.MenuService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Component
@RequiredArgsConstructor
public class MainInterceptor implements HandlerInterceptor {

    private final MenuService menuService;

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if(modelAndView != null) {
            List<MenuResponseDto> menuResponseDtoList = menuService.findAll();
            modelAndView.addObject("menuList", menuResponseDtoList);
        }
    }
}

MainInterceptor에서 menuService.findAll()을 통해 Menu를 List로 가져오고 view에 등록합니다.

 

 

 

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MenuService {

    private final MenuRepository menuRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Cacheable(value="findAll")
    public List<MenuResponseDto> findAll() {
        log.info("Cache data");
        return menuRepository.findAll();
}

MenuService 클래스에서 findAll 메서드를 @Cacheable 어노테이션을 사용하여 캐시된 데이터를 반환하고 있습니다.

캐시된 데이터가 없다면 캐시에 저장해줍니다.

이를 통해 메뉴 데이터를 처음 한번만 조회하고 이후 캐시된 데이터를 사용합니다.

 

 

 

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MenuService {
    ...
    
    @Transactional
    public void create(CreateMenuRequestDto createMenuRequestDto) {
        Menu menu = new Menu(createMenuRequestDto.getMenuNm(), createMenuRequestDto.getUrl());
        menuRepository.save(menu);
        eventPublisher.publishEvent(new CacheReloadEvent(this));
    }

    @Transactional
    public void update(UpdateMenuRequestDto updateMenuRequestDto) {
        menuRepository.findById(Integer.parseInt(updateMenuRequestDto.getId()))
                        .orElseThrow(() -> new IllegalArgumentException("menu" + updateMenuRequestDto.getId() + " not found."));
        menuRepository.update(updateMenuRequestDto);
        eventPublisher.publishEvent(new CacheReloadEvent(this));
    }
    
    ...
}

새로운 Menu 객체가 등록되거나 기존 Menu 객체가 수정되면, eventPublisher를 통해 CacheReloadEvent가 발생합니다. 이를 통해 MenuService에서 변화가 있을 시 캐시를 지우는 이벤트가 동작하게 됩니다.

 

 

 

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MenuService {

    ...

    @CacheEvict(value = "findAll", allEntries = true)
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleCacheReloadEvent(CacheReloadEvent event) {
        log.info("Evicting cache");
    }
}

위의 코드에서 eventPublisher가 CacheReloadEvent를 발생시키고, 커밋이 된 이후 @CacheEvict 어노테이션이 붙은 handleCacheReloadEvent 메서드를 통해 기존 findAll 캐시를 지웁니다.

 

 

 

메뉴가 있는 화면을 로드할 때마다 MainInterceptor가 동작하며, 캐시된 메뉴 데이터가 없을 경우 MenuService.findAll()을 통해 새로운 메뉴 데이터를 캐시에 저장합니다. 이를 통해 메뉴 데이터가 최신 상태로 업데이트 됩니다.

댓글


반응형
반응형