Write-Back Cache 패턴 실습: Redis와 MySQL을 활용한 구현
Write-Back Cache 패턴은 데이터를 캐시에 먼저 쓰고, 데이터베이스로는 일정 주기로 배치 작업을 통해 동기화하는 패턴입니다. 이 패턴은 쓰기 성능을 극대화할 수 있는 장점이 있지만, 캐시 손실 시 데이터 일관성 문제를 발생시킬 수 있습니다. 이번 실습에서는 Spring Boot와 Redis, 그리고 MySQL을 사용하여 Write-Back 패턴을 구현하고, 주기적인 동기화 작업을 배치로 처리하는 방법을 다룹니다.
1. Write-Back Cache 패턴 실습 개요
핵심 동작 원리:
- 쓰기 작업: 데이터는 먼저 Redis 캐시에 저장. 데이터베이스에는 바로 반영하지 않음.
- 배치 작업: 주기적으로 Redis 캐시에 저장된 데이터를 MySQL 데이터베이스에 동기화.
- 읽기 작업: Redis에서 데이터를 먼저 읽고, 캐시에 없을 경우에만 MySQL에서 데이터를 조회한 후 캐시에 저장.
2. 환경 설정
Docker로 Redis와 MySQL 실행
# MySQL 실행
docker run -d --name mysql-server -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=fastsns mysql:8.0
# Redis 실행
docker run -d --name redis-server -p 6379:6379 redis
3. Spring Boot 설정
application.yaml 설정
spring:
datasource:
url: jdbc:mysql://localhost:3306/fastsns
username: root
password: root
jpa:
hibernate.ddl-auto: update
show-sql: true
redis:
host: localhost
port: 6379
4. Write-Back 패턴 구현
4.1. RedisConfig 클래스
Redis와의 연결을 설정하기 위한 클래스입니다. Jedis 라이브러리를 사용해 Redis Pool을 구성합니다.
package com.example.writeback;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@Component
public class RedisConfig {
@Bean
public JedisPool jedisPool() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setJmxEnabled(false); // JMX 비활성화
return new JedisPool(poolConfig, "127.0.0.1", 6379);
}
}
4.2. User 엔티티
User
엔티티는 MySQL 데이터베이스에 저장되는 사용자 정보를 나타내며, JPA를 사용해 관리됩니다.
package com.example.writeback;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 50)
private String name;
@Column(length = 100)
private String email;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
4.3. UserController 클래스
사용자의 데이터를 저장할 때 Redis에 먼저 저장하고, 주기적으로 배치 작업을 통해 MySQL에 동기화하는 흐름을 처리합니다.
package com.example.writeback;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.time.LocalDateTime;
@RestController
@RequiredArgsConstructor
public class UserController {
private final JedisPool jedisPool;
private final UserRepository userRepository;
// 사용자 저장: Redis에 먼저 저장되고, 배치 작업을 통해 MySQL에 반영됨
@PostMapping("/users")
public String saveUser(@RequestParam String name, @RequestParam String email) {
try (Jedis jedis = jedisPool.getResource()) {
Long id = System.currentTimeMillis(); // 임시로 시간값을 ID로 사용
String redisKey = String.format("users:%d", id);
User user = User.builder()
.id(id)
.name(name)
.email(email)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
// Redis에 데이터 저장
jedis.hset(redisKey, "name", user.getName());
jedis.hset(redisKey, "email", user.getEmail());
jedis.hset(redisKey, "createdAt", user.getCreatedAt().toString());
jedis.hset(redisKey, "updatedAt", user.getUpdatedAt().toString());
return "User saved in Redis!";
}
}
// 사용자 조회: Redis에서 먼저 조회하고, 없으면 MySQL에서 조회
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
try (Jedis jedis = jedisPool.getResource()) {
String redisKey = String.format("users:%d", id);
if (jedis.exists(redisKey)) {
return User.builder()
.id(id)
.name(jedis.hget(redisKey, "name"))
.email(jedis.hget(redisKey, "email"))
.createdAt(LocalDateTime.parse(jedis.hget(redisKey, "createdAt")))
.updatedAt(LocalDateTime.parse(jedis.hget(redisKey, "updatedAt")))
.build();
} else {
return userRepository.findById(id).orElse(null);
}
}
}
}
4.4. UserRepository 인터페이스
JPA를 사용하여 MySQL과 연동하여 사용자 데이터를 관리합니다.
package com.example.writeback;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
5. 배치 작업 (Spring Scheduler 사용)
5.1. WriteBackScheduler 클래스
Redis에 저장된 데이터를 주기적으로 MySQL에 동기화하는 작업을 배치 작업으로 처리합니다. Spring Scheduler를 통해 배치 작업이 실행됩니다.
package com.example.writeback;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.time.LocalDateTime;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class WriteBackScheduler {
private final JedisPool jedisPool;
private final UserRepository userRepository;
// 1분마다 실행하여 Redis의 데이터를 MySQL로 동기화
@Scheduled(fixedRate = 60000)
public void syncRedisToDb() {
try (Jedis jedis = jedisPool.getResource()) {
// Redis의 모든 사용자 데이터 키 가져오기
for (String key : jedis.keys("users:*")) {
Map<String, String> userData = jedis.hgetAll(key);
Long id = Long.valueOf(key.split(":")[1]);
// Redis에서 가져온 데이터를 MySQL에 저장
User user = User.builder()
.id(id)
.name(userData.get("name"))
.email(userData.get("email"))
.createdAt(LocalDateTime.parse(userData.get("createdAt")))
.updatedAt(LocalDateTime.parse(userData.get("updatedAt")))
.build();
userRepository.save(user);
// 데이터가 MySQL로 저장되면 Redis에서 삭제
jedis.del(key);
}
}
}
}
5.2. Spring Scheduler 활성화
@EnableScheduling
을 사용해 Spring에서 스케줄링 기능을 활성화합니다.
package com.example.writeback;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class WriteBackApplication {
public static void main(String[] args) {
SpringApplication.run(WriteBackApplication.class, args);
}
}
6. 테스트 방법
데이터 저장 (POST 요청):
curl -X POST "localhost:8080/users?name=John&email=john@example.com"
이 요청은 사용자 데이터를 Redis에 먼저 저장합니다.
데이터 조회 (GET 요청):
curl "localhost:8080/users/{id}"
사용자 데이터는 Redis에서 먼저 조회되며, 캐시가 만료되거나 배치 작업이 진행된 경우 MySQL에서 조회됩니다.
배치 작업 실행 후 MySQL 확인:
1분 후 배치 작업이 실행되면, Redis에 있던 데이터가 MySQL로 동기화됩니다.mysql> select * from user;
7. 결론
Write-Back Cache 패턴을 통해 데이터를 Redis에 먼저 저장하고, 배치 작업
을 통해 주기적으로 MySQL로 동기화하는 방식을 구현했습니다. 이를 통해 쓰기 성능을 크게 향상시킬 수 있으며, 데이터베이스의 부하를 줄일 수 있습니다. 하지만 캐시 손실 시 데이터 손실을 방지하기 위한 추가적인 복구 메커니즘이 필요할 수 있으며, 주기적인 동기화 주기를 설정하는 데도 주의가 필요합니다.
이번 실습을 통해 Write-Back 패턴을 성공적으로 구현하고, 이를 실무에 적용할 수 있는 방법을 학습했습니다.
'DB > Redis' 카테고리의 다른 글
Redis 모니터링 (feat. Prometheus, Grafana, Redis 및 Redis Exporter) (4) | 2024.09.15 |
---|---|
Spring Boot Cache 실습 (1) | 2024.09.14 |
Redis Cache 실습(Aka. Jedis, Cache Aside) (0) | 2024.09.14 |
Redis Cache 활용법: 성능 최적화를 위한 캐시 전략 (0) | 2024.09.12 |
Redis Key와 Scan 명령어: 대규모 데이터 검색 (0) | 2024.09.11 |