티스토리 뷰

SpringBoot

SpringBoot의 기본 Cache 사용하기

파미페럿 2021. 7. 24. 00:22

개발을 하다보면 어라? 이 데이터 계속 똑같이 사용되고 업데이트 될 일이 없는데? 하는 것들이 보인다. 데이터 업데이트가 자주 이뤄지지도 않고 자주 호출되는 데이터인데 계속 DB에 가서 데이터를 가져온다. DB에 한 번 갔다 하는데도 적은 데이터의 경우는 매우 그 시간이 짧지만 많은 데이터면 데이터일 수록 그 시간이 점점 늘어나 나중에는 사용자가 불편을 느낄 정도로 데이터를 가져오는 시간이 길어진다.

 

그럴 때 캐싱(Caching) 기능을 사용해서 똑같은 데이터는 DB에서 가져오지 않고 미리 캐싱해놓은 데이터를 가져오고 만일 데이터 업데이트가 이뤄지면 캐싱된 데이터를 업데이트하고 캐싱된 데이터가 너무 자리를 많이 차지하면 아예 캐싱된 데이터를 지워버리는 등 DB에 가서 데이터를 가져오지 않고 그보다 가깝고 빠른 캐싱 데이터를 가져와 데이터를 가져오는 시간을 줄일 수 있다.

 

 

SpringBoot 기본 Cache 사용하기 위한 설정

스프링부트에서는 고맙게도 기본 캐시 기능을 제공해준다. 원래 캐시를 저장해야할 곳도 따로 마련해야하는데 스프링부트는 스프링부트 어플리케이션을 실행하면 해당 어플리케이션과 함께 살아있는 캐시 공간을 사용한다. 이 캐시 공간은 메모리를 차지하므로 많은 캐시를 저장하는 것에는 적합하지 않다.

또한 이 캐시 데이터는 어플리케이션이 죽으면 없어지는 캐시 데이터로, 어플리케이션이 죽어도 캐시 데이터가 사라지지 않고 죽은 어플리케이션을 다시 실행시키면 기존 캐시 데이터를 사용하게 하고 싶으면 따로 캐시 데이터를 저장하는 캐싱 서버를 두는 분산 캐싱을 해야한다.

 

 

1. SpringBoot에 종속성 추가하기

- gradle

implementation 'org.springframework.boot:spring-boot-starter-cache'

 

- maven

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

 

 

2. @EnableCacheing 설정

위의 캐시에 필요한 종속들을 추가하면 이제 캐시와 관련된 어노테이션을 사용할 수 있다.

캐싱 기능을 사용할 스프링부트 어플리케이션에 @EnableCaching이라는 어노테이션을 붙여준다. 이 어노테이션을 붙여주면 이제 해당 애플리케이션의 캐싱 기능은 활성화 되어 DB에서 데이터터를 읽어오는 부분에서 다른 어노테이션을 사용해 캐싱 기능을 이용할 수 있다.

package com.juylee.pinachigong;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class TestApplication {

	public static void main(String[] args) {
		SpringApplication.run(TestApplication.class, args);
	}

}

 

물론 이 @EnableCaching 어노테이션을 스프링부트 어플리케이션 부분에 붙이지 않고 따로 설정파일 역할을 하는 클래스에 생성해서 사용하거나 XML 파일에 별도로 캐싱기능을 사용할 부분에 대해 CacheManager 빈을 만들어 좀 더 세부적으로 설정할 수 있는 것 같지만 우선 이번에는 간단하게 캐싱 기능을 사용해보고 익히는 것이므로 그냥 스프링 어플리케이션에 어노테이션을 붙여 해당 어플리케이션에 캐싱 기능을 모두 활성화하는 것으로 하겠다.

 

 

@Cacheable: 캐시 데이터 생성 및 사용

위의 기본 설정을 끝냈으면 이제 스프링부트의 기본 캐싱 기능을 사용할 수 있다.

나는 이번 캐싱 기능을 테스트 하기 위해 간단하게 메뉴 리스트를 저장하는 아래 테이블을 생성했다.

 

단순하게 메뉴 이름과 해당 메뉴가 치킨인지 피자인지 저장하는 테이블로 미리 값을 다 넣어놨다.

 

그리고 MenuService를 생성해 아래와 같이 메뉴 리스트를 모두 가져오는 함수를 생성했다.

@Service
@RequiredArgsConstructor
@Transactional
public class MenuService {
	private final MenuListRepository menuListRepository;
	
	// 메뉴 가져오기
	@Transactional(readOnly = true)
	public List<MenuList> getMenu() {
		return this.menuListRepository.findAll();
	}
}

 

 

