캐싱은 웹기술뿐만 아니라, 디스크/메모리가 동시에 탑재된 여러가지 컴퓨터장치에서

성능을 위해 활용되는 기술입니다. 디스크는 상대적으로 속도가 느리기 때문에

반복적으로 발생하는 변경없는 데이터의 Read에대해 , 메모리가 일정시간 저장하고 있다가

디스크또는 네트워크에 부하를 주지않고 빨리 반환하는 기술이라고 볼수 있으며 응답이 중요한 웹에서도 중요한 요소입니다.


캐시 기초개념



일반적 인 캐시처리 방식

Write-through with no-write-allocationWrite-back with write-allocation

참고 : https://en.wikipedia.org/wiki/Cache_(computing)


캐시기능자체는 복잡해보이지만, 모듈을 통한  캐시기능 사용자체는 간단합니다. 

더욱이 사용자는 이것이 캐시처리가 된것인지 아닌지? 모르게 은닉을 하는게 목적입니다.

사용자가 뭔가 정보갱신이 안되고 있다라고 의심을 한다고하면 캐싱기능을 검토해야할것입니다.

웹응답을 높이기위한 CACHE종류

  • 브라우져 레벨의 캐시기능 ( 동일 요청에 대해 일정시간 이내 요청없이 스토어된 데이터로 처리 )
  • Proxy,장비 캐시기능(Proxy,라우터등에서 지정된 시간이내 동일 요청에 대해 메모리 반환처리)
  • 웹 서비스(서버) 레벨에서의 캐시구현 ( 직접 구현)

여기서는 좀더 커스텀하고/디테일한 캐싱처리가 가능한 웹서비스에서 캐싱처리를

직접 구현하는 방법을 살펴보겠습니다.


웹서버 캐시처리방법

  • 자신의 노드에서만 메모리 캐시처리
  • Redis등 메모리DB 사용하여, 전체 분산노드에 캐시기능 확장

간단한 캐시기능 적용(자신의노드에서만)



활용할 캐시 라이브러리

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>


사용스텝1. 

Application에 @EnableCaching 지정
@SpringBootApplication 
@EnableCaching 
public class Application { 
......,


사용스텝2.

package com.example.demo.data2;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

@Component
public class SimpleUserRepository {	
	@Cacheable("books")
	public String getByIsbn(String isbn) throws Exception {
	 long time = 3000L; 
	 Thread.sleep(time);
	 return "Some String-" +isbn;
	}
}

단일노드에서에 자체 캐시처리는 아주단순합니다.  

단지 캐시처리할 대상을 Cacheble 어노테이션으로 묶어주고, 네이밍을 하면됩니다. ( 네이밍에따라 다른 캐시정책 설정가능)

getByIsbn 함수는 기본적으로 IO를 발생하는 느린 함수다라고 가정했을시(3초 인위적 딜레이발생)


인자값에따라 아래와같이 시간이 소모되게 됩니다.

  • getByIsbn('1') - 3초걸림

  • getByIsbn('1') - 빠르게수행
  • getByIsbn('1') - 빠르게수행
  • getByIsbn('2') - 3초걸림
  • getByIsbn('2') - 빠르게수행
  • getByIsbn('1') - 빠르게수행

차이점은, 최초검색 옵션일때만 느리고  동일한 검색옵션이 재사용되는경우

이미 캐싱되어 반환할 값을 가지고 있기때문에 빠르게 메모리 반환을 하는것입니다.


여기서는 기본캐싱옵션을 사용하였지만, 캐싱 옵셔에따라 다양한 캐시전략을 수행할수 있습니다.

  • 동일요청 캐싱시간을 최대 몇초유지할지?
  • 전체 캐시데이터를 몇개까지 최대 보관할지?
  • 데이터 변경시 캐싱데이터를 무효화 시킬지?
  • 캐싱에 관여하는 인자값 키처리를 별도로 지정할지?


더 복잡한 캐시처리

SQL 하나를 호출하기 위한 인자값이 7개정도가 되며 , RESTAPI를 통해 서비스를 한다고

가정해봅시다. 다양한 변수가 생길수 있는 상황에서

DB를 덜 호출하기 위해서 기본캐시기능에서 몇가지 요소가 필요합니다.

