본문 바로가기
Projects

[Slack AI Bot] 사용자 부재 이벤트 감지 방법 -> Redis?

by hxxyeoniii 2025. 11. 30.

사용자의 부재 시간을 감지하는 방법 고르기

 

방법 1. 메시지 이벤트로 활동 추적

(+) 구현 간단

(+) 실시간 추적

(+) 슬랙 API 호출 불필요

 

(-) 메시지를 안 보내도 채널을 보고 있는 사람은 감지할 수 없음

(-) 읽음 여부를 알 수 없음

 

 

 

방법 2. Slack Presence API 사용

(+) 실제 온/오프라인 상태 감지

(+) 메시지를 안 보내도 "보고 있는" 사람 감지 가능

 

(-) Slack API 호출 필요 -> Rate Limit 고려...

(-) 모든 사용자를 추적해야 함 -> 리소스 우려...

 

 

 

방법 3. 데이터베이스 기반 추적

(+) 데이터 영구 보존

(+) 서버 재시작 시에도 데이터 유지

 

(-) DB 설정 및 관리 필요

(-) 메시지마다 DB Write -> 부하 우려...

 

 

 

방법 4. Redis 기반 추적

(+) 메모리 기반

(+) 속도 빠름

(+) 서버 재시작해도 데이터 유지 -> Redis 별도 운영 시

(+) ConcurrentHashMap보다 안정적


Redis 기반 추적으로 결정 !

 

* Redis = Remote Dictionary Server

  • 메모리 기반 Key - Value 저장소
  • 매우 빠름
  • 데이터 영속성 지원
  • TTL 기능 지원

 

 

 

1. build.gradle 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

2. Docker로 Redis 실행

-> Redis 컨테이너 실행 완료

-> 컨테이너 ID : ae63b08c0e64

-> 포트 : 6379

 

 

3. docker-compose에 Redis 설정 추가

environment:
      - SPRING_REDIS_HOST=redis
      - SPRING_REDIS_PORT=6379
      - SPRING_REDIS_PASSWORD=
      - SPRING_AI_OPENAI_API_KEY=${OPENAI_API_KEY}
      - SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN}
      - SLACK_SIGNING_SECRET=${SLACK_SIGNING_SECRET}
      
      ...
      
# Redis
  redis:
    image: redis:7-alpine
    container_name: slack-redis
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes
    networks:
      - slack-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped

 

RedisConfig 추가

package com.group.slack_ai_summary.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
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.RedisStandaloneConfiguration;
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;

/**
 * Redis 설정 (환경변수 기반)
 */
@Configuration
public class RedisConfig {

    private static final Logger logger = LoggerFactory.getLogger(RedisConfig.class);

    @Value("${SPRING_REDIS_HOST:localhost}")
    private String redisHost;

    @Value("${SPRING_REDIS_PORT:6379}")
    private int redisPort;

    @Value("${SPRING_REDIS_PASSWORD:}")
    private String redisPassword;

    /**
     * Redis 연결 팩토리 설정
     */
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        logger.info("Redis 연결 설정 - Host: {}, Port: {}", redisHost, redisPort);

        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName(redisHost);
        config.setPort(redisPort);

        // 비밀번호가 있으면 설정
        if (redisPassword != null && !redisPassword.trim().isEmpty()) {
            config.setPassword(redisPassword);
            logger.info("Redis 비밀번호 설정됨");
        }

        return new LettuceConnectionFactory(config);
    }

    /**
     * RedisTemplate 설정
     *
     * Key: String 직렬화
     * Value: JSON 직렬화 (LocalDateTime 지원)
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory connectionFactory) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // String Serializer
        StringRedisSerializer stringSerializer = new StringRedisSerializer();

        // JSON Serializer
        Jackson2JsonRedisSerializer<Object> jsonSerializer =
                new Jackson2JsonRedisSerializer<>(Object.class);
        jsonSerializer.setObjectMapper(objectMapper());

        // Key는 String으로 직렬화
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);

        // Value는 JSON으로 직렬화
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        template.afterPropertiesSet();

        logger.info("RedisTemplate 설정 완료");

        return template;
    }

    /**
     * ObjectMapper 설정
     */
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        return mapper;
    }
}