위 서비스 함수를 호출해보면 메뉴 리스트가 가져오는데 콘솔을 보면 그 때마다 계속 select를 해오는 것을 볼 수 있다.

 

더보기

JPA에서는 기본적으로 실제 돌아가는 sql문이 보이지 않는다. 하지만 개발하다보면, 특히 쿼리함수로 개발을 하다보면 내가 짠 쿼리함수가 내가 원하는대로 sql을 날리고 있는지 궁금해진다.

그럴 때 application.properties의 아래 설정을 추가하면 console에 실제 동작하는 sql문을 볼 수 있다.

spring.jpa.show-sql=true

 

@Cacheable 어노테이션은 처음에 캐시 데이터를 만들어주고 그 다음부터는 DB에서가 아닌 캐시에서 데이터를 가져오도록 설정해주는 어노테이션이다.

@Cacheable에는 여러 가지 넣을 수 있는 파라미터가 있지만 그것은 나중에 자세히 살펴보도록 하고 우선 기본 사용 법을 보면 아래와 같다.

@Service
@RequiredArgsConstructor
@Transactional
public class MenuService {
	private final MenuListRepository menuListRepository;
	
	// 메뉴 가져오기
	@Transactional(readOnly = true)
   	@Cacheable("menu")
	public List<MenuList> getMenuList() {
		return this.menuListRepository.findAll();
	}
    
    	// 메뉴 하나 가져오기
	@Transactional(readOnly = true)
	@Cacheable(value = "menu", key = "#id")
	public MenuList getOneMenu(int id) {
		return this.menuListRepository.findById(id).orElseThrow();
	}
}

 

 

@Cacheable 어노테이션은 위와 같이 기본으로 텍스트 값을 넣어줘야하는데 그게 바로 캐시 데이터 저장 공간의 이름이 된다. 물론 value, key 파라미터로 명시를 해줄 수 있다. value는 캐시 데이터의 이름이 되고 key는 캐시 데이터의 키 값이 된다.

 

getOneMenu(int id)에서는 key를 '#id'라고 지정을 해놨는데 이럴 경우 getOneMenu(int id)에서 받는 파라미터 id 값대로 캐시 데이터가 저장되어 추후 각 캐시 데이터를 key 값에 따라 부분 업데이트를 할 수 있다. 즉, 아래와 같이 menu안에 key value와 같이 캐시 데이터가 저장되는 것이다. 만일 키를 따로 설정을 안 하면 해당 메소드의 파라미터로 들어오는 값들로 키가 설정된다.

 

 

위와 같이 캐싱 기능을 사용하겠다고 설정을 하고 다시 getMenu를 여러 번 호출하면 아래와 같이 select는 처음 한 번만 이뤄지고 그 후에는 select가 이뤄지지 않는 것을 확인할 수 있다.

(좀 확인을 잘 할 수 있도록 controller 단에 log를 찍어봤다.)

 

즉, @Cacheable 어노테이션으로 캐싱 기능을 사용하게 되었고, 이로 인해서 처음에는 DB에 가서 데이터를 읽어 'menu'라는 이름으로 캐시를 저장하지만 그 후에는 DB에서 데이터를 읽지 않고 저장해놓은 캐시 데이터를 읽는 것이다. 이로 인해 DB로 가는 시간을 줄일 수 있게 되었다.

 

 

 

@CachePut: key 값에 따른 캐시 데이터 부분 업데이트

만일 DB의 데이터가 업데이트되면 자동으로 캐시 데이터를 업데이트해주나? 라고 생각할 수 있으나 그것은 잘못된 생각이다. @Cacheable은 처음에 캐시에 데이터를 넣으면 그 후로 DB가 변경되든 말든 신경 쓰지 않고 저장된 캐시 데이터만 가져온다.

 

위의 리스트를 가져왔던 캐시 데이터의 경우 부분 캐시 데이터 부분 업데이트보다는 캐시데이터를 전체 삭제해야하므로 @CachePut 예시에서는 메뉴 하나를 가져오는 아래 함수를 통해 캐시 데이터 부분 업데이트를 해본다.

@Service
@RequiredArgsConstructor
@Transactional
public class MenuService {
	private final MenuListRepository menuListRepository;
	
    	...
    
	// 메뉴 하나 가져오기
	@Transactional(readOnly = true)
	@Cacheable(value = "menu", key = "#id")
	public MenuList getOneMenu(int id) {
		return this.menuListRepository.findById(id).orElseThrow();
	}
}

 

 

