REST API 속도 개선(Java/Spring/Cache)
목차
- 상황
- REST API 속도 개선 기술
- 스프링 캐시
- 용례1 - 1억건 통계 데이터 처리 API 캐싱
- 용례2 - KMS 캐싱 (EHCache를 통한 기능 추가)
상황
-
우연히 1억건의 DB 데이터를 처리하는 API 서버를 개발하게 되었다. 그러나 해당 API 서버에서의 요청 처리 시간은 5초 ~ 15초, 끔직한 상황이다. REST API의 속도 개선이 절실히 필요하다.
REST API 속도 개선 기술
REST API(HTTP 통신)는 일단 느리다. 그러나, 우리는 웹어플리케이션(REST API)의 속도 개선을 위해 다음과 같은 기술들을 사용할 수 있다.
-
HTTP 프로토콜의 Cache-Control, ETag를 통한 네트워크 레이턴시 감소
-
DBMS의 딕셔너리 캐시/라이브러리 캐시/ 버퍼 캐시의 사용으로 인한 디스크 I/O 최소화
-
DBMS SQL 쿼리 튜닝
-
어플리케이션의 소스 코드 개선
HTTP 프로토콜의 Cache-Control과 ETag를 동적 컨텐츠 즉, REST API에 적용하고 사용하기가 까다로우며(클라이언트의 수정이 요함), 효과가 미비하다.
DBMS의 엔진 튜닝 및 SQL 쿼리 튜닝도 한계가 있다(나는 전문가가 아니다.) SQL 튜닝 해봤자 실행계획 보고 쿼리 변경 및 인덱스 추가이다. 이번에는 쿼리 소트 부하가 상당하며, 해당사항을 해결하기 위한 SQL 튜닝 지식이 부족했다.
SQL 결과 값을 재가공하기 때문에 최소 O(n2)의 시간복잡도를 갖고 있다. 이런 시간복잡도는 어쩔 수 없다. 결론은 어플리케이션의 소스 코드 개선은 가성비가 좋지 못하다.
결론은 어플리케이션의 캐시 기술을 사용한다. 즉, REST API 결과를 캐싱하는 것이다. 우리는 주변에서 많은 캐싱 기술과 용례를 보고 있었는데 왜 이것을 여지껏 몰랐을까? 정말 의문이다.
스프링 캐시
스프링은 3.1 이상부터 캐시 추상화 기술을 제공한다. 설정과 어노테이션 추가를 통해 바로 쉽게 캐시를 적용할 수 있다(4.1에 확장을 했지만 기능은 잘 모르겠다.). 트랜잭션과 마찬가지로 AOP를 이용해 메소드 실행 과정에 투명하게 적용된다. 캐시 관련 코드를 어플리케이션 로직에 적용하지 않아도 된다. 또한 캐시 서비스의 종류가 달라져도 괜찮다(캐시 impl/provider 변경). 캐시 기능과 어플리케이션의 기능이 이렇게 분리 되어있다. 해당 사항은 스프링에 대표적인 기술인 서비스 추상화 기술이다.
스프링 캐시를 사용하기 위해서는 CacheManager 설정과 해당 설정을 적용할 Service 메소드를 지정해야한다. Service 메소드는 트랜잭션의 설정과 같이 어노테이션 지정만으로 깔끔하게 캐시 기능을 구현할 수 있다.
CacheManager 설정은 용례1,2와 같이 설정한다. 간단한 설정은 용례 1과 같이하면된다.
Service 메소드에 적용하는 어노테이션은 다음과 같다.
@Cacheable
-
해당 어노테이션이 지정된 메소드는 캐시기능을 제공할 수 있다. 해당 어노테이션에서 자주 사용하는 값은 key, value, condition 정도이다.
-
value 값을 통해 캐시 분류(예: 사용자 조회 캐시/게시글 조회 캐시 분리)
-
key 값을 통해 인자별 캐시 분류(예: 1번(key) 사용자에 대한 캐싱) SpEL을 활용할 수 있다.
-
condition을 통해 일정 조건에 대해 충족하는 경우에만 캐싱
-
sync는 Spring 4.3 이상 지원되는 기능이며, 해당 기능을 통해 쓰레드 세이프하게 캐시를 생성한다(여러 스레드가 한번에 같은 캐시를 생성하는 것을 막는다.).
@CacheEvicit
-
캐시된 데이터는 갱신되어야 한다. 해당 어노테이션이 지정된 메소드는 캐시 삭제 기능을 제공할 수 있다.
-
value/key/condition은 @Cacheable 설명과 같다. 다만 캐시 삭제에 초점이 맞춰져있다.
-
allEntiries 값을 통해 key 별 캐시 삭제가 아닌, 모든 key의 캐시 삭제가 가능하다.
-
beforeInvocation을 통해 메소드 실행 전 캐시를 삭제할 수 있다.
@CachePut
-
해당 어노테이션이 지정된 메소드는 캐시하고자 하는 데이터를 저장’만’ 한다. 용례는 다음과 같다. 많은 데이터의 효율적인 캐시 기능 제공을 위해, 미리 저장만 하고 추후에 조회 API는 캐시된 데이터만 반환한다.
- 아래의 코드 블럭으로 통해 설명이 가능하다.
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
CacheManager
AOP를 이용해 캐시 부가기능을 적용할 수 있게 한다. 동시에 캐시 기술(프로바이더) 상관없이 추상화된 스프링 캐시 API를 이용할 수 잇게 서비스 추상화를 제공한다. 캐시 매니저는 CacheManager 인터페이스를 사용하며, 5가지의 concrete class를 제공한다. Concrete class는 다음과 같다.
ConcurrentMapCacheManager
ConcurrentHashMap을 이용하여 간단한 캐시 기능을 구현한 CacheManager이다. 해당 CahceManager의 경우, 캐시 용량 제한, 캐싱 알고리즘, TTL 등의 부가 기능은 없기 떄문에, 테스트 용도로만 사용을 권고한다(용례 1에 해당한다.).
SimpleCacheManager
기본적으로 제공하는 캐시가 없으며, 직접 등록해줘야 한다. 스프링 Cache 인터페이스를 직접 개발했을 때 테스트할 때 사용할 수 있다(용례 1에 해당한다.).
EhCacheCacheManager
EhCahce는 캐시 용량 제한, 캐싱 알고리즘, TTL, TTI, 파일 캐싱, 분산 서버 캐싱 등의 고급 기능을 제공하는 Java에서 제일 인기 있는 캐시 프레임워크이다. 해당 설정들은 XML 또는 JavaConfig를 통해 설정할 수 있다. 용례 2는 JavaConfig를 통해 설정하였다(XML 설정은 정보가 많으나, JavaConfig는 정보가 정말 부족하다.)(용례 2에 해당한다.).
CompositeCacheManager
CompositeCacheManager는 하나 이상의 캐시 매니저를 사용하게 지원해주는 혼합 캐시 매니저이다.
NoOpCacheManager
패스ㅋ
용례1 - 1억건 통계 데이터 처리 API 캐싱
상황
1. DB 로우는 1억건
2. SQL을 통해 조회 하는 값 백~만 건
3. 해당 사항을 어플리케이션의 데이터 재가공
pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${org.springframework-version}</version>
</dependency>
스프링 캐시 추상화를 사용하기 위해서는 spring-context-support JAR가 필요하다.
AppConfig.class( Cache 부분 발췌)
@EnableCaching
public class AppConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches( Arrays.asList(
new ConcurrentMapCache( "default-keyword" ),
new ConcurrentMapCache( "frequent-keyword" ),
new ConcurrentMapCache( "trend-keyword" ),
new ConcurrentMapCache( "relation-keyword" )
));
return cacheManager;
}
스프링에서 기본으로 제공하는 SimpleCacheManager를 사용한다.
Service.class
@Cacheable(cacheNames = "frequent-keyword", key="{#request.getParameter('returnCnt'), #request.getParameter('ageCode')," +
" #request.getParameter('sex'),#request.getParameter('city'),#request.getParameter('county') }")
public List<Map<String, Object>> selectFrequentKeyword( HttpServletRequest request ){ … }
@Cacheable 어노테이션을 통해 key별 캐시 데이터를 생성할 수 있다.
정말 간단하게 5초~15초 걸리던 API는 캐시를 통해 0.2~0.1초로 바꿀 수 있었다. 그러나, 우리는 여기서 간과한 사항이 있다. 먼저, 캐시의 주의사항을 파악한 후, 생각해 본다. 캐시 사용 주의사항은 다음과 같다.
-
연산의 결과가 달라졌을 때의 데이터의 갱신 전략
-
캐시의 TTL(Time To Live), 캐시 만료일
-
캐시 메모리 교체 알고리즘의 선택(LRU, LFU 등)
-
캐시 저장소의 선택(파일, 메모리, DBMS, 네트워크 등)
제일 중요한 데이터 갱신 전략이 적용되지 않았다. 그러나, 다행이 이 프로젝트의 DB 값을 변경될 예정이 없고 항상 같은 값을 반환하는 API이기 때문에 데이터의 갱신 전략은 생략해도 괜찮았다. 또한 부가 기능도 필요없었다.
하지만 너무 아쉽다. 삽이 필요하다.
결국 EhCache 삽질을 한다. 용례2와 같다.
용례2 - 조회 API 캐싱 (EHCache를 통한 기능 추가)
조회 부분에 적용한다. 설정 내역은 아래와 같다.
pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.9.0</version>
</dependency>
EhCacheCacheManager를 사용하기 위해서는 EhCache 라이브러리가 필요하다.
AppConfig(KeyCacheConfig)
@Configuration
@EnableCaching(order=400)
public class KeyCacheConfig implements CachingConfigurer {
private final CacheManager cacheManager;
private final net.sf.ehcache.CacheManager ehCacheManager;
public KeyCacheConfig() {
CacheConfiguration cacheConfiguration = new CacheConfiguration()
.name( "default-cache" )
.memoryStoreEvictionPolicy( "LRU" )
.maxEntriesLocalHeap( 1000 )
.timeToLiveSeconds( 86400 /*60 * 60 * 24 */ );
net.sf.ehcache.config.Configuration config = new net.sf.ehcache.config.Configuration();
// 디스크 캐시 사용할 경우 적용
// setDiskCache( config, cacheConfiguration );
config.addCache( cacheConfiguration );
this.ehCacheManager = net.sf.ehcache.CacheManager.newInstance( config );
this.cacheManager = new EhCacheCacheManager( ehCacheManager );
}
private void setDiskCache( net.sf.ehcache.config.Configuration config, CacheConfiguration cacheConfiguration ) {
PersistenceConfiguration persistanceConf = new PersistenceConfiguration();
persistanceConf.strategy( Strategy.LOCALTEMPSWAP );
cacheConfiguration.addPersistence( persistanceConf );
DiskStoreConfiguration diskCacheConfig = new DiskStoreConfiguration();
String path = this.getClass().getClassLoader().getResource( "./cache" ).getPath();
diskCacheConfig.path( path );
config.addDiskStore( diskCacheConfig );
}
@Bean(destroyMethod="shutdown")
public net.sf.ehcache.CacheManager ehCacheManager() {
return ehCacheManager;
}
@Bean
@Override
public CacheManager cacheManager() {
return cacheManager;
}
@Bean
@Override
public CacheResolver cacheResolver() {
return new SimpleCacheResolver( cacheManager );
}
@Bean
@Override
public KeyGenerator keyGenerator() {
return new SimpleKeyGenerator();
}
@Bean
@Override
public CacheErrorHandler errorHandler() {
return new SimpleCacheErrorHandler();
}
}
해당 설정은 아래의 URL을 참고하였다.
https://stackoverflow.com/questions/21944202/using-ehcache-in-spring-4-without-xml
CacheConfiguration 설정을 통해 다음의 설정을 할 수 있다.
-
캐싱 알고리즘
-
캐시 생성 최대 개수
-
캐시 생성 최대 메모리
-
TTL(캐시 생성 후, 자동으로 캐시 데이터 삭제 일)
-
TTI(캐시 생성 후, 자동으로 캐시 데이터 삭제 일, 다만 사용할 때 마다 초기화)
-
기타 등등
또한 setDiskCache 메소드를 통해 디스크 캐시를 사용할 수 있다. 예를들어 캐시 데이터가 20MB 이며, 해당 데이터가 많을 경우, 메모리를 사용하면 Memory leak이 발생할 것이다. 해당 경우를 상쇄하기 위해 key 값은 메모리가 갖고 있고 value 값은 디스크가 갖고있는다.
또한 암호키 조회의 경우, 암호키 값/기간 무결성 검증 로직이 있기 때문에 TTL은 하루만 설정하였다.
여기서 나오는 아쉬움은 암호키의 값/기간 무결성 검증 로직 또한 AOP로 분류 했으면 해당 암호키 캐싱은 암호키가 폐기할 때 까지 캐시할 수 있을 것이다. 정말 아쉽다.
Service
@Override
@AsyncUriAnnotation
@Transactional
@Cacheable( value="default-cache", key="{#identifier}" )
public Map<String, List<Map<String, Object>>> getKey(String identifier, HttpServletRequest request )
{ ... }
여기서 집중해야할 어노테이션은 Cacheable이다.
참고문헌
토비의 스프링 vol 2
https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache
https://www.baeldung.com/spring-cache-tutorial#use-caching-with-annotations
https://www.baeldung.com/spring-cache-tutorial
https://www.ehcache.org/documentation/2.7/configuration/fast-restart.html
https://www.ehcache.org/documentation/2.8/get-started/storage-options.html#diskstore-
https://minwan1.github.io/2018/03/18/2018-03-18-Spring-Cache/
https://javacan.tistory.com/entry/133
https://stackoverflow.com/questions/13381731/caching-with-multiple-keys
https://stackoverflow.com/questions/8181768/can-i-set-a-ttl-for-cacheable
https://mkyong.com/spring/spring-caching-and-ehcache-example/
https://stackoverflow.com/questions/21944202/using-ehcache-in-spring-4-without-xml
'개발관련 > 삽질' 카테고리의 다른 글
Python TensorFlow를 이용한 몸무게 예측 프로그램 (0) | 2020.05.07 |
---|---|
샤미르의 비밀 공유(SSS, Shamir's Secret Sharing) - 이론 (6) | 2020.05.07 |
SQLite 개념/구조/멀티 DB 실사용기 (0) | 2020.01.08 |
AOP(SpringAOP/AspectJ) (0) | 2019.12.02 |
AJP 프로토콜 모든 것을 분석 해보자 (1) | 2019.10.28 |