스프링부트 환경에서 레디스 Pub/Sub 구현하기
Redis는 Key-Value 기반의 캐시 저장소이지만, 캐시 기능 외에도 다양하게 사용되는 오픈소스이다. 이 글에서는 Redis에서 제공하는 Pub/Sub 기능에 대해서 검토할 예정이며, 스프링 부트 환경에서 Pub/Sub 를 연동하는 샘플 코드를 소개한다.
해당 글에서는, 스프링 부트 환경에서의 Redis Pub/Sub 에 대한 샘플 코드를 소개할 예정입니다. 레디스에 익숙한 개발자는 4장부터 읽으시길 바라며, 필자가 시간이 없어서, 부득이하게 코드에 대한 설명은 대부분 생략하였습니다.
"필요한 부분만 간단하게 참고"하시고, 상세한 내용은 공식 레퍼런스를 참고하시길 바랍니다.
https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#pubsub
https://redis.io/topics/pubsub
이 글에서는 먼저 Pub/Sub 아키텍처에 대해서 알아볼 예정이며, 레디스의 Pub/Sub 기능을 간단하게 살펴본다. 그리고, 레디스 자바 클라이언트 중 Lettuce 의 Pub/Sub 에 대해서 알아본다. 최종적으로 스프링부트에서의 연동을 검토한다.
1. Pub/Sub 아키텍처
2. Redis Pub/Sub
3. Java Redis Client, Lettuce
4. Spring Data Redis Pub/Sub (1)
5. Spring Data Redis Pub/Sub (2)
1장에서는 Pub/Sub 아키텍처에 대해서 소개한다.
이 글은 메시징 방법 중 Pub/Sub 에 대해서 상세하게 다룰 예정이지만, 그전에 시스템 통합에 대한 기본 개념을 소개한다.
일반적인 시스템 통합 방법은 아래와 같다.
- 파일 전송
- 공유 데이터베이스
- HTTP Request/Response
- 메시징
아마도, 대부분 개발자는 HTTP Request/Response 방식을 많이 사용할 것이다. Rest API, GraphQL 등이 HTTP Request/Response 방법의 대표적인 적용 예이다.
Request/Response 방식은 HTTP 프로토콜을 사용하기 때문에 Stateless 한 특징이 있으며, 심플하고 단순하기 때문에 구현하기 아주 쉽다. 하지만, 클라이언트-서버 사이에 강한 의존성이 생기며, 서버가 반드시 실행 중일 때만 데이터를 전달받을 수 있다. 그리고, 클라이언트는 서버가 다음 메시지를 보내기 전까지 응답을 기다리고 있다. 즉, 동기식 통신 방식으로 동작한다.
장점
- 심플하고 구현하기 쉽다.
- Stateless (HTTP)
단점
- 클라이언트-서버 시스템 사이 높은 의존성
- 서버가 반드시 실행 중일 때 동작
- 동기식 통신 방식
"HTTP Request/Response 통신은 단단하게 결합된 시스템 아키텍처이다. "
HTTP 통신과는 반대로, 느슨하게 결한 된 비동기 시스템 통합 방식 중 대표 기술은 바로 "메시징"이다. "메시징"은 중간 시스템을 통해 발신자에서 수신자로 데이터를 전송하는 포괄적인 용어이다.
중간 시스템을 통해서 전송하기 때문에, 발신자(서버)는 데이터를 전송받는 수신자(클라이언트)에 대해서 전혀 알지 못한다. 메시지 채널만 알고 있으면 된다.
장점
- 수신자(클라이언트)를 확장하기 쉽다.
- 마이크로서비스 아키텍처에 적합하다.
- 느슨한 연결
단점
- 복잡도 증가
- 기술 스택 추가
"메시징"에 대해서 아주 간단하게 설명하였다. 상세한 내용이 궁금한 개발자는 아래 책을 반드시 읽어보길 바란다.
http://www.yes24.com/Product/Goods/14631181?scode=032&OzSrank=2
글 초반에 설명했듯이, "메시징"이라는 단어는 매우 포괄적인 용어이며, "메시징"을 구현하는 방법은 매우 다양하다. 다양한 메시징 방법 중에서, Pub/Sub과 메시지 큐잉에 대해서 구분할 필요가 있다.
(필자의 지극히 개인적인 생각일 뿐이다. 혹시, 다른 의견은 댓글로 알려주길 바란다.)
- 메시지 큐잉(Point-to-Point Channel)
- Publish-Subscribe(Pub/Sub)
메시지 큐잉은 Point-to-Point Channel 방식으로, 오직 한 수신자만 메시지를 수신(소비)하게 된다. 주문에 대한 처리를 수신하는 클라이언트가 있다고 가정하면, 아래와 같이 3건의 주문을 순차적으로 받아서 처리하게 된다.
순차적으로 메시지를 소비할 때, 처리 시간이 너무 느려서 메시지들이 채널에 쌓이게 되는 경우가 발생하는 경우가 발생한다면, 심각한 병목현상이 발생할 수 있다. 해결하기 위해서, 소비할 수 있는 클라이언트를 늘려주면, 동시에 여러 메시지를 처리할 수 있게 구축할 수 있다. 아래 그림은, 주문 3건을 3개의 클라이언트가 각 한 개씩 처리하는 그림이다.
서버는 주문 이벤트를 전달하고, 주문에 대한 처리를 클라이언트에서 처리한다고 가정한다. 클라이언트는 각각 독립된 환경에서 실행되기 때문에, 각각 "경쟁 소비자"가 된다. 이런 경우, 각 메시지에 대한 처리 순서가 보장되지는 않는다. 예를 들어서, 네트워크 장애가 발생해서 Client1 에 전송된 "주문1" 이 제대로 처리되지 않았다면, 서버에서는 주문1 을 재전송하게 되며, 해당 이벤트는 주문2, 주문3 이후에 처리가 될 수도 있다. 이벤트에 대한 순서를 보장하기 위해서는 별도의 작업이 필요할 것이다.
(해당 방법은 메시지 브로커 및 아키텍처 마다 다양한 방법이 존재한다. 예를 들어서, "Kafka" 브로커의 경우에는 파티션을 지정해서 메시지를 전송하는 방법 등이 있다. 자세한 설명은 생략한다.)
메시지 큐잉과는 반대로 Pub/Sub 은 수신자(클라이언트) 모두에게 메시지를 전송하게 된다. 날씨와 주식 데이터를 다수의 클라이언트에 동일한 메시지를 전송한다고 가정하자. 아래 그림과 같다.
사실, RabbitMQ, Apache ActiveMQ, Amazon SQS 등은 메시지 큐잉 용도로 초기 설계가 되었고, Apach Kafka 는 Pub/Sub 사용을 위해서 초기에 설계가 되었다고 들었다.(뇌피셜...)
하지만, RabbitMQ, Active MQ, SQS 를 사용해서 메시지 큐잉 외에 Pub/Sub 방식을 구현할 수 있고, 반대로 Apache Kafka를 사용해서 Pub/Sub 기능 외에 "메시지 큐잉" 으로도 사용할 수 있다.
필자의 실무 경험에 의하면 메시시 시스템의 메시지 큐잉, Pub/Sub 에 대한 구분이 조금 애매한 상황이다. (전문가는 의견을 댓글로 알려주길 바란다. 필자의 확실하지 않은 개념을 반드시 잡아주길 부탁드립니다.)
어쨌든, 어떤 기술을 사용할지에 대해서,
비즈니스 요구사항, 개발팀 기술력, 유지보수 비용 등 전반적인 상황을 고려해서 현명하게 잘 선택해야 할 것이다.
이 글에서는 앞으로 Redis Pub/Sub 에 대해서 계속 얘기할 예정이지만, RabbitMQ 와 Kafka 에 대해서 조금만 더 자세히 알아보도록 하자.
Redis Pub/Sub 은 매우 심플한 Pub/Sub 기능을 제공하는데, 특이한 사실은 Redis에서는 Pub/Sub 메시지를 별도로 저장하지 않는다. 메시지를 한번 보내면 끝이다. 다른 기술스택과 간단하게 비교를 해보자.
Kafka는
초당 최대 수백만 개의 메시지를 보낼 수 있다고 한다. 대량의 데이터를 저장하면서 높은 처리량이 필요하다면, Kafka 를 사용하는게 좋다.
RabbitMQ는
다양한 기능을 제공하는 대표적인 메시지 브로커인데, 비즈니스에 의한 복잡한 라우팅 설계가 필요하다면, Kafka 대신 RabbitMQ 를 선택하는것도 좋은 결정이다. 또한, 일부 회사에서 RabbitMQ 를 도입해서 대용량 트래픽을 처리했다는 사례가 꽤 있다. 높은 처리량을 위해서 Kafka 를 선택하는게 좋겠지만, 그렇다고 RabbitMQ 의 처리량이 낮은건 절대 아니다.
사실, 필자는 RabbitMQ 가 Kafka 보다는 신뢰성 있는 메시지를 전송한다고 생각했기 때문에 시스템 통합에는 RabbitMQ 가 적합하다고 생각했었다. 하지만, RabbitMQ 도 잘 알고 사용해야하며, 잘 모르고 대충 적용한다면 시스템 복잡도만 증가시킬 것이다.
@추가의견
Kafka 와 RabbitMQ 에 대해서 필자가 경험이 많지 않아서 확신이 없다. 잘못된 정보일 수 있으니 가볍게 참고만 하길 바란다.
Redis Pub/Sub 는
다른 메시지 브로커와는 다르게, Redis Pub/Sub 메시지 지속성이 없다. 즉, 메시지를 전송한 후 해당 메시지는 삭제되는데, Redis 어디에도 저장되지 않는다. 실시간 데이터 처리에는 매우 적합하지만, 메시지가 저장되지 않는다는 점은 개발자가 반드시 인지하고 있어야 한다. 또한, 수신자(클라이언트)가 메시지를 받는 것을 보장하지 않는다. 그래서, (개인적인 생각으로는) Redis의 Pub/Sub 기능이 매우 심플하고 괜찮지만, 메시지 전송 신뢰성을 보장하지 않기 때문에, 발신자/수신자 측에서 단점을 보완하는 별도의 추가 구현을 해야할 수도 있다. 참고로, Redis 5.0 에서 도입된 Streams 가 대안이 될 수 있지만, 이 글에서는 Redis Streams 에 대해서는 다루지 않을 예정이다. 또한 Redis list 를 사용하는 것도 대안이 될 수도 있다. 자세한 설명은 생략하며, 이 글에서는 Pub/Sub 에 대해서만 다룬다.
1장에서는 시스템 통합의 방법으로 HTTP 통신과 메시징 을 비교하였고, Pub/Sub 을 구현할 수 있는 메시지 브로커를 간단하게 소개하였다. 이제 본격적으로 Redis Pub/Sub 에 대해서 알아보자.
2장에서는 Redis Pub/Sub 에 대해서 알아보겠다.
Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes with radius queries and streams. Redis has built-in replication, Lua scripting, LRU eviction, transactions and different levels of on-disk persistence, and provides high availability via Redis Sentinel and automatic partitioning with Redis Cluster. - 공식 레퍼런스를 참고하였다.
"subscribe channel" : "ch01" 이라는 이름의 채널에 메시지를 수신해보겠다.
"publish channel message" : "ch01"이라는 채널에 메시지를 발행한다.
2라는 숫자는 2개의 수신자에서 메시지를 받았다는 것을 의미한다. 아래와 같이 정상적으로 메시지 수신하는 것을 확인할 수 있다.
"pubsub numsub channel" : pubsub 명령어를 사용하면 해당 채널에 커넥션을 연동 중인 수신자의 개수를 확인할 수 있다.
자세한 내용은 생략하겠다.
Redis Pub/Sub 에 대한 기능에 대해서 아주 간단하게 설명을 하였다. 필자의 글은 무시해도 된다.
공식 레퍼런스를 참고하길 바란다.
https://redis.io/topics/pubsub
일반적으로 자주 사용하는 Java Redis Client 는 아래와 같다. 스프링 부트 환경에서 Redis 연동 경험이 있다면 3장은 넘어가길 바란다.
- Lettuce
- Jedis
- Redisson
3장에서는, 스프링 부트에서 가장 자주 사용되는 Lettuce 에 대해서 알아볼 예정인데, 시간관계상 소비자 측면에서 메시지 수신에 대해서만 간략하게 살펴보겠다. 메시지 Pub 발행은 레디스에서 직접 Redis-cli 에서 날려보겠다.
RedisPubSubListener 를 익명클래스로 작성하였는데, 별도의 클래스로 분리하는게 깔끔하다. pub 메시지 발행은 redis-cli 에서 실행하였다. 아래와 같이 메시지를 잘 구독하는 것을 확인할 수 있다.
위와 유사한 방법이지만, 비동기 메서드를 사용해보자. 또한, 익명클래스를 별도의 클래스로 분리하였다.
RedisPubSubListener 를 구현하는, RedisListener 라는 클래스를 만들었다.
자세한 설명은 생략한다.
자세한 설명은 시간이 없어서 생략한다.
https://lettuce.io/core/release/reference/
https://github.com/lettuce-io/lettuce-core/wiki/Pub-Sub
샘플 코드는 필자의 github 에서 확인할 수 있다.
https://github.com/sieunkr/spring-data-redis/tree/master/lettuce-sub
스프링부트에서 Redis Pub/Sub 연동을 구현해보자.
디펜던시
스프링부트 2.2.X 환경에서, spring-data-redis 디펜던시를 추가한다.
필자는 로컬에 설치한 레디스를 사용한다.
구현
RedisMessageListenerContainer 를 정의해야 한다.
MessageListenerAdapter
레디스에서 메시지를 주고받을 채널을 설정한다.
최종적으로, RedisListener 를 구현한다. 실제로 메시지를 수신하게 되면 처리하는 로직이다.
메시지를 수신하는 구독자를 먼저 구현하였다.
https://github.com/sieunkr/spring-data-redis/tree/master/spring-redis-sub
자세한 설명은 생략한다.
디펜던시
동일하다.
구현
메시지를 발송하기 위해서 RedisTemplate 를 정의한다.
아주 심플하게 스프링부트 메인 클래스에, 앱이 실행되면 바로 메시지를 보내도록 구현하였다.
샘플 코드를 참고하길 바란다.
https://github.com/sieunkr/spring-data-redis/tree/master/spring-redis-pub
설명 생략
테스트
애플리케이션을 먼저 실행한 후, Pub 애플리케이션을 실행하면 메시지를 정상적으로 구독/발행 하는 것을 확인할 수 있다.
정리
어렵지 않게, 메시지 Pub/Sub 을 구현하였다.
https://github.com/sieunkr/spring-data-redis/tree/master/spring-redis-pub
다음에는, 단순 String 문자열이 아닌, JSON 직렬화된, DTO 메시지를 전달해보자.
자세한 설명은 생략한다.
주고받을 메시지 스펙을 정의한다.
... 코드에 대한 설명은 전부 생략한다.
...
...
...
메시지를 보낸다.
메시지 전송, 수신이 잘 된다.
Lettuce 라이브러리는 기본적으로 수신한 메시지를 리턴하지만, Spring Data 에서 제공하는 Pub/Sub 기능은 수신한 클라이언트의 수를 리턴하지 않는다. 왜 Spring Data Redis 에서는 리턴하지 않을까? 정확한 이유는 모르겠다. 일단, 라이브러리 소스를 분석해보자.
RedisTemplate 에서 제공하는 pubsub 메서드는 convertAndSend 이다. 해당 메서드는 리턴값이 없다. 정확히는 null 을 리턴한다. 하지만, connection.publish 메서드는 리턴값이 존재한다..
해당 메서드의 execute 구문을 보면, connection.publish 를 실행하게 되는데, 메서드를 들어가서 보자.
Spring Data Redis 는 구현체로 Lettuce, Jedis 를 사용할 수 있다. 필자는 이 글에서 Lettuce 를 사용하고 있다. LettuceConnection 클래스의 publish 메서드를 살펴보자. 아래와 같다.
해당 메서드의 리턴은 Long 이다. 또한, getConnection().publish 로 실행하는 구문 역시 Long 리턴이다.
Long integer-reply the number of clients that received the message...
왜...?? 정답을 알고 있는 개발자는 제보를 해주길 부탁한다.
RedisTemplate 를 커스터마이징해서, Pub 메시징 발행 시 클라이언트의 수를 리턴 받도록 변경할 수 있다. RedisTemplate 를 상속받는 래퍼 클래스를 만들어서 수정하면 된다. 단, 이 글에서는 회사 코드 중 일부가 겹치는 내용이 있어서 보안상 공개하지 않겠다..
설명 생략
시간 관계상 글을 급하게 마무리하였는데, 소스 코드에 대한 설명을 제대로 하지 못해서 아쉬운 마음이다. Redis Pub/Sub 은 사용하기 매우 심플하지만, 메시지 전송에 대한 신뢰성이 보장되지 않는다는 단점이 있다. 허접한 이 글을 끝까지 읽은 개발자는 거의 없을 것 같지만, 혹시라도 Redis Pub/Sub 구현 경험이 있는 개발자는 댓글로 의견을 남겨주길 바란다.
기업통합패턴, 그레거 호프 & 바비 울프 지음, 차정호 옮김, 에이콘 출판사
https://redis.io/topics/pubsub
https://www.baeldung.com/java-redis-lettuce
https://lettuce.io/core/release/reference/
https://dzone.com/articles/comparing-publish-subscribe-messaging-and-message
https://www.baeldung.com/spring-data-redis-pub-sub
https://otonomo.io/blog/redis-kafka-or-rabbitmq-which-microservices-message-broker-to-choose/
https://stackoverflow.com/questions/32037803/redis-pub-sub-ack-nack