본문으로 건너뛰기

SpringBoot에서 Redis 캐시를 사용하기

이번에는 SpringBoot와 데이터접근과 관련해서 작성 해 보려고한다

많은 경우에 Spring은 DB에 담긴 데이터를 가져오고 전달하는 역할을 하고있는데 이 때, Redis를 가지고 데이터에 빠르게 접근하는 방법을 정리 해 보려고 한다

Redis는 데이터베이스로도 사용되고, Message Broker로도 사용되지만 Cache Manager에 더 많이 사용된다

한번 만들어보면서 정리 해 보도록 하겠다

In this post, I will write about data access with SpringBoot.

In many cases, Spring plays a role in fetching and delivering data stored in the database. Here, I will summarize how to quickly access data using Redis.

Redis can be used as a database or as a Message Broker, but it is most commonly used as a Cache Manager.

Let's build one and organize what we learn along the way.

사용 할 Redis 간단 설명

  • Redis 사용 형태

    • In Memory Database
      • 데이터베이스 작업을 수행하기 위해 사용, No SQL 데이터베이스 역할을 함
      • No Tables, No Sequences, No Joins의 개념이 있음
      • String, Hash, List, Set 등의 형태로 데이터 저장
  • Cache

    • Redis를 캐시로 사용하여 애플리케이션의 성능을 높인다
  • Message Broker(MQ)

    • Redis를 MQ로 사용하여 메시지를 전달한다
  • Redis 캐시란

    • Redis Cache는 Redis에서 제공하는 캐시 관리 기능
    • 사용자가 응용프로그램의 더 나은 성능을 위해 적용/사용
    • 엑세스 되는 데이터를 저장하는 캐시
    • 한번의 데이터 요청은 하나의 네트워크 호출인데, 이를 최소화 할 수 있음
  • Redis Cache 활성화를 위한 @Annotation

    • @EnableCaching

      • SpringBoot에게 캐싱기능이 필요하다고 전달
      • SpringBoot Starter class에 적용
    • @Cacheable

      • DB에서 애플리케이션으로 데이터를 가져오고 Cache에 저장하는데 사용
      • DB에서 데이터를 가져오는 메서드에 적용
    • @CachePut

      • DB의 데이터 업데이트가 있을 때 Redis Cache에 데이터를 업데이트
      • DB에서 PUT/PATCH와 같은 업데이트에서 사용
    • @CacheEvict

      • DB의 데이터 삭제가 있을 때 Redis Cache에 데이터를 삭제
      • DB에서 DELETE와 같은 삭제에서 사용

Brief Introduction to Redis

  • Redis Usage Types

    • In Memory Database
      • Used to perform database operations, serving as a NoSQL database
      • Has the concept of No Tables, No Sequences, No Joins
      • Stores data in formats such as String, Hash, List, Set, etc.
  • Cache

    • Using Redis as a cache improves application performance
  • Message Broker (MQ)

    • Using Redis as MQ to deliver messages
  • What is Redis Cache

    • Redis Cache is a cache management feature provided by Redis
    • Applied/used by users for better application performance
    • A cache that stores frequently accessed data
    • A single data request is one network call, and this can be minimized
  • @Annotation for Enabling Redis Cache

    • @EnableCaching

      • Tells SpringBoot that caching functionality is needed
      • Applied to the SpringBoot Starter class
    • @Cacheable

      • Used to fetch data from DB to the application and store it in Cache
      • Applied to methods that fetch data from DB
    • @CachePut

      • Updates data in Redis Cache when there is a data update in DB
      • Used in updates like PUT/PATCH operations in DB
    • @CacheEvict

      • Deletes data in Redis Cache when there is a data deletion in DB
      • Used in deletions like DELETE operations in DB

프로젝트 구조

아래는 이해를 돕기 위해서 사용 한 프로젝트 구조를 적어보았다

Project Structure

Below is the project structure used to help with understanding.

├── HELP.md
├── README.md
├── build
├── build.gradle
├── docker-compose-database.yml
├── example-docker-data
├── gradle
│   └── wrapper
│   ├── gradle-wrapper.jar
│   └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
├── main
│   ├── java
│   │   └── com
│   │   └── example
│   │   └── springbootredissimplestarter
│   │   ├── SpringBootRedisSimpleStarterApplication.java
│   │   ├── config
│   │   │   └── RedisCacheConfig.java
│   │   ├── controller
│   │   │   └── OrderController.java
│   │   ├── domain
│   │   │   └── Order.java
│   │   ├── exception
│   │   │   ├── OrderNotFoundException.java
│   │   │   └── OrderStatusException.java
│   │   ├── repository
│   │   │   └── OrderRepository.java
│   │   └── service
│   │   ├── OrderService.java
│   │   └── OrderServiceImpl.java
│   └── resources
│   ├── application.properties
│   ├── static
│   └── templates
└── test
└── java
└── com
└── example
└── springbootredissimplestarter
└── SpringBootRedisSimpleStarterApplicationTests.java

