[카카오 프로젝트] Redis 도입

paduck·2023년 3월 24일
0

프로젝트

목록 보기
7/11

배경

실제 채팅방 세션을 처리하는 로직을 작성해서 진행하면 거의 필수적으로 필요하겠지만, 이번 프로젝트에서는 WebRTC 와 관련한 기능을 API 로 구현할 예정이기 때문에 필요하지는 않았다.
하지만, Redis 에 대한 요구사항이 높아지고 있고 구현하려는 목적 자체가 채팅방 세션을 빠르게 불러와서 API 요청을 줄이기 위함이기 때문에 도입하게 되었다.

Redis 사용 목적

  • 인메모리 데이터베이스
    키-값 데이터 구조를 저장하므로, 인덱싱과 검색에 높은 성능 제공
    이를 이용하여, Redis는 작은 데이터베이스 또는 캐시 데이터베이스를 구축하는 데 사용
    이를 통해 빠른 응답 속도 유지 가능

  • 세션 관리
    본래 목적은 세션 데이터를 저장하기 위해 탄생
    세션 데이터는 서버 측에서 사용자 정보를 저장하는 데 사용되는데, 이전에 사용했던 세션 데이터를 다시 사용하여 세션 데이터를 효율적으로 관리할 수 있음

  • 퍼블리싱 / 구독 시스템
    메시지를 브로드캐스트하고 구독자에게 실시간으로 전송할 수 있음
    이를 이용하여, 실시간 알림 및 채팅 애플리케이션을 구축하는 데 사용

  • 분산 락
    분산 락을 구현하는 데도 사용
    여러 클라이언트가 동시에 접근할 수 있는 공유 리소스를 보호하는 데 사용되는 기술을 분산 락이라고 하는데, 이를 위해 Lua 스크립팅을 지원하며, 이를 사용하여 안전하게 분산 락을 구현 가능

요약하면 빠른 요청 처리를 위한 데이터베이스가 필요하거나, 기존 데이터베이스에서 일부 결과값(세션)을 캐싱해 서버 트래픽을 줄이거나, 공유 자원에 대하여 발생하는 경쟁 상황을 해결하거나, pub/sub 을 통해 실시간 처리가 필요할 경우에 사용한다고 볼 수 있다.

Redis 종류

이를 구현하기 위해 Redis 자체적으로 구성되는 환경 설정이 존재한다.

  • Redis standalone:
    단일 Redis 인스턴스가 하나의 서버에서 실행되는 구성으로, 데이터 복제 및 고가용성을 제공하지 않는다. 하나의 인스턴스로 인해 오류 발생시 문제가 될 수 있어 이럴 경우 주 데이터베이스 와의 동기화가 중요하다

  • Redis Master-Replica
    Redis Standalone 구성에서 데이터 복제를 추가한 형태이다.
    마스터 노드는 쓰기 작업을 처리하고, 복제본 노드는 마스터 노드에서 데이터를 복제하여 읽기 작업에 사용된다.
    마스터 노드에서만 쓰기 작업이 진행된다는 것이 핵심!

  • Redis Sentinel
    Redis Master-Replica 구성에서 고가용성을 제공하기 위한 분산 시스템이다.
    Sentinel은 Redis 마스터 노드의 상태를 모니터링하고 필요에 따라 복제본 노드를 마스터로 승격시켜 오류 발생시 대처가 효과적이다.

  • Redis Socket
    UNIX 소켓을 사용하여 다른 프로세스 또는 컴퓨터와 통신하는 Redis의 통신 방법이다.
    이 방법은 네트워크 연결이 필요하지 않으므로 더 빠르고 안정적인 통신을 제공한다.
    (해당 방법은 이론적으로만 알고 있고, 실 사용을 해보지 않아 추후 사용 상황이 주어지면 추가 학습이 가능할 것 같다)

  • Redis Cluster
    여러 Redis 노드를 사용하여 데이터를 수평으로 분할하는 구성이다.
    각 노드는 데이터 일부를 저장하고 클러스터 내의 다른 노드와 상호 작용하는데,
    그렇다 보니 고가용성과 높은 확장성을 제공한다.
    Sentinel 과의 차이점은 개발자가 마스터 노드만 확인하느냐 모든 노드를 확인하느냐이다.

스프링 부트에서의 Redis

각 메서드에 대한 이론적인 설명은 제끼고 바로 코드로 들어가겠습니다.
spring boot 3.x 버전 이상에서 구동되었고, gradle로 이용 중입니다.

dependency 설정

디펜던시 설정 해주세요~

Redis Config 설정

설정 파일 만들어주세요~

일반적으로는 application.yml 이나 .properties 파일에 redis 관련 설정을 작성하는 걸로 알고 있다.
하지만, 현재 프로젝트 내에서는 하나의 서비스에서만(우선적으로는) redis 사용이 필요하여 해당 서비스를 구동하는 코드에만 적용이 필요했습니다. 그렇다보니, 모든 프로젝트에서 적용되는 .yml 파일에 굳이 작성할 필요성을 느끼지 못했습니다.

