#Redis

Redis 알아보기

이글은 Redis에 대해 들어봤거나 어렴풋이 알고 있는 독자들을 대상으로 작성하였습니다.

우테코 레벨 3 에서 프로젝트를 진행하면서 Redis를 적용해보게 되었다. Redis라는 단어를 많이 들어봤고 어렴풋이 캐싱을 통하여 속도를 빠르게 해준다는 것을 들어봤었다. 하지만 어떻게 쓰고 왜 속도가 빠른지에 대해서는 알지 못했었다. 이번 글을 통하여 Redis에 대해 좀 더 깊게 알아보자. Redis를 알기 전에 캐시의 개념을 먼저 알아야 한다.

캐시는 무엇일까?

캐시라는 것이 무엇일까? 개발하면서 무언가 수정을 하고 다시 새로 고침을 눌렀을 때 수정한 부분이 전혀 반영되지 않아서 당황한 경험이 있을 것이다. 이때 우리는 강력 새로 고침을 통해 캐시를 비워주곤 했다. 이처럼 캐시는 요청된 결과를 미리 저장해두어서 빠르게 데이터를 전달하기 위해서 사용한다. CPU에만 캐시가 있다고 오해를 할 수 있는데 캐시는 좀 더 빠른 속도를 위하여 다른 저장공간에 캐싱해둔다면 모두 캐시라고 할 수 있다. 예를 들면 웹 서버에서 빠르게 정보를 로딩하기 위해 DB가 아닌 다른 장소(ex. Browser Caches)에 저장하는 것도 캐시이다.

웹 서비스의 구조를 간략하게 보면 아래와 같을 것이다.

image

DB에는 수많은 데이터가 저장되어 있고 이러한 것들은 클라이언트가 요청하면 꺼내 클라이언트에게 전달해준다. 이때 수많은 클라이언트의 요청으로 데이터를 전달하는데 많은 시간이 걸려 클라이언트가 불만을 표출 했다고 생각해보자. 문제를 해결하기 위해 조사를 해보니 클라이언트가 요청하는 데이터의 80%는 DB 데이터의 일부인 20% 만 차지할 뿐 이였다. 그래서 이를 해결하기 위해 캐시를 도입하였다.

image

이제 웹서버는 DB에 데이터를 확인하기 전에 캐시를 먼저 조회하여 있다면 캐시에서 정보를 가져오게 된다. 캐시는 일반적으로 조회속도가 DB에서 조회하는 것보다 빠르기 때문에 캐시에 데이터가 있다면 기존의 속도 보다 더 빠르게 요청을 처리할 수 있다.

프로젝트에서 캐시 도입 배경

레벨 3 기간동안 진행한 프로젝트는 여러 사람의 만나기 좋은 중간지점을 찾아주는 서비스였다. 이때 각 사용자마다 위치정보를 통해 중간값을 찾은 후 중간 위치의 근처의 역정보들을 활용하여 해당 역까지 걸리는 교통정보를 활용하여 모든 사람에게 공평한 위치를 찾아주는 로직을 구현하였다. 서울에는 수많은 역이 있다 보니 로직을 수행하는 데 많은 시간이 걸렸고 처음 로직을 완성 하였을 때 1분 이상의 시간이 걸렸다. 사용자 입장에서는 단순히 중간 지점을 찾기 위해 서비스를 이용하고 있는데 이런 서비스가 1분 이상이 걸린다면 우리의 서비스를 이용할 사람은 아무도 없다는 생각이 들었다. 그래서 우리 팀은 캐시를 통해 문제를 해결해보기로 하였다.

Redis란?

Redis는 Remote Dictionary Server의 약자다. “key-value” 구조로 데이터를 저장하고 관리하기 위한 비 관계형 데이터베이스로 모든 데이터를 메모리에서 처리하는 메모리 기반 DB이다. 메모리를 통해 사용하기 때문에 일반적인 DB보다 빠른 성능을 보여준다.

Redis의 적용

프로젝트에서 경로를 찾아올 때 캐시에서 이야기했던 것처럼 Redis에서 존재하면 그 값을 반환하였고 아니라면 로직을 처리하고 그 결과를 Redis에 저장하였다. 아래는 실제 Redis를 적용한 코드이다. Redis에 먼저 조회를 하여 데이터가 없으면 로직을 계산하고 Redis에 데이터를 저장하였다.

private List<PathTransferResult> calculatedPathTransferResults(Points points,
        UtilityResponse response, Point target) {
        return points.getPoints().parallelStream()
            .map((source) -> new PathTransferResult(
                source,
                target,
                pathResultRedisRepository.findById(source.toString() + target)
                    .orElseGet(
                        () -> saveRedisCachePathResult(source, target, response.getPlaceName())))
            )
            .collect(Collectors.toList());
 }

