[Basic-1] 기초 이론 및 구축 실습

2025. 7. 1. 12:01·Database/Redis
Redis의 깊은 이해를 위해 먼저 RDBMS와 NoSQL의 개념을 간단히 비교해보고,
이후 Redis가 어떤 특성을 가지고 어떻게 활용되는지 살펴보도록 하겠습니다.

 

1. RDBMS

 RDBMS는 Relational DataBase Management System의 약자로, 관계형 데이터베이스 관리 시스템을 의미한다. 데이터를 2차원의 열(Column)과 행(Row) 형태의 테이블로 표현하며, 이러한 테이블 간의 관계(Join)를 정의해 복잡한 데이터 관계를 효과적으로 다룰 수 있다. RDBMS는 ACID(원자성, 일관성, 고립성, 지속성) 원칙을 따르며, 데이터 정합성과 무결성을 강력하게 보장한다.

[1] 데이터 정합성은 데이터 간의 값이 서로 일치하는 상태를 의미하며, 스키마에 의해 데이터 구조가 명확하게 정의되기 때문에 구현이 용이하다.
[2] 데이터 무결성은 데이터가 전송·저장·처리되는 모든 과정에서 손상되지 않고 정확성과 일관성을 유지한다는 개념이다.

하지만 관계형 데이터베이스는 테이블 간의 관계가 복잡해질수록 JOIN 연산이 많아져 쿼리가 복잡해지고, 시스템이 커질수록 Scale-up(하드웨어 증설)만으로 성능을 확장해야 한다는 한계가 있다. 또한 고정된 스키마로 인해 데이터 구조가 변경되면 수정이 어렵고 유연성이 떨어진다는 단점이 있다.

📌데이터 구조가 명확하고 변경이 적은 시스템에는 RDBMS가 적합하다.

 


2. NoSQL

 NoSQL은 Not Only SQL의 약자로, 관계형 DB의 한계를 보완하기 위해 등장했다. 고정된 스키마 없이 자유로운 형태로 데이터를 저장할 수 있어, 빠르게 변화하는 비정형 데이터나 대규모 데이터를 처리하기에 유리하다. NoSQL의 대표적인 종류는 다음과 같다.

[1] Key-Value DB
데이터를 Key-Value 쌍으로 저장하며, Key는 중복되지 않고 거의 모든 데이터 타입을 수용할 수 있다. 메모리 기반으로 작동해 읽기/쓰기 속도가 매우 빠르며, Redis, AWS DynamoDB 등이 대표적이다.
[2] Document DB
JSON이나 XML 같은 계층적인 데이터(Document)를 Key에 매핑해서 저장하는 방식이다. HTTP 기반의 웹 애플리케이션과 데이터 교환이 편리하며, MongoDB, CouchDB 등이 대표적이다.

NoSQL은 스키마가 없어 유연하고, 데이터 분산(Scale-out) 설계가 쉽다는 장점이 있어 대량의 데이터를 빠르게 처리하거나, 데이터 구조가 자주 변할 가능성이 있는 서비스에 적합하다. 다만 스키마가 없기 때문에 데이터 일관성을 유지하기가 어렵고, 데이터 중복이 발생할 가능성이 있어 주의가 필요하다.

📌 데이터 구조가 유동적이며, 대규모 데이터 처리와 확장이 중요한 시스템에는 NoSQL이 적합하다.

3. Redis

3.1. Redis 개념

Redis는 Key-Value 구조의 고성능 In-Memory 데이터 저장소이다. 즉, 데이터를 디스크가 아닌 메모리(RAM)에 저장해서

데이터를 매우 빠르게 읽고 쓸 수 있도록 최적화된 NoSQL 솔루션이다. 메모리에 데이터를 올려서 사용하기 때문에 디스크 I/O를 거치지 않아 데이터를 처리하는 속도가 획기적으로 빨라지며, DB, 캐시(Cache), 메시지 큐, 공유 메모리 등 다양한 용도로 활용된다. Redis는 오픈소스이며, 비정형 데이터(문자열, 해시, 리스트, 세트, 정렬된 세트 등)를 다양하게 지원하여 유연한 데이터 처리가 가능하다.

3.2. Redis 사용 이유

많은 사용자가 동시에 서비스를 이용하면 DB 서버는 디스크에 계속 접근하면서 병목이 생길 수 있다. 이때 Redis를 캐시 서버로 두어 자주 조회되는 데이터를 메모리에 저장해두면 DB 부하를 획기적으로 줄이면서도 빠른 응답을 제공할 수 있다.

3.3. Redis 주의사항

[1] 단일 스레드(Single Threaded) 기반
한 번에 하나의 명령만 처리할 수 있기 때문에 과도한 요청이 들어오면 처리 순서에 따라 지연이 발생할 수 있다.