준비 할 것

당연하지만 Spring BootRedis를 준비해야 한다.

SpringBoot는 Spring Data Redis라는 디펜던시를 통해서 Redis와 연결을 지원한다. 이 디펜던시를 통해서 Redis와 소통 할 것이고, Redis 서버를 docker를 사용해서 띄워 사용하려고 한다

What to Prepare

Obviously, you need to prepare Spring Boot and Redis.

SpringBoot supports connection to Redis through a dependency called Spring Data Redis. We will communicate with Redis through this dependency, and we plan to use Docker to run the Redis server.

Database

데이터베이스 환경을 먼저 구축하려고 한다. 모두의 컴퓨터환경이 같지 않기때문에, 현재 글 작성일 기준으로 docker-composeDatabase를 구성하고자한다.

I will set up the database environment first. Since everyone's computer environment is different, I will configure the Database using docker-compose as of the time of writing this post.

  • docker-compose-database.yml
version: "3"
services:
mysql-docker:
image: arm64v8/mariadb
ports:
- "3306:3306"
environment:
TZ: Asia/Seoul
MYSQL_ROOT_PASSWORD: qwerqwer123
MYSQL_DATABASE: rediswithspring
MYSQL_USER: paul
MYSQL_PASSWORD: qwerqwer123
container_name: "docker-maria"
volumes:
- ./example-docker-data/maria:/var/lib/mysql

redis-docker:
image: redis:latest
command: redis-server --port 6379
container_name: "docker-redis"
volumes:
- ./example-docker-data/redis:/data
labels:
- "name=redis"
- "mode=standalone"
ports:
- 6379:6379
  • mysql
  • mysql-docker는 docker환경으로 설치된 mysql(maria)서버이다
  • image로 arm64v8/mariadb를 사용했는데, 이는 m1에서 mysql을 구동하기 위함이다
  • 각 환경에 맞게 이미지를 변경시켜주면 된다
  • mariadb와 mysql의 docker세팅이 비슷하기때문에 mysql / mariadb 동일하게 사용해도 된다
  • mysql에서 사용할 환경변수파일은 .mysql_env로 작성했으며 내용은 아래와 같다(docker-compose에 명시하지않고 env파일로 빼고싶은 사람만 사용하면 될 것 같다)
  • mysql-docker is a mysql (maria) server installed in a Docker environment
  • I used arm64v8/mariadb as the image, which is to run mysql on M1
  • You can change the image according to your environment
  • Since the Docker settings for mariadb and mysql are similar, you can use mysql / mariadb interchangeably
  • The environment variable file used for mysql is written as .mysql_env and the contents are as follows (only use this if you want to separate it into an env file instead of specifying it in docker-compose)
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_ROOT_PASSWORD=qwerqwer123
MYSQL_DATABASE=rediswithspring
MYSQL_USER=paul
MYSQL_PASSWORD=qwerqwer123
  • redis
  • redis-docker는 docker환경으로 설치된 redis 서버이다
  • command에 --requirepass로 비밀번호를 설정 해 주고, --port로 6379 포트를 열어주었다
  • redis-docker is a Redis server installed in a Docker environment
  • I set the password with --requirepass in the command and opened port 6379 with --port

SpringBoot

SpringBoot Starter를 사용해 프로젝트를 시작하고, Dependency 세팅을 해 준다

Start the project using SpringBoot Starter and set up the dependencies.

  • Dependency
    • Spring Web
    • Spring Data JPA
    • MYSQL Driver
    • Spring Data Redis
    • Lombok
    • Spring Boot Devtools

SpringBoot와 Redis를 사용하여 프로젝트 작성

  • 정교하고 자세한 모델은 아니지만, 배달음식 주문 어플리케이션을 만들어보려고 한다
  • 자세한 기능보다는, CRUD와 해당하는 쿼리가 캐싱이 되었는지에 초점을 맞춰보려고 한다

Writing a Project Using SpringBoot and Redis

  • Although not a sophisticated and detailed model, I am going to create a food delivery order application
  • Rather than detailed functionality, I will focus on whether CRUD and corresponding queries are cached

SpringBoot 어플리케이션 세팅

SpringBoot에게 Redis Cache를 사용 할 것이라고 알려주어야 한다. 위에 적어놓은 어노테이션 중 @EnableCaching을 스타터 클래스에 적용한다

SpringBoot Application Setup

You need to tell SpringBoot that you will use Redis Cache. Apply @EnableCaching from the annotations mentioned above to the starter class.

