DB/Redis

Redis Cache 실습(Aka. Write Back)

hyeseong-dev 2024. 9. 14. 16:44

Write-Back Cache 패턴 실습: Redis와 MySQL을 활용한 구현

Write-Back Cache 패턴은 데이터를 캐시에 먼저 쓰고, 데이터베이스로는 일정 주기로 배치 작업을 통해 동기화하는 패턴입니다. 이 패턴은 쓰기 성능을 극대화할 수 있는 장점이 있지만, 캐시 손실 시 데이터 일관성 문제를 발생시킬 수 있습니다. 이번 실습에서는 Spring BootRedis, 그리고 MySQL을 사용하여 Write-Back 패턴을 구현하고, 주기적인 동기화 작업을 배치로 처리하는 방법을 다룹니다.


1. Write-Back Cache 패턴 실습 개요

핵심 동작 원리:

  1. 쓰기 작업: 데이터는 먼저 Redis 캐시에 저장. 데이터베이스에는 바로 반영하지 않음.
  2. 배치 작업: 주기적으로 Redis 캐시에 저장된 데이터를 MySQL 데이터베이스에 동기화.
  3. 읽기 작업: 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. 테스트 방법

  1. 데이터 저장 (POST 요청):

    curl -X POST "localhost:8080/users?name=John&email=john@example.com"

    이 요청은 사용자 데이터를 Redis에 먼저 저장합니다.

  2. 데이터 조회 (GET 요청):

    curl "localhost:8080/users/{id}"

    사용자 데이터는 Redis에서 먼저 조회되며, 캐시가 만료되거나 배치 작업이 진행된 경우 MySQL에서 조회됩니다.

  3. 배치 작업 실행 후 MySQL 확인:
    1분 후 배치 작업이 실행되면, Redis에 있던 데이터가 MySQL로 동기화됩니다.

    mysql> select * from user;

7. 결론

Write-Back Cache 패턴을 통해 데이터를 Redis에 먼저 저장하고, 배치 작업

을 통해 주기적으로 MySQL로 동기화하는 방식을 구현했습니다. 이를 통해 쓰기 성능을 크게 향상시킬 수 있으며, 데이터베이스의 부하를 줄일 수 있습니다. 하지만 캐시 손실 시 데이터 손실을 방지하기 위한 추가적인 복구 메커니즘이 필요할 수 있으며, 주기적인 동기화 주기를 설정하는 데도 주의가 필요합니다.

이번 실습을 통해 Write-Back 패턴을 성공적으로 구현하고, 이를 실무에 적용할 수 있는 방법을 학습했습니다.