예를 들어 아래와 같이 'FRIED_CHICKEN'의 part_name을 'PIZZA'로 바꾸고 getOneMenu(int id)로 4번 메뉴를 가져오면 아래와 같이 'PIZZA'로 변경하기 전에 캐시되어 있는 데이터가 있으면 캐시되어 있는 데이터대로 'CHICKEN' 값을 갖고 있는 것을 확인할 수 있다.

 

이렇게 데이터가 업데이트 되어 저장되어 있던 캐시 데이터의 부분적인 업데이트가 필요할 때 데이터를 변경하는 메소드에 @CachePut을 사용해서 key 값에 해당하는 캐시 데이터를 업데이트 해준다.

 

@Service
@RequiredArgsConstructor
@Transactional
public class MenuService {
	private final MenuListRepository menuListRepository;
	
    	...
    
	// 메뉴 업데이트 하기
	@CachePut(value = "menu", key = "#menuRequest.id")
	public MenuList updateMenu(MenuRequest menuRequest) {
		MenuList menuList = this.menuListRepository.findById(menuRequest.getId())
					.orElse(new MenuList((int)this.menuListRepository.count()));
		
		if(menuRequest.getMenuName() != null) {
			menuList.updateMenuName(menuRequest.getMenuName());			
		}
		
		if(menuRequest.getPartName() != null) {
			menuList.updatePartName(menuRequest.getPartName());			
		}
		
		this.menuListRepository.save(menuList);
		
		return menuList;
	}
}

 

위 코드를 보면 @CachePut(value = "menu", key = "menuRequest.id")라고 어노테이션을 설정해놓은 것을 확인할 수 있다.

여기서 value는 위에서 설명했듯이 캐시 데이터 이름이다. 위에서 @Cacheable에 'menu'라고 설정을 해놨으니 똑같이 'menu'라고 설정을 해줘야 거기서 데이터를 읽을 수 있다.

여기서 중요한 것은 key 값인데 이 key 값이 위의 getOneMenu(int id)에서 id를 key 값으로 해서 캐시 데이터를 저장했으므로 updateMenu(MenuRequest menuRequest)에서도 똑같이 해당 id 값의 캐시를 업데이트 해야한다. 그러면 key 값을 똑같이 id로 해야하는데 이번에는 파라미터를 int 형식이 아닌 별도 지정한 DTO인 MenuRequest로 받아온다. 이와 같이 dto 안의 값을 key 값으로 사용해야할 경우에는 '#menuRequest.id'와 같이 해당 'dto.${field}' 형식으로 작성을 해주면 된다.

 

해당 필드 값이 private이니까 getter를 써야하지 않나요? 라고 생각할 수 있지만 여기서는 private여도 해당 필드 값을 가져올 수 있어서  '#menuRequest.id' 처럼 쓰든  '#menuRequest.getId()'로 쓰든 둘 다 동일하게 동작한다.

 

참고로 key와 cache 어노테이션의 추가 파라미터인 condition(캐싱 조건)에 쓰는 문법은 spel 문법이다. spel 문법에 대한 자세한 내용은 아래 링크 참고 바란다.

https://www.baeldung.com/spring-expression-language

 

 

여하튼 위와 같이 updateMenu(MenuRequest menuRequst)에 @CachePut(value = "", key = "")를 설정해놓고 getOneMenu(int id)로 테스트를 해보면 아래와 같이 결과가 나온다.

 

우선 getOneMenu(int id)로 프라이드 치킨인 4번 메뉴를 가져오면 위의 @Cacheable 설정 설명처럼 처음에만 DB에서 데이터를 가져오지 그 다음에는 DB에 가지 않는다.

 

하지만 만일 update(MenuRequest menuRequest)를 통해 4번 메뉴의 partName을 PIZZA로 업데이트를 한다면?

 

update(MenuRequest menuRequeset)에서 DB를 업데이트 함과 동시에 4번에 해당하는 캐시 데이터도 업데이트해서 아래와 같이 다시 4번 메뉴를 getOneMenu(int id)로 가져와도 DB를 타지 않고 캐시 데이터를 통해 가져오며 변경된 데이터인 PIZZA로 잘 가져오는 것을 볼 수 있다.

 

즉, DB를 거치는 것은 update를 할 때 한 번 뿐이고 그 후에는 업데이트 된 캐시 데이터를 이용할 수 있는 것이다.

 

 

더보기

가끔가다 보면 캐시 데이터를 업데이트 했는데 다시 캐시 데이터를 읽어보면 null 값이 나온다는 경우가 있다. 그것은 캐시 데이터를 업데이트 해주는 곳에서 업데이트 해줄 캐시 데이터를 return 하지 않기 때문이다.

