본문 바로가기
Study/Spring Boot

[Spring Boot] Spring Data Redis 사용해보기

by DevJaewoo 2022. 9. 4.
반응형

Redis LOGO

Intro

Github나 다른 블로그를 돌아다니며 남들이 진행한 프로젝트를 보다 보면 백엔드 구조에 Redis란 것을 자주 볼 수 있다.

주로 캐시 데이터나 세션을 저장하는 용도로 사용되는데, 이번 시간에는 Redis가 무엇인지, 어떤 특징을 갖고 있는지, 언제 사용하는지에 대해 알아보고, 간단한 예제를 통해 직접 사용해보자.


Redis란?

Remote Dictionary Server의 약자로, Key-Value 구조의 비정형 데이터를 저장하고 관리하기 위한 DBMS이다.

인메모리 방식의 데이터 저장소로, 일반적인 DB에 비해 속도가 빠르다.


Redis의 특징

  • 데이터를 디스크가 아닌 메모리에서 처리하기 때문에 속도가 매우 빠르다.
  • String, Set, Sorted Set, Hash, List와 같이 다양한 데이터 타입을 지원한다.
  • Single Thread 구조이기 때문에 처리 시간이 긴 요청이 들어올 경우 해당 요청을 처리할 때 까지 다른 요청도 응답을 받을 수 없다.
  • Master Redis 서버의 데이터를 Slave Redis 서버에 복제할 수 있다.
  • 메시지 큐 용도로도 사용할 수 있다.

Redis를 사용하는 경우

모종의 이유로 서버를 여러 대 두고 로드밸런싱을 시켜야 하는 상황이라고 가정해보자.

 

사용자 A의 로그인 요청을 첫번째 서버에 서버해서 처리했을 경우, 사용자 A의 세션 정보는 첫번째 서버에만 저장되어 있을 것이다.

 

그런데 만약 사용자 A가 로그인 후 다른 요청을 보냈는데, 하필 그 타이밍에 서버1에 부하가 높아 로드밸런서가 서버2로 요청을 넘겨줬다면 어떻게 될까?

 

서버2는 사용자A에 대한 세션 정보를 갖고 있지 않아 로그인 되지 않은 사용자로 인식하고 다시 로그인을 시킬 것이다.

 

운이 좋으면 사용자A의 요청이 계속 서버1에만 할당되어 서비스를 정상적으로 이용할 수 있겠지만, 그렇지 않은 경우 뭐 하나 할 때마다 수시로 로그인을 해야 할 것이다.

 

이런 현상을 방지하기 위해선 서버끼리의 세션 정보를 공유해야 하는데, 총 3가지 방법이 존재한다.

 

세션 클러스터링

한 서버에서 세션을 생성할 경우 다른 모든 서버에 방금 생성한 세션 정보를 보내 동기화 하는 방식이다.

 

세션이 생성될 때마다 다른 서버에 보내줘야 하기 때문에서버 메모리에 큰 부담이 간다.

 

서버가 늘어나면 늘어날 수록 세션을 보내야 할 서버도 늘어나기에

자칫하면 서버 부하를 줄이려다 오히려 늘어나는 혹 떼려다 혹 붙이는 꼴이 될 수도 있다.

 

Sticky Session

로드밸런서에서 사용자의 세션이 생성됐던 서버에 해당 사용자의 요청을 계속 보내주는 방식이다.

 

예를 들어 사용자 A의 세션이 서버1에서 생성되고, 사용자 B의 세션이 서버3에서 생성됐을 경우

로드밸런서가 앞으로 사용자 A에게서 오는 모든 요청은 서버 1로 보내주고, 사용자 B에게서 오는 요청은 서버3으로 보내준다.

 

세션 클러스터링이 서버를 괴롭히는 방식이었다면 Sticky Session은 로드밸런서를 괴롭힌다.

또한 한 서버에서 로그인 처리가 많이 됐다면, 해당 서버로 부하가 집중된다.

 

Redis 세션 클러스터링

Redis 서버를 따로 두어 Redis 서버에 모든 세션 데이터를 저장하는 방식이다.

서버는 Redis 서버에만 세션 데이터를 넣으면 되니 부담이 줄어들고, 로드밸런서도 사용자를 일일이 기억할 필요가 없어 부담이 줄어드는, 서버와 로드밸런서 모두가 행복해지는 방식이다.

 

