이전에 LocalCache 를 사용해서 Cache 서비스를 구현한 적이 있었습니다.
이번에는 이후에 MSA 서비스 확장성을 고려해서 Global Cache 를 사용하자는 요구사항이 나와서 Redis 를 사용해 Global Cache 를 구현해보겠습니다.
Local 환경에 Redis 설치와 실행이 끝났다는 가정하에 쓴 글입니다.
아직 설치가 되지 않았다면 해당 블로그를 참고해주세요. (mac)
🧤 Config
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
redis, cache 기능을 사용하기 위해 의존성을 추가해줍니다.
application.yml
spring:
redis:
host: localhost
port: 6379
timeout: 1
어플리케이션 설정파일에 redis 관련 host 와 port 번호를 매핑해줍니다.
Application.java
@SpringBootApplication
@EnableCaching
public class DayfilmApplication {
public static void main(String[] args) {
SpringApplication.run(DayfilmApplication.class, args);
}
}
캐시 기능을 사용하기 위해 @EnableCaching 어노테이션을 추가해줍니다. 캐싱을 사용하겠다고 선언하는 의미입니다.
RedisConfig.java
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean(name = "cacheManager") // 캐시에서 Redis를 사용하기 위해 설정
// RedisCacheManager를 Bean으로 등록하면 기본 CacheManager를 RedisCacheManager로 사용함.
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues() // null value 캐시안함
.entryTtl(Duration.ofHours(timeout)) // 캐시의 기본 유효시간 설정
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // redis 캐시 키 저장방식을 StringSeriallizer로 지정
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // redis 캐시 값 저장방식을 GenericJackson2JsonRedisSerializer로 지정
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory).cacheDefaults(configuration).build();
}
}
application.yml 에서 지정한 값을 가져온 후, 캐시매니저를 만들 때 값을 매핑해 생성합니다. 자세히 알아보겠습니다.
- redisConnectionFactory() 메소드를 통해, 해당 ip 의 포트 번호의 redis 서버와 연결할 수 있는 ConnectionFactory 를 생성할 수 있습니다. Redis 통신 프레임워크는 크게 Jedis 와 Letture 두가지가 있는데 Lettuce 가 사용하기 간편하고 성능이 좋다고 알려져 Lettuce 를 사용하였습니다.
- Spring Data Redis 는 RedisTemplate 와 RedisRepository 두가지 방식으로 접근을 허용합니다. RedisTemplate 는 포괄적인 API 를 제공하는 저수준 Redis 클라이언트이며 유연한 Redis 모든 명령을 실행할 수 있다는 단점이 있습니다. 반대로 RedisRepository 는 Spring Data JPA 와 같은 미리 정의된 메소드를 통해 Redis 에 접근할 수 있습니다. 필자는 RedisTemplate 를 사용해 Redis 서버에 접근하는 방식을 선택했습니다.
- 캐시 서비스를 사용하기 위해서는 cacheManager 를 생성해야 합니다. 가장 기본인 default 설정을 통해 생성해줬으며 null 인 값들은 캐시하지 않는 설정과 유효시간, 직렬화 방식을 설정해주었습니다. 역시 여러 종류의 Serializer 가 존재하지만, 키는 String Serializer로 값은 모든 classType 을 json 으로 저장하는 Serializer 를 적용하였습니다.
🧤 캐시 적용
SelectDetailItemDto.class
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SelectDetailItemDto implements Serializable {
@ApiModelProperty(value="게시글 제목", example="캐논 ~카메라")
private String title;
@ApiModelProperty(value="카테고리(json)", example="CAMERA")
private Category category;
@ApiModelProperty(value="게시글 상세 설명", example="캐논 ~카메라는 ~~")
private String detail;
@ApiModelProperty(value="하루 렌탈가격(1일)", example="10000")
private Integer pricePerOne;
@ApiModelProperty(value="하루 렌탈가격(5일 이상)", example="8000")
private Integer pricePerFive;
@ApiModelProperty(value="하루 렌탈가격(10일 이상)", example="5000")
private Integer pricePerTen;
@ApiModelProperty(value="상품 브랜드 명", example="캐논")
private String brandName;
@ApiModelProperty(value="상품 모델 명", example="오토보이 S2")
private String modelName;
@ApiModelProperty(value="대여 방법", example="PARCEL")
private Method method;
@ApiModelProperty(value="재고", example="5")
private Integer quantity;
@ApiModelProperty(value="이미지 정보들")
private List<SelectDetailImageDto> images;
}
캐시를 사용할 Dto 를 생성합니다. 이 때 Dto 는 Serializable 를 구현해야 합니다.
Spring 에서는 어노테이션을 통해 캐싱을 제어할 수 있습니다.
- @Cacheable : 캐시가 존재하지 않을 때 메소드의 리턴 값을 캐시에 저장한다. 존재하면 메소드를 실행하지 않고 결과값을 반환한다.
- @CachePut : 보통 수정할 때 사용하며, 메소드의 리턴 값이 캐시에 없을 시 저장하고 있을 경우 갱신한다.
- @CacheEvict : 캐시를 삭제한다.
ItemServiceImpl.class
@Override
@Transactional(readOnly = true)
@Cacheable(value = "item-detail", key="#id", cacheManager = "cacheManager", unless="#result == null")
public SelectDetailItemDto selectDetailItem(Long id) {
return itemRepository.selectItem(id);
}
해당 메소드는 파라미터로 넘어온 id 값을 키로 잡고, 캐시된 데이터가 있다면 그대로 반환하고 없다면 함수의 실행 결과값을 반환하는 메소드입니다. 아이템 상세페이지에서 캐시를 적용하기로 사전에 전략을 세웠기 때문에 캐시를 적용했습니다.
@Override
@Transactional
@CachePut(value = "item-detail", key="#itemId", cacheManager = "cacheManager", unless="#result == null")
public void modifyItem(Long itemId, List<MultipartFile> images, ModifyItemRequestDto dto) {
try {
...
} catch (IOException e) {
log.debug("error message : {}", e.getMessage());
throw new CustomException("Item 수정 실패");
}
}
해당 메소드는 아이템에 대한 정보를 수정하는 메소드입니다. @CachePut 어노테이션을 통해 해당 키에 저장되있는 캐시 값이 존재하다면 값을 update 해 처리합니다.
🧤 마치며
캐시는 사용자의 만족도를 위해 서비스 내에 반드시 필요한 기능이라고 생각합니다.
캐시를 적용하는 방법은 다양하며, 역시 상황에 맞게 캐시 전략을 사용해야합니다.
각 캐시의 장단점과 서비스 내에 캐시를 적용할 항목을 확실히 구분짓는게 중요합니다.
'개인 공부 > Spring' 카테고리의 다른 글
[Spring] AWS S3 Bucket 에 이미지 등록하기 (0) | 2023.02.28 |
---|---|
[Spring] Bean Scope (0) | 2022.11.22 |
[Spring] Multi Thread (2) | 2022.11.21 |
[Spring] HashMap 으로 Cache 구현하기 (0) | 2022.11.14 |
[Spring] Redis vs EHcache vs HashMap (0) | 2022.11.09 |