추후 서비스를 진행한다면 당연히 이를 Sentinel이나 Cluster 방식으로 개선하고 싶은데 그에 대한 구현의 난이도도 있고 말했듯이 하나의 서비스에서만 돌아가고, 학습 목적이 크다 보니 StandAlone 형태로 구현하였습니다.

추가로, Redis에서 기본적으로 제공하는 기능들(특히, 경합 해결)을 이용하기 위해 RedisTemplate을 정의하고 이를 활용하기로 하였습니다.

Redis 실 사용

Create / Update

채팅방과 사용자의 데이터만을 저장하려는 목적으로 사용하는 상황입니다.
채팅방을 키값으로 설정하여, 사용자를 받는데 똑같은 사용자가 입장 요청을 하였을 경우 중복 저장이 불필요하여 Set으로 구성하였습니다.
추가적으로 사용자가 설정한 시간 동안만 세션이 살아있으면 되어 해당 값을 변수로 지정하고, 이를 통해 TimeUnit 을 구성하였습니다.


redisTemplate 메서드를 사용하다보니 코드가 굉장히 깔끔해지는 부분이 있었습니다.
다만, 여기서 고민이었던건 서버에서도 입장할 수 있는 인원수를 확인하고, 적은 확률이겠지만 같은 방에 여러 사용자가 동시에 입장할 경우에 대한 처리를 해줘야하는 로직이었습니다.
우선 해당 문제를 해결하는 로직도 작성하였으니 참고 하시면 될 것 같습니다.


해당 로직을 구현하는데 조금 불편했던 점은 경쟁 상황을 해결하기 위해 분산 락을 구현했던 부분입니다.
Redis Lock 이라는 주석이 달린 코드인데, 보시면 setNX 메서드를 이용하기 위해 RedisCallback 인스턴스를 활용합니다.
논리적인 고민이었지만, 사용자가 동시에 접근할 경우 결국 setNX 전까지의 코드는 모두 실행이 된다는 점입니다. 그러면 자체적인 처리를 통해 누군가는 해당 명령문이 실행이 되고, 누군가는 되지 않아야 했는데 이걸 어떻게 구현하는 고민이 들었습니다.

일반적으로는 락을 획득하는 시간을 최소화하기 위해 위에서 언급했던 Redis 종류를 아예 바꿔버리거나, Redis 클라이언트와 서버 사이의 네트워크 지연을 최소화하거나, 다양한 라이브러리를 이용하거나, Redis 자체적으로 트랜잭션을 감시하여 동시성을 유지하는 방법이 있었습니다.
그렇지만 이건 너무 어렵잖아요!

다행히, Redis 자체적으로 이런 문제를 해결하기 위한 알고리즘이 존재한다고 합니다. Redis Clusted Lock, Redlock 등의 알고리즘이 그 예인데 이름에서 알 수 있듯이 여러 개의 노드, 여러 개의 인스턴스를 사용해 분산 락을 구현해준다고 합니다.

Read


이 메서드도 조금 귀찮았는데, Redis 가 key-value 로 값을 저장하는 데이터베이스이다 보니, 각 키와 관련된 메서드들은 많은데 데이터베이스 전체의 key의 value를 한 번에 보여주는 기능은 없었습니다.

그래서 이걸 직접 구현해줘야 했는데, Redis 자체적으로 Hash 형태가 존재하고, 이 구조 타입은 하나의 key에 여러 subkey 들이 딸려오고, 각 subkey 안에 value 가 저장되는 형식입니다.

결국, MainKey에 모든 데이터를 물린다면 전체 조회가 가능할거라 생각했고, 각 키를 순회한 값을 Hash 타입 키에 저장해서 구현했습니다.

Delete


여기서 또 다른 번거로움이 등장하는데, 전체 목록 조회를 위해 구조를 하나 더 짰다보니 삭제 시 Hash Key에서 관련 데이터를 추가로 삭제해야 하는 로직이 필요했습니다.

또, Redis의 Key 에는 반드시 value 가 존재해야 하기 때문에, value만 지운다는 생각으로 접근하면 Key에는 Empty 값이 존재할 수 없다는 오류를 보게 될 것입니다.
그래서, DB 안에서 해당 Key를 지울 수 있는 메서드를 사용해야 했습니다.

마무리...

RedisTemplate 를 사용하면 로직 자체가 간단해지기 때문에 편하게 구현할 수 있습니다.
다만, 생각보다 많은 부분이 기본적으로 구현되어 있지 않다보니 개발자가 임의에 목적에 따라 하드 코딩을 해야할 수 있습니다.

이제 이럴 경우 RedisTemplate 이 아닌 CRUDRepository를 상속받은 상황에서 모든 내용을 일일이 저장해야 합니다.
물론, 이렇게 하면 value에 하나의 타입이 아닌, 다양한 타입의 데이터를 저장할 수 있는 장점이 있겠지만 Redis에서 기본적으로 제공해주는 Lock 등의 기능 또한 개발자가 직접 구현해야 하는 문제점이 있어 잘 고민해보고 접근해야 할 것 같습니다~

profile
끈질기게 들러붙기

0개의 댓글