[2] 메모리 단편화
크고 작은 데이터를 할당/해제하는 과정에서 메모리 공간이 비효율적으로 쪼개지는 단편화 현상이 발생할 수 있다.

[3] 휘발성
RAM 특성상 서버가 재시작되면 데이터가 사라질 수 있으므로, 이를 방지하기 위해 RDB(AOF) 등의 영속화 기능을 함께 설정해야 한다.
📌 빠른 처리 속도가 필요한 데이터, 변경 빈도가 높거나 캐싱이 필요한 데이터를 다루는 시스템에는 Redis가 적합하다.

4. 실습(1): Redis 설치 및 실행

4.0 사전준비

redis-insight와 redis-server에 대한 포트포워딩을 설정한다. 각 요소들에 대한 자세한 설명은 아래에 작성했으므로, 일단 해당 과정을 따라하자.

4.1. Docker를 이용한 Redis 실행

Ubuntu VM에서 Docker 환경을 활용해 Redis를 손쉽게 실행할 수 있다. Redis 공식 이미지 또는 RedisInsight를 배포하는 Redislabs의 이미지를 사용할 수 있으며, DockerHub에서 관련 이미지를 확인할 수 있다.

[1] Redis & Redis Insights 설치

vi docker-compose.yaml
---
version: '3.8'
services:
  redis:
    image: redis:latest
    container_name: redis
    restart: always
    command: redis-server --requirepass redis1234
    volumes:
      - redis_volume_data:/data
    ports:
      - 6379:6379

  redis-insight:
    image: redislabs/redisinsight:latest
    container_name: redis_insight
    restart: always
    ports:
      - 5540:5540
    volumes:
      - redis_insight_volume_data:/db

volumes:
  redis_volume_data:
  redis_insight_volume_data:
docker-compose up -d

4.2. Redis 접속

Redis는 기본적으로 인증 비밀번호가 설정되지 않기 때문에 누구나 접속 가능하지만, 보안을 위해 비밀번호를 반드시 설정해주는 것이 좋다. 아래는 Redis 컨테이너 내에서 비밀번호를 설정하고 동작을 확인하는 예제이다.

[1] 컨테이너 내부 접속

docker exec -it redis bash
redis-cli

[2] 비밀번호 인증

127.0.0.1:6379> AUTH redis1234

[3] 테스트용 데이터 입력

127.0.0.1:6379> set test 1234

[4] 데이터 조회

127.0.0.1:6379> get test
# 출력 결과
"1234"

4.3. Redis Insight로 시각화 및 모니터링

 RedisInsight는 Redis를 좀 더 시각적으로 관리할 수 있는 유용한 도구이다. 위에서 이미 설치했기 때문에 웹 브라우저에 127.0.0.1:5540 입력하여 접속해보자.

 

 

 


5. 실습(2): Redis 문법

5.1. Redis Key

 Redis에서의 Key는 데이터를 식별하기 위한 고유한 이름이다. 문자열(String)로 저장되며, 바이너리 안전(binary-safe)하여 이모지나 바이너리 데이터도 사용할 수 있다.

❗ 최대 길이는 512MB까지 가능하지만, 짧고 의미 있는 Key를 쓰는 것이 성능과 관리 측면에서 권장된다.
전략  설명  예시
네임스페이스 사용 콜론(:)을 이용해 계층적 의미 부여 user:1001:name
짧고 명확한 key key 길이를 최소화하고 의미 있는 구조로 설계 order:20250701:9999
패턴 고정화 key naming을 코드/문서에서 통일 session:{sessionId}
데이터 타입과 목적 구분 key에 타입·목적을 포함해 용도 식별 가능 cache:product:123, lock:order:9999
만료시간 적극 활용 TTL(Time-To-Live)로 리소스 절약 session:abc123 (30분)
HASH, SET 등 복합 구조 활용 key space 낭비를 방지하고 데이터 집합으로 효율적 관리 user:1001 (Hash로 name, age 저장)

5.2. Strings

 

문자열은 Redis에서 가장 기본적인 자료형이다. 간단한 값 저장/읽기부터, 숫자 증가 연산 등도 가능하다.

# key-value 저장
set testkey testvalue
get testkey
# 결과: "testvalue"

# NX: key가 없을 때만 저장
set testkey newval nx
# 결과: (nil) → 이미 key가 있어 저장 안됨

# DEL: key 삭제
del testkey
get testkey
# 결과: (nil)

# 숫자 증가
set counter 100
incr counter
incrby counter 50
get counter
# 결과: 151

