카테고리 없음
Redis /재난 대응 프로젝트 Redis 설계 및 활용 정리
creator7087
2025. 6. 7. 16:56
✅ 1. Redis 정의
- Redis(REmote DIctionary Server)는 인메모리 기반의 Key-Value 스토어
- 빠른 읽기/쓰기 성능
- 다양한 데이터 구조 지원: String, Hash, List, Set, Sorted Set, Streams, HyperLogLog, Bitmaps, Geospatial
✅ 2. 사용 목적
목적 | 설명 |
캐시(Cache) | 자주 조회되는 데이터를 메모리에 저장하여 DB 부하 감소 |
세션 관리 | 사용자 세션 정보를 Redis에 저장하여 빠른 인증 처리 |
분산 락 | Redisson 등을 활용하여 분산 환경에서의 락 처리 |
실시간 순위/통계 | Sorted Set 등을 활용하여 실시간 랭킹 시스템 구현 |
메시지 브로커 | Pub/Sub 또는 Streams를 활용한 이벤트 메시징 처리 |
지리정보 처리 | GEO 명령어로 위치 기반 검색 가능 |
✅ 3. Pub/Sub vs Streams vs Sorted Set 비교
항목 | Pub/SubRedis | Streams | Sorted Set (ZSet) |
개념 | 발행/구독 모델 | 로그 기반 메시지 큐 | 우선순위 기반의 정렬된 컬렉션 |
데이터 유지 | ❌ 유지되지 않음 (실시간) | ✅ 메시지 보존됨 | ✅ 데이터 보존 |
소비 모델 | 실시간 브로드캐스트 | 소비자 그룹 기반 소비 (Kafka 유사) | 데이터 조회 |
재처리 | ❌ 불가 | ✅ 오프셋 기반 재처리 가능 | ✅ 항목 재조회 가능 |
사용 목적 | 실시간 알림, 단순 브로드캐스트 | 안정적인 메시지 처리 (로그 수집, 비동기 처리 등) | 랭킹 시스템, 스코어 기반 정렬 등 |
적합한 예시 | 채팅방 메시지 전파, 알림 | 주문 처리 큐, 로그 저장 | 실시간 인기 검색어, 게임 점수 보드 |
✅ 4. 선택 기준 및 설정 방식 적합성
시나리오추천 | 데이터 구조 | 설정 시 고려할 점 |
실시간 알림 전파 | Pub/Sub | 메시지 유실 가능성, 소비자 수 적을 때 적합 |
비동기 로그 수집 / 안정적 이벤트 처리 | Streams | 메시지 저장 및 소비 그룹, 오프셋 관리 필요 |
랭킹 / 인기 검색어 집계 | Sorted Set (ZSet) | 정렬 기준 스코어 지정, TTL 설정 고려 |
단순 캐싱 | String / Hash | TTL (만료시간), evict 정책 고려 |
사용자 세션 저장 | Hash / String | 세션 만료 정책과 함께 사용 |
분산 락 | Redisson 기반의 SET NX / Lua Script | 락 해제 시점, 클럭 동기화 주의 |
✅ 요약
- Pub/Sub: 실시간 전파 → "속도 우선, 신뢰도 낮음"
- Streams: 로그/이벤트 큐 → "속도 + 안정성, 소비자 그룹 처리"
- Sorted Set: 정렬된 값 저장/집계 → "우선순위 기반 데이터 처리"
✅ 참고 공식 문서
- Redis Pub/Sub Docs(https://redis.io/docs/latest/develop/interact/pubsub/)
- Redis Streams Guide(https://redis.io/docs/latest/develop/data-types/streams/)
- Redis Sorted Sets(https://redis.io/docs/latest/develop/data-types/sorted-sets/)
1. 주요 요구사항 및 사용 목적
기능 | 사용 목적 | Redis 자료구조 | 설명 및 역할 |
재난 이벤트 기록 및 큐 처리 | 재난 발생 로그 저장, 장애 복구, 재처리 | Streams | 이벤트 순서 보장, 다중 소비자 그룹 지원, 장애 복구에 유리 |
사용자 위치 정보 저장 (시/구 단위) | 지역 단위 위치 정보 조회 및 관리 | Hash/String | 빠른 조회 및 갱신, user:location:<userId> 형태로 저장 |
사용자 위도/경도 좌표 저장 및 반경 검색 | 특정 반경 내 사용자 또는 대피소 탐색 | Geospatial | GEOADD, GEORADIUS 명령어 활용, 반경 탐색 지원 |
로그인 Refresh Token 관리 | 안전한 토큰 저장, 검증 및 만료 처리 | String (Key-Value) | refreshToken:<token> 키로 사용자 ID 저장, TTL 적용 자동 만료 |
실시간 알림 | → Kafka 사용 (Redis Pub/Sub는 사용하지 않음) | N/A | Kafka로 알림 처리 |
2. Redis 네임스페이스 및 키 설계 예
데이터 타입 | 키 | 예시설명 |
Streams | disaster:events | 재난 이벤트 로그 스트림 |
Hash/String | location:<userId> | 사용자 위치 시/구 정보 저장 |
Geospatial | location(위도,경도):users | 사용자 좌표 저장 및 반경 검색 |
String (Refresh Token) | refreshToken:<token> | Refresh Token 저장 및 만료 관리 |
3. 실제 코드 (서비스 분리 필요)
더보기
1. RedisConfig (기본 RedisTemplate 설정)
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String redisHost;
@Value("${spring.data.redis.port}")
private int redisPort;
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}
@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
2. RefreshTokenRepository (로그인 토큰 관리)
@Repository
@RequiredArgsConstructor
public class RefreshTokenRepository {
private final RedisTemplate<String, String> redisTemplate;
private static final Duration REFRESH_TOKEN_TTL = Duration.ofDays(7);
public void saveToken(String userId, String refreshToken) {
redisTemplate.opsForValue().set(userId, refreshToken, REFRESH_TOKEN_TTL);
}
public String getToken(String userId) {
return redisTemplate.opsForValue().get(userId);
}
public void deleteToken(String userId) {
redisTemplate.delete(userId);
}
}
3. LocationRepository (사용자 위치 관리 — Geo 저장 + TTL)
@Repository
@RequiredArgsConstructor
public class LocationRepository {
private final RedisTemplate<String, String> redisTemplate;
private static final String LOCATION_KEY = "user:locations";
public void saveCoordinates(String userId, double latitude, double longitude) {
redisTemplate.opsForGeo().add(LOCATION_KEY, new Point(longitude, latitude), userId);
String ttlKey = "location:ttl:" + userId;
redisTemplate.opsForValue().set(ttlKey, "1", Duration.ofMinutes(1));
}
public Point getCoordinates(String userId) {
List<Point> positions = redisTemplate.opsForGeo().position(LOCATION_KEY, userId);
if (positions == null || positions.isEmpty()) return null;
return positions.get(0);
}
public List<String> findUsersWithinRadius(double latitude, double longitude, double radiusMeters) {
GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo()
.radius(LOCATION_KEY, new Circle(new Point(longitude, latitude),
new Distance(radiusMeters / 1000.0, Metrics.KILOMETERS)));
if (results == null) return Collections.emptyList();
return results.getContent().stream()
.map(r -> r.getContent().getName())
.toList();
}
}
4. RegionRepository (행정구역 기반 사용자 관리 — List + TTL)
@Repository
@RequiredArgsConstructor
public class RegionRepository {
private final RedisTemplate<String, String> redisTemplate;
public void saveRegion(String si, String gu, String userId) {
String key = si + " " + gu;
redisTemplate.opsForList().rightPush(key, userId);
redisTemplate.expire(key, Duration.ofMinutes(5));
}
public List<String> getUsersByRegion(String regionKey) {
return redisTemplate.opsForList().range(regionKey, 0, -1);
}
}
5. DisasterEventStreamRepository (재난 이벤트 스트림 관리)
@Repository
@RequiredArgsConstructor
public class DisasterEventStreamRepository {
private final RedisTemplate<String, String> redisTemplate;
private static final String STREAM_KEY = "disaster:events";
public String addEvent(Map<String, String> eventData) {
RecordId recordId = redisTemplate.opsForStream()
.add(StreamRecords.newRecord().in(STREAM_KEY).ofMap(eventData));
return recordId.getValue();
}
public void createConsumerGroup(String groupName) {
try {
redisTemplate.opsForStream()
.createGroup(STREAM_KEY, ReadOffset.lastConsumed(), groupName);
} catch (Exception e) {
// 그룹이 이미 존재할 경우 무시
}
}
public List<MapRecord<String, String, String>> readEvents(String groupName, String consumerName, int count, Duration blockMillis) {
return redisTemplate.opsForStream()
.read(Consumer.from(groupName, consumerName),
StreamReadOptions.empty().count(count).block(blockMillis),
StreamOffset.create(STREAM_KEY, ReadOffset.lastConsumed()));
}
public void acknowledge(String groupName, List<String> recordIds) {
redisTemplate.opsForStream().acknowledge(STREAM_KEY, groupName, recordIds.toArray(new String[0]));
}
}
6. RedisService (통합 서비스 예시)
@Service
@RequiredArgsConstructor
public class RedisService {
private final RefreshTokenRepository refreshTokenRepository;
private final LocationRepository locationRepository;
private final RegionRepository regionRepository;
private final DisasterEventStreamRepository disasterEventStreamRepository;
// --- 로그인 토큰 관리 ---
public void saveRefreshToken(String userId, String refreshToken) {
refreshTokenRepository.saveToken(userId, refreshToken);
}
public String getRefreshToken(String userId) {
return refreshTokenRepository.getToken(userId);
}
public void deleteRefreshToken(String userId) {
refreshTokenRepository.deleteToken(userId);
}
// --- 위치정보 관리 ---
public void saveUserCoordinates(String userId, double latitude, double longitude) {
locationRepository.saveCoordinates(userId, latitude, longitude);
}
public Point getUserCoordinates(String userId) {
return locationRepository.getCoordinates(userId);
}
public List<String> findUsersNearLocation(double latitude, double longitude, double radiusMeters) {
return locationRepository.findUsersWithinRadius(latitude, longitude, radiusMeters);
}
// --- 행정구역 기반 사용자 관리 ---
public void saveUserRegion(String si, String gu, String userId) {
regionRepository.saveRegion(si, gu, userId);
}
public List<String> getUsersByRegion(String region) {
return regionRepository.getUsersByRegion(region);
}
// --- 재난 이벤트 스트림 관리 ---
public String publishDisasterEvent(Map<String, String> eventData) {
return disasterEventStreamRepository.addEvent(eventData);
}
public void createDisasterConsumerGroup(String groupName) {
disasterEventStreamRepository.createConsumerGroup(groupName);
}
public List<MapRecord<String, String, String>> consumeDisasterEvents(String groupName, String consumerName, int count, Duration block) {
return disasterEventStreamRepository.readEvents(groupName, consumerName, count, block);
}
public void acknowledgeDisasterEvents(String groupName, List<String> recordIds) {
disasterEventStreamRepository.acknowledge(groupName, recordIds);
}
}