package com.example.springbootredissimplestarter;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class SpringBootRedisSimpleStarterApplication {

public static void main(String[] args) {
SpringApplication.run(SpringBootRedisSimpleStarterApplication.class, args);
}

}

application properties

Application Properties

spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/rediswithspring
spring.datasource.username=paul
spring.datasource.password=qwerqwer123
spring.jpa.database-platform=org.hibernate.dialect.MariaDB103Dialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.cache.type=redis
spring.cache.redis.cache-null-values=true
  • database가 mariadb로 올렸기때문에 jpa platform과 datasource driver-class 를 mariadb으로 해주었다
  • mysql로 하고싶다면 드라이버 이름과 platform이름을 아래와 같이 적용하면 됨
  • Since the database was set up with mariadb, I set the jpa platform and datasource driver-class to mariadb
  • If you want to use mysql, apply the driver name and platform name as follows

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect

SpringBoot Redis 환경설정 세팅

  • properties와 spring starter class에 캐시를 적용한다고 알려주었다
  • Data를 가져오고 보낼 때, 우리가 만든 도메인 모델을 Serialize 해 주기 위해 설정이 필요하다
  • config 패키지를 만들고 하위에 RedisCacheConfig.java 를 생성 해 주었다
  • 여기서 생성한 testCacheManger 객체를 앞으로 사용 할 레디스 어노테이션에 명시 해 주어야 한다

SpringBoot Redis Configuration Setup

  • We informed the properties and spring starter class that caching will be applied
  • Configuration is needed to serialize our domain model when fetching and sending data
  • Created a config package and created RedisCacheConfig.java under it
  • The testCacheManager object created here must be specified in the Redis annotations we will use
package com.example.springbootredissimplestarter.config;


import org.springframework.cache.CacheManager;
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.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
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;

@Configuration
@EnableCaching
public class RedisCacheConfig {

@Bean
public CacheManager testCacheManager(RedisConnectionFactory cf) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofMinutes(3L));

return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build();
}
}

Domain 모델

  • 배달 어플리케이션을 위해 모델을 생성
  • Order모델 생성
  • 이 때 주의점은, DDL에 order이 들어가기때문에 테이블 생성 시 order 예약어 로 인한 생성오류가 발생한다
  • 테이블 이름 명시(@Table)가 필요하다
  • domain패키지 하위에 Order.java를 생성

Domain Model

  • Create a model for the delivery application
  • Create Order model
  • Note that since "order" is in the DDL, a creation error will occur due to the order reserved word when creating the table
  • Table name specification (@Table) is required
  • Create Order.java under the domain package
package com.example.springbootredissimplestarter.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "ORDERS")
public class Order {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue
private Integer id;
private String orderCode;
private String orderObject;
private String orderStatus;
private Integer orderPrice;

}

Repository

  • JpaRepository를 상속받아서 생성
  • repository 패키지 하위에 OrderRepository.java를 생성

Repository

  • Create by inheriting from JpaRepository
  • Create OrderRepository.java under the repository package
package com.example.springbootredissimplestarter.repository;

import com.example.springbootredissimplestarter.domain.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderRepository extends JpaRepository<Order, Integer> {

}

Custom exception

  • 데이터 CRUD를 구현하기 전, Exception을 따로 주기 위해서 Custom Exception 생성
  • exception 패키지 하위에 OrderNotFoundException.javaOrderStatusException.java를 생성

Custom Exception

  • Create Custom Exception to provide separate exceptions before implementing data CRUD
  • Create OrderNotFoundException.java and OrderStatusException.java under the exception package
// OrderNotFoundException.java
package com.example.springbootredissimplestarter.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class OrderNotFoundException extends RuntimeException {

private static final long serialVersionUID = 1L;

public OrderNotFoundException(String message) {
super(message);
}
}
// OrderStatusException.java
package com.example.springbootredissimplestarter.exception;


import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class OrderStatusException extends RuntimeException {

private static final long serialVersionUID = 1L;

public OrderStatusException(String message) {
super(message);
}
}

Service

  • service 패키지 하위에 OrderService.java 인터페이스와 OrderServiceImpl.java를 생성
  • OrderService 인터페이스를 생성
  • OrderServiceImpl 에서 OrderService 구현

Service

  • Create OrderService.java interface and OrderServiceImpl.java under the service package
  • Create OrderService interface
  • Implement OrderService in OrderServiceImpl
// OrderService.java - interface
package com.example.springbootredissimplestarter.service;

import com.example.springbootredissimplestarter.domain.Order;

import java.util.List;