# 여러 key 설정/조회
mset x 10 y 20 z 30
mget x y z
# 결과: "10" "20" "30"

# 존재 여부 확인
exists testkey
# 결과: 0 (key 없음)

# 타입 확인
type x
# 결과: string

 

5.3. Lists

 

리스트는 순서가 있는 문자열 집합이다. 양쪽에서 값을 삽입/삭제할 수 있어 큐/스택 구조에 활용된다.

# 오른쪽 삽입
rpush testlist x y z
# 결과: 3 (리스트 길이)

# 리스트 전체 조회
lrange testlist 0 -1
# 결과: "x" "y" "z"

# 왼쪽에서 삽입
lpush testlist a
lrange testlist 0 -1
# 결과: "a" "x" "y" "z"

# 오른쪽에서 pop
rpop testlist
# 결과: "z"

# 특정 요소 제거
lrem testlist 1 x
# "x"를 한 개 제거

 

5.4. Hashes

 

Hash는 하나의 Key 아래 여러 필드와 값을 저장할 수 있다. 사용자 프로필 정보 등에 적합하다.

# 사용자 정보 저장
hset user:1000 username antirez birthyear 1977 verified 1

# 전체 조회
hgetall user:1000
# 결과:
# 1) "username"
# 2) "antirez"
# 3) "birthyear"
# 4) "1977"
# 5) "verified"
# 6) "1"

# 특정 필드 조회
hmget user:1000 username birthyear
# 결과: "antirez" "1977"

# 필드 값 증가
hincrby user:1000 birthyear 10
# 결과: 1987

# 필드 삭제
hdel user:1000 birthyear
hgetall user:1000

 

5.5. Sets

 

Set은 중복 없는 값의 집합이다. 태그, 좋아요, 팔로우 등에서 활용된다.

# 값 추가 (중복 X)
sadd testset 1 2 3
scard testset
# 결과: 3

# 특정 값 포함 여부
sismember testset 2
# 결과: 1 (있음)
sismember testset 99
# 결과: 0 (없음)

# 사용자 좋아요 예시
sadd user:123:favorites 123 456 789
sadd user:456:favorites 456

# 공통 관심사
sinter user:123:favorites user:456:favorites
# 결과: "456"

# 차집합
sdiff user:123:favorites user:456:favorites
# 결과: "123" "789"

# 합집합
sunion user:123:favorites user:456:favorites
# 결과: "123" "456" "789"

 

5.6. Sorted Sets

 

Sorted Set은 점수(score)를 기준으로 자동 정렬되는 집합이다. 순위, 랭킹 등에 적합하다.

# 점수와 함께 값 저장
zadd clients 1940 "Alan Kay"
zadd clients 1953 "Richard Stallman"
zadd clients 1957 "Sophie Wilson"
zadd clients 1949 "Anita Borg"
zadd clients 1914 "Hedy Lamarr"
zadd clients 1916 "Claude Shannon"

# 오름차순 정렬 조회
zrange clients 0 -1
# 결과: "Hedy Lamarr", "Claude Shannon", "Alan Kay", ...

# 내림차순 정렬
zrange clients 0 -1 rev
# 결과: "Sophie Wilson", "Richard Stallman", ...

# 점수 포함 출력
zrange clients 0 -1 withscores

# 점수가 낮은 사람 TOP 3
zrange clients 0 2
# 결과: "Hedy Lamarr", "Claude Shannon", "Alan Kay"

# 특정 점수 범위 조회
zrangebyscore clients -inf 1945
# 결과: "Hedy Lamarr", "Claude Shannon", "Alan Kay"

 


6. 실습(3): Springboot + Redis 기초 구축

6.1. 패키지 및 클래스 구성

src
└── main
    ├── java
    │    └── io.redis.basic
    │          ├── config
    │          │     └── RedisConfig.java                 # Redis 연결 설정
    │          │
    │          ├── controller
    │          │     ├── BasicRedisController.java        # 기본 Redis 연산 컨트롤러
    │          │     └── DataStructureController.java     # 데이터 구조별 Redis 컨트롤러
    │          │
    │          ├── service
    │          │     ├── BasicRedisService.java           # 기본 Redis 서비스
    │          │     ├── HashOperationService.java        # Hash 연산
    │          │     ├── ListOperationService.java        # List 연산
    │          │     ├── SetOperationService.java         # Set 연산
    │          │     └── SortedSetOperationService.java   # SortedSet 연산
    │          │
    │          └── Step00RedisBasicApplication.java       # Spring Boot 메인 클래스
    │
    └── resources
         ├── static                    # 정적 리소스(css, js, 이미지)
         ├── templates                 # 뷰 템플릿(Thymeleaf 등)
         └── application.properties    # Redis, 로깅 설정

 

