Java Spring - Cache 도입기 (2 / 2): Redis 적용기
2024. 3. 14. 23:41ㆍSpring Boot
728x90
반응형
#1. Redis 설정 기록
1. 라이브러리 추가
https://velog.io/@qotndus43/Cache
1) 그래들 추가
dependencies {
// Spring Boot Starters
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
// Other dependencies
}
2) redis 설정
spring:
data:
redis:
host: localhost
port: 6379
- 각 yaml 파일마다 별도의 host를 갖게 해도 괜찮음
- 그러면 각 서버마다 별도의 redis 서버를 갖게 됨
2. cache manager 등록
- config
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public CacheManager userCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues()
.entryTtl(Duration.ofHours(5L));
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory).cacheDefaults(redisCacheConfiguration).build();
}
}
- @EnableCaching
- 스프링의 캐싱 지원을 활성화하는 어노테이션
- 어디서나 @Cacheable / @CacheEvict 등의 어노테이션을 사용할 수 있게끔해줌
- defaultCacheConfig
- 설정
- TTL
- disableCachingNullValues
- Key&value 직렬화
- cacheEvict에서 allEntries를 true로 만드는 옵션을 활성화 시키고 싶으면:
@Bean
public CacheManager userCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues()
.entryTtl(Duration.ofHours(5L));
// return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory)
// .cacheDefaults(redisCacheConfiguration)
// .build();
return RedisCacheManager.builder(
RedisCacheWriter.nonLockingRedisCacheWriter(
connectionFactory,
BatchStrategies.scan(1000)))
.cacheDefaults(redisCacheConfiguration)
.build();
}
3. 서비스 레이어 어노테이션 선언
1) 예시
@Cacheable(value = "menu", key = "#menuId", unless = "#result == null")
- value는 카테고리 같은 느낌
- key는 캐시에 저장되어있는 값을 찾기 위함
#2. cache 에 저장해야 되는 데이터 기준
- 어떤 데이터를 캐시에 저장해야할 까
- 자주 접근하는 데이터들
- 읽기 중심의 변경 빈도가 낮은 데이터
- 가변성이 잦을 때 캐시화 하기 불리하다.
- 해당 데이터들에 대해 변경이 있을 때 캐시에서도 수정 / 삭제를 해야하기에 이는 오히려 속도 저하를 유발할 수 있기 때문이다.
- 계산, 전송 비용이 높은 데이터
- 해당 데이터들은 DB 연산 / 서버 연산에서 비용이 많이 들기 때문에,
- cache 처리를 해주는 데이터에 대해서는 해당 값이 수정 / 삭제 되는 상황에 대해서도 고려해야한다.
- @CacheEvict 와 @CachePut 어노테이션이 각각 캐시 삭제 / 수정에 대한 대응 어노테이션이다.
https://ykh6242.tistory.com/entry/Spring-Cache-Abstraction-정리
#3. 캐시 어노테이션 사용법
1. @Cacheable
- 캐시에 넣고, 해당 메서드 호출 시, 기존 키가 이미 존재하면 여기서 조회함
@Cacheable(value = "menuItem", key = "#tokenId + #menuId", unless = "#result == null")
- value는 캐시의 이름
- cacheNames로 바꿔서 써도 문제 없음
- 기존엔 value를 썼다가 직관성을 위해 cacheNames로 바꿔서 쓰는 중
- key는 캐시에 저장되어있는 값을 찾기 위한 값
- 보통 key는 상수값과 메서드 호출 시의 파라미터를 사용해서 만듦.
- #변수값을 통해서 key처리할 수 있음
- key는 여러 변수들을 이어붙여서 사용할 수 있음
- 멀티 테넌트인 경우 각 key를 테넌트로 구분하는 게 좋음
- 위 경우는 tokenId
- condition field 로 key 에 조건을 달아서 특정 key일 때만 cache 처리를 할 수 있음
@Cacheable(cacheNames = "cacheTest", key = "#tokenId + #standardYear + #standardMonth",
condition = "#standardYear == T(java.time.Year).now().value && #standardMonth == T(java.time.LocalDate).now().monthValue")
2. @CacheEvict
- 캐시를 삭제함
@CacheEvict(value = "menuAll", allEntries = true)
- value 내의 모든 캐싱 값을 삭제할 수 있는 옵션인 allEntries = true
- 각 key에 대한 지정을 하여 개별 삭제도 할 수 있음
- 데이터 수정 / 삭제 / 등록 시 사용할 여지가 있음
- 전체 조회용 값에 대해 삭제할 필요가 있음
3. @CachePut
- 캐시를 업데이트함
@CachePut(value = "books", key = "#book.id")
public Book updateBook(Book book) {
// 책 업데이트 로직
return updatedBook;
}
- 대신 사용할 때 메서드의 리턴 값 타입이 기존 캐시랑 맞아야 함
- 메서드의 실행 결과를 캐시에 저장
- 따라서 수정 시 데이터를 리턴하지 않고 int 값을 리턴한다면 그냥 @CacheEvict를 사용하는 것이 좋아보임
- 데이터 수정시 사용 여지가 있음
4. @Caching
- 같은 타입의 어노테이션을 여러 개 적용할 필요가 있을 때 사용
@Caching(evict = {
@CacheEvict(cacheNames = "menuAll", allEntries = true),
@CacheEvict(cacheNames = "menuItem", key = "#tokenId + #menuDto.getMenu_id()")
}
)
5. @CacheConfig
- 클래스 레벨에서 공통 캐시 설정을 정의하는 데 사용
- 즉 각 메서드에서 사용할 캐싱 어노테이션에서 적용될 캐시 이름을 한번에 설정할 수 있음\
- 코드의 중복을 줄이고 가독성을 높일 수 있음
- 만약 해당 클래스 내에서 캐싱 어노테이션에서 별도의 캐시 이름을 지정하면, 해당 명칭이 @CacheConfig 에서 지정한 이름보다 우선적으로 적용 됨
@CacheConfig(cacheNames = "books")
public class BookRepository {
@Cacheable
public Book findBookById(Long id) {
return book;
}
@CachePut(key = "#book.id")
public Book updateBook(Book book) {
return updatedBook;
}
@CacheEvict(key = "#id")
public void deleteBook(Long id) {
}
}
#4. redis 모니터링
https://velog.io/@tpwns3382/redis.conf-및-Redis-운영-주의사항
https://freeblogger.tistory.com/10
- 명령어
- monitor
- 모니터링
- flushall
- 캐시 flush
- monitor
#5. redis 용량 제한
- 각 캐시 이름에 대해 별도의 용량 제한을 설정하지는 않음
- 전체 용량에 따라 사용.
- 전체 용량을 제한할 수 있음
- 메뉴 125개의 행은 28736 바이트를 차지하고 있음
- 각 행은 아래와 같이 8개의 열을 가지고 있음
- 즉 redis 허용 용량이 2기가면 1.3 프로 차지하고 있음
{ "menuId": 124, "menuNm": "블로그 글 작성", "menuType": "menu_type_1", "upMenuId": 122, "menuLevelNum": 2, "useYn": true, "otputOdr": 99, "childrenMenu": [] }
127.0.0.1:6379> memory usage "menuItem::127"
(integer) 216
127.0.0.1:6379> memory usage "menuAll"
(integer) 28736
- 특정 메뉴 행menuItem::127은 6개의 열을 가지고 있고, 216 바이트를 차지고 하고 있음
- 대략 0.00001프로 차지.
- 종합하면, 하나의 행의 하나의 열은 평균 28 ~36 바이트, 즉 32 바이트 정도 가지고 있음
- 32바이트는 utf-8 방식으로 대략 10개의 한글 / 32개의 알파벳 정도
- 만약 캐시 대상이 대략 15개의 테이블에서 나온다고 하고, 대부분 테이블이 20개의 열을 갖는다고 가정.
- 캐시에 들어갈 데이터 행은 고객사 평균 100명 정도에 의해 5시간마다 각각 다른 하나를 접근한다고 가정.
💡 1000개 고객사, 15개 테이블, 100개 행, 20개 열, 각 데이터마다 32바이트
- 총 계산을 하면 960,000,000 바이트라는 값이 나옴
- 이를 기가바이트로 환산하면 대략 0.894 기가바이트라는 값이 나옴
- 2기가바이트로도 충분할 것으로 예상이 되지만, 캐시 적용할 테이블의 개수에 따라 해당 수치를 크게 바뀔 수 있을 것
#6. 캐시 네임스페이스
💡 캐시 value와 key를 어떻게 이름 짓는 지에 관한 의견
- 캐시 value
- 즉 cacheNames
- 기본적으로 데이터 DTO와 같은 이름을 가지고, 그 후에 만약 조회하는 방법에 따라 여러개를 생성할 시, url 값을 붙임
- ex) menuAll / menuItem
- 캐시 key
- 전역 공통이 아닐 경우, 정확한 키 식별을 위해 회사 토큰값을 키에 넣어줘야함
key = "#tokenId + #key"
- 만약 특정 조회 메서드가 파라미터가 많고, 각 파라미터가 리턴값을 특정시킬 수 있을 경우, 모든 파라미터를 key 에 값을 넣어줘야함.
- key가 너무 길어지게 되면, 오히려 redis에 너무 과한 메모리를 차지할 수 있게 되기에, 이런 경우 해당 조회 메서드에 캐싱 어노테이션을 달아주는 것에 대해 고민해볼 필요가 있음.
- 혹은 특정 파라미터에 조건을 달아, 어떤 조건에서만 캐싱 처리를 할 수도 있음
@Cacheable(cacheNames = "menuAll", key = "#tokenId + #menuLevelNum + #menuType", unless = "#result == null", condition = "#isTree == true")
public List<MenuDto> getMenuList(String[] menuType, Integer menuLevelNum, TokenDto tokenDto) {
// 메서드 구현
}
728x90
반응형
'Spring Boot' 카테고리의 다른 글
스웨거(Swagger) - 어노테이션 간단 정리 (0) | 2024.03.14 |
---|---|
DTO 상속에 대하여 (0) | 2024.03.14 |
Java Spring - Cache 도입기 (1 / 2): 어떤 캐시를 쓸까? (0) | 2024.03.14 |
Java Spring 으로 백엔드 개발 시 지양해야할 코딩 패턴: (1) | 2024.03.14 |
네이버 코딩컨벤션 적용하기 - formatter / rules / suppressions .xml (2) | 2024.03.14 |