public interface OrderService {

public Order createOrder(Order order);

public Order getOrder(Integer orderId);

public Order updateOrder(Order order, Integer orderId);

public void deleteOrder(Integer orderId);

public List<Order> getAllOrders();
}
// OrderServiceImpl.java - implementation
package com.example.springbootredissimplestarter.service;

import com.example.springbootredissimplestarter.domain.Order;
import com.example.springbootredissimplestarter.exception.OrderNotFoundException;
import com.example.springbootredissimplestarter.exception.OrderStatusException;
import com.example.springbootredissimplestarter.repository.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;


@Service
public class OrderServiceImpl implements OrderService {

@Autowired
private OrderRepository orderRepository;

@Override
public Order createOrder(Order order) {

return orderRepository.save(order);

}

@Override
@Cacheable(value = "Order", key = "#orderId", cacheManager = "testCacheManager")
public Order getOrder(Integer orderId) {
return orderRepository.findById(orderId).orElseThrow(() -> new OrderNotFoundException("Order Not Found"));
}

@Override
@CachePut(value = "Order", key = "#orderId", cacheManager = "testCacheManager")
public Order updateOrder(Order order, Integer orderId) {
/*
order status 변화주기
status: ready -> processing -> shipped -> delivered
*/

Order orderObject = orderRepository.findById(orderId).orElseThrow(() -> new OrderNotFoundException("Order Not Found"));
if (orderObject.getOrderStatus().equals("ready")) {
orderObject.setOrderStatus("processing");
} else if (orderObject.getOrderStatus().equals("processing")) {
orderObject.setOrderStatus("shipped");
} else if (orderObject.getOrderStatus().equals("shipped")) {
orderObject.setOrderStatus("delivered");
} else {
throw new OrderStatusException("Order Status Cannot Change");
}

return orderRepository.save(orderObject);
}

@Override
@CacheEvict(value = "Order", key = "#orderId", cacheManager = "testCacheManager")
public void deleteOrder(Integer orderId) {

Order orderObject = orderRepository.findById(orderId).orElseThrow(() -> new OrderNotFoundException("Order Not Found"));
orderRepository.delete(orderObject);

}

@Override
@Cacheable(value = "Order", cacheManager = "testCacheManager")
public List<Order> getAllOrders() {
return orderRepository.findAll();
}
}

Controller

  • RESTful API 서비스를 위한 Controller 구현
  • controller 패키지를 생성, 하위에 OrderController.java 생성

Controller

  • Implement Controller for RESTful API service
  • Create controller package and create OrderController.java under it
package com.example.springbootredissimplestarter.controller;


import com.example.springbootredissimplestarter.domain.Order;
import com.example.springbootredissimplestarter.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/order")
public class OrderController {

@Autowired
OrderService orderService;

@PostMapping()
public Order createOrder(@RequestBody Order order) {
return orderService.createOrder(order);
}

@GetMapping()
public ResponseEntity<List<Order>> getOrder() {
return ResponseEntity.ok(orderService.getAllOrders());
}

@GetMapping("/{id}")
public Order getOrder(@PathVariable Integer id) {
return orderService.getOrder(id);
}

@PutMapping("/{id}")
public Order updateOrder(@PathVariable Integer id, @RequestBody Order order) {
return orderService.updateOrder(order, id);
}

@DeleteMapping("/{id}")
public String deleteOrder(@PathVariable Integer id) {
orderService.deleteOrder(id);

return "Order with id: " + id + " deleted.";
}
}

실행해보기

  • 기본적으로 생성한 CRUD가 잘 실행되는지 확인한다
  • POST를 할 때에, 우리가 생성한 domain객체 혹은 각자 적용한 dto/vo객체의 형태대로 작성을 해 준다
  • 생성한 객체를 조회 할 경우에 application.properties의 설정에 따라서 쿼리가 찍히게 된다
  • 이 때, GET 요청을 많이 날려 보았을 때 쿼리가 Request 한 만큼 찍히지 않는 것이 보인다면 Redis Cache 기능을 사용하고 있는 것을 확인 할 수 있다

Running the Application

  • Basically, check if the created CRUD works well
  • When doing a POST, write according to the format of the domain object we created or the dto/vo object you applied
  • When querying the created object, queries will be logged according to the settings in application.properties
  • At this point, if you send many GET requests and see that queries are not logged as many times as the requests, you can confirm that the Redis Cache feature is being used

API 요청

API Requests

  • POST /order
curl -d '{"orderCode":"AGGEKF123","orderObject":"로제떡볶이","orderStatus":"ready","orderPrice":17000}' -H "Accept: application/json" -H "Content-Type: application/json" -X POST http://localhost:8080/order
  • GET /order
curl -X GET http://localhost:8080/order
  • GET /order/1
curl -X GET http://localhost:8080/order/1