6.2.  프로젝트 세팅

[1] 프로젝트 정보

항목  내용
프로젝트명 step01_redis_basic
group io.redis
package io.redis.basic
주요 dependency lombok, Spring Web, Spring Data Redis, DevTools
추가 라이브러리 commons-pool2, jackson-databind

[2] pom.xml 

더보기
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.3</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>io.redis</groupId>
	<artifactId>step01_redis_basic</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>step01_redis_basic</name>
	<description>Demo project for Spring Boot</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<annotationProcessorPaths>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
							<version>1.18.34</version>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

[3] application.properties 

더보기
# Application Name
spring.application.name=step01_redis_basic

# Server Configuration
server.port=8081 

# Redis Configuration
spring.data.redis.host=127.0.0.1
spring.data.redis.client-name=default
spring.data.redis.password=redis1234
spring.data.redis.port=6379
spring.data.redis.timeout=2000

spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0
spring.data.redis.lettuce.pool.max-wait=-1

# Logging Configuration
logging.level.root=INFO
logging.level.io.lettuce.core=DEBUG
logging.level.org.springframework.data.redis=DEBUG

 

6.3. Spring Boot + Redis

[1] Configuration

io/redis/basic/config/RedisConfig.java

더보기
@Configuration 
@EnableCaching // 스프링의 캐시 기능을 활성화합니다. (@Cacheable 등 사용 가능)
@Slf4j         
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private int port;

    @Value("${spring.data.redis.password}")
    String password;

    @PostConstruct
    public void printRedisConfig() {
        log.info("host={}", host);
        log.info("port={}", port);
        log.info("password={}", password);
        System.out.println("Redis Config  >>> host: " + host + ", port: " + port + ", password: " + password);
    }

    // Redis에 연결할 수 있도록 하는 Factory Bean입니다.
    // LettuceConnectionFactory는 넌블로킹 Redis 클라이언트입니다.
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port); // 단일 Redis 서버 연결 설정
        config.setPassword(password); // 인증 비밀번호 설정
        return new LettuceConnectionFactory(config);
    }

    // RedisTemplate: Spring에서 Redis에 데이터를 저장하거나 가져올 때 사용하는 핵심 객체입니다.
    // 다양한 자료형 (String, List, Hash 등) 처리 가능
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory());

        // Key는 문자열로 직렬화
        template.setKeySerializer(new StringRedisSerializer());

        // Value는 JSON 형식으로 직렬화 (Java 객체 → JSON → Redis 저장)
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        // Hash 구조에서 키/값 직렬화 방식도 설정
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        return template;
    }

    // String만 주로 사용하는 경우 더 간단한 StringRedisTemplate 사용
    // 내부적으로 RedisTemplate<String, String>을 상속받습니다.
    @Bean
    public StringRedisTemplate stringRedisTemplate() {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory()); // Redis 연결 설정
        return template;
    }
}

[2] Service

io/redis/basic/service/BasicRedisService.java

더보기
@Service
@RequiredArgsConstructor
public class BasicRedisService {

    private final StringRedisTemplate stringRedisTemplate;
    private final RedisTemplate<String, Object> redisTemplate;

    // 문자열 저장/조회
    public void setString(String key, String value) {
        stringRedisTemplate.opsForValue().set(key, value);
    }