이런 식으로 공유 데이터를 저쟝하기 위해 사용할 수 있다.

이외에도 메시지 큐로 사용할 수도 있다고 한다.


Redis 환경 구성

간단한 예제를 통해 Redis를 직접 사용해보자.

소스코드는 Github에 등록 되어있다.

 

GitHub - DevJaewoo/blog-code

Contribute to DevJaewoo/blog-code development by creating an account on GitHub.

github.com

 

Redis 설치

예제 환경은 Docker에 구성했다.

아래의 명령어로 Redis 컨테이너를 실행시켜주자. 정상적으로 실행됐다면 redis_server 컨테이너가 생성됐을 것이다.

docker run --name redis_server -it -d -p 6379:6379 redis

 

Docker 설치 및 사용법은 아래의 글에서 확인할 수 있다.

 

[Docker] 도커 간단 사용법

도커 간단 사용법 이전 글에서 Docker를 설치하는 방법에 대해 알아봤다. 이번엔 Docker의 이미지, 컨테이너 관리 방법에 대해 알아보자. [Docker] 도커 설치하기 도커 설치 및 간단한 사용법 이번

devjaewoo.tistory.com

 

실행 후 Docker Desktop이나 명령어를 실행해 로그를 확인하면 아래와 같은 메시지가 보일 것이다.

docker logs -f redis_server

docker redis log

 

프로젝트 생성

아래와 같이 Dependency를 추가하여 프로젝트를 생성하자.

H2는 없어도 되지만 혹시 몰라서 넣었다.

프로젝트 생성

 

Redis 컨테이너 연결 정보 설정을 위해 application.yml을 수정한다.

spring:
  redis:
    host: localhost
    port: 6379

 

그 다음 위의 연결 정보를 갖고 연결을 수립해주는 RedisConnectionFactory Bean을 등록하자.

Redis 구현체로는 Jedis와 Lettuce가 있는데, Lettuce가 성능이 더 좋아 많이 사용된다고 한다.

@Configuration
@RequiredArgsConstructor
public class RedisConfig {

    private final RedisProperties redisProperties;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
    }
}

 

작성했으면 서버가 에러 없이 잘 실행되는지 확인해보자.


Redis 사용해보기

Redis는 클래스 단위로 저장하는 RedisRepository와 String, List, Set, Hash 등의 단위로 저장하는 RedisTemplate 총 2가지 사용방법이 있다.

 

우선 RedisRepository를 통한 클래스 저장 / 조회 방법부터 알아보자.

 

RedisRepository 사용해보기

Entity

@Getter
@RedisHash(value = "member", timeToLive = 3600)
public class Member {

    @Id
    private String id;
    private String name;
    private int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

 

@RedisHash를 통해 Redis에 저장할 클래스를 생성한다.

value는 Redis에 들어갈 객체의 key가 되며, 데이터를 넣으면 [key]:[entity id]와 같이 Id가 설정된다.

timeToLive에는 객체가 만료되는 시간을 설정한다. seconds 단위이고, 만료되지 않게 하려면 -1L을 설정하면 된다.

주의해야 할 점은 JPA와 다르게 @Id org.springframework.data.annotaion.Id이다. javax.persistence.Id로 import 하면 실행 시 에러가 발생한다.

 

Repository

public interface MemberRedisRepository extends CrudRepository<Member, String> {
}

Entity를 Redis에 CRUD할 수 있는 Repository다. Spring Data JPA의 Repository와 비슷하다.

 

테스트 코드

@SpringBootTest
public class RedisRepositoryTest {

    @Autowired MemberRedisRepository memberRedisRepository;

    @Test
    public void save() throws Exception {
        //given
        Member member = new Member("member", 23);
        memberRedisRepository.save(member);

        //when
        Member result = memberRedisRepository.findById(member.getId()).orElseThrow();

        //then
        Assertions.assertThat(result.getName()).isEqualTo(member.getName());
    }

