Java Spring - Cache 도입기 (2 / 2): Redis 적용기

2024. 3. 14. 23:41자바/자바스프링

반응형

 

#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 에 저장해야 되는 데이터 기준

  • 어떤 데이터를 캐시에 저장해야할 까
  1. 자주 접근하는 데이터들
  2. 읽기 중심의 변경 빈도가 낮은 데이터
    • 가변성이 잦을 때 캐시화 하기 불리하다.
    • 해당 데이터들에 대해 변경이 있을 때 캐시에서도 수정 / 삭제를 해야하기에 이는 오히려 속도 저하를 유발할 수 있기 때문이다.
  3. 계산, 전송 비용이 높은 데이터
    • 해당 데이터들은 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)

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

#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) {
    // 메서드 구현
}

 

반응형