일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- union_find
- 힙덤프
- sql
- dfs
- CSS
- spring boot
- priority_queue
- GC로그수집
- math
- NIO
- html
- map
- BFS
- 스택
- Calendar
- Union-find
- deque
- 리소스모니터링
- string
- set
- 큐
- javascript
- scanner
- Properties
- alter
- Java
- List
- date
- 스프링부트
- JPA
- Today
- Total
매일 조금씩
[스프링] 서버 캐시(Cache) 사용해서 GET 서비스 성능 개선하기 본문
캐시에 대한 설명과 사용법을 익힌 후, 실제 프로젝트에 적용한 코드를 마지막에 추가했다.
[목차]
개요
캐시란?
캐시(Cache)는 컴퓨터 과학에서 데이터나 값을 미리 저장해 놓는 임시 장소를 의미한다.
캐시 접근 시간에 비해 원래 데이터를 접근하는 시간이 오래걸리거나, 값을 다시 계산하는 시간을 절약할 때 주로 사용되며, 동일 결과를 리턴하는 반복적인 작업이 많고, 각 작업의 시간이 오래걸리거나 서버에 부담을 주는 경우 사용하면 효율적이다.
만약 매번 다른 결과를 돌려줘야하는 작업이거나 수정이 빈번한 데이터의 경우 캐시를 적용해봐야 오히려 성능이 떨어질 것이다.
로컬 캐시? 글로벌 캐시?
Local Cache
- 서버 내부에 캐시를 저장한다.
- 다른 서버의 캐시를 참조하기 어렵다.
- 서버 내에서 작동하기 때문에 속도가 빠르다.
- 로컬 서버 장비의 Resource를 이용한다. (Memory, Disk)
Global Cache
- 여러 서버에서 캐시 서버에 접근하여 참조 할 수 있다.
- 별도의 캐시 서버를 이용하기 때문에 서버 간 데이터 공유가 쉽다.
- 네트워크 트래픽을 사용해야 해서 로컬 캐시보다는 느리다.
- 데이터를 분산하여 저장 할 수 있다.
내 프로젝트는?
하나의 서버에서 하나의 앱을 구동하는 모놀리틱 아키텍처이므로 캐시 역시 로컬캐시로 적용했다.
스프링과 캐시 추상화
스프링은 캐시 서비스를 추상화하여 제공하며, AOP(Aspect-oriented programming)를 이용해 빈의 메서드에 애노테이션으로 손쉽게 AOP 설정을 할 수 있다.
또한 추상화된 서비스 덕분에 구체적인 캐시 구현 기술에 종속되지 않으므로 환경이 바뀌거나 적용할 기술을 변경해서 캐시 서비스의 종류가 달라지더라도 애플리케이션 코드에 영향을 주지 않는다.
내 프로젝트에서 캐시가 필요한 이유는?
해당 프로젝트는 화상강의 프로젝트이다. 따라서 강사가 문서를 공유하게 되면 썸네일화된 페이지 리스트가 상단에 있고, 거기서 선택된 페이지가 아래에 크게 이미지 파일로 보이는 형태이다. 이때, 한 번 본 페이지의 이미지를 다시 선택해서 볼때, 다시 서버에서 파일을 가져온다.
이 부분을 캐시를 사용해서 성능을 개선하려한다.
한번 조회해온 페이지 이미지는 캐시에 저장되어 다시 조회할때 서버에서 가져오지 않고 캐시에서 가져오며, 문서 공유가 종료되면 해당 캐시를 날려버리도록 구현했다.
스프링 캐시 설정, 캐시 매니저
기본 설정
1. 의존성 추가
- gradle
implementation 'org.springframework.boot:spring-boot-starter-cache'
2. 캐시 설정
XML을 이용한 캐시 설정이 가능하지만, 개인적으로 XML파일을 쓰는걸 좋아하지 않아서 자바 파일로 설정을 했다.
- CacheConfig 클래스와 CacheManager 설정과 구현
@EnableCaching
@Configuration
public class CacheConfig(){
@Bean
public CacheManager cacheManager(){
SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
simpleCacheManager.setCaches(List.of(new ConcurrentMapCache("캐시이름")));
return simpleCacheManager;
}
}
- @EnableCaching : 캐싱기능을 사용하겠다는 선언, 앱 클래스에 애노테이션을 붙여도 된다.
- CacheManager : 스프링에서 제공하는 캐시 추상화 API로 여러가지 구현 클래스가 존재하나 여기서는 대표적인 3가지만 알아보겠다.
- SimpleCacheManager : 구현체인 캐시가 없는 깡통 매니저. setCaches를 통해 구현체를 넘겨서 등록할 수 있다. 위의 예시코드에서는 스프링 기본 캐시 구현체인 ConcurrentMapCache를 만들어 등록했다.
- ConcurrentMapCacheManager : ConcurrentMapCache만을 사용하는 캐시 매니저. 캐시 정보를 Map 타입으로 메모리에 저장하므로 속도가 매우 빠르고 별다른 설정이 필요없지만 본격적인 캐시로 사용하기에는 기능이 빈약하다. 캐시별 용량제한이나 다양한 저장방식지원, 다중 서버 분산과 같은 기능이 없으므로 테스트 용도로 사용하길 추천한다.
- EhCacheManager : 자바에서 가장 인기있는 캐시 프레임워크인 EhCache를 지원하는 캐시 매니저다. 본격적으로 캐시 기능을 적용하려면 사용을 고려해볼만 하다.
EhCache 설정
위의 코드에서는 스프링이 제공하는 기본구현체 ConcurrentMapCache를 사용하는 설정을 알아봤지만, 본 프로젝트에서는 조금 더 다양한 부가기능을 사용하기 위해 EhCache를 사용했다.
공부 목적으로 Reference가 풍부한 2 버전으로 진행했으며, 현재 가장 최신버전은 3.8 버전이다.
1. 의존성 추가
- gradle
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation group: 'net.sf.ehcache', name: 'ehcache', version: '2.10.9.2'
그래들 기준으로 스프링부트 캐시 + ehcache 둘을 의존성 추가해준다.
2. 캐시 설정 (위와 같은 맥락으로 자바 config 파일 작성)
@Configuration
public class CacheConfig {
/*
- Eh캐시 매니저 생성 도우미
빈등록을 해놓으면 된다.
*/
@Bean
public EhCacheManagerFactoryBean cacheManagerFactoryBean(){
return new EhCacheManagerFactoryBean();
}
/*
- EhcacheManager 등록
*/
@Bean
public EhCacheCacheManager ehCacheCacheManager(){
// 캐시 설정
CacheConfiguration layoutCacheConfiguration = new CacheConfiguration()
.eternal(false)
.timeToIdleSeconds(0)
.timeToLiveSeconds(21600)
.maxEntriesLocalHeap(0)
.memoryStoreEvictionPolicy("LRU")
.name("layoutCaching");
// 설정을 가지고 캐시 생성
Cache layoutCache = new net.sf.ehcache.Cache(layoutCacheConfiguration);
// 캐시 팩토리에 생성한 eh캐시를 추가
Objects.requireNonNull(cacheManagerFactoryBean().getObject()).addCache(layoutCache);
// 캐시 팩토리를 넘겨서 eh캐시 매니저 생성
return new EhCacheCacheManager(Objects.requireNonNull(cacheManagerFactoryBean().getObject()));
}}
필수 설정 요소에 대한 설명은 다음과 같다.
요소 | 설명 |
---|---|
EhCacheManagerFactoryBean | CacheManager의 적절한 관리 및 인스턴스를 제공하는데 필요하며 EhCache 설정 리소스를 구성한다. |
maxEntriesLocalHeap | Heap 캐시 메모리 pool size 설정, GC대상이 됨. |
memoryStoreEvictionPolicy | 캐시가 가득 찼을 때 관리 알고리즘 설정 default "LRU" |
timeToLiveSeconds | Element가 존재하는 시간. 이 시간이 지나면 캐시에서 제거된다. 이 시간이 0이면 만료 시간을 지정하지 않는다. |
timeToIdleSeconds | Element가 지정한 시간 동안 사용(조회)되지 않으면 캐시에서 제거된다. 이 값이 0인 경우 조회 관련 만료 시간을 지정하지 않는다. |
eternal | true일 경우 timeout 관련 설정이 무시, element가 캐시에서 삭제되지 않음 |
name | 캐시명 |
이 요소들을 위코드에선..
- EhCacheManagerFactoryBean로 @Bean을 사용하여 EhCache 생성 도우미를 등록한다.
- EhCacheCacheManager로 @Bean을 사용하여 EhCache 매니저를 등록하는데..
- CacheConfiguration을 사용하여 캐시를 설정한다.
- 설정을 한 것을 가지고 Cache를 생성한다.
- 캐시 팩토리에 생성한 eh캐시를 추가한다. addCache(생성한 eh캐시)
- 캐시 팩토리를 return해서 eh캐시 매니저를 생성한다.
여기서 EhCacheManager를 빈으로 등록할 때, 디폴트 설정을 사용하면 코드 실행시 CacheManager execption이 발생하므로 자바 Config 파일에서 직접 빈 등록을 하는 것이 좋다.
어노테이션 기반 캐시 사용하기
@Cacheable
스프링에 Eh캐시를 등록하는 설정을 마쳤다면 이제 캐시를 적용하기만 하면된다. 스프링은 AOP 기반으로 캐시가 작동하며 어노테이션으로 AOP 설정을 할 수 있기 때문에 우리는 아주 간편하게 캐시를 사용할 수 있다.
또한 캐시는 저장할 내용과 속성 정보로 메소드의 리턴값과 파라미터를 사용하기 때문에, 보통 메소드 단위로 설정하게 되며, 클래스나 인터페이스 레벨에 캐시를 하는일은 드물다.
캐시 적용 방법
@Cacheable(value = "layoutRecentArticleCaching", key = "#lastArticleId")
public List<Article> getRecentArticles(Long lastArticleId) {
....중략...
}
- 캐시를 적용하고자 하는 메소드 위에 @Cacheable 만 달아주면 된다.
- value : 해당 반환값을 저장하고자 하는 캐시 저장소 이름. value라고 해서 헷갈리지말자. 캐시 이름이지 저장데이터의 키밸류값이 아니다.
- key : 캐시는 기본적으로 map 구조로 저장되므로 캐시 저장소에 저장할 키값을 지정해주고 반환값은 밸류값으로 저장된다. #파라미터 로 입력하면 해당 파라미터를 키값으로, 반환값을 밸류로 저장하게 된다.
- condition : 파라미터 값이 특정 조건의 경우에만 캐시를 적용하고 그 외에는 캐시를 사용안할 경우 condition을 어노테이션값에 작성해주면 된다.
@CacheEvict
캐시는 적절한 시점에 제거되야 한다. 물론 캐시 매니저 설정을 통해 캐시 수명주기를 설정할 수 있지만, 캐싱된 데이터 원본이 수정되거나 삭제 추가 될 경우 캐싱된 데이터와 정합성이 깨지게 되므로 캐시를 적절하게 제거하지 않으면 잘못된 결과가 리턴되어 버릴것이다.
따라서 스프링에서는 해당 메서드를 호출할때 특정 캐시를 초기화하는 어노테이션도 제공해준다. @CacheEvict 를 사용하면 데이터의 정합성을 유지할 수 있다.
사용법
@CacheEvict(value = {"layoutCaching", "layoutRecentArticleCaching","seoCaching"}, allEntries = true)
public void editArticle(Long articleId, ArticleForm articleForm) {
...중략...
}
위의 코드는 여러 캐시를 한번에 초기화하기 위해 캐시명을 적는 value값에 배열로 파라미터를 넘겼으며, 해당 캐시의 모든 데이터를 초기화한다는 의미로 allEntries에 true 값을 넣었다.
프로젝트에 적용하기
가장먼저 main 클래스에 @EnableCaching 을 추가한다.
> MeetingDocumentApplication
다음 config 파일을 작성한다.
> CacheConfig
package com.tmax.meeting.document.config;
import ...
@Configuration
public class CacheConfig {
@Bean
public EhCacheManagerFactoryBean cacheManagerFactoryBean(){
return new EhCacheManagerFactoryBean();
}
@Bean
public EhCacheCacheManager ehCacheCacheManager(){
CacheConfiguration layoutCacheConfiguration = new CacheConfiguration()
.eternal(false)
.timeToIdleSeconds(0)
.timeToLiveSeconds(21600)
.maxEntriesLocalHeap(0)
.memoryStoreEvictionPolicy("LRU")
.name("imageCaching");
Cache layoutCache = new net.sf.ehcache.Cache(layoutCacheConfiguration);
Objects.requireNonNull(cacheManagerFactoryBean().getObject()).addCache(layoutCache);
return new EhCacheCacheManager(Objects.requireNonNull(cacheManagerFactoryBean().getObject()));
}
}
실행시 캐시를 생성할 메소드엔 @Cacheable, 삭제할 메소드 위엔 @CacheEvict를 붙여준다.
> ServiceImpl
@Override
@Cacheable(value = "imageCaching", key = "{#documentId, #pageNumber}")
public ResponseEntity<Resource> getImageById(Long documentId, int pageNumber) throws IOException {
DirectoryUtil directoryUtil = new DirectoryUtil(documentId);
Path targetPath = Paths.get(directoryUtil.getImageDirectoryPath(), pageNumber + ".png")
.toAbsolutePath();
HttpHeaders headers = new HttpHeaders();
String contentType = Files.probeContentType(targetPath);
headers.add(HttpHeaders.CONTENT_TYPE, contentType);
return new ResponseEntity<>(new ByteArrayResource(Files.readAllBytes(targetPath)), headers, HttpStatus.OK);
}
@Override
@CacheEvict(value = "imageCaching", allEntries = true)
public ResponseModel updateDocumentSharedState(Long documentId, boolean isShared,
String updatedBy) {
Document document = documentRepository.findById(documentId)
.orElseThrow(() -> new RuntimeException());
int currentPage = documentRepository.findByDocumentId(documentId).getCurrentPageNum();
document.setShared(isShared);
if(isShared && currentPage == 0 ){
document.setCurrentPageNum(1);
}
document.setUpdatedBy(updatedBy);
return modelMapper.map(documentRepository.save(document), ResponseModel.class);
}
파일의 페이지별 이미지파일을 가져올때,
파일의 id와 페이지 넘버로 캐시를 만들어 저장하고 공유상태가 변경되면 캐시를 삭제하도록 구현되었다.
'Spring Framework' 카테고리의 다른 글
[Spring] range에 따른 동영상 분기 처리(한번에/range request) - FileSystemResource (0) | 2022.04.18 |
---|---|
@Builder 와 @NoArgsConstructor 를 함께 사용했을 때 발생하는 오류 (0) | 2022.03.18 |
[스프링] HTTP Range Requests로 비디오 스트리밍 만들기 (0) | 2022.03.04 |
[스프링] 비동기 서비스 구현하기 (@Async 사용) (0) | 2022.02.24 |
[스프링] 파일 삭제 기능 recursive 구현 JPA 사용 (0) | 2021.12.22 |