Spring Data Redis
Spring Data Redis
는 Spring Framework에서 Redis와의 통합을 쉽게 처리할 수 있도록 지원하는 라이브러리입니다. 이 라이브러리를 사용하면 Redis의 다양한 데이터 구조(문자열, 리스트, 해시, 셋 등)를 다루는 데 필요한 기능을 간편하게 구현할 수 있습니다. 또한, Spring Data Redis는 Redis를 캐시, 세션 관리, 실시간 데이터 처리 등의 목적으로 사용할 수 있도록 돕습니다.
설정 및 설치
Spring Data Redis를 Gradle과 Maven에서 설치하는 코드를 아래와 같이 보여드리겠습니다.
1. Gradle
build.gradle
파일에 아래와 같이 의존성을 추가합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
2. Maven
pom.xml
파일에 아래와 같이 의존성을 추가합니다.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
위 코드를 각각의 빌드 도구에 맞춰 추가하면 Spring Data Redis를 사용할 수 있습니다.
클라이언트
Spring Data Redis에서 사용하는 주요 클라이언트는 Lettuce와 Jedis 두 가지가 있습니다. 이 두 클라이언트는 Redis와의 통신을 담당하며, 각기 다른 특징과 사용 사례가 있습니다. 각각의 특징, 차이점, 공통점, 그리고 언제 사용하는 것이 적절한지 설명드리겠습니다.
1. Lettuce
Lettuce는 Spring Data Redis에서 기본 클라이언트로 사용되는 비동기 및 동기, 반응형 Redis 클라이언트입니다.
특징:
- 비동기 프로그래밍 지원: Lettuce는 비동기적 I/O 처리가 가능하여, 많은 요청을 처리할 때 성능을 극대화할 수 있습니다. 이를 통해 더 나은 확장성과 성능을 제공합니다.
- Reactive Streams 지원:
Spring WebFlux
나Project Reactor
와 같은 반응형 프로그래밍을 위한 완전한 지원을 제공합니다. - Thread-safe: Lettuce는 싱글 인스턴스에서 멀티스레드 환경에서도 안전하게 사용할 수 있습니다. 이는 여러 스레드에서 동시에 Redis에 접근할 때 유리합니다.
장점:
- 비동기 및 동기 처리, 반응형 지원이 모두 가능하여 유연성 제공.
- 멀티스레드 환경에서 안전하게 사용 가능.
단점:
- 비동기 처리를 잘 이해하고 사용하는 것이 필요하기 때문에 러닝 커브가 있을 수 있음.
2. Jedis
Jedis는 Spring Data Redis에서 오래전부터 사용된 Redis 클라이언트입니다. 동기 방식의 처리를 기본으로 합니다.
특징:
- 동기 처리 방식: Jedis는 동기 방식으로 작동하여 각 요청이 완료될 때까지 블로킹이 발생합니다.
- 스레드 안전성: Jedis는 멀티스레드 환경에서 스레드당 하나의 연결을 사용해야 안전하게 작동합니다. 이를 위해 JedisPool을 사용하여 여러 스레드에서 안전하게 Redis에 접근할 수 있습니다.
장점:
- 동기 방식의 간결한 처리.
- Redis의 고급 기능(파이프라이닝, 트랜잭션 등)을 손쉽게 사용할 수 있음.
단점:
- 멀티스레드 환경에서는 JedisPool을 사용해야 하며, 연결 관리가 추가적으로 필요.
- 비동기 및 반응형 프로그래밍을 지원하지 않음.
3. Lettuce와 Jedis의 공통점
- Redis 클라이언트: 둘 다 Redis와 통신하기 위한 클라이언트로, 기본적인 Redis 명령어를 지원하며 Spring Data Redis에서 쉽게 사용할 수 있습니다.
- Spring 지원: 두 클라이언트 모두 Spring Data Redis와 완벽하게 통합되며, Spring Boot 환경에서 설정이 매우 간편합니다.
4. Lettuce와 Jedis의 차이점
동기/비동기 처리:
- Lettuce는 비동기, 동기 및 반응형 처리를 모두 지원합니다.
- Jedis는 동기 처리만 지원하며, 비동기/반응형 프로그래밍을 지원하지 않습니다.
스레드 안전성:
- Lettuce는 멀티스레드 환경에서도 스레드 안전한 방식으로 동작합니다.
- Jedis는 스레드당 하나의 연결을 사용해야 하므로, 멀티스레드 환경에서는 JedisPool을 사용해야 합니다.
5. 어떤 경우에 각각을 사용해야 할까?
Lettuce를 사용해야 하는 경우:
- 비동기 프로그래밍, 반응형 애플리케이션이 필요한 경우.
- 멀티스레드 환경에서 Redis를 사용해야 할 때.
Jedis를 사용해야 하는 경우:
- 동기 방식이 충분하고, 간단한 Redis 사용 사례가 있는 경우.
- 스레드 안전성 관리가 가능한 단일 스레드 환경이나 JedisPool을 활용하는 멀티스레드 환경에서 사용할 때.
따라서, 성능이 중요한 비동기, 반응형 프로그래밍이 필요한 경우 Lettuce를 사용하는 것이 적합하며, 단순 동기 처리와 Redis의 기본적인 사용에 중점을 둔 애플리케이션에서는 Jedis를 사용하는 것이 좋습니다.
Redis Template
RedisTemplate
은 Spring Data Redis에서 Redis와의 상호작용을 쉽게 하기 위해 제공되는 핵심 클래스입니다. Redis 서버와의 데이터 읽기, 쓰기, 삭제와 같은 다양한 작업을 추상화하여 쉽게 수행할 수 있도록 도와줍니다. RedisTemplate
은 여러 Redis 데이터 타입을 지원하며, Spring에서 Redis를 더 효율적으로 사용할 수 있도록 설계되었습니다.
1. RedisTemplate
의 주요 특징
- 높은 추상화 수준: Redis의 복잡한 명령어들을 추상화하여 쉽게 사용할 수 있습니다. 직접 Redis 명령어를 사용하지 않아도, 키-값 저장소로서의 기능을 쉽게 활용할 수 있습니다.
- 다양한 데이터 타입 지원: String, List, Set, ZSet, Hash와 같은 Redis의 다양한 데이터 구조를 지원합니다.
- 직렬화와 역직렬화: Redis에서 데이터를 저장하고 읽어올 때 직렬화(Serialization) 및 역직렬화(Deserialization)가 필요한데,
RedisTemplate
은 이를 처리하는 기능을 제공합니다. 기본적으로 바이트 배열로 데이터를 처리하지만, 문자열이나 JSON 등 다양한 직렬화 방식을 설정할 수 있습니다. - 통합된 API: Redis 명령어를 통합된 API로 제공하여 Redis의 여러 기능을 하나의 인터페이스로 편리하게 사용할 수 있습니다.
2. RedisTemplate
의 구조
RedisTemplate
은 기본적으로 제네릭 타입을 사용하여 다양한 데이터 타입을 처리할 수 있도록 설계되어 있습니다. 보통 RedisTemplate<String, Object>
로 사용하여 문자열 키와 객체 데이터를 저장하거나 조회하는 것이 일반적입니다.
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;
}
위 코드에서 RedisConnectionFactory
는 Redis 서버와의 연결을 관리하는 역할을 합니다.
3. 주요 메소드
RedisTemplate
은 Redis와 상호작용하기 위한 다양한 메소드를 제공합니다.
opsForValue(): Redis의 String 데이터를 처리할 때 사용됩니다. 단순한 키-값 구조에서 사용됩니다.
redisTemplate.opsForValue().set("key", "value"); String value = redisTemplate.opsForValue().get("key");
opsForList(): Redis의 List 데이터를 처리할 때 사용됩니다.
redisTemplate.opsForList().rightPush("myList", "value1"); String value = redisTemplate.opsForList().leftPop("myList");
opsForSet(): Redis의 Set 데이터를 처리할 때 사용됩니다.
redisTemplate.opsForSet().add("mySet", "value1", "value2"); Set<String> members = redisTemplate.opsForSet().members("mySet");
opsForZSet(): Redis의 Sorted Set 데이터를 처리할 때 사용됩니다.
redisTemplate.opsForZSet().add("myZSet", "value", 1.0); Set<String> range = redisTemplate.opsForZSet().range("myZSet", 0, -1);
opsForHash(): Redis의 Hash 데이터를 처리할 때 사용됩니다.
redisTemplate.opsForHash().put("myHash", "field1", "value1"); String value = (String) redisTemplate.opsForHash().get("myHash", "field1");
4. 직렬화 설정
Redis는 기본적으로 데이터를 바이트 배열로 저장하기 때문에, RedisTemplate
은 데이터를 직렬화 및 역직렬화해야 합니다. 기본적으로는 JdkSerializationRedisSerializer를 사용하지만, 일반적으로 성능을 개선하기 위해 StringRedisSerializer나 Jackson2JsonRedisSerializer를 설정합니다.
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Key를 String으로 직렬화
template.setKeySerializer(new StringRedisSerializer());
// Value를 JSON으로 직렬화
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
return template;
}
5. RedisTemplate
사용 시 고려사항
- 직렬화 문제: 객체를 Redis에 저장할 때 객체가 직렬화되어 Redis에 저장됩니다. 이때 객체가 직렬화 가능해야 하고, 필요에 따라 적절한 직렬화 방식을 선택해야 합니다.
- 성능: 기본적으로 Redis는 매우 빠른 데이터 저장소이지만, 직렬화 방식이나 네트워크 상태에 따라 성능이 달라질 수 있습니다. 직렬화 형식으로 JSON을 사용하면 데이터를 사람이 읽기 쉬워지지만, JDK 직렬화보다는 느릴 수 있습니다.
6. 실제 사용 예시
@Service
public class RedisService {
private final RedisTemplate<String, Object> redisTemplate;
public RedisService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void saveData(String key, Object data) {
redisTemplate.opsForValue().set(key, data);
}
public Object getData(String key) {
return redisTemplate.opsForValue().get(key);
}
public void deleteData(String key) {
redisTemplate.delete(key);
}
}
요약:
RedisTemplate
은 Spring Data Redis에서 Redis와의 상호작용을 단순화하고 추상화된 API를 제공함으로써 개발자가 Redis를 더 쉽게 사용할 수 있도록 도와줍니다. 다양한 데이터 타입 지원과 직렬화 옵션, 그리고 통합된 API를 통해 Redis의 기능을 효과적으로 활용할 수 있습니다.
Spring Data Redis를 활용한 캐시 실습
Redis는 NoSQL 데이터베이스로, 빠른 성능과 다양한 데이터 구조를 제공하는 인메모리 데이터 저장소입니다. Redis는 특히 캐싱과 세션 관리 등 다양한 분야에서 활용되며, Spring Data Redis는 Spring 애플리케이션과 Redis 간의 통합을 쉽게 처리할 수 있도록 돕는 라이브러리입니다. 이번 글에서는 Spring Boot와 Spring Data Redis를 활용하여 캐시 시스템을 구축하는 방법을 실습을 통해 알아보겠습니다.
1. 프로젝트 환경 설정
1.1 Gradle 의존성 추가
Spring Data Redis와 JPA를 사용하기 위해 아래와 같은 의존성을 build.gradle
파일에 추가합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
1.2 Application 설정
application.yml
파일에서 Redis 서버에 대한 설정을 추가합니다. 여기서는 Redis가 로컬에 설치되어 있다고 가정하겠습니다.
spring:
application:
name: cache
datasource:
url: "jdbc:mysql://localhost:3306/fastsns"
username: root
password: root
redis:
host: localhost
port: 6379
2. Entity 및 Repository 설정
2.1 User 엔티티
먼저, MySQL에 저장할 User
엔티티를 정의합니다.
package com.example.cache.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Builder
@Getter
@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 30, nullable = false)
private String name;
@Column(length = 100, nullable = false, unique = true)
private String email;
@CreatedDate
@Column(name = "created_at")
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
2.2 User Repository
이제 User 데이터를 저장하고 조회할 수 있도록 UserRepository
를 정의합니다.
package com.example.cache.repository;
import com.example.cache.domain.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
3. Redis 설정
Redis와의 통신을 위한 RedisTemplate을 설정합니다. 여기서는 JSON 직렬화를 통해 Redis에 데이터를 저장합니다.
package com.example.cache.config;
import com.example.cache.domain.entity.User;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
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
RedisTemplate<String, User> userRedisTemplate(RedisConnectionFactory connectionFactory) {
var objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
var template = new RedisTemplate<String, User>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, User.class));
return template;
}
}
userRedisTemplate
메소드는 Spring Data Redis에서 Redis와의 상호작용을 더 쉽게 하고, User 객체를 Redis에 저장하고 불러오기 위한 RedisTemplate
인스턴스를 생성하고 설정하는 메소드입니다. 이 메소드에서 Redis 서버와의 연결을 설정하고, 데이터를 저장할 때와 읽어올 때 사용할 직렬화 방식을 정의합니다.
메소드 정의
@Bean
public RedisTemplate<String, User> userRedisTemplate(RedisConnectionFactory connectionFactory) {
var objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
var template = new RedisTemplate<String, User>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, User.class));
return template;
}
userRedisTemplate 메소드 설명
@Bean
어노테이션:- 이 메소드는 Spring의 빈으로 등록되므로, 다른 곳에서 의존성 주입을 통해 사용할 수 있습니다.
RedisTemplate<String, User>
타입의 빈을 생성하며, 이 템플릿은 Redis에서 String을 키로, User 객체를 값으로 저장하고 불러올 때 사용됩니다.
ObjectMapper
설정:- ObjectMapper는 Jackson 라이브러리를 이용한 JSON 직렬화 및 역직렬화 도구입니다.
- 이 코드에서는 Jackson2를 사용하여 Java 객체와 JSON 간 변환을 정의하고 있으며,
JavaTimeModule
을 등록하여 Java 8의LocalDateTime
과 같은 시간 관련 객체도 처리할 수 있도록 설정합니다. SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
는 비활성화되어, 날짜가 타임스탬프로 변환되지 않고 ISO-8601 형식의 문자열로 직렬화됩니다.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
는 설정된 대로false
로 비활성화되어, JSON에서 알 수 없는 필드가 있을 때 오류를 발생시키지 않도록 설정합니다.
RedisTemplate<String, User>
인스턴스 생성:RedisTemplate
은 Spring Data Redis에서 Redis와 상호작용하기 위한 핵심 클래스입니다. 이 템플릿을 사용하여 Redis에 데이터를 저장하고 조회하는 작업을 처리합니다.
ConnectionFactory 설정
:template.setConnectionFactory(connectionFactory)
는 Redis 서버와의 연결을 관리하는 RedisConnectionFactory를 템플릿에 설정합니다. 이 설정을 통해 Redis와의 연결이 관리됩니다.
Key와 Value에 대한 직렬화 설정:
Key Serializer:
template.setKeySerializer(new StringRedisSerializer())
는 Redis에서 사용하는 키를 문자열로 직렬화합니다. 키는String
타입으로 처리되며, 보통RedisTemplate<String, ...>
에서StringRedisSerializer
를 사용합니다.
Value Serializer:
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, User.class))
는 User 객체를 JSON 형식으로 직렬화 및 역직렬화할 때 사용됩니다. Jackson을 사용해 Java 객체(User)를 JSON으로 변환하여 Redis에 저장하고, 다시 Java 객체로 변환해 가져올 수 있도록 합니다.- 이 과정에서,
User
클래스는 Jackson2를 통해 JSON 형태로 저장됩니다.
template
반환:- 마지막으로
template
인스턴스를 반환하여, Spring에서 이RedisTemplate<String, User>
빈을 사용해 Redis와 상호작용할 수 있도록 합니다.
- 마지막으로
동작 설명
데이터 저장:
- Redis에 User 객체를 저장할 때, 키는
StringRedisSerializer
를 사용하여 문자열로 직렬화되며, 값(User 객체)은Jackson2JsonRedisSerializer
를 사용하여 JSON 형식으로 저장됩니다.
- Redis에 User 객체를 저장할 때, 키는
데이터 조회:
- Redis에서 데이터를 조회할 때도 동일한 직렬화 설정을 사용하여 데이터를 JSON에서 다시 User 객체로 역직렬화하여 반환합니다.
Jackson 설정:
ObjectMapper
는 Jackson에서 사용되며, 이 설정을 통해 날짜 및 시간 필드나 알 수 없는 JSON 필드와 같은 특수한 상황을 처리할 수 있습니다.
요약
userRedisTemplate
메소드는 Redis에서 User 객체를 저장하고 불러오기 위한RedisTemplate<String, User>
를 생성하는 역할을 합니다.- 키는
StringRedisSerializer
를 사용해 문자열로 저장되고, 값은Jackson2JsonRedisSerializer
를 사용해 JSON 형식으로 직렬화된 User 객체로 저장됩니다. - Redis와의 연결을 관리하는
RedisConnectionFactory
를 템플릿에 설정하여, Spring 애플리케이션이 Redis 서버와 안전하게 통신할 수 있도록 합니다.
이 설정을 통해 RedisTemplate이 Redis 서버와 연결되며, User 객체를 직렬화하여 Redis에 저장할 수 있습니다.
4. 캐시 적용 - Cache Aside 패턴
이제 UserService에서 캐시를 사용하는 로직을 구현하겠습니다. 캐시된 데이터가 있는 경우 Redis에서 데이터를 가져오고, 없으면 DB에서 가져와 Redis에 캐싱하는 Cache Aside 패턴을 사용합니다.
package com.example.cache.service;
import com.example.cache.domain.entity.User;
import com.example.cache.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final RedisTemplate<String, User> userRedisTemplate;
public User getUser(final Long id) {
var key = "users:%d".formatted(id);
var cachedUser = userRedisTemplate.opsForValue().get(key);
if (cachedUser != null) return cachedUser;
User user = userRepository.findById(id).orElseThrow();
userRedisTemplate.opsForValue().set(key, user);
return user;
}
}
5. 컨트롤러 설정
마지막으로, REST API를 통해 특정 사용자를 조회하는 컨트롤러를 구현합니다.
package com.example.cache.controller;
import com.example.cache.domain.entity.User;
import com.example.cache.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUser(id);
}
}
6. Redis 로그 및 서버 로그
6.1 Redis 서버 로그 확인
Redis는 데이터를 빠르게 처리하기 때문에, Redis CLI의 monitor
명령어를 사용하여 실시간으로 Redis 명령어를 확인할 수 있습니다.
docker exec -it redis-server redis-cli
127.0.0.1:6379> monitor
OK
위 명령어를 실행하면 Redis 서버에서 실행되는 모든 명령어를 실시간으로 볼 수 있습니다.
6.2 Spring 서버 로그
Spring 서버 로그에서 MySQL 쿼리와 Redis 캐시 사용을 확인할 수 있습니다. 첫 번째 요청 시에는 데이터베이스에서 조회하지만, 이후의 요청에서는 캐시에서 데이터를 반환합니다.
Hibernate: select u1_0.id,u1_0.created_at,u1_0.email,u1_0.name,u1_0.updated_at from user u1_0 where u1_0.id=?
MySQL 쿼리가 실행된 것을 확인할 수 있으며, 이후 Redis 캐시에 저장됩니다.
127.0.0.1:6379> GET users:1
Redis 서버 로그에서 캐시된 데이터를 가져오는 GET
명령어가 기록된 것을 확인할 수 있습니다.
7. 캐시 적용 테스트 - curl
이제 curl
을 사용하여 API를 테스트합니다.
curl localhost:8080/users/1
처음 요청 시에는 DB에서 데이터를 가져오고, Redis에 캐싱합니다. 두 번째 요청부터는 캐시된 데이터를 사용합니다.
테스트 예시
1차 요청 (DB에서 조회)
curl localhost:8080/users/1
Spring 서버 로그:
Hibernate: select u1_0.id,u1_0.created_at,u1_0.email,u1_0.name,u1_0.updated_at from user u1_0 where u1_0.id=?
Redis 로그:
1726284873.209768 [0 172.17.0.1:63524] "GET" "users:1"
1726284873.266730 [0 172.17.0.1:63524] "SET" "users:1" "{\"id\":1,\"name\":\"noa\",\"email\":\"noa@gmail.com\",\"createdAt\":\"2024-09-14T11:24:35.241581\",\"updatedAt\":\"2024-09-14T11:24:35.241581\"}"
2차 요청 (캐시에서 조회)
curl localhost:8080/users/1
Redis 로그:
1726284981.916808 [0 172.17.0.1:63524] "GET" "users:1"
두 번째 요청부터는 DB를 조회하지 않고 Redis에서 캐시된 데이터를 가져옵니다.
예상 문제점
위 실습을 통해서 userRedisTempalte은 객체마다 새로운 템플릿을 만들어야 이슈가 있습니다. 여기서는 User 엔티티에 대응하여 만들었습니다. WAS에서는 수많은 객체를 취급해야 합니다. 이는 엄청난 불편함. 즉 반복되고 중복된 하드 코딩 작업을 유발 할 수 있습니다. 이를 최소화하거나 재활용 할 수 있는 패턴을 사용해야 합니다.
RedisConfig
package com.example.cache.config;
import com.example.cache.domain.entity.User;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
RedisTemplate<String, User> userRedisTemplate(RedisConnectionFactory connectionFactory){
var objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
var template = new RedisTemplate<String, User>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, User.class));
return template;
}
@Bean
RedisTemplate<String, Object> objectRedisTemplate(RedisConnectionFactory connectionFactory){
var objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
var template = new RedisTemplate<String, Object>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper));
return template;
}
}
UserService
package com.example.cache.service;
import com.example.cache.domain.entity.User;
import com.example.cache.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final RedisTemplate<String, User> userRedisTemplate;
private final RedisTemplate<String, Object> objectRedisTemplate;
public User getUser(final Long id){
var key = "users:%d".formatted(id);
var cachedUser = objectRedisTemplate.opsForValue().get(key);
if(cachedUser != null) return (User) cachedUser;
User user = userRepository.findById(id).orElseThrow();
objectRedisTemplate.opsForValue().set(key, user, Duration.ofSeconds(30));
return user;
}
}
최초 호출
curl localhost:8080/users/5
{"id":5,"name":"bob","email":"bob@gmail.com","createdAt":"2024-09-14T11:30:03.915556","updatedAt":"2024-09-14T11:30:03.915556"}%
1726285906.502768 [0 172.17.0.1:63628] "GET" "users:5"
1726285906.587150 [0 172.17.0.1:63628] "SETEX" "users:5" "30" "{\"id\":5,\"name\":\"bob\",\"email\":\"bob@gmail.com\",\"createdAt\":\"2024-09-14T11:30:03.915556\",\"updatedAt\":\"2024-09-14T11:30:03.915556\"}"
Hibernate: select u1_0.id,u1_0.created_at,u1_0.email,u1_0.name,u1_0.updated_at from user u1_0 where u1_0.id=?
오류 발생 - 동일 호출
curl localhost:8080/users/5
{"timestamp":"2024-09-14T03:51:48.911+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.example.cache.domain.entity.User (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; com.example.cache.domain.entity.User is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @36fe9494)\n\tat com.example.cache.service.UserService.getUser(UserService.java:22)\n\tat com.example.cache.controller.UserController.getUser(UserController.java:17)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:384)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:904)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)\n\tat java.base/java.lang.Thread.run(Thread.java:840)\n","message":"class java.util.LinkedHashMap cannot be cast to class com.example.cache.domain.entity.User (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; com.example.cache.domain.entity.User is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @36fe9494)","path":"/users/5"}%
- 위 오류를 해결하기 위해 아래 RedisConfig 클래스의 objectRedisTemplate 메소드에서 PolymorphicTypeValidator 타입의 객체를 생성하고 이를 objectMapper 객체의 activateDefaultTyping 메소드에 설정해줍니다.
package com.example.cache.config;
import com.example.cache.domain.entity.User;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
RedisTemplate<String, User> userRedisTemplate(RedisConnectionFactory connectionFactory){
var objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
var template = new RedisTemplate<String, User>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, User.class));
return template;
}
@Bean
RedisTemplate<String, Object> objectRedisTemplate(RedisConnectionFactory connectionFactory){
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder().allowIfSubType(Object.class).build();
var objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
var template = new RedisTemplate<String, Object>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper));
return template;
}
}
Redis Hash Annotation 실습
이번 실습에서는 Spring Data Redis에서 제공하는 @RedisHash
애너테이션을 활용하여, Redis의 Hash 자료 구조를 쉽게 다루는 방법을 소개합니다. 이를 통해 Redis에 데이터를 저장하고, 캐싱 및 데이터 만료 시간을 설정할 수 있는 방식으로 애플리케이션의 성능을 향상시킬 수 있습니다.
1. @RedisHash
애너테이션을 사용한 엔티티 정의
코드 분석
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@RedisHash(value = "redishash-user", timeToLive = 30L)
public class RedisHashUser {
@Id
private Long id;
private String name;
@Indexed
private String email;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@RedisHash
: 해당 클래스가 Redis의 Hash 구조로 저장될 수 있도록 지정하는 애너테이션입니다.value
속성은 Redis에서 키의 접두사를 지정하며,timeToLive
는 해당 데이터가 Redis에서 유지되는 시간(초 단위)을 설정합니다. 위 코드에서는 30초 동안만 데이터가 Redis에 유지됩니다.@Indexed
: 필드에 인덱스를 적용하여 Redis에서 효율적으로 데이터를 검색할 수 있도록 합니다.@Id
: Redis에서 해당 필드를 키로 사용합니다.
2. Repository 정의
public interface RedisHashUserRepository extends CrudRepository<RedisHashUser, Long> {
}
CrudRepository
를 상속받아 Redis에 저장된 데이터를 쉽게 조회하고 저장할 수 있습니다.
3. 서비스 로직
@Service
@RequiredArgsConstructor
public class UserService {
private final RedisHashUserRepository redisHashUserRepository;
private final UserRepository userRepository;
public RedisHashUser getUser2(final Long id) {
// Redis에서 값을 조회, 없으면 DB에서 가져와 Redis에 저장
return redisHashUserRepository.findById(id).orElseGet(() -> {
User user = userRepository.findById(id).orElseThrow();
return redisHashUserRepository.save(
RedisHashUser.builder()
.id(user.getId())
.name(user.getName())
.email(user.getEmail())
.createdAt(user.getCreatedAt())
.updatedAt(user.getUpdatedAt())
.build()
);
});
}
}
- Redis 우선 조회: Redis에서 해당 키(
id
)에 해당하는 값을 먼저 찾고, 없을 경우 DB에서 데이터를 가져온 뒤 Redis에 저장합니다. - 캐싱된 데이터는 30초 동안 Redis에 유지되며, 만료되면 다시 DB에서 데이터를 가져와 저장하게 됩니다.
4. 컨트롤러
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/redishash-users/{id}")
public RedisHashUser getUser2(@PathVariable Long id) {
return userService.getUser2(id);
}
}
- API 호출:
GET
요청으로/redishash-users/{id}
에 접근하면, 해당id
에 대응하는 사용자의 정보를 Redis에서 조회하고 반환합니다.
Redis 명령어 로그 분석
Redis 서버에서의 작업 로그
1726289704.566303 [0 172.17.0.1:64126] "HGETALL" "redishash-user:4"
1726289704.633755 [0 172.17.0.1:64126] "DEL" "redishash-user:4"
1726289704.641130 [0 172.17.0.1:64126] "HMSET" "redishash-user:4" "_class" "com.example.cache.domain.entity.RedisHashUser" "createdAt" "2024-09-14T11:30:03.898036" "email" "noa@gmail.com" "id" "4" "name" "noa" "updatedAt" "2024-09-14T11:30:03.898036"
1726289704.645208 [0 172.17.0.1:64126] "SADD" "redishash-user" "4"
1726289704.649745 [0 172.17.0.1:64126] "EXPIRE" "redishash-user:4" "30"
HGETALL
: Redis에서 해당 사용자의 모든 필드 데이터를 조회합니다. 여기서redishash-user:4
라는 키로 조회가 일어납니다.DEL
: 만료된 데이터를 삭제합니다. 캐시가 30초로 설정되어 있으므로 이후에 데이터를 삭제하고 다시 저장하는 로직이 발생합니다.HMSET
: 새로운 데이터를 Redis Hash 구조로 저장합니다. 각 필드가 Hash로 저장되고, Redis에서 관리됩니다.EXPIRE
: 해당 키의 만료 시간을 30초로 설정합니다.
테스트
curl
명령어를 사용하여 API를 테스트한 로그입니다:
curl localhost:8080/redishash-users/4
{"id":4,"name":"noa","email":"noa@gmail.com","createdAt":"2024-09-14T11:30:03.898036","updatedAt":"2024-09-14T11:30:03.898036"}%
- 첫 번째 조회: Redis에 데이터가 없으므로, DB에서 데이터를 조회한 뒤 Redis에 저장하고 응답합니다.
curl localhost:8080/redishash-users/5
{"id":5,"name":"bob","email":"bob@gmail.com","createdAt":"2024-09-14T11:30:03.915556","updatedAt":"2024-09-14T11:30:03.915556"}%
- 반복 조회: 이후 반복된 조회에서는 Redis에서 데이터를 바로 가져오기 때문에 빠르게 응답합니다.
요약
@RedisHash
를 활용한 이 방식은 Redis를 캐시로 사용하는 애플리케이션에서 매우 유용합니다. 특히, 데이터를 자동으로 Redis Hash 구조로 저장하고, TTL(Time to Live) 설정을 통해 캐시의 만료 시간을 제어할 수 있다는 장점이 있습니다. 또한, @Indexed
를 통해 특정 필드로 빠르게 검색할 수 있습니다.
이 방식은 RedisTemplate을 사용하지 않고, Spring Data Redis의 애너테이션 기반으로 데이터를 다룰 수 있다는 점에서 간편하며, 캐시 및 데이터를 Redis에 저장하고 관리하는 다양한 상황에서 사용할 수 있습니다.
1. "SADD" "redishash-user" "4" 명령어 발생 이유
SADD
명령어는 Redis에서 Set 자료 구조에 데이터를 추가할 때 사용됩니다. 즉, "SADD" "redishash-user" "4"
명령어는 redishash-user
라는 Set에 4
라는 값을 추가하는 작업을 의미합니다.
이 명령어가 발생하는 이유:
- Spring Data Redis에서는
@RedisHash
애너테이션을 사용하여 Redis에 데이터를 저장할 때, 해당 엔티티를 Redis Set에 추가하는 방식으로 관리합니다. - Set을 사용한 이유는 Redis에서 데이터를 관리하고 조회할 때 인덱스 처리를 효율적으로 할 수 있도록 지원하기 위함입니다.
SADD
명령어를 통해 엔티티의 ID(여기서는 4)를 Set에 추가하여, 빠르게 해당 키를 조회하거나 관리할 수 있습니다. - 이 과정은 Redis에서 데이터 관리 효율성을 높이기 위해 자동으로 처리되며, 데이터를 삭제하거나 만료 시킬 때도 해당 Set을 이용하여 데이터를 쉽게 관리합니다.
2. TTL(Time To Live) 적용 여부
TTL(Time to Live)이 적용되지 않은 것은 아닙니다. TTL은 정상적으로 적용되었습니다. Redis 로그를 보면 EXPIRE
명령어가 호출되고 있으며, 이는 해당 Redis 키에 만료 시간을 설정하는 작업을 의미합니다.
"EXPIRE" "redishash-user:4" "30"
- 위 로그에서 확인할 수 있듯이, "redishash-user:4"라는 키에 대해 TTL이 30초로 설정되었습니다. 이는 30초 후 해당 키가 자동으로 삭제됨을 의미합니다.
- TTL이 설정된 데이터는 만료 시간이 지나면 Redis에서 자동으로 제거됩니다. 즉, TTL이 정상적으로 적용되었고, 30초 후에는 Redis에서 이 데이터가 사라지게 됩니다.
요약
- "SADD" "redishash-user" "4" 명령어는 Redis Set을 사용하여 엔티티의 ID를 관리하는 데 사용됩니다. 이는 Spring Data Redis가 데이터를 효율적으로 관리하고 인덱싱할 수 있도록 도와줍니다.
- TTL이 적용되었으며,
EXPIRE
명령어를 통해 30초의 만료 시간이 설정된 것을 확인할 수 있습니다.
RedisTemplate
과 RedisHash
애너테이션을 사용하는 두 가지 방법은 Spring Data Redis에서 Redis와 상호작용하는 방식으로, 각각의 목적과 특성에 따라 사용됩니다. 두 방법의 차이점과 각 방법을 사용하는 경우, 그리고 사용 시 주의해야 할 사항을 비교하여 설명하겠습니다.
1. RedisTemplate
RedisTemplate
은 Redis의 다양한 데이터 구조(문자열, 리스트, 해시, 집합 등)와 상호작용하기 위한 범용적인 클래스입니다. 이 방법은 Redis 명령어를 직접적으로 다루고, 개발자가 Redis에서 데이터를 어떻게 저장하고 가져올지를 세밀하게 제어할 수 있습니다.
특징
- 유연성:
RedisTemplate
은 Redis의 다양한 데이터 구조를 직접 다룰 수 있어, 문자열, 리스트, 해시, 셋 등 다양한 데이터를 쉽게 처리할 수 있습니다. - 직접 제어: 개발자는 Redis의 키, 값, 만료 시간 등을 직접 제어할 수 있으며, 명령어 수준의 제어가 가능합니다.
- 복잡한 데이터 구조 지원: Redis의 모든 데이터 타입과 명령어를 사용할 수 있어, 복잡한 캐싱 로직이나 데이터 구조를 처리할 때 적합합니다.
사용 시점
- 캐시 처리: 예를 들어, 데이터베이스에서 자주 조회되는 값을 캐시로 저장하고 싶을 때 유용합니다.
- 데이터 타입: 리스트, 셋, 해시맵 등 Redis의 다양한 데이터 구조를 활용하고자 할 때 유리합니다.
- 복잡한 데이터: 여러 개의 필드를 갖는 복잡한 데이터를 관리할 때 사용합니다. 또한, 데이터 만료 시간을 세밀하게 관리할 수 있습니다.
주의점
- 직렬화 설정 필요: Redis에 저장될 데이터를 직렬화하고 역직렬화할 때 사용되는 방식(예: JSON, JDK 직렬화)을 직접 설정해야 합니다. 직렬화 설정이 잘못될 경우 데이터 읽기/쓰기가 제대로 되지 않을 수 있습니다.
- 성능 문제: Redis의 단순 키-값 구조에 비해 다소 복잡한 설정을 요구할 수 있으며, 직렬화에 따른 성능 저하가 발생할 수 있습니다.
2. RedisHash 애너테이션
RedisHash
는 엔티티 클래스에 붙여 사용하여 Spring Data의 Repository 패턴을 사용해 Redis에 데이터를 저장하고 관리할 수 있는 방식입니다. 이 방법은 JPA와 유사하게 데이터를 Redis에 저장하고, 저장된 데이터를 쉽게 조회할 수 있도록 도와줍니다.
특징
- 자동 관리: 엔티티를 저장할 때 Redis의 해시(Hash) 자료구조를 이용해 데이터를 저장하며, 데이터를 자동으로 관리합니다.
- 레포지토리 패턴 사용:
@RedisHash
는 Redis와 상호작용하는 Repository 인터페이스를 통해 데이터를 저장하고 조회할 수 있습니다. 즉, JPA에서 엔티티를 관리하듯이 Redis에서도 엔티티를 쉽게 관리할 수 있습니다. - TTL 설정: 엔티티에 TTL(Time to Live)을 설정하여 자동 만료 기능을 사용할 수 있습니다.
사용 시점
- 간단한 키-값 저장: 엔티티를 Redis에 저장하고, 간단한 조회 및 저장 작업을 할 때 적합합니다. 복잡한 캐싱 로직이 필요 없을 때 유리합니다.
- 자동 TTL 관리: 엔티티의 만료 시간(TTL)을 자동으로 설정하고 싶을 때 유용합니다. 예를 들어, 세션 데이터나 일시적 캐시 데이터를 저장할 때 좋습니다.
- JPA와 유사한 사용 방식: JPA 스타일의 개발 방식에 익숙한 경우, 별도의 복잡한 로직 없이 데이터베이스처럼 Redis를 쉽게 사용할 수 있습니다.
주의점
- 제한된 유연성:
RedisTemplate
에 비해 Redis의 다양한 명령어를 사용할 수 없고, Redis의 특정 자료구조(해시)에만 데이터를 저장합니다. 다양한 Redis 명령어를 직접 사용할 수 있는 유연성이 떨어집니다. - 복잡한 데이터 구조 처리 제한: 복잡한 데이터 구조나 특정 요구사항이 있는 경우,
RedisHash
방식은 불편할 수 있습니다. - 성능: 큰 데이터를 많이 저장하거나 조회할 때는 Redis의 해시 구조를 사용하므로 성능에 한계가 있을 수 있습니다.
3. RedisTemplate vs RedisHash의 차이점
특징 | RedisTemplate | RedisHash |
---|---|---|
유연성 | Redis 명령어와 다양한 데이터 타입을 자유롭게 사용 가능 | 해시(Hash) 자료구조만 사용 가능 |
직렬화 | 사용자 지정 직렬화 방식 설정 필요 | 기본적으로 제공되는 직렬화 사용, 사용자 개입 적음 |
TTL 설정 | 직접 명시적으로 설정 가능 | @RedisHash 애너테이션을 통해 간단히 설정 가능 |
데이터 접근 방식 | 명령어 단위로 세밀한 데이터 접근 및 제어 | Repository 패턴을 통해 JPA처럼 간단히 데이터 접근 |
적용 범위 | 복잡한 데이터 구조 및 다양한 Redis 기능 활용 가능 | 간단한 데이터 저장 및 조회에 적합 |
관리 편의성 | Redis의 다양한 기능 사용을 위한 설정 필요 | 기본 설정으로 간편한 데이터 관리 가능 |
4. 어떤 경우에 사용하는 것이 더 좋은가?
RedisTemplate이 적합한 경우:
- Redis의 다양한 명령어를 사용하거나 복잡한 데이터 처리가 필요한 경우.
- Redis의 리스트(List), 셋(Set), 정렬된 셋(ZSet)과 같은 데이터 구조를 활용하고자 할 때.
- 데이터 캐싱과 복잡한 TTL 설정 등이 필요한 경우.
RedisHash가 적합한 경우:
- Redis를 데이터베이스처럼 간단히 저장소로 사용하고, JPA와 유사한 개발 방식이 필요한 경우.
- 간단한 엔티티 관리 및 TTL 관리가 필요할 때.
- Redis의 세부 기능을 직접 제어할 필요 없이 단순한 키-값 저장 및 조회를 원하는 경우.
5. 사용 시 유의점
RedisTemplate:
- 직렬화 및 역직렬화 설정이 필요합니다. 특히 Jackson2JsonRedisSerializer나 GenericJackson2JsonRedisSerializer와 같은 직렬화 방법을 선택할 때, 저장되는 데이터의 직렬화 형식을 일관되게 맞춰야 합니다.
- Redis의 명령어와 데이터 구조를 직접 사용해야 하므로, 명령어에 대한 이해가 필요합니다.
- TTL 관리는 명시적으로 설정해야 하며, 데이터를 적절하게 삭제하거나 관리할 책임이 개발자에게 있습니다.
RedisHash:
- 해시(Hash) 자료구조에 데이터를 저장하므로, 복잡한 데이터 구조나 다양한 데이터 타입이 필요한 경우 제약이 있을 수 있습니다.
- 직렬화나 TTL 설정이 자동으로 관리되지만, 세부 설정을 조정할 수 있는 유연성이 적습니다.
- 데이터가 만료된 후 TTL 설정에 따라 자동으로 삭제되므로, 데이터 유실 가능성에 주의해야 합니다.
요약
- RedisTemplate은 유연하고 강력한 데이터 처리 기능을 제공하며, 다양한 Redis 명령어와 데이터 구조를 활용할 수 있습니다. 복잡한 데이터 처리가 필요하거나 고급 기능을 사용할 때 적합합니다.
- RedisHash는 간단한 데이터 저장 및 조회를 위한 방법으로, JPA처럼 엔티티를 관리하며 TTL 설정 등을 쉽게 처리할 수 있는 장점이 있습니다. 단순한 엔티티 관리나 세션 데이터 저장에 적합합니다.
1) Redis 데이터 확인:
아래는 Redis에서 저장된 키 목록입니다. 특히, redishash-user
접두사가 붙은 키들이 눈에 띕니다. 각각의 키가 어떤 데이터인지 확인할 수 있습니다:
1) "users:1"
2) "cache1::user:6"
3) "redishash-user:email:bob@gmail.com"
4) "redishash-user:3:idx"
5) "redishash-user:4:idx"
6) "redishash-user"
7) "redishash-user:email:noa@gmail.com"
8) "redishash-user:5:idx"
9) "cache1::user:7"
10) "redishash-user:email:creg@gmail.com"
2) redishash-user
Prefix:
redishash-user
로 시작하는 키들은 Spring Data Redis에서 @RedisHash 애너테이션을 사용하여 저장된 데이터입니다. 이 애너테이션은 Redis의 Hash 구조를 사용하여 데이터를 저장할 때 사용됩니다.- 여기서
@RedisHash(value = "redishash-user", timeToLive = 30L)
에서 지정된value
값이 Redis에 저장될 때 key의 prefix로 사용됩니다.- 예를 들어,
redishash-user:email:bob@gmail.com
은email
필드가 인덱싱되어 검색할 수 있도록 Redis에 저장된 것입니다.
- 예를 들어,
redishash-user:idx
접미사가 붙은 키들은 인덱스를 관리하기 위한 Redis 내부 키로, 인덱스된 필드(예:email
)에 따라 저장된 데이터를 관리하고 조회할 때 사용됩니다.
3) TTL 30초 설정 문제:
- TTL(Time To Live) 설정이 30초로 되어 있음에도 불구하고 데이터가 30초가 지나도 삭제되지 않는 이유는 다음과 같습니다:
- Redis의 Set이나 Indexed 필드에서 관리되는 데이터는, Hash 엔티티가 만료되더라도 관련된 인덱스 데이터가 삭제되지 않는 경우가 있습니다. 이는
@Indexed
필드가 있는 경우, 관련 인덱스가 별도의 Redis 키로 관리되기 때문입니다. - 즉,
redishash-user:email:bob@gmail.com
같은 인덱스 필드는 엔티티 자체와 분리되어 Redis에서 관리되며, TTL이 적용되지 않을 수 있습니다. 이로 인해 인덱스 필드나 관련 Set 키가 남아 있게 됩니다.
- Redis의 Set이나 Indexed 필드에서 관리되는 데이터는, Hash 엔티티가 만료되더라도 관련된 인덱스 데이터가 삭제되지 않는 경우가 있습니다. 이는
해결 방법:
- 자동으로 TTL이 적용되지 않은 인덱스 관리: 인덱스 키에 TTL을 적용하거나 수동으로 관리해주는 방법을 사용해야 합니다.
- Redis에 저장된 인덱스 필드들을 주기적으로 삭제하거나 TTL을 따로 관리하는 로직을 추가하는 것을 고려해야 합니다.
Spring Cache Abstraction 개요
Spring Cache Abstraction
은 Spring Framework에서 제공하는 캐싱을 위한 통합 추상화 계층입니다. 이 추상화 계층을 사용하면 다양한 캐시 제공자(Redis, EhCache, Caffeine, Hazelcast 등)와 독립적으로 캐시 메커니즘을 구현할 수 있으며, 캐시 관련 코드의 의존성을 최소화하고 통일된 방식으로 관리할 수 있습니다.
주요 개념
캐시 추상화(Caching Abstraction) Spring은 캐싱을 처리하기 위한 표준화된 인터페이스를 제공합니다. 개발자는 애플리케이션 코드에서 구체적인 캐시 구현체와 상관없이 캐시를 사용할 수 있습니다.
캐시 인터페이스(Cache Interface) 캐시 추상화는
Cache
인터페이스로 정의됩니다. 이 인터페이스는 다음과 같은 주요 메서드를 포함하고 있습니다:put
: 캐시에 데이터를 저장하는 메서드.get
: 캐시에서 데이터를 조회하는 메서드.evict
: 캐시에서 특정 데이터를 삭제하는 메서드.clear
: 캐시를 완전히 비우는 메서드.
캐시 매니저(CacheManager)
CacheManager
는 여러 캐시 객체를 관리하는 인터페이스입니다.CacheManager
를 사용하여 하나 이상의 캐시를 제어할 수 있으며, 애플리케이션에서 사용되는 다양한 캐시를 설정하고 관리하는 역할을 합니다. Spring은 다양한 캐시 구현체를 지원하며,CacheManager
를 통해 캐시를 관리합니다.Spring에서 지원하는
CacheManager
구현체로는ConcurrentMapCacheManager
(기본 제공),RedisCacheManager
,EhCacheCacheManager
,CaffeineCacheManager
등이 있습니다.
사용 방법
- 의존성 추가
spring-cache
모듈은spring-context
에 포함되어 있으므로 별도의 의존성 추가는 필요하지 않지만, 사용할 캐시 공급자에 대한 의존성을 추가해야 합니다. 예를 들어, Redis를 사용하는 경우 아래와 같이 의존성을 추가합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-cache'
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
캐시 설정 캐시를 활성화하려면 Spring Boot 애플리케이션에서
@EnableCaching
을 사용해야 합니다.```java package com.example.cache.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.databind.ser.std.StringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@EnableCaching
@Configuration
public class CacheConfig {
public static final String CACHE1 = "cache1";
public static final String CACHE2 = "cache2";
@AllArgsConstructor
@Getter public static class CacheProperty{
private String name;
private Integer ttl;
}
@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder().allowIfSubType(Object.class).build();
var objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
List<CacheProperty> cacheProperties = List.of(
new CacheProperty(CACHE1, 300),
new CacheProperty(CACHE2, 30)
);
return (builder -> {
cacheProperties.forEach(i -> {
builder.withCacheConfiguration(i.getName(), RedisCacheConfiguration
.defaultCacheConfig()
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)))
.entryTtl(Duration.ofSeconds(i.getTtl()))
);
});
});
}
}
이 코드는 Spring에서 Redis 캐시를 설정하는 **CacheConfig** 클래스입니다. **Spring Cache Abstraction**을 이용하여 캐시를 설정하고, Redis를 캐시 저장소로 사용하도록 구성한 것입니다. 아래는 코드의 각 부분을 설명하는 내용입니다.
### 1. **클래스 개요**
```java
@EnableCaching
@Configuration
public class CacheConfig {
@EnableCaching
: Spring Cache 기능을 활성화합니다. 이 애너테이션을 통해 애플리케이션에서 캐시 기능을 사용할 수 있으며, @Cacheable, @CachePut, @CacheEvict와 같은 캐시 관련 애너테이션이 동작하게 됩니다.@Configuration
: Spring의 설정 클래스를 의미합니다. 이 클래스에서 정의된 메서드는 Spring Bean으로 등록되며, 다른 곳에서 주입되어 사용됩니다.
2. 상수 선언
public static final String CACHE1 = "cache1";
public static final String CACHE2 = "cache2";
- 캐시 이름을 정의한 상수입니다.
CACHE1
과CACHE2
는 각각 캐시의 이름으로 사용됩니다. 이를 통해 특정 캐시 영역에 데이터를 저장하거나 조회할 수 있습니다.
3. CacheProperty 클래스
@AllArgsConstructor
@Getter
public static class CacheProperty{
private String name;
private Integer ttl;
}
- CacheProperty: 각 캐시의 이름과 TTL(Time To Live)을 설정하기 위한 내부 클래스로, 캐시의 이름과 TTL을 속성으로 가집니다.
@AllArgsConstructor
: 모든 필드를 파라미터로 갖는 생성자를 생성합니다.@Getter
:name
과ttl
필드에 대한 getter 메서드를 자동으로 생성합니다.
4. ObjectMapper 설정
var objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
- ObjectMapper는 Jackson 라이브러리를 사용하여 객체를 직렬화/역직렬화하는 데 사용됩니다.
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
: JSON에서 알 수 없는 속성이 있을 때 오류를 무시하도록 설정합니다.JavaTimeModule
: Java 8의LocalDateTime
과 같은 시간 관련 객체를 직렬화/역직렬화할 수 있게 합니다.activateDefaultTyping
: 다형성 직렬화를 활성화합니다. PolymorphicTypeValidator를 통해 특정 타입의 객체만 직렬화/역직렬화하도록 제한을 설정합니다.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
: Java 날짜/시간 객체를 ISO-8601 문자열로 직렬화합니다.
5. 캐시 설정
List<CacheProperty> cacheProperties = List.of(
new CacheProperty(CACHE1, 300),
new CacheProperty(CACHE2, 30)
);
- CacheProperty 리스트에 두 개의 캐시를 정의합니다:
CACHE1
: TTL을 300초로 설정.CACHE2
: TTL을 30초로 설정.
6. RedisCacheManagerBuilderCustomizer
@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
return (builder -> {
cacheProperties.forEach(i -> {
builder.withCacheConfiguration(i.getName(), RedisCacheConfiguration
.defaultCacheConfig()
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)))
.entryTtl(Duration.ofSeconds(i.getTtl()))
);
});
});
}
- RedisCacheManagerBuilderCustomizer는 Redis를 캐시 저장소로 사용하는 CacheManager를 사용자 정의하는 데 사용됩니다.
builder.withCacheConfiguration
: 캐시 구성 옵션을 설정합니다.RedisCacheConfiguration.defaultCacheConfig()
: 기본 캐시 설정을 불러옵니다.disableCachingNullValues()
:null
값을 캐시하지 않도록 설정합니다.null
을 캐시하면 불필요한 메모리 사용을 초래할 수 있기 때문에 이를 방지하는 설정입니다.serializeKeysWith
: Redis에 저장될 키를 StringRedisSerializer를 통해 문자열로 직렬화합니다.serializeValuesWith
: Redis에 저장될 값을 GenericJackson2JsonRedisSerializer를 통해 JSON 형식으로 직렬화합니다.entryTtl
: 캐시의 TTL(Time To Live)을 설정합니다. 각 캐시마다 TTL을 다르게 설정할 수 있으며,Duration.ofSeconds()
메서드를 통해 TTL을 지정합니다.
7. 주요 기능 요약
- 캐시 이름과 TTL 설정:
CACHE1
,CACHE2
의 캐시 이름과 각각의 TTL(300초, 30초)을 설정합니다. - 객체 직렬화/역직렬화: Jackson을 통해 객체를 직렬화하고, Redis에 JSON 형식으로 저장합니다.
- TTL 설정: 캐시에 저장된 데이터는 일정 시간(30초, 300초) 이후 자동으로 만료됩니다.
- Redis Cache 설정: 캐시가 Redis에 저장되며, 키는 문자열로 직렬화되고 값은 JSON으로 직렬화됩니다.
8. RedisCacheManagerBuilderCustomizer
- 이 메서드는 RedisCacheManager를 구성할 때 특정 캐시 이름에 대해 각기 다른 설정을 적용할 수 있게 해줍니다. RedisCacheManager는 캐시의 생성, 관리 및 캐시 저장소와의 통신을 담당합니다.
캐시 사용 캐시 추상화는 주로 메서드 수준에서 사용됩니다. Spring은 캐시 관련 애너테이션을 제공하여 캐시 작업을 간편하게 처리할 수 있도록 돕습니다.
@Cacheable
: 메서드 호출 결과를 캐시에 저장합니다. 다음 호출 시에는 캐시된 데이터를 반환합니다.@CachePut
: 메서드를 실행한 후 결과를 캐시에 저장합니다.@CacheEvict
: 캐시에서 데이터를 삭제합니다.
```java package com.example.cache.service;
import com.example.cache.domain.entity.RedisHashUser;
import com.example.cache.domain.entity.User;
import com.example.cache.repository.RedisHashUserRepository;
import com.example.cache.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import static com.example.cache.config.CacheConfig.CACHE1;
@Service
@RequiredArgsConstructor
public class UserService {
@Cacheable(cacheNames = CACHE1, key="'user:' + #id")
public User getUser3(final Long id){
return userRepository.findById(id).orElseThrow();
}
}
### **주요 애너테이션**
- **`@Cacheable`**: 메서드의 실행 결과를 캐시에 저장하고, 이후 동일한 입력값으로 호출될 때 캐시된 데이터를 반환합니다. 처음 호출 시에는 메서드가 실행되지만, 이후 호출 시에는 캐시된 값을 반환합니다.
```java
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
return userRepository.findById(id).orElseThrow();
}
@CachePut
: 메서드 실행 결과를 캐시에 저장하지만 항상 메서드 자체가 실행됩니다. 캐시된 값을 무조건 갱신할 때 사용됩니다.@CachePut(value = "users", key = "#user.id") public User updateUser(User user) { return userRepository.save(user); }
@CacheEvict
: 캐시에서 특정 데이터를 제거합니다. 캐시 무효화 작업에 사용됩니다.@CacheEvict(value = "users", key = "#id") public void deleteUser(Long id) { userRepository.deleteById(id); }
@Caching
: 여러 개의 캐시 애너테이션을 동시에 사용할 수 있도록 해주는 애너테이션입니다.@Caching(evict = { @CacheEvict(value = "users", key = "#user.id"), @CacheEvict(value = "userDetails", key = "#user.username") }) public void deleteUser(User user) { userRepository.delete(user); }
사용 사례
데이터베이스 조회 최적화: 데이터베이스에서 자주 조회되는 데이터를 캐시에 저장하여 성능을 향상시킬 수 있습니다. 예를 들어, 사용자 정보를 데이터베이스에서 조회할 때, 같은 사용자에 대한 반복된 조회는 캐시된 데이터를 반환하도록 설정할 수 있습니다.
API 호출 캐싱: 외부 API 호출의 결과를 캐시에 저장하여 성능을 개선할 수 있습니다. API의 결과가 일정 시간 동안 유효하다면, 해당 데이터를 캐시에 저장하고 재사용할 수 있습니다.
계산 결과 캐싱: 시간이 오래 걸리는 계산 작업의 결과를 캐시에 저장하여 반복된 계산을 방지하고 성능을 최적화할 수 있습니다.
장점
- 통일된 인터페이스: 다양한 캐시 제공자와 통합할 수 있으며, 코드의 변경 없이 캐시 제공자를 교체할 수 있습니다.
- 간단한 설정: 애너테이션을 통해 간단하게 캐시를 적용할 수 있어 코드 변경이 최소화됩니다.
- 다양한 캐시 전략 지원: 캐시 저장, 갱신, 삭제 전략을 다양한 캐시 구현체를 통해 유연하게 설정할 수 있습니다.
캐시 제공자
Spring Cache Abstraction은 다양한 캐시 제공자와 통합될 수 있습니다. 대표적인 캐시 제공자는 다음과 같습니다:
- Redis: 분산 캐시 및 인메모리 데이터 저장소로 널리 사용됨.
- EhCache: JVM 기반의 로컬 캐시로, 성능이 뛰어나고 설정이 간단함.
- Caffeine: Java 기반의 고성능 로컬 캐시.
- Hazelcast: 분산형 캐시 및 데이터 그리드 솔루션.
요약
Spring Cache Abstraction은 다양한 캐시 제공자와 손쉽게 통합할 수 있는 강력한 캐시 관리 기능을 제공합니다. 이를 통해 애플리케이션의 성능을 최적화하고, 데이터베이스 또는 외부 API 호출을 줄여 애플리케이션의 효율성을 극대화할 수 있습니다.
LOAD TEST
- vegeta 유틸리티를 사용하여 load test를 진행합니다.
설치
brew install vegeta
1차 로드 테스트 redis 없이 mysql db로 진행
- service 혹은 controller에서 redis로 처리 하지 않도록 주석을 걸어 주거나 수정해주어야 합니다.
- request1.txt 파일을 만들어 줍니다.
GET http://localhost:8080/users/1
GET http://localhost:8080/users/2
GET http://localhost:8080/users/3
GET http://localhost:8080/users/4
GET http://localhost:8080/users/5
GET http://localhost:8080/users/6
부하 테스트를 위해서 아래 vegeta attack 명령어를 작성합니다.
vegeta attack -timeout=7s -duration=15s -rate=5000/1s -targets=request1.txt | tee v_result.bin | vegeta report
보고서를 만듭니다.
vegeta report v_result.bin > report.txt
보고서의 내용입니다.
Requests [total, rate, throughput] 54252, 3542.79, 91.03 Duration [total, attack, wait] 22.048s, 15.313s, 6.734s Latencies [min, mean, 50, 90, 95, 99, max] 930.792µs, 7.706s, 7.846s, 9.661s, 14.041s, 15.327s, 17.644s Bytes In [total, mean] 255569, 4.71 Bytes Out [total, mean] 0, 0.00 Success [ratio] 3.70% Status Codes [code:count] 0:52245 200:2007 Error Set: Get "http://localhost:8080/users/2": dial tcp 0.0.0.0:0->127.0.0.1:8080: bind: can't assign requested address Get "http://localhost:8080/users/5": dial tcp 0.0.0.0:0->127.0.0.1:8080: bind: can't assign requested address Get "http://localhost:8080/users/4": dial tcp 0.0.0.0:0->127.0.0.1:8080: bind: can't assign requested address Get "http://localhost:8080/users/3": dial tcp 0.0.0.0:0->127.0.0.1:8080: bind: can't assign requested address
1차 로드 테스트 결과 분석
1. Requests
- Total: 54,252번의 요청이 발생했습니다.
- Rate: 초당 약 3,542.79번의 요청이 발생했습니다.
- Throughput: 실제 처리된 요청 중 응답을 받은 요청은 초당 91.03으로, 매우 낮은 비율입니다.
2. Duration
- Total: 총 22.048초 동안 테스트가 진행되었습니다.
- Attack: 요청을 발생시킨 공격(부하) 시간은 15.313초였습니다.
- Wait: 응답을 기다린 시간은 6.734초입니다. 이는 서버가 요청을 처리하는 데 시간이 걸린다는 것을 의미합니다.
3. Latencies (응답 지연 시간)
- Min: 가장 빠른 응답은 930.792µs (마이크로초)입니다.
- Mean: 평균 응답 시간은 7.706초로, 상당히 느립니다.
- 50th Percentile (Median): 응답의 중간값은 7.846초입니다.
- 90th Percentile: 상위 10%의 응답 시간은 9.661초입니다.
- 95th Percentile: 상위 5%는 14.041초로 상당히 느립니다.
- 99th Percentile: 상위 1%는 15.327초로, 이 경우 거의 타임아웃에 가까운 응답입니다.
- Max: 최악의 응답 시간은 17.644초로, 매우 오래 걸렸습니다.
4. Bytes In
- Total: 응답 바이트 총량은 255,569 바이트였습니다.
- Mean: 요청당 평균 4.71 바이트가 응답되었습니다. 이는 상당히 적은 데이터양을 나타냅니다.
5. Bytes Out
- Total/Mean: 요청 시 전송된 바이트 수는 0입니다. 테스트에서 요청 본문이 없었거나 무시된 것일 수 있습니다.
6. Success
- Success Ratio: 성공한 요청의 비율은 3.70%에 불과합니다. 이는 전체 요청의 96% 이상이 실패했음을 의미합니다.
7. Status Codes
- 0 (Timeout 또는 실패): 52,245번의 요청이 타임아웃 또는 실패했습니다.
- 200 (성공): 2,007번의 요청만이 성공했습니다.
분석 요약:
- 성공률 저조: 54,252건의 요청 중 성공한 요청은 2,007건으로, 성공률이 3.70%에 불과합니다. 대부분의 요청이 타임아웃 또는 실패했습니다.
- 응답 지연: 평균 응답 시간(7.706초)이 매우 높으며, 90% 이상의 요청이 9초 이상 걸렸습니다. 응답이 너무 느려 서버의 성능 문제 또는 과부하 상태가 의심됩니다.
- Throughput: 초당 3,500건 이상의 요청을 처리해야 했으나, 실제 처리된 요청은 초당 91건에 불과합니다. 성능이 기준에 크게 미치지 못합니다.
2차 로드 테스트 redis도 함께 사용
- 100% 성공 됨을 알수 있습니다.
vegeta attack -timeout=10s -duration=15s -rate=5000/1s -targets=request1.txt | tee v_result2.bin | vegeta report
Requests [total, rate, throughput] 75000, 5000.06, 4999.48
Duration [total, attack, wait] 15.002s, 15s, 1.738ms
Latencies [min, mean, 50, 90, 95, 99, max] 845.666µs, 4.128ms, 3.133ms, 6.853ms, 9.723ms, 21.859ms, 43.983ms
Bytes In [total, mean] 9550000, 127.33
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:75000
Error Set:
Vegeta Load Test 결과 분석
테스트 설정:
- 총 요청 수: 75,000 (초당 5,000개의 요청)
- 테스트 지속 시간: 15초
주요 지표 분석
1. Requests:
- 총 요청 수: 75,000
- 초당 요청 수(rate): 5,000.06
처리량(throughput): 4,999.48 (초당 실제로 처리된 요청 수)
분석: 설정된 목표 속도(5,000 요청/초)에 근접하며, 처리량도 거의 동일하게 유지됨. 서버가 부하를 잘 처리하고 있음.
2. Duration:
- total: 15.002s
- attack: 15s
wait: 1.738ms
분석: 공격 시간이 정확히 15초이며, 추가로 발생한 대기 시간이 1.738ms로 매우 짧음. 이 대기 시간은 요청 처리 지연 시간이 거의 없다는 것을 의미.
3. Latencies:
- 최소 지연 시간(min): 845.666µs (마이크로초)
- 평균 지연 시간(mean): 4.128ms (밀리초)
- 중간값(50%): 3.133ms
- 상위 90% 지연 시간: 6.853ms
- 상위 95% 지연 시간: 9.723ms
- 상위 99% 지연 시간: 21.859ms
최대 지연 시간(max): 43.983ms
분석:
- 요청의 지연 시간이 전반적으로 짧으며, 평균 지연 시간은 4.128ms로 서버가 빠르게 응답하고 있음.
- 최대 지연 시간은 43.983ms로 큰 폭으로 증가한 요청이 있지만, 이는 극히 소수의 경우에 해당.
- 상위 95%의 지연 시간은 9.723ms로, 대부분의 요청이 10ms 이내에 처리되었음을 보여줌.
4. Bytes In:
- 총 수신 데이터(Bytes In): 9,550,000바이트
평균 수신 데이터: 127.33바이트 (요청당 수신된 데이터 크기)
분석: 요청마다 127.33바이트의 데이터를 반환받음. 적절한 응답 크기로 판단됨.
5. Bytes Out:
총 송신 데이터(Bytes Out): 0바이트
분석: 요청에는 데이터를 보내지 않았으며, 단순 조회 형태의 요청으로 추측.
6. Success:
성공률: 100.00%
분석: 모든 요청이 성공적으로 처리됨. 서버에 문제가 없고, 모든 요청에 정상적인 응답이 돌아왔음을 의미.
7. Status Codes:
200:75000: 모든 요청이 성공적인 HTTP 200 상태 코드로 응답.
분석: 모든 요청이 성공적으로 처리되었음을 의미하며, 서버는 안정적인 상태로 작동 중.
종합 분석
성능:
- 서버는 초당 5,000개의 요청을 성공적으로 처리하며, 모든 요청이 100% 성공률을 기록.
- 평균 응답 시간은 4.128ms로 매우 빠르고, 요청의 95%는 10ms 이내에 처리됨.
부하 처리 능력:
- Redis 캐시 사용 덕분에 지연 시간이 짧고, 매우 높은 요청 처리 속도를 유지함.
- Redis를 통한 캐시 사용으로 조회 요청의 성능 최적화가 잘 이루어졌음을 확인.
최종 평가:
- 부하 테스트 결과에 따르면, 서버는 높은 요청량에서도 매우 빠른 응답 속도를 제공하며, 안정적이고 성능이 뛰어남.
- 특히 캐시 시스템을 통한 성능 최적화가 잘 적용되어 성능 병목 현상이 거의 없는 것으로 판단됨.
'DB > Redis' 카테고리의 다른 글
Redis Replication (1) | 2024.09.17 |
---|---|
Redis 모니터링 (feat. Prometheus, Grafana, Redis 및 Redis Exporter) (4) | 2024.09.15 |
Redis Cache 실습(Aka. Write Back) (1) | 2024.09.14 |
Redis Cache 실습(Aka. Jedis, Cache Aside) (0) | 2024.09.14 |
Redis Cache 활용법: 성능 최적화를 위한 캐시 전략 (0) | 2024.09.12 |