    @Test
    public void delete() throws Exception {
        //given
        Member member = new Member("member", 23);
        memberRedisRepository.save(member);

        //when
        memberRedisRepository.delete(member);
        Member result = memberRedisRepository.findById(member.getId()).orElse(null);

        //then
        Assertions.assertThat(result).isNull();
    }
}

RedisRepository를 사용해 Member를 저장 / 삭제 해보는 테스트 코드이다.

JPA와 사용방법이 동일한 것을 볼 수 있다.

 

테스트가 정상적으로 수행되는지 확인하고, redis-cli를 열어 데이터가 저장됐는지 확인해보자.

id는 상황에 맞게 바꿔야 한다.

docker exec -it redis_server redis-cli

127.0.0.1:6379> keys *

127.0.0.1:6379> type member
127.0.0.1:6379> smembers member

127.0.0.1:6379> type member:f5bbfccb-033f-4e70-919a-434735c2d5fc
127.0.0.1:6379> hgetall member:f5bbfccb-033f-4e70-919a-434735c2d5fc

Redis 데이터 확인

Set 형태의 member와, Hash 형태의 member:id 객체를 볼 수 있다.

  • member: Member들의 id를 저장한다.
  • member:id: Member 객체를 Hash 구조로 저장한다. 

RedisTemplate 사용해보기

이번엔 클래스가 아닌 자료구조 형태로 저장해보자.

RedisTemplate를 사용하기 위해선 Config에 Bean으로 등록해줘야 한다.

 

@Configuration
@RequiredArgsConstructor
public class RedisConfig {

    private final RedisProperties redisProperties;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }
}

 

테스트 코드

@SpringBootTest
public class RedisTemplateTest {

    @Autowired RedisTemplate<String, Object> redisTemplate;

    @Test
    public void opsForValue() throws Exception {
        //given
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        String key = "stringKey";
        String value = "stringValue";

        valueOperations.set(key, value);

        //when
        String result = (String) valueOperations.get(key);

        //then
        Assertions.assertThat(result).isEqualTo(value);
    }

    @Test
    public void opsForList() throws Exception {
        //given
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        String listKey = "listKey";

        List<Long> list = List.of(1L, 2L, 3L, 4L, 5L);

        //when
        for(Long l : list) {
            listOperations.leftPush(listKey, l);
        }

        //then
        for (Long l : list) {
            Long result = (Long) listOperations.rightPop(listKey);
            Assertions.assertThat(result).isEqualTo(l);
        }
    }

    @Test
    public void opsForSet() throws Exception {
        //given
        SetOperations<String, Object> setOperations = redisTemplate.opsForSet();
        String setKey = "setKey";

        //when
        setOperations.add(setKey, "h", "e", "l", "l", "o");
        Set<String> result = setOperations.members(setKey).stream().map(it -> (String) it).collect(Collectors.toSet());

        //then
        Assertions.assertThat(result).containsOnly("h", "e", "l", "o");
    }

    @Test
    public void opsForHash() {
        //given
        HashOperations<String, Object, Object> hashOperations = redisTemplate.opsForHash();
        String key = "hashKey";

        Map<String, String> map = new HashMap<>();
        map.put("key1", "value1");
        map.put("key2", "value2");
        map.put("key3", "value3");

        hashOperations.putAll(key, map);

        //when
        Map<Object, Object> resultMap = hashOperations.entries(key);
        String resultValue = (String) hashOperations.get(key, "key1");

        //then
        Assertions.assertThat(resultMap).containsAllEntriesOf(map);
        Assertions.assertThat(resultValue).isEqualTo("value1");
    }
}

Value, List, Set, Hash에 대한 테스트 코드이다.

RedisTemplate에서 자료구조에 대한 Operations를 받아 사용할 수 있다.

사용 가능한 Data Type과 그에 해당하는 Operations 종류는 다음과 같다.

Data Type Method Operations
Value opsForValue() ValueOperations
List opsForList() ListOperations
Set opsForSet() SetOperations
ZSet (Sorted Set) opsForZSet() ZSetOperations
Hash opsForHash() HashOperations
Stream opsForStream() StreamOperations
Geo opsForGeo() GetOperations
HyperLogLog opsForHyperLogLog() HyperLogLogOperations
Cluster opsForCluster() ClusterOperations

 

테스트를 실행하여 Fail되는 것이 없는지 확인해보자.

테스트 결과


참고자료

반응형