설모의 기록
[Spring] Ehcache 캐시 사용 본문
캐시 적용을 왜 해야 하는거지?
컴퓨터 전공자라면, 컴퓨터 관련 과목을 수강해 본 적이 있으시다면 '캐시(Cache)' 라는 말을 들어보신 적이 있을 것입니다.
캐시라고 구글링 해보시면 많은 자료가 나올텐데요. 결국 캐시는 성능을 개선하기 위해 적용합니다.
웹 사이트를 개발하는 데에 있어서 적용할 수 있는 캐시는 브라우저 캐시와 서버단에서의 캐시가 있을 것입니다.
브라우저 캐시는 이미 받아왔던 자원들은 캐시에 저장해둔 후 일정 기간동안 같은 리소스 요청은 캐시에 있는 내용을 반환하는 것입니다. 그러면 서버와의 통신에 따른 비용을 줄일 수 있습니다.
서버에서의 캐시는 DB를 조회하는 비용을 줄이기 위해서 많이 사용할 것입니다. 쇼핑몰을 예로 들면, 브라우저를 새로 로드할 때마다 상품 리스트를 보내줘야 하는 경우가 있을 것입니다. 만약 상품이 1억개가 있다면, 상품 리스트를 조회하는 것만으로도 비용이 어마어마 하겠죠? 이럴 때 서버단에 캐시를 두어 일정 기간동안에 들어오는 요청에 한해서는 캐시에 있는 내용을 보내주는 것이죠. 그러면 DB에서 조회하는 것에 따른 비용을 줄일 수 있습니다.
이렇게 성능을 개선하기 위해 적용하는 것이 캐시인데요. Spring 에서는 JCache, Redis, Ehcache 등 여러 캐시 종류가 있습니다. 그 중에서도 경량의 빠른 캐시엔진인 Ehcache 에 대해 알아보겠습니다.
Ehcache
설명을 시작하기 전에, 앞으로 설명하는 코드에 대한 개발환경은 SpringBoot, gradle, LogBack 환경이며 개발툴은 Intellij Ultimate 를 사용합니다.
먼저 Ehcache 에 대한 gradle 을 추가하겠습니다.
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-cache')
compile('org.projectlombok:lombok')
runtime('net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1')
testCompile('org.springframework.boot:spring-boot-starter-test')
compile group: 'net.sf.ehcache', name: 'ehcache', version: '2.10.3'
}
compile('org.springframework.boot:spring-boot-starter-cache') 와 compile group: 'net.sf.ehcache', name: 'ehcache', version: '2.10.3' 을 추가하시면 됩니다.
spring-boot-starter-cache 를 추가하면 기본적으로 기본 설정인 SimpleCacheConfiguration 이 동작하며 CacheManager 로는 ConcurrentMapCacheManager 가 Bean 으로 등록됩니다.
다음으로 해야할 일은 ehcache.xml 파일을 생성하는 것입니다. ehcache.xml 의 기본 형태는 아래와 같습니다.
코드를 보시면 <defaultCache> 태그와<cache> 태그가 있습니다. <defaultCache> 는 캐시를 직접 생성할 때 캐시의 기본 설정이 저장되기 때문에 필수적으로 필요합니다. 만약 파일에 모든 캐시에 대한 정보를 저장하는 경우에는 에러가 나지 않지만, 코드에서 캐시를 직접 생성하는 경우에는 defaultCache 태그가 없다면 에러가 발생할 것입니다.
<cache> 태그는 각 캐시마다 필요한 태그입니다. 예시에 있는 캐시는 프로모션 데이터를 위한 캐시입니다. 캐시를 의미있게 분류해 관리하는 것 또한 중요합니다.
이제 설정 옵션에 대해 하나하나 알아보겠습니다.
ecache.xml 설정 옵션
<diskStore path="java.io.tmpdir"/> 임시저장경로를 지정하는 설정입니다.
maxEntriesLocalHeap="10000" 메모리에 생성될 최대 객체 갯수입니다.
maxEntriesLocalDisk="10000" 디스크에 생성될 최대 객체 갯수입니다.
eternal="false" 저장될 캐시를 삭제할 것인가에 대한 설정입니다. true 로 설정하면 아래의 timeToIdleSeconds, timeToLiveSeconds 설정이 무시됩니다.
timeToIdleSeconds="120" 해당 초 동안 캐시가 사용되지 않으면 캐시를 삭제합니다.
timeToLiveSeconds="120" 해당 초가 지나면 캐시를 삭제합니다.
diskExpiryThreadIntervalSeconds="120" Disk Expiry 스레드의 작업 수행 간격을 설정합니다.
memoryStoreEvictionPolicy="LRU" 객체 갯수가 maxEntriesLocalHeap 에 도달하면 객체를 제거하고 새로 추가하는 정책을 정의합니다. 종류로는 LRU, FIFO, LFU 가 있습니다.
다음으로 코드에 캐시를 적용하는 방법에 대해 알아보겠습니다.
Config 파일에 CacheManager 를 Bean 으로 등록
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Slf4j
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Bean
public CacheManager cacheManager() {
return new EhCacheCacheManager(ehCacheCacheManager().getObject());
}
@Bean
public EhCacheManagerFactoryBean ehCacheCacheManager() {
EhCacheManagerFactoryBean factory = new EhCacheManagerFactoryBean();
factory.setConfigLocation(new ClassPathResource("ehcache.xml"));
factory.setShared(true);
return factory;
}
}
위와 같이 CacheManager 를 Bean 으로 등록하고 EhCacheManagerFactoryBean 또한 등록한 후 new ClassPathResource() 부분에 위에서 생성한 ehcache.xml 파일 이름을 등록합니다.
캐시적용
제 코드에서는 서비스와 컨트롤러가 존재합니다. 프로모션 데이터를 DB로부터 불러오는 코드에 캐시를 적용해보겠습니다.
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@EnableCaching
@RequestMapping("/api/promotions")
public class ApiPromotionController {
@Autowired
private PromotionService promotionService;
@GetMapping("")
public Iterable<Promotion> list() {
return promotionService.gets();
}
@PutMapping("/{id}")
public ResponseEntity<Promotion> update(@PathVariable Long id, @RequestBody PromotionDto updatedPromotionDto) {
return ResponseEntity.status(HttpStatus.OK).body(promotionService.update(id, updatedPromotionDto));
}
}
우선 위의 코드는 프로모션에 대한 컨트롤러입니다.
@EnableCaching 은 캐시 기능을 사용하겠다는 선언입니다. list, update 메소드를 보시면 promotionService 의 코드를 수행합니다. 저는 서비스단에 캐시 설정을 했습니다. 아래의 코드를 보겠습니다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@Service
public class PromotionService {
@Autowired
private PromotionRepository promotionRepository;
@Cacheable(value = "promotionsCache")
public Iterable<Promotion> gets() {
return promotionRepository.findAll();
}
@Transactional
@CacheEvict(value = "promotionsCache", allEntries = true)public Promotion update(Long id, PromotionDto updatedPromotionDto) {
Promotion savedPromotion = getPromotionById(id);
return savedPromotion.update(updatedPromotionDto);
}
private Promotion getPromotionById(Long id) {
return promotionRepository.findById(id).orElseThrow(NotMatchedException::new);
}
}
위의 코드는 Service 코드입니다.
DB에서 프로모션 데이터를 불러오는 메소드인 list 에서는 @Cacheable(value = "promotionsCache") 어노테이션을 붙입니다. @Cacheable 은 해당 캐시를 사용하겠다는 어노테이션으로 value 값으로는 ehcache.xml 에서 설정한 캐시 이름을 설정합니다.
프로모션 데이터를 업데이트하는 update 메소드에는 @CacheEvict(value = "promotionsCache") 어노테이션을 붙입니다. @CacheEvict 는 해당 캐시를 지우겠다는 어노테이션으로 value 값으로는 @Cacheable 과 같이 캐시 이름을 설정하며 allEntries 를 true 로 해주어야 캐시내의 모든 내용이 갱신됩니다. 그렇지 않으면 key="#id" 를 붙여 해당 id 에 해당하는 캐시 내용만 갱신되도록 설정해야 합니다.
이런식으로 캐시를 설정하면 잘 변하지 않는 데이터를 굳이 DB단까지 가서 데이터를 가져올 필요없이 서버에서 캐시를 통해 데이터를 반환하는 것입니다.
캐시를 잘만 적용하면 서비스의 성능을 훨씬 개선할 수 있으니 적절하게 사용하실 수 있길 바랍니다.
참고 사이트
'언어 > Spring' 카테고리의 다른 글
[Spring] 상속 관계 매핑 (4) | 2018.10.05 |
---|---|
[Spring] WebSocket 구현하기 (5) | 2018.08.26 |
[Spring] 다대일, 일대다 연관관계 매핑 (1) | 2018.08.17 |
[Spring] 영속성이란 (persistence) (11) | 2018.08.12 |
[JUnit] 단위테스트를 위한 JUnit Framework (0) | 2018.07.04 |