    public String getString(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    // 객체 저장/조회
    public void setObject(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public Object getObject(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    // TTL 설정
    public void setWithTTL(String key, String value, long timeout, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    public Long getTTL(String key) {
        return stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    // 키 존재 여부 확인
    public Boolean exists(String key) {
        return stringRedisTemplate.hasKey(key);
    }

    // 키 삭제
    public Boolean delete(String key) {
        return stringRedisTemplate.delete(key);
    }
}

[3] Controller

io/redis/basic/controller/BasicRedisController.java

더보기
@RestController
@RequestMapping("/api/redis/basic")
@RequiredArgsConstructor
public class BasicRedisController {

    private final BasicRedisService basicRedisService;
    private final CounterService counterService;

    @PostMapping("/string")
    public ResponseEntity<String> setString(@RequestParam String key,
                                            @RequestParam String value) {
        basicRedisService.setString(key, value);
        return ResponseEntity.ok("Saved");
    }

    @GetMapping("/string/{key}")
    public ResponseEntity<String> getString(@PathVariable String key) {
        return ResponseEntity.ok(basicRedisService.getString(key));
    }

    @PostMapping("/ttl")
    public ResponseEntity<String> setWithTTL(@RequestParam String key,
                                             @RequestParam String value,
                                             @RequestParam long seconds) {
        basicRedisService.setWithTTL(key, value, seconds, TimeUnit.SECONDS);
        return ResponseEntity.ok("Saved with TTL: " + seconds + " seconds");
    }

    @PostMapping("/counter/{key}/increment")
    public ResponseEntity<Long> increment(@PathVariable String key) {
        return ResponseEntity.ok(counterService.increment(key));
    }

    @GetMapping("/view/{contentId}")
    public ResponseEntity<Long> getViewCount(@PathVariable String contentId) {
        return ResponseEntity.ok(counterService.incrementViewCount(contentId));
    }

    @GetMapping("/visitor/view/{userId}")
    public ResponseEntity<Long> getVisitorViewCount(@PathVariable String userId) {
        return ResponseEntity.ok(counterService.incrementDailyVisitor(userId));
    }
}

[4] Counter Service 추가

io/redis/basic/service/CounterService

더보기
@Service
@RequiredArgsConstructor
public class CounterService {

    private final StringRedisTemplate stringRedisTemplate;

    // [1] Count 증가
    public Long increment(String key) {
        return stringRedisTemplate.opsForValue().increment(key);
    }

    // [2] 조회수 카운터
    public Long incrementViewCount(String contentId) {

        // key
        String key = "view:count:" + contentId;
        Long count = increment(key); // 내부 메서드 호출

        // 처음 생성된 키라면 -> 30일 TTL
        if (count == 1) {
            stringRedisTemplate.expire(key, 30, TimeUnit.DAYS);
        }
        return count;
    }

    /**
     * [3] 일일 방문자 카운터 구현
     *      ㄴ 키 설계 - visitor:<today>
     *      ㄴ 중복 방문 방지(카운트 무시) - set 사용
     *      ㄴ 일일 설정 -
     */
    public Long incrementDailyVisitor(String userId) {
        String today = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
        String key = "visitor:" + today;

        stringRedisTemplate.opsForSet().add(key, userId);

        LocalDateTime tommorow = LocalDate.now().plusDays(1).atStartOfDay();
        long ttl = ChronoUnit.SECONDS.between(LocalDateTime.now(), tommorow);
        stringRedisTemplate.expire(key, ttl, TimeUnit.SECONDS);

        return stringRedisTemplate.opsForSet().size(key);
    }

}

 


6.4. 결과 확인

 

 


 

7. 실습(4): Springboot + Redis 심화 구축 (Data Strcture 사용)

7.0. DataStructureController

더보기
@RestController
@RequestMapping("/api/redis/data-structures")
@RequiredArgsConstructor
public class DataStructureController {

    private final ListOperationService listService;
    private final SetOperationService setService;
    private final HashOperationService hashService;
    private final SortedSetOperationService sortedSetService;

     [1] List Operations
     //최근 활동 기록
    @PostMapping("/list/activity/{userId}")
    public ResponseEntity<String> addActivity(@PathVariable String userId,
                                              @RequestBody String activity) {
        listService.addRecentActivity(userId, activity);
        return ResponseEntity.ok("Activity added");
    }

    @GetMapping("/list/activity/{userId}")
    public ResponseEntity<List<Object>> getRecentActivities(@PathVariable String userId) {
        return ResponseEntity.ok(listService.getRecentActivities(userId));
    }

    @PostMapping("/list/queue/{queueName}")
    public ResponseEntity<String> pushToQueue(@PathVariable String queueName,
                                              @RequestBody Object message) {
        listService.pushMessage(queueName, message);
        return ResponseEntity.ok("Message pushed to queue");
    }

    @GetMapping("/list/queue/{queueName}/pop")
    public ResponseEntity<Object> popFromQueue(@PathVariable String queueName,
                                               @RequestParam(defaultValue = "5") long timeout) {
        Object message = listService.popMessage(queueName, timeout, TimeUnit.SECONDS);
        return message != null ? ResponseEntity.ok(message) : ResponseEntity.noContent().build();
    }



    //[2] Set Operations

    @PostMapping("/set/tags/{articleId}")
    public ResponseEntity<String> addTags(@PathVariable String articleId,
                                          @RequestBody Set<String> tags) {
        setService.addTags(articleId, tags);
        return ResponseEntity.ok("Tags added");
    }

    @GetMapping("/set/tags/{articleId}")
    public ResponseEntity<Set<Object>> getTags(@PathVariable String articleId) {
        return ResponseEntity.ok(setService.getTags(articleId));
    }

    @PostMapping("/set/like/{contentId}/{userId}")
    public ResponseEntity<Map<String, Object>> toggleLike(@PathVariable String contentId,
                                                          @PathVariable String userId) {
        boolean liked = setService.toggleLike(contentId, userId);
        Long count = setService.getLikeCount(contentId);

        Map<String, Object> response = new HashMap<>();
        response.put("liked", liked);
        response.put("totalLikes", count);
        return ResponseEntity.ok(response);
    }

    @PostMapping("/set/online/{userId}")
    public ResponseEntity<String> setOnline(@PathVariable String userId) {
        setService.setUserOnline(userId);
        return ResponseEntity.ok("User set as online");
    }

    @GetMapping("/set/online-users")
    public ResponseEntity<Set<Object>> getOnlineUsers() {
        return ResponseEntity.ok(setService.getOnlineUsers());
    }



    // [3] Hash Operations

    @PostMapping("/hash/profile/{userId}")
    public ResponseEntity<String> saveProfile(@PathVariable String userId,
                                              @RequestBody HashOperationService.UserProfile profile) {
        hashService.saveUserProfile(userId, profile);
        return ResponseEntity.ok("Profile saved");
    }

    @GetMapping("/hash/profile/{userId}")
    public ResponseEntity<HashOperationService.UserProfile> getProfile(@PathVariable String userId) {
        HashOperationService.UserProfile profile = hashService.getUserProfile(userId);
        return profile != null ? ResponseEntity.ok(profile) : ResponseEntity.notFound().build();
    }

    @PostMapping("/hash/cart/{userId}")
    public ResponseEntity<String> addToCart(@PathVariable String userId,
                                            @RequestParam String productId,
                                            @RequestParam int quantity) {
        hashService.addToCart(userId, productId, quantity);
        return ResponseEntity.ok("Added to cart");
    }

    @GetMapping("/hash/cart/{userId}")
    public ResponseEntity<Map<Object, Object>> getCart(@PathVariable String userId) {
        return ResponseEntity.ok(hashService.getCart(userId));
    }

    @PostMapping("/hash/stats/{statType}")
    public ResponseEntity<String> incrementStat(@PathVariable String statType,
                                                @RequestParam String field,
                                                @RequestParam(defaultValue = "1") long delta) {
        hashService.incrementStat(statType, field, delta);
        return ResponseEntity.ok("Stat incremented");
    }



    // [4] Sorted Set Operations

    @PostMapping("/zset/leaderboard/{leaderboardName}")
    public ResponseEntity<String> updateScore(@PathVariable String leaderboardName,
                                              @RequestParam String playerId,
                                              @RequestParam double score) {
        sortedSetService.updateScore(leaderboardName, playerId, score);
        return ResponseEntity.ok("Score updated");
    }

    @GetMapping("/zset/leaderboard/{leaderboardName}/top")
    public ResponseEntity<List<Map<String, Object>>> getTopPlayers(
            @PathVariable String leaderboardName,
            @RequestParam(defaultValue = "10") int count) {

        Set<ZSetOperations.TypedTuple<Object>> topPlayers =
                sortedSetService.getTopPlayers(leaderboardName, count);

        List<Map<String, Object>> result = new ArrayList<>();
        int rank = 1;
        for (ZSetOperations.TypedTuple<Object> player : topPlayers) {
            Map<String, Object> entry = new HashMap<>();
            entry.put("rank", rank++);
            entry.put("playerId", player.getValue());
            entry.put("score", player.getScore());
            result.add(entry);
        }

        return ResponseEntity.ok(result);
    }

    @GetMapping("/zset/leaderboard/{leaderboardName}/rank/{playerId}")
    public ResponseEntity<Map<String, Object>> getPlayerRank(
            @PathVariable String leaderboardName,
            @PathVariable String playerId) {

        Long rank = sortedSetService.getRank(leaderboardName, playerId);
        Map<String, Object> response = new HashMap<>();
        response.put("playerId", playerId);
        response.put("rank", rank);

        return ResponseEntity.ok(response);
    }

    @PostMapping("/zset/trending/posts/{postId}/view")
    public ResponseEntity<String> recordView(@PathVariable String postId) {
        sortedSetService.recordPostView(postId);
        return ResponseEntity.ok("View recorded");
    }

    @GetMapping("/zset/trending/posts")
    public ResponseEntity<List<String>> getTrendingPosts(
            @RequestParam(defaultValue = "10") int count) {
        return ResponseEntity.ok(sortedSetService.getTrendingPosts(count));
    }

    @PostMapping("/zset/search")
    public ResponseEntity<String> recordSearch(@RequestParam String keyword) {
        sortedSetService.recordSearch(keyword);
        return ResponseEntity.ok("Search recorded");
    }

    @GetMapping("/zset/search/ranking")
    public ResponseEntity<List<Map<String, Object>>> getSearchRanking(
            @RequestParam(defaultValue = "10") int count) {
        return ResponseEntity.ok(sortedSetService.getSearchRanking(count));
    }
}

7.1. List

리스트는 순서 상관없이 연산속도 동일하고 pop, push를 left, right 모두 가능함. 그래서 보통 최근 활동 기록이랑 메세지 큐를 구현할 때 용함.

더보기
@Service
@RequiredArgsConstructor
public class ListOperationService {

    private final RedisTemplate<String, Object> redisTemplate;

    // 최근 활동 기록
    public void addRecentActivity(String userId, String activity) {

        // key
        String key = "recent:activity:" + userId;

        ListOperations<String, Object> listOps = redisTemplate.opsForList();
        listOps.leftPush(key, activity);

        // 최대 10개 유지
        listOps.trim(key, 0, 9);

        // 7일 TTL -> 7일 뒤 자동 삭제
        redisTemplate.expire(key, 7, TimeUnit.DAYS);
    }

    public List<Object> getRecentActivities(String userId) {
        String key = "recent:activity:" + userId;
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    // 메시지 큐 구현
    public void pushMessage(String queueName, Object message) {
        redisTemplate.opsForList().rightPush("queue:" + queueName, message);
    }

    public Object popMessage(String queueName, long timeout, TimeUnit unit) {
        return redisTemplate.opsForList().leftPop("queue:" + queueName, timeout, unit);
    }
}

7.2. Hash

더보기
@Service
@RequiredArgsConstructor
public class HashOperationService {

    private final RedisTemplate<String, Object> redisTemplate;

    // 사용자 프로필 관리
    @Data
    @Builder
    public static class UserProfile {
        private String name;
        private String email;
        private Integer age;    // Integer지만 Redis에 저장될때는 String이라서 밑에 HashOperations<String, String, Object>
        private String location;
    }

    // 유저 정보 저장
    public void saveUserProfile(String userId, UserProfile profile) {
        String key = "user:profile:" + userId;

        HashOperations<String, String, Object> hashOps = redisTemplate.opsForHash();

        // v1
//        hashOps.put(key, "name", profile.getName());
//        hashOps.put(key, "email", profile.getEmail());
//        hashOps.put(key, "age", profile.getAge());
//        hashOps.put(key, "location", profile.getLocation());

        // v2
        Map<String, Object> profileMap = new HashMap<>();
        profileMap.put("name", profile.getName());
        profileMap.put("email", profile.getEmail());
        profileMap.put("age", profile.getAge());
        profileMap.put("location", profile.getLocation());
        hashOps.putAll(key, profileMap);

    }

    // 유저 정보 조회
    public UserProfile getUserProfile(String userId) {
        String key = "user:profile:" + userId;

        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key); // key에 대한 하위 서브 key와 value를 한꺼번에 가져옴
        //System.out.println(entries);

        return UserProfile.builder()
                .name((String) entries.get("name"))
                .email((String) entries.get("email"))
                .age((Integer) entries.get("age"))
                .location((String) entries.get("location"))
                .build();
    }

    // 장바구니 관리
    public void addToCart(String userId, String productId, int quantity) {
        String key = "cart:" + userId;
        redisTemplate.opsForHash().put(key, productId, quantity);

        // 7일 저장 TTL
        redisTemplate.expire(key, 7, TimeUnit.DAYS);

    }

    public Map<Object, Object> getCart(String userId) {
        String key = "cart:" + userId;
        return redisTemplate.opsForHash().entries(key);
    }

    // 실시간 통계
    public void incrementStat(String statType, String field, long delta) {
        String key = "stats:" + statType + ":" + LocalDate.now();

        // field: count, delta: 숫자 -> increment()
        // 7일 저장 TTL
        redisTemplate.opsForHash().increment(key, field, delta);
        redisTemplate.expire(key, 7, TimeUnit.DAYS);
    }
}

7.3. Set

더보기
@Service
@RequiredArgsConstructor
public class SetOperationService {

    private final RedisTemplate<String, Object> redisTemplate;

    // 태그 관리 (중복 태그 처리)
    public void addTags(String articleId, Set<String> tags) {
        String key = "article:tags:" + articleId;
        redisTemplate.opsForSet().add(key, tags);
    }

    public Set<Object> getTags(String articleId) {
        String key = "article:tags:" + articleId;
        return redisTemplate.opsForSet().members(key);
    }

    // 좋아요 기능
    public boolean toggleLike(String contentId, String userId) {
        String key = "likes:" + contentId;

        SetOperations<String, Object> setOps = redisTemplate.opsForSet();

        // isMember, remove, add
        if (setOps.isMember(key, userId)) {
            // 이미 좋아요 누른 user -> 좋아요 취소(false)
            setOps.remove(key, userId);
            return false;
        } else {
            // 아직 좋아요 누르지 않은 user -> 좋아요 추가(true)
            setOps.add(key, userId);
            return true; // 좋아요 등록
        }
    }

    public Long getLikeCount(String contentId) {
        String key = "likes:" + contentId;
        return redisTemplate.opsForSet().size(key);
    }

    // 온라인 사용자 관리
    public void setUserOnline(String userId) {
        String key = "online:users";

        // 개별 사용자 TTL 관리 (5분)
        redisTemplate.opsForSet().add(key, userId);

        // TTL 관리 (5분)
        String userKey = "online:user:" + userId;
        redisTemplate.opsForValue().set(userKey, true, 5, TimeUnit.MINUTES);

    }

    public Set<Object> getOnlineUsers() {
        String key = "online:users";
        return redisTemplate.opsForSet().members(key);
    }
}

7.4. SortedSet

더보기
@Service
@RequiredArgsConstructor
public class SortedSetOperationService {

    private final RedisTemplate<String, Object> redisTemplate;

    // 리더보드 (점수 기반 랭킹)
    public void updateScore(String leaderboardName, String playerId, double score) {
        String key = "leaderboard:" + leaderboardName;
        ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet();
        zSetOps.add(key, playerId, score);
    }

    public Set<ZSetOperations.TypedTuple<Object>> getTopPlayers(String leaderboardName, int count) {
        String key = "leaderboard:" + leaderboardName;
        // 높은 점수 순으로 상위 3명
        ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet();
        return zSetOps.reverseRangeWithScores(key, 0, count - 1);
    }

    public Long getRank(String leaderboardName, String playerId) {
        String key = "leaderboard:" + leaderboardName;
        ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet();
        return zSetOps.reverseRank(key, playerId);
    }

    // 인기 게시물 (시간 가중치 적용)
    public void recordPostView(String postId) {
        String key = "trending:posts";
        ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet();

        // 현재시간을 점수로 넣어서 최신 클릭이 점수가 높게
        double score = System.currentTimeMillis() / 1000.0;
        zSetOps.add(key, postId, score);

        // 7일 이전 데이터는 제거
        long sevenDays = System.currentTimeMillis() - ( 7 * 24 * 60 * 60 * 1000);
        zSetOps.removeRangeByScore(key, 0, sevenDays / 1000.0);
    }

    public List<String> getTrendingPosts(int count) {
        String key = "trending:posts";
        ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet();

        // reverseRange
        Set<Object> posts = zSetOps.reverseRange(key, 0, count - 1);

        return posts.stream()
                .map(obj -> obj.toString())
                .collect(Collectors.toList());
    }

    // 실시간 검색어 순위
    public void recordSearch(String keyword) {
        String key = "search:ranking:" + LocalDate.now();
        ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet();

        // intcrementScore: 검색 할 때 마다 -> score 증가
        zSetOps.incrementScore(key, keyword, 1);
    }

    public List<Map<String, Object>> getSearchRanking(int count) {
        String key = "search:ranking:" + LocalDate.now();
        Set<ZSetOperations.TypedTuple<Object>> ranking =
                redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, count - 1);
        // 1. 랭킹에 대한 부분을 가져와야 한다. ZSetOperation 형식으로 객체를 가져오고,
        // 2. 원하는 형식(List<Map<String, Object>> 형식으로 리턴해줘야 한다.
        List<Map<String, Object>> result = new ArrayList<>();
        int rank = 1;
        for (ZSetOperations.TypedTuple<Object> tuple : ranking) {
            Map<String, Object> rankedSearch = new HashMap<>();
            // - rank: 순위 값
            // - keyword: 검색 키워드
            // - count: 검색 총 카운트
            rankedSearch.put("rank", rank++);
            rankedSearch.put("keyword", tuple.getValue());
            rankedSearch.put("count", tuple.getScore().intValue());

            result.add(rankedSearch);
        }
        return result;
    }
}

 

 

 

 

'Database > Redis' 카테고리의 다른 글

[Optimization-2] 캐싱 개념 (2)  (0) 2025.12.28
[Optimization-1] 캐싱 개념(1)  (0) 2025.12.28
[Basic-4] 세션 관리  (0) 2025.07.03
[Basic-3] DB 병렬 작업과 Lock 전략  (0) 2025.07.03
[Basic-2] 캐싱 전략  (0) 2025.07.02
'Database/Redis' 카테고리의 다른 글
  • [Optimization-1] 캐싱 개념(1)
  • [Basic-4] 세션 관리
  • [Basic-3] DB 병렬 작업과 Lock 전략
  • [Basic-2] 캐싱 전략
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30)
        • Core (18)
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[Basic-1] 기초 이론 및 구축 실습
상단으로

티스토리툴바