# Spring Boot with Redis Pub/Sub

Spring Boot 프로젝트에서 Redis Pub/Sub을 사용하는 방법에 대해 정리한다. 예제는 다음과 같이 두 개의 프로젝트로 구성된다.

우선 Redis 서버를 구동하자.

$ brew services start redis
$ brew services list
Name                  Status  User     File
redis                 started yologger ~/Library/LaunchAgents/homebrew.mxcl.redis.plist

# Subscriber

채널을 구독하는 Subscriber 프로젝트는 다음과 같다.

# application.yml
server:
  port: 9090
spring:
  redis:
    host: localhost
    port: 6379

MessageListener의 구현체는 채널을 구독하는 컴포넌트다.

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service
@RequiredArgsConstructor
public class RedisSubscriber implements MessageListener {
    @Override
    public void onMessage(Message message, byte[] pattern) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            ChatMessage chatMessage = mapper.readValue(message.getBody(), ChatMessage.class);
            System.out.println("sender: " + chatMessage.getSender());
            System.out.println("context: " + chatMessage.getContext());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

이제 Redis 구성파일에 MessageListener의 구현체를 등록한다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

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

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

    // 채널 등록
    @Bean
    ChannelTopic topic() {
        return new ChannelTopic("channel1");
    }

    // MessageListener의 구현체 등록
    @Bean
    public MessageListenerAdapter messageListenerAdapter(RedisSubscriber redisSubscriber) {
        return new MessageListenerAdapter(redisSubscriber);
    }

    // MessageListener의 구현체와 채널 연결
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory, MessageListenerAdapter messageListenerAdapter, ChannelTopic topic) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(redisConnectionFactory);
        container.addMessageListener(messageListenerAdapter, topic);
        return container;
    }
}

# Publisher

채널로 메시지를 발행하는 Publisher 프로젝트는 다음과 같다.

# application.yml
server:
  port: 8080
spring:
  redis:
    host: localhost
    port: 6379

Redis 구성파일은 다음과 같다.

// RedisConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

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

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(ChatMessage.class));
        return redisTemplate;
    }
}

메시지로 사용할 데이터 클래스는 다음과 같다.

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
    private String sender;
    private String context;
}

채널로 메시지를 전송하는 서비스 컴포넌트는 다음과 같이 구현한다.

// RedisPublisher.java
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class RedisPublisher {
    private final RedisTemplate<String, Object> redisTemplate;

    public void sendMessage(ChatMessage chatMessage) {
        redisTemplate.convertAndSend("channel1", chatMessage);
    }
}

마지막으로 클라이언트를 위한 REST API 엔드포인트를 다음과 같이 구성한다.

// RedisController.java
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class RedisController {
    private final RedisPublisher redisPublisher;

    @PostMapping("/publish")
    public String publish(@RequestBody ChatMessage chatMessage) {
        redisPublisher.sendMessage(chatMessage);
        return "success";
    }
}

# 결과 확인

이제 HTTP Client(Postman)로 Publisher에 요청을 보내보자.

Subscriber 로그에 메시지가 출력되는 것을 확인할 수 있다.