// 메뉴 업데이트 하기
@CachePut(value = "menu", key = "#menuRequest.id")
public void updateMenu(MenuRequest menuRequest) {
	...
		
}

 

즉, 위와 같이 함수의 반환형을 void로 해놓고 아무 데이터도 반환하지 않으면 캐시에 저장될 데이터 또한 없어 해당 key 값의 캐시 데이터는 null로 업데이트가 되어 버린다.

 

따라서 아래와 같이 굳이 반환할 필요가 없는 함수여도 캐시 데이터 저장을 위해 캐시 데이터에 저장해줄 값을 반환해야 한다.

// 메뉴 업데이트 하기
@CachePut(value = "menu", key = "#menuRequest.id")
public MenuList updateMenu(MenuRequest menuRequest) {
	....
		
	return menuList;
}

 

 

@CacheEvict: 해당 캐시 데이터 모두 지우기

위에서 리스트를 반환하는 함수에 캐시 데이터를 적용 해놓으면 캐시 부분 업데이트가 힘들다고 했다. 그러면 어떻게 해야할까? 간단하다. 캐시 데이터를 모두 지우고 refresh 하면 된다. 캐시 데이터를 지우는 기능 바로 그것을 하는 것이 @CacheEvict 어노테이션이 하는 역할이다.

 

예를 들어 아래와 같이 메뉴 하나를 삭제하는 메소드가 있을 때 menu 캐시에 있는 모든 데이터를 다 지우도록 하자.

@Service
@RequiredArgsConstructor
@Transactional
public class MenuService {
	private final MenuListRepository menuListRepository;
	
    	...
    
	// 메뉴 하나 삭제하기
	@CacheEvict(value = "menu", allEntries = true)
	public void deleteOneMenu(int id) {
		this.menuListRepository.deleteById(id);
	}
}

 

@CacheEvict 어노테이션을 보면 allEntries라고 눈에 띄는 파라미터가 있는데 이 파라미터를 true로 설정하면 어노테이션이 붙어 있는 메소드에서 어떤 파라미터를 받더라도 그냥 해당 캐시 데이터를 다 삭제해버린다. 즉, 위에서 id 값을 파라미터로 받아 id를 key 값으로 저장해놓은 캐시 데이터를 구분할 수 있더라도 'allEntries = true'로 인해 'menu' 안의 모든 캐시 데이터가 지워지는 것이다.

 

 

예를 들어 아래와 같이 기존에 메뉴 리스트를 가져오면 캐시 데이터를 이용하던 것을

 

deleteOneMenu(int id)로 3번 메뉴를 삭제하고 나서 다시 리스트를 조회 해보면

 

DB로 다시 가서 변경된 데이터를 가져오는 것을 확인할 수 있다.

 

 

즉, @CacheEvict 어노테이션으로 인해 menu 캐시 데이터가 모두 지워져 다시 DB에서 데이터를 가져와 menu 캐시 데이터로 저장했다는 것을 알 수 있다.

 

만일 위에서 캐시 데이터의 key 값을 id로 지정한 상황에서 해당 id 값의 캐시만 지우고 싶다면 아래와 같이 어노테이션을 사용하면 된다.

@CacheEvict(value = "menu", key = "#id")​

 

그러면 해당 key 값에 대한 캐시 데이터만 삭제가 되고 menu 캐시에 있는 다른 데이터들은 남아 있다.

 

만일 @CacheEvict에 key 파라미터와 allEntries 파라미터를 같이 사용할 경우에 에러는 발생하지 않지만 key 파라미터는 힘을 쓰지 못하고 allEntries만 힘을 발휘해 해당 캐시에 있는 모든 데이터를 다 삭제해버린다. 즉, 위를 예를 들면 3번 메뉴만 삭제했지만 menu 캐시 데이터가 다 사라져 메뉴 리스트를 가져올 때 DB에 다시 갔다 온다는 것이다.

 

 

여러 개의 캐시 기능 및 value 사용하기

캐싱 기능을 사용하다보면 여러 개의 key 또는 캐시 기능을 사용해야할 때가 있다. 그럴 때는 어노테이션을 아래와 같이 작성해주면 된다.

@Caching(
	cacheable = {
			@Cacheable( value = {"name1", "name2"}, key = "key"),
			@Cacheable("name3")
	}
)

위와 같이 어노테이션을 작성하면 @Cacheable이 총 2개가 동작하게 되고 그 중 하나는 name1, name2에 각각 key라는 key 값을 가지게 캐시 데이터가 저장되고 name3에는 key 값을 별도 지정하지 않은 캐시 데이터가 저장된다.

 

 

 

 

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
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
글 보관함