private PathResult saveRedisCachePathResult(Point source, Point target, String placeName) {
        PathResult result = minPathResult(source, target, placeName);
        pathResultRedisRepository.save(result);
        return result;
}

Redis는 마치 JPA를 사용하는 것처럼 사용할 수 있었다. 아래는 RedisRepository를 나타낸다. 인터페이스를 구현하는 클래스를 만들지 않아도 pathResultRedisRepository.save(result) 와 같이 사용할 수 있다.

public interface PathResultRedisRepository extends CrudRepository<PathResult, String> {

}

이번에는 간단하게 Redis를 사용하기 위해 Bean으로 등록하는 코드를 살펴보자.

@Configuration
@EnableRedisRepositories
public class RedisConfig {

    @Value("${redis.host}")
    private String redisHost;

    @Value("${redis.port}")
    private int redisPort;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);
    }

    @Bean
    public RedisTemplate<?, ?> redisTemplate() {
        RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }
}

Redis의 데이터 구조

Redis는 관계형 데이터베이스가 아니다. INT, DATETIME 등 일반적으로 관계형 데이터베이스에서 사용할 수 있었던 데이터 타입은 사용할 수 없다. 오직 String, Hash, List, Set 타입 만을 사용할 수 있다.

  • 문자열(String)
  • 해시(Hash)
  • 리스트(List)
  • 셋(Set)

Redis 스키마

Redis는 관계형 데이터베이스가 아닌 key-value를 활용한 비 관계형 데이터베이스이다. 따라서 일반적인 데이터베이스와의 스키마 구조와는 다르다. : 을 이용하여 데이터를 구분하여 스키마를 구성한다.

사용자의 이메일, 닉네임, 최근 접속 시간을 저장한다고 생각해보자. “redis@redis.com”, “redis”, “Sep-02-2021 22:30:10” 라는 데이터를 저장하려고 한다면 아래와 같이 저장하면 된다.

  • user:userid:email:redis@redis.com
  • user:userid:nickname:redis
  • user:userid:lastlogin:Sep-02-2021 22:30:10

위와 같은 스키마로 구성하여 사용자의 아이디만 알면 이메일, 닉네임 등을 알 수 있다.

Redis와 Memcached

결론부터 말하면 우리팀은 Redis를 통하여 문제를 해결하였다. 1분 이상 걸리던 것을 10초대로 줄일 수 있었고 다음에 멀티 스레드를 통하여 시간을 더욱더 단축할 수 있었다. 그렇다면 왜 Redis를 사용하였을까? 우리가 선택할 수 있는 선택지들중 대표적인 Memcached와 Redis를 비교해보자.

Redis

  • “key-value” 구조의 인 메모리 비 관계형 데이터베이스
  • 메모리와 디스크에 저장되어 영속성을 지원한다
  • 쓰기 성능 증대를 위한 샤딩을 지원
  • String, Set, Sorted Set, Hash, List의 데이터 형식을 지원

Memcached

  • 데이터가 메모리에만 저장된다. 데이터는 프로세스가 종료되면 사라진다
  • 문자열만 지원
  • 만료일을 지정하여 만료되면 데이터가 사라짐
  • 처리속도가 빠르다
  • 저장소 메모리 재사용

사실 두 가지의 특징을 비교 하였을 때 우리 서비스에서 꼭 Redis를 사용해야한다 라고 단정 지을만한 이유는 없었다. 하지만 아래와 같은 차이점으로 우리 팀은 Redis를 선택하였다.

  • Memcached 보다 훨씬 많은 관련 문서
  • 영속성을 줄 수 있다
  • Replica 를 통한 master, slave구조를 구현 가능
  • 다양한 자료타입

아직 서비스의 이용자가 많지 않은 시점에서는 당장 필요하다고는 할 수 없다. 하지만 우리의 서비스 이용자가 계속 늘어난다면 위와 같은 사항들은 많은 도움이 될 것이라 판단하였다.

정리

이번 글은 프로젝트를 진행하면서 캐시에 대한 필요성을 느꼈고 Redis를 통하여 문제를 해결하는 것을 적었다. Redis는 JPA를 사용하는 것과 매우 비슷한 느낌을 받았고 매우 쉽게 적용을 할 수 있다는 느낌을 받았다. 독자 여러분들도 무엇인가 데이터를 더 빠르게 반환하고 싶다면 Redis를 한번 고려해보면 좋을 것 같다.

참고