  • 여러개의 인자값 조합을 캐시처리기능이 구분 처리 해야합니다.
  • 휘발을 시키는 시간조건 설정이 가능해야합니다. 예를들면 하루동안 변경되지 않는 데이터이면 하루동안 유지할수 있습니다.
  • 캐시타임동안 쌓이게될 데이터 총량을 예측을 할수 없기때문에, 최대 개수설정이 가능해야합니다. (메모리풀방지)

Caffeine 캐시준비

# application.property
spring.cache.cache-names: instruments, directory
spring.cache.caffeine.spec: maximumSize=1000, expireAfterAccess=10s   // 초(s) 분(m) 일(d)
logging.level.com.memorynotfound=info

# pom.xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- caching provider -->
<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
</dependency>

JPA Repository와 연동이되어 위 요구사항을 충족 시킬수 있는 캐시모듈입니다.

instruments, directory 두개의 캐시공간을 사용예정이란것을 정의한것이며

10초동안 각각 다른 데이터 종류인 1000개의 캐시를 유지하겠다란 설정입니다.

이 설정으로, 10초동안 동일요청에대해 빠른 응답처리가 가능해집니다.  


검색인자값정의

public class InputOpt {
    .............................생략
	private int ecode;
	public int getEcode() {
		return hashCode();
	}
	@Override
    public int hashCode() {
		HashCodeBuilder builder = new HashCodeBuilder();
		builder.append(category1);
		builder.append(router);
		builder.append(action);
		builder.append(sort);
		builder.append(sortdir);
		builder.append(size);
		builder.append(userprofile);
        return builder.toHashCode();
	}
}

검색 인자값의 다양성에따라 각각 고유하다란것을 해시코드재정의를 통해 활용할수 있습니다.

HashCodeBuilder를 통해 간결하게 작성이 가능합니다.  ecode는 단지 유니크하다라는 식별값이며

이 식별자는 캐시를 할지 말지 판단하는 중요한 판별기능에 요소로 사용이됩니다.


캐시 서비스 정의

//인터페이스 정의
public interface GoodsDataService {	
	Iterable<GoodsDataRC> findAll( BooleanBuilder searchOpt , Pageable pageRequest , InputOpt inputopt );	
}

----- 파일분리
//구 현
@Service
@CacheConfig(cacheNames = {"directory", "instruments"})
public class GoodsDataServiceImpl implements GoodsDataService{
	
	static final Logger logger = LoggerFactory.getLogger(GoodsDataService.class);	
	@Autowired
	private GoodsDataRCRepo	goodsDataRCRepo; // 이것은 읽기전용의 JPA Repository
	
    @Cacheable( value="instruments", key="#inputopt.ecode" )
    public Iterable<GoodsDataRC> findAll( BooleanBuilder searchOpt , Pageable pageRequest , InputOpt inputopt ) {    	
    	logger.info("hash == %d",inputopt.hashCode() );   	
        return goodsDataRCRepo.findAll(searchOpt,pageRequest);
    }
}

DB를 탐색하는 findAll에 캐시기능을 부여하였습니다. 우리가 지정한 instruments 이름의 캐시공간에

우리가 정의한 inputopt.ecode 를통해, 중복처리에대해 캐시 처리가 될것입니다.  

캐시 DB사용하기

	@Autowired
	GoodsDataService service;		//for cache db
    .......................
	Iterable<GoodsDataRC> dbResult = service.findAll(searchOpt,pageRequest,inputOpt);

캐시를 사용하기위해 준비해야할게 몇가지 있었지만, 사용하는 코드쪽에서 

기존 JPA함수를 사용하여 DB를 직접호출하는 코드와 크게 다를것이 없어보입니다.

단지  Api 옵션을 판별하기 위해 우리가 정의한 inputOpt를 다시 전달했을뿐입니다.

이것을 어떻게 확인하느냐?  JPA호출시 sql문을 보여주는 옵션을 켜고 테스트를 하면

수많은 중복요청에대해 특정시간내에 단한번만 sql문을 호출할것입니다.


참고: EndPoint에 캐시설정

http://javasampleapproach.com/spring-framework/cache-data-spring-cache-using-spring-boot


캐시기능을 여러노드에 공통적으로 확장하기



동일한 DB요청을 각각 다른 사용자가 동일시간(+-5초)에 했다고 가정해봅시다.

  • A사용자 최근 게시물 100개 조회 ( 0번노드)
  • B사용자 최근 게시물 100개 조회 ( 1번노드)
  • C사용자 최근 게시물 100개 조회 ( 2번노드)

웹서비스에는 노드밸런스란 기능이있어서 사용자를 각각 다른노드로 분배를시킵니다.

이것은 대부분의 웹서비스가 사용하는 방식입니다.

이와 같은 상황에서 문제가 무엇일까요? 각 노드에 DB호출수를 줄이기위해

각각의 서버에 캐시기능을 적용하였지만, 캐시기능은 아무런 쓸모없이

설정한 캐시시간을 무시하고, 동일한 데이터 조회를 연속으로 하게됩니다.

이때 필요한 캐시기능이, 여러노드에서도 중복호출을 막는 클러스터 캐시입니다.




  • No labels