본문 바로가기
Spring/DB

Ehcache 를 사용한 Cache 이용해보기

by YoonJong 2023. 1. 2.
728x90

쇼핑몰 개인프로젝트를 만드는 중, 캐시에 대한 궁금증이 생겨 적용을 해보고 싶은 계획이 있었다.

대부분 캐시를 사용하면 redis 를 사용하는 것을 봐와서 나도 redis를 도입해야 하나? 라는 생각을 했다.

Ehcache 를 적용한 이유는 별도의 서버를 사용해서 생길 수 있는 네트워크 지연이나 단절과 같은 이슈가 없이 사용이 가능하며 ( Spring 내부적으로 동작 ) 서버 애플리케이션과 라이프사이클을 같이 하므로 사용하기 쉽기 때문에 선택했다.

 

도중에, "개발바닥" 유튜브에서 포털사이트의 뉴스 등에 EhCache 를 많이 사용한다는 것을 듣고 어떤 캐시방식인지 궁금해 찾아보게 되었다.

 

c2c쇼핑몰이라고 하면,  상품을 클릭하면 나오는 상세페이지에 사용할 수 있지 않을까 생각했다.

이유는, 상품 사진이 많고 상세내용이 길 수록 불러오는 시간이 많을거라 생각했고, 한번 등록한 상세페이지는 보통 많이 변경하지 않는다고 생각했다.

 

일단 먼저 어떤건지 확인하고 적용해보자!


먼저 캐시란, 동일한 요청이 들어오면 복잡한 작업을 수행해서 결과를 만드는 대신, 이미 보관된 결과를 바로 보여주는 방식이다.

 

캐시를 고려해볼 조건은 아래와 같다.

1. 반복적으로 동일한 결과를 돌려주는 작업

2. 각 작업의 시간이 오래 걸리거나 서버에 부담을 주는 경우

 

 

캐시의 동작은 아래와 같다.

1. 데이터를 요청

2. 캐시에 있는지 확인

3. 캐시에 있다면 캐시에서 데이털르 가져오고 없으면 실제 저장 공간에서 데이터를 획득

4. 실제 저장 공간에서 데이터를 가지고 왔다면 캐시에 저장

 

cache hit : 요청한 데이터가 캐시에 존재할 때

cache miss : 요청한 데이터가 캐시에 존재하지 않을 때


캐시의 종류

많이 사용하는 캐시는 Memcached , Reids , Ehcache 가 있다.

 

특징 

Memcached 와 Redis 는 오픈소스이다.공통점으로는 1ms 이하의 짧은 응답대기시간, 개발의 용이성, 다양한 프로그래밍 언어 등이 있다.

 

Memcached 의 특징

- 멀티스레드를 지원하기 때문에 멀티프로세스코어를 사용할 수 있다.

 

Redis 의 특징

- 다양한 데이터 구조를 가진다 ex) sorted set 을 이용한 상위 랭크 정보 가져오기 등- 특정 시점에 데이터를 디스크에 저장할 수 있다. - 복구에 사용가능- 복제가 가능하다.- 위치기반 데이터 타입 지원 - 맛집, 길찾기 등 사용가능

 

Redis 의 특징을 보면 어떤 상황에서든 사용해야할 것만 같다. ( 대부분의 프로젝트에서도 그런것 같았다 .)하지만, 싱글스레드이기 때문에 1번에 1개의 명령어만 실행 가능하며, RDB 작업 시 속도가 매우 오래 걸린다는 단점이 있다.AWS 60기가 메모리 기준으로 10분 소요된다고 한다.서버의 이슈가 생기면 Redis 를 사용할 수 없다.

 

Ehcahe 의 특징

- java 기반의 오픈소스 캐시 라이브러리- spring 내부적으로 동작해서 캐싱 처리- 서버의 이슈가 발생하더라도 이상없이 작동이 가능하다.


설정 및 사용방법

1. gradle 에 의존성을 추가해준다.

// 캐시 설정 (ehcache)
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'net.sf.ehcache:ehcache:2.10.3'

2. config 파일을 생성해준다. ( 대부분 블로그에서 xml 파일로 되어있는데, config 파일로 생성했다 )

아래 코드에서   timeToIdleSeconds : 10 와  timeToLiveSeconds : 10 으로 테스트할 겸 설정(10초 뒤에 캐시삭제)해놓았다.

유지 시간은 캐시를 어디다가 사용할건지에 따라 많은 차이가 있을거라 생각했다.

@EnableCaching
@Configuration
public class CacheConfig {
    private net.sf.ehcache.CacheManager createCacheManager() {
        net.sf.ehcache.config.Configuration configuration = new net.sf.ehcache.config.Configuration();
        configuration.diskStore(new DiskStoreConfiguration().path("java.io.tmpdir"));
        return net.sf.ehcache.CacheManager.create(configuration);
    }

    @Bean
    public EhCacheCacheManager ehCacheCacheManager() {

        net.sf.ehcache.CacheManager manager = this.createCacheManager();

   /*
     name : 코드에서 사용할 캐시 name
     maxEntriesLocalHeap : 메모리에 생성 될 최대 캐시 갯수
     maxEntriesLocalDisk : 디스크에 생성 될 최대 캐시 갯수
     eternal : 영속성 캐시 설정(지워지는 캐시인지?) true 이면 timeToldleSecond, timeToLiveSeconds 설정이 무시
     diskSpoolBufferSizeMB :스풀버퍼에 대한 디스크(DiskStore) 크기 설정한다.
     timeToIdleSeconds : 해당 초 동안 캐시가 호출 되지 않으면 삭제
     timeToLiveSeconds : 해당 초가 지나면 캐시가 삭제
     memoryStoreEvictionPolicy : 캐시의 객체 수가 maxEntriesLocalHeap 에 도달하면 객체를 추가하고 제거하는 정책 설정
     LRU : 가장 오랫동안 호출 되지 않은 캐시를 삭제, LFU : 호출 빈도가 가장 적은 캐시를 삭제 FIFO : 캐시가 생성된 순서대로 가장 오래된 캐시를 삭제
     transactionalMode : 트랜잭션 모드 설정
   */

        Cache getMenuCache = new Cache(new CacheConfiguration()
                .maxEntriesLocalHeap(10000)
                .maxEntriesLocalDisk(1000)
                .eternal(false)
                .timeToIdleSeconds(10)
                .timeToLiveSeconds(10)
                .memoryStoreEvictionPolicy("LFU")
                .transactionalMode(CacheConfiguration.TransactionalMode.OFF)
                .persistence(new PersistenceConfiguration().strategy(PersistenceConfiguration.Strategy.LOCALTEMPSWAP))
                .name("goodsFind")
        );
        manager.addCache(getMenuCache);

        return new EhCacheCacheManager(manager);
    }
}

3. Service 에 적용한다.

상품 상세조회에 캐시를 적용해보려고 한다.

 2초간 딜레이를 주기 위해 slowQuery 메서드를 생성하고, 캐시 미사용 / 캐시 사용 하는 서비스에 각각 적용했다.

@Override
@Transactional(readOnly = true)
public GoodsResponse goodsDetailFindNoCache(Long goodsId) {
    Goods goods = goodsRepository.findById(goodsId).orElseThrow(
            () -> new BusinessException(ErrorCode.NOT_FOUND_GOODS));
    slowQuery(2000);
    return GoodsResponse.toResponse(goods);
}


@Override
@Transactional(readOnly = true)
@Cacheable(value = "goodsFind", key = "#goodsId")
public GoodsResponse goodsDetailFindCache(Long goodsId) {
    Goods goods = goodsRepository.findById(goodsId).orElseThrow(
            () -> new BusinessException(ErrorCode.NOT_FOUND_GOODS));
    slowQuery(2000);
    return GoodsResponse.toResponse(goods);
}

private void slowQuery(long seconds) {
    try {
        Thread.sleep(seconds);
    } catch (InterruptedException e) {
        throw new IllegalStateException(e);
    }
}

4. Controller 를 생성한다.

수행시간의 차이를 보기 위해 로그를 찍어본다.

@GetMapping("/goodsNoCache/{goodsId}")
@ResponseStatus(HttpStatus.OK)
@ApiOperation(value = "상품 단품 상세 조회 - 캐시 미사용")
public GoodsResponse goodsDetailFind(@PathVariable("goodsId") Long goodsId) {
    long start = System.currentTimeMillis(); // 수행시간 측정
    GoodsResponse goodsResponse = goodsService.goodsDetailFindNoCache(goodsId);
    long end = System.currentTimeMillis();
    log.info(goodsId+ " NoCache수행시간 : "+ (end - start)); // 수행시간 logging

    return goodsResponse;
}

@GetMapping("/goodsCache/{goodsId}")
@ResponseStatus(HttpStatus.OK)
@ApiOperation(value = "상품 단품 상세 조회 - 캐시사용")
public GoodsResponse goodsDetailFindCache(@PathVariable("goodsId") Long goodsId) {
    long start = System.currentTimeMillis(); // 수행시간 측정
    GoodsResponse goodsResponse = goodsService.goodsDetailFindCache(goodsId);
    long end = System.currentTimeMillis();
    log.info(goodsId+ " Cache수행시간 : "+ (end - start)); // 수행시간 logging

    return goodsResponse;
}

 

POSTMAN 으로 테스트를 진행해보자.

총 3번을 조회 했으며, 먼저 캐시를 사용하지 않았을 때이다. 각각 조회할 때마다 2초의 시간이 걸리는 것을 볼 수 있다.

캐시를 사용하는 메서드를 3번 실행해보면 처음에는 2초가 걸리지만 10초 이내로 재조회 했을 경우 즉시 조회되는 것을 볼 수 있다. 

10 초로 설정했기 때문에 10초 이후에는 캐시가 삭제되며, 다시 2초 딜레이 후 캐시에 저장된다

 

728x90

댓글