Spring Boot, Redis 저장소에 CRUD 로직 구현하기

개요

  • Redis는 바이너리 데이터 저장에 최적화된 인메모리 Key-Value 스토어로 RDBMS를 제외한 데이터 저장소 중 가장 유명하고 널리 쓰이고 있다. 캐시 저장소로 Redis를 적절히 활용하면 API 응답 시간을 10ms 수준으로 낮출 수 있다.

목표

  • Spring Boot 기반으로 Redis 저장소에 대한 CRUD를 수행할 수 있다.
  • Java 오브젝트를 바이너리 변환된 JSONMessagePack 형식으로 Redis 저장소에 저장할 수 있다.

라이브러리 종속성 추가

프로젝트 루트의 build.gradle에 아래 내용을 추가한다.

dependencies {
    compile 'org.springframework.data:spring-data-redis'
    compile group: 'redis.clients', name: 'jedis', version: '2.9.0'
    compile group: 'org.msgpack', name: 'jackson-dataformat-msgpack', version: '0.8.13'
}
  • jackson-dataformat-msgpack 아티팩트는 MessagePack 형식을 지원하기 위한 Jackson의 확장 라이브러리이다. MessagePackJSON 문자열의 내용은 그대로 유지하면서 바이너리 형식에 최적화한 것으로 Redis와 같은 바이너리 저장소에 특화된 형식이다. 저장소의 메모리 공간을 절약할 수 있다.

@Configuration 클래스 작성

package com.jsonobject.example.config;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;

@Configuration
public class RedisConfig {

    @Bean("jedisConnectionFactory")
    public JedisConnectionFactory jedisConnectionFactory() {

        JedisConnectionFactory factory = new JedisConnectionFactory();
        factory.setHostName({HOST_NAME});
        factory.setPort({PORT});
        factory.setPassword({PASSWORD});
        factory.setUsePool(true);

        return factory;
    }

    @Bean("stringRedisTemplate")
    public StringRedisTemplate stringRedisTemplate(@Qualifier("jedisConnectionFactory") JedisConnectionFactory jedisConnectionFactory) {

        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(jedisConnectionFactory);

        return template;
    }

    @Bean("messagePackRedisTemplate")
    public RedisTemplate<String, byte[]> messagePackRedisTemplate(@Qualifier("jedisConnectionFactory") JedisConnectionFactory jedisConnectionFactory) {

        RedisTemplate<String, byte[]> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setEnableDefaultSerializer(false);

        return template;
    }

    @Bean("messagePackObjectMapper")
    public ObjectMapper messagePackObjectMapper() {

        return new ObjectMapper(new MessagePackFactory()).registerModule(new JavaTimeModule()).disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
}
  • Redis는 기본적으로 Key, Value를 바이트의 배열로 저장한다. StringRedisTemplate를 사용하면 Key, Value 모두 문자열로 저장할 수 있다.
  • RedisTemplateRedis 저장소에 오브젝트를 저장할 때 기본값으로 정의된 JdkSerializationRedisSerializer을 이용한다. 따라서 해당 오브젝트는 반드시 java.io.Serializable 인터페이스를 구현해야 한다. 이 방식의 문제점은 다른 언어 환경에서 Redis 저장소에 접근할 경우 값을 인식하지 못한다는 것이다. 또한 오브젝트의 클래스 메타 정보를 저장하다보니 크기 또한 커진다. 특정 언어에 종속시키지 않으려면 저장되는 값으로 JSON 문자열 또는 MessagePack 형식을 고려해야 한다. 이를 위해 messagePackObjectMappermessagePackRedisTemplate 빈을 작성했다.

@Repository 클래스 작성

  • 아래는 토큰 오브젝트를 MessagePack 형식으로 Redis 저장소에 저장하고 불러오는 예제이다.
@Repository
public class TokenDAO {

    @Autowired
    @Qualifier("messagePackRedisTemplate")
    private RedisTemplate<String, byte[]> messagePackRedisTemplate;

    @Autowired
    @Qualifier("messagePackObjectMapper")
    private ObjectMapper messagePackObjectMapper;

    public void createToken(Token token) {

        messagePackRedisTemplate.opsForValue().set("token:" + token.getToken(), messagePackObjectMapper.writeValueAsBytes(accessToken));
    }

    public Token getToken(String token) {

        return messagePackObjectMapper.readValue(messagePackRedisTemplate.opsForValue().get("token:" + token, Token.class);
    }
}
  • createToken()을 통해 앞서 정의된 messagePackObjectMapper 빈을 이용하여 Java POJO 오브젝트를 플랫폼 중립적인 순수한 MesssagePack 형식으로 저장한다. JSON 문자열 대비 2/3 절약된 크기로 오브젝트를 저장할 수 있으며 어떤 언어에서도 해당 값에 접근하고 해석할 수 있다.

  • 마찬가지로 getToken()을 통해 저장된 MessagePack 값을 오브젝트로 변환하여 불러온다.

일치하는 패턴의 Key 이름 조회

  • Redis는 초고속의 Key-Value 저장소로서 조회 및 검색 관점에서 RDBMS와 비교하면 제공하는 기능이 매우 단순하고 제한적이다.
  • 특히 특정 패턴과 일치하는 Key 이름을 조회하는 것은 성능 문제로 운영 환경에서 가장 지양해야 하지만 불가피하게 필요할 때가 있다.
  • 가장 먼저 아래는 StringRedisTemplate을 이용하여 KEYS 명령을 수행하는 예제이다. USER_ID:로 시작하는 Key 이름을 한 번에 조회한다. KEYS 명령이 실행되면 응답이 완료되기 까지 다른 요청은 모두 블록되기 때문에 운영 환경에서 절대 사용을 자제해야할 안티 패턴으로 취급된다.
// KEYS "USER_ID:*"
Set<String> keys = stringRedisTemplate.keys("USER_ID:*");
Iterator<String> it = redisKeys.iterator();
while (it.hasNext()) {
    System.out.println(it.next()); // 조회된 Key의 이름을 출력
}
  • 아래는 SCAN 명령을 수행하여 위와 동일한 결과를 출력하는 예제이다. SCAN 명령은 커서를 기반으로 부분적으로 데이터를 조회하기 때문에 KEYS보다는 운영 환경에 미치는 부담이 상대적으로 적다.
// SCAN 0 MATCH "USER_ID:*" COUNT 10
RedisConnection redisConnection = null;
try {
    redisConnection = stringRedisTemplate.getConnectionFactory().getConnection();
    ScanOptions options = ScanOptions.scanOptions().match("USER_ID:*").count(10).build();
    Cursor<byte[]> cursor = redisConnection.scan(options);
    while (cursor.hasNext()) {
        System.out.println(new String(cursor.next())); // 조회된 Key의 이름을 출력
    }
} finally {
    redisConnection.close();
}

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

'SW 개발 > Spring > Jedis' 카테고리의 다른 글

Spring Boot, Redis 저장소에 CRUD 로직 구현하기  (0) 2017.09.04