지난 번에 웹소켓에 대한 설명에 이어 이번에는 저희 프로젝트에서 처리량과 지연 시간 부분에서 큰 역할을 한 Redis에 대해서 설명하겠습니다.
1. 도입 배경: 왜 Redis가 필요했나
웹소켓을 통해 실시간 통신 환경을 구축했지만, 또 다른 병목 현상이 기다리고 있었습니다. 바로 데이터베이스(RDB)의 부하였습니다.
- 캐싱의 필요성: 초당 수만 건의 시세 조회 요청을 매번 PostgreSQL 같은 RDB로 처리한다면 응답 속도가 현저히 느려지고 서버는 금방 한계에 도달합니다.
- 서버 간 동기화 문제: 프로젝트 규모가 커지며 모놀리식 구조에서 MSA(Microservices Architecture)로 전환할 때, 여러 대의 서버 인스턴스 간에 실시간 데이터를 어떻게 일관성 있게 공유할 것인가라는 숙제가 생겼습니다.
이를 해결하기 위해 인메모리 데이터 구조 저장소인 Redis를 도입했습니다.
2. Spring Boot & Redis 설정 (Configuration)
먼저 build.gradle에 의존성을 추가하고, AWS EC2 환경에서도 원활히 동작하도록 설정을 구성했습니다.
① 의존성 추가
dependencies {
// Redis 사용을 위한 스타터 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
② RedisConfig 설정
특히 LocalDateTime 같은 자바 8 날짜 타입을 JSON으로 안전하게 직렬화하기 위해 ObjectMapper 설정을 세심하게 커스터마이징했습니다.
@Configuration
public class RedisConfig {
// ... host, port, password 설정
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
if (!password.isEmpty()) config.setPassword(password);
return new LettuceConnectionFactory(config);
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule()); // 날짜 타입 지원
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return objectMapper;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Key는 String, Value는 JSON으로 직렬화 설정
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(RedisSerializer.json());
return template;
}
}
3. 캐싱 전략: Look-Aside 패턴 적용
가장 빈번하게 호출되는 '현재 시세 조회' 로직에 캐싱을 적용했습니다. 데이터가 있다면 Redis에서 즉시 반환하고, 없다면 DB 조회 후 Redis에 저장하는 Look-Aside 패턴입니다.
public TradeResponse getCurrentTrade(Long categoryId) {
String key = getTickerKey(categoryId);
// 1. Redis에서 캐싱된 시세 조회
String cachedPrice = redisTemplate.opsForValue().get(key);
if (cachedPrice != null) {
return TradeResponse.builder()
.tradePrice(new BigDecimal(cachedPrice))
.build();
}
// 2. 캐시 미스 시 DB(또는 최근 체결 내역) 조회
TradeResponse recent = getRecentTrade(categoryId);
BigDecimal price = (recent != null) ? recent.getTradePrice() : BigDecimal.ZERO;
// 3. 조회된 데이터를 60초간 Redis에 캐싱
if (price.compareTo(BigDecimal.ZERO) > 0) {
redisTemplate.opsForValue().set(key, price.toPlainString(), Duration.ofSeconds(60));
}
return TradeResponse.builder().tradePrice(price).build();
}
결과: 반복적인 DB 접근이 사라지면서 API 응답 시간이 밀리초(ms) 단위로 단축되었습니다.
4. MSA의 핵심: Redis Pub/Sub을 통한 데이터 전파
서버를 여러 대 두는 분산 환경(MSA)에서는 사용자가 어떤 서버 인스턴스에 접속하느냐에 따라 다른 시세를 볼 위험이 있습니다. A 서버에서 발생한 체결 정보가 B 서버 접속자에게도 즉시 전파되어야 하기 때문입니다. 이를 해결하기 위해 Redis Pub/Sub을 도입했습니다.
① 메시지 발행 (Publish)
체결 엔진으로부터 가공된 데이터를 받으면, stringRedisTemplate을 통해 각 기능별 채널로 메시지를 발행합니다.
// TradeMarketBroadcaster.java 중 일부
private void sendWebSocketData(Long categoryId, TradeResponse response) {
// ... 시세 가공 로직 (ConcurrentHashMap 활용)
// JSON 직렬화 후 각 채널에 데이터 발행
stringRedisTemplate.convertAndSend("ws-ticker-channel", objectMapper.writeValueAsString(ticker));
stringRedisTemplate.convertAndSend("ws-trades-channel", objectMapper.writeValueAsString(trades));
}
② 메시지 구독 및 관리 (RedisConfig)
모든 서버 인스턴스가 Redis 채널을 리스닝하도록 설정하여, 어떤 서버에서 발생한 데이터든 즉시 모든 서버로 전파되게 했습니다.
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter tickerListenerAdapter,
MessageListenerAdapter tradesListenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 채널별로 전용 리스너 등록
container.addMessageListener(tickerListenerAdapter, new ChannelTopic("ws-ticker-channel"));
container.addMessageListener(tradesListenerAdapter, new ChannelTopic("ws-trades-channel"));
return container;
}
이 구조를 통해 우리 서비스는 다음과 같은 흐름을 갖게 되었습니다.
- 조회 시: Redis Cache를 먼저 확인하여 DB 부하 최소화.
- 갱신 시: 데이터 가공 후 Redis Pub/Sub 채널에 발행하여 전 서버에 전파.
- 수신 시: 각 서버의 Subscriber가 메시지를 받아 웹소켓으로 클라이언트에게 최종 전송.
'Project' 카테고리의 다른 글
| 실시간 시세 서비스를 위한 WebSocket 도입 (0) | 2026.03.16 |
|---|