Redis
레디스(Redis)는 "REmote DIctionary System"의 약자로 메모리 기반의 Key/Value Store 이다. 고성능 key-value 저장소로서 List, Hash, Set, Sorted Set 등 여러 형식의 자료구조를 지원한다. Redis는 메모리에 위치해있기 때문에 디스크보다 훨씬 빠르다. 그래서 RDBMS의 캐시 솔루션으로서 주로 사용되고 있다.
단순한 메모리 기반의 Key/Value Store인 memcached 오픈소스와는 달리 다양한 자료구조를 지원한다는 것이 캐시로서 Redis의 큰 장점이다. (자세한 비교)
Redis 실행하기
docker image로 redis를 받아와서 실행해주면 된다.
- redis 디폴트 폴트: 6379
- docker hub _Redis
$ docker pull redis # redis 이미지 받기
$ docer images # redis 이미지 확인
$ docker run -p 6379:6379 --name some-redis -d redis # redis 시작하기
$ docker ps # redis 실행 확인
Spring에서 Redis로 Cache 사용하기
Spring Data Redis에서 데이터를 set, get 하는 방법은 크게 2가지가 있다.
- CrudRepository 를 이용하는 방법 ( high level API, JPA 처럼 사용 )
- RedisTemplate 를 이용하는 방법 ( low level API )
Spring 프로젝트 생성
Spring redis cache를 사용하기 위해 프로젝트를 생성한다. DB와 Cache 둘 다 사용하여 비교해볼 것이기 때문에 Mysql Connector도 추가하였다.
spring-data-redis 의존성 추가
이미 생성되어 있는 기존 스프링 프로젝트에서는 아래 의존성을 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
Redis Config
스프링 앱이 캐싱 기능을 사용할 수 있게 @EnablaeCaching 애노테이션을 추가해준다. spring boot 2.0 이상부터는 jedis 가 아닌 lettuce를 이용해서 redis 에 접속하는게 디폴트이다. jedis, lettuce 모두 redis 접속 connection pool 관리 라이브러리이며, lettuce 가 성능이 더 좋기에 디폴트로 세팅되어 있다.
@Configuration
@EnableCaching
public class RedisConfig {
}
Application.yml
기본 설정으로 host: localhost, port: 6379는 설정되어있지만 명시적으로 일단 작성해준다.
spring:
redis:
host: locahost
port: 6379
회원(Member)의 장바구니(Cart)에 아이템(Item)을 1시간 동안 보관하고 있는 비즈니스 로직을 작성해보자.
1. CrudRepository (high level)
Member 도메인 생성
Redis는 byte code로 데이터를 담기 때문에 Serializable을 구현해줘야 한다.
@Entity
@Data
public class Member implements Serializable{
public enum Gender {
MALE, FEMALE
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Gender gender;
private int grade;
}
MemberRepository 생성
@Repository
public interface MemberRepository extends CrudRepository<Member, Long> {
}
MemberController 생성
@RestController
@RequestMapping("/member")
@RequiredArgsConstructor
@Slf4j
public class MemberController {
private final MemberRepository memberRepository;
@PostMapping
public Member save(@RequestBody Member member){
Member saveMember = memberRepository.save(member);
log.info("save to DB : " + member);
return saveMember;
}
@GetMapping("/{id}")
@Cacheable(key = "#id", cacheNames = "member")
public Member getMember(@PathVariable Long id){
Member findMember = memberRepository.findById(id).orElseThrow(() -> new RuntimeException(id + " 멤버는 존재하지 않습니다."));
log.info("Member fetching from DB : " + id);
return findMember;
}
}
멤버 저장 및 조회
1) 저장하기
쓰기 작업은 캐시의 유무와 상관없이 멤버 객체를 DB에 저장해준다.
2) 멤버 조회 (Cache Miss)
처음 멤버를 조회할 때는 캐시에 저장된 데이터가 없기 때문에 DB에서 데이터를 가져온다. Controller에 작성한 그대로 log가 생기는 것을 확인할 수 있다. 그리고 반환되는 데이터를 @Cacheable에 명시된 키와 값대로 Redis에 저장한다.
redis-cli monitor 화면
"HELLO"는 연결되었다는 뜻이다. 먼저 캐시에 데이터가 있는지 "GET"으로 조회해보고 데이터가 존재하지 않는 것(Cache Miss)을 확인하고 DB에서 읽어온다. 그리고 DB에서 읽어온 데이터를 "SET"으로 저장한다.
3) 멤버 조회 (Cache Hit)
캐시에 저장된 이후 부터는 Sql Log가 더 이상 남지 않는다. 이젠 DB가 아닌 Cache에서 데이터를 읽어오기 때문이다. @Cacheable은 Cache Hit이 될 경우 메서드를 실행시키지 않고 바로 캐시 데이터를 반환시킨다.
읽기 속도의 차이는 확연하다. 디스크 읽기는 622ms가 걸린 반면 메모리 읽기는 9ms (약 69배) 밖에 걸리지 않았다.
redis-cli monitor 화면
멤버를 조회할 때마다 "GET"으로 데이터를 가져오는 것을 확인할 수 있다. 원래 메모리 관리를 위해 TTL을 설정해줘야 하지만 일단은 생략했다.
2. RedisTemplate (low level)
RedisTemplate은 Redis와 상호작용하도록 높은 수준으로 추상화를 제공하는 클래스로 다양한 Redis 연산, 예외 변환, 트랜잭션, 직렬화 커스텀 기능을 제공한다.
- Redis 트랜잭션 기능: Redis는 싱글 쓰레드, Atomic한 자료구조로 RaceCondition을 피할 수 있다. Redis 트랜잭션 기능 좀 더 큰 단위로 그러한 Atomic한 여러 명령어들을 한 묶음으로 묶어주는 기능이다. MULTI → commands → EXEC/DISCARD
이제 RedisTemplate으로 장바구니 아이템을 캐싱해보자.
Redis Config
ConnectionFactory는 lettuce로 설정한 다음 RedisTemplate 빈을 등록해준다.
Serializer
- JdkSerializationRedisSerializer: 디폴트로 등록되어있는 Serializer이다.
- StringRedisSerializer: String 값을 정상적으로 읽어서 저장한다. 그러나 엔티티나 VO같은 타입은 cast 할 수 없다.
- Jackson2JsonRedisSerializer(classType.class): classType 값을 json 형태로 저장한다. 특정 클래스(classType)에게만 직속되어있다는 단점이 있다.
- GenericJackson2JsonRedisSerializer: 모든 classType을 json 형태로 저장할 수 있는 범용적인 Jackson2JsonRedisSerializer이다. 캐싱에 클래스 타입도 저장된다는 단점이 있지만 RedisTemplate을 이용해 다양한 타입 객체를 캐싱할 때 사용하기에 좋다
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory(){
return new LettuceConnectionFactory();
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(ItemDto.class);
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
// redisTemplate.setValueSerializer(new StringRedisSerializer());
// redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
return redisTemplate;
CartDao 및 ItemDto 생성
캐시에 저장할 Item 객체를 만들어준다.
@Data
public class ItemDto implements Serializable {
private String name;
private int price;
private int quantity;
}
그리고 DB에 접근하는 클래스를 Repository, Cache에 접근하는 클래스를 Dao로 구분지어줬다. CartDao에는 아이템을 저장하는 addItem 메서드와 장바구니를 조회하는 findByMemberId 메서드를 생성해준다.
- 요구사항으로 장바구니에 아이템이 보관되는 시간은 1시간으로 가정한다.
- 저장할 때 값이 primitve type만 사용하는 경우가 아니고 엔티티 객체나 VO를 파라미터로 사용한다면 KeyGen을 생성하여 key를 생성해서 사용하는 게 좋다.
@Repository
@RequiredArgsConstructor
public class CartDao {
private final RedisTemplate<String, Object> redisTemplate;
public void addItem(ItemDto itemDto, Long memberId){
String key = KeyGen.cartKeyGenerate(memberId);
redisTemplate.opsForValue().set(key, itemDto);
redisTemplate.expire(key, 60, TimeUnit.MINUTES);
}
public ItemDto findByMemberId(Long memberId){
String key = KeyGen.cartKeyGenerate(memberId);
return (ItemDto) redisTemplate.opsForValue().get(key);
}
}
// /util/KeyGen.java
public class KeyGen {
private static final String CART_KEY = "cart";
public static String cartKeyGenerate(Long memberId){
return CART_KEY + ":" + memberId;
}
}
CartController 생성
@RestController
@RequestMapping("/cart")
@RequiredArgsConstructor
@Slf4j
public class CartController {
private final CartDao cartDao;
@PostMapping("/{id}")
public String save(@PathVariable(name = "id") Long memberId, @RequestBody ItemDto itemDto){
cartDao.addItem(itemDto, memberId);
log.info("save cart to cache :" + memberId +" - [" + itemDto + "]");
return "success caching";
}
@GetMapping("/{id}")
public Object getByMemberId(@PathVariable(name = "id") Long memberId){
log.info("find cart by member id :" + memberId);
return cartDao.findByMemberId(memberId);
}
}
장바구니에 아이템 저장 및 조회
1) 저장하기
정상적으로 Redis에 저장되는 것을 확인할 수 있다.
redis-cli monitor
ttl 명령어로 cart:1 컬렉션에 설정된 시간을 조회해보면 정상적으로 60분이 적용된 것을 확인할 수 있다.
2) 조회하기
캐시에 저장된 데이터가 정상적으로 조회되는 것을 확인할 수 있다.
redis-cli monitor
참고
https://spring.io/projects/spring-data-redis
https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#redis
https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache
https://techblog.woowahan.com/2551/
https://yakolla.tistory.com/46
'Dot Programming > Spring' 카테고리의 다른 글
[Spring] 어디서든 실행 가능한 Redis 통합 테스트 환경 구축하기 (TestContainers) (0) | 2022.04.19 |
---|---|
[Spring] CM4SB와 Jmeter로 API 응답 지연 테스트하기 (카오스 엔지니어링) (0) | 2022.04.18 |
[Spring] Spring에서 Local-Memory로 간단하게 Cache 사용하기 (0) | 2022.04.17 |
[Spring] TestContainers로 멱등성있는 MySql 테스트 환경 구축하기 (0) | 2022.04.03 |
생산성 향상을 위해 반드시 테스트 코드를 작성하자 (0) | 2022.03.28 |