[Spring Boot] Redis Cache, MySQL 이용한 간단한 API 제작

2020. 12. 22. 19:00Spring/Spring Boot

1. 개요

1) Cache

캐시는 서버의 부하를 감소히키고 빠른 처리 성능(조회)을 확보하여 보다 쾌적한 서비스를 제공하는 것이 목적이다. 캐시의 대상 되는 정보의 조건은 다음과 같다.

  • 정보의 단순성
  • 빈번한 동일요청의 반복
  • 높은 단위 처리 비용
  • 정보의 최신화가 반드시 실시간으로 이루어지지 않아도 서비스 품질에 영향을 거의 주지 않는 정보
  • ... 등등

 

대표적인 예시로는 검색어, 베스트셀러, 추천상품, 카테고리와 카테고리별 등록상품 수, 방문자 수, 조회수, 추천수, 1회성 인증정보, 공지사항, 등이 있다.

 

캐시를 이용한 서비스를 구성할 때에는 캐시 정책(정보의 선택, 정보의 유효기간(TTL), 정보의 갱신 시점)을 수립하는게 좋다. 많은 캐시 서버 중에서도 Redis를 사용하는 이유는 추상화된 API와 어노테이션 및 Spring Boot Starter Kit의 제공과 Spring Boot의 Auto Configuration 적용으로 캐시 서버 설정이 간결하기 때문이다.

어노테이션 설명
@Cacheable 캐시가 있으면 캐시의 정보를 가져오고 없으면 등록한다.
@CachePut 무조건 캐시에 저장한다.
@CacheEvict 캐시를 삭제한다.

예를 들면 Post 객체에 담긴 실제 값 중 'shares'의 값이 500보다 작지 않은 경우에만 캐시를 적용하도록 설정한 코드이다. 즉, 500보다 작으면 굳이 캐시에 저장하지 않고 서비스를 직접 호출해서 값을 반환하겠다는 코드이다.

@Cacheable(value = "post-single", key = "#id", unless = "#result.shares < 500") 
@GetMapping("/{id}") 
public Post getPostByID(@PathVariable String id) throws PostNotFoundException { 
  log.info("get post with id {}", id); return postService.getPostByID(id); 
}

특정 ID의 글을 지우는 요청하나로 서비스에서 실제 데이터를 지우고 동시에 Key로 ID로 저장되어 있는 Redis 키를 삭제할 수 있다.

@CacheEvict(value = "post-single", key = "#id") 
@DeleteMapping("/delete/{id}") 
public void deletePostByID(@PathVariable String id) throws PostNotFoundException { 
  log.info("delete post with id {}", id);
  postService.deletePost(id);
}

예시처럼 간단한 어노테이셔으로 캐시를 할 수 있지만 더 디테일하게 캐시를 사용하고 싶다면(Redis 키에 데이터를 저장하고 꺼낼 때 처리할 Serializer의 지정, 키의 TTL 설정, 키의 Prefix 설정, 등) 별도의 cacheManager를 오버라이딩하여 사용하면 된다.

2) 전체적인 구조

캐시는 한번 읽은 데이터를 일정 공간에 저장해 두었다가 같은 데이터를 또 다시 요청할 때 바로 전달해주는 기술이다. 그 중 Redis Cache는 가장 대중적으로 사용하는 분산 캐시로, 주로 게시판의 첫 페이지, 랭킹 등 데이터 지속적 액세스 영역에 많이 사용된다.

전체적인 구조

로컬 캐시는 하나의 JVM 안에 하나의 캐시가 저장되는 형태로, 캐시 데이터가 별도의 메모리 공간을 사용하지 않고 JVM안에 함께 저장되기 때문에 많은 데이터를 처리하게 되면 메모리가 기하급수적으로 증가한다.

 

반면 분산 캐시 구조는 별도의 공간(캐시 서버)에 캐시가 저장되므로 로컬 메모리를 크게 잡을 필요가 없다.

로컬 캐시와 분산 캐시 구조

 

 

 

2. Redis, MySQL

※ 하나의 인스턴스에 설치하는 내용입니다. (6379, 3306 포트 개방 허용 필수)

1) Redis 설치

sudo yum -y update
sudo yum -y install gcc make


// Redis 설치
wget https://download.redis.io/releases/redis-4.0.0.tar.gz
tar xzf redis-4.0.0.tar.gz
cd redis-4.0.0
make


// 디렉토리 생성 및 설정 파일 복사
sudo mkdir /etc/redis 
sudo mkdir /var/lib/redis
sudo cp src/redis-server src/redis-cli /usr/local/bin/
sudo cp redis.conf /etc/redis/


// 설정 파일 수정
sudo vim /etc/redis/redis.conf
----------------------------------------------
daemonize yes
bind 0.0.0.0
dir /var/lib/redis
logfile /var/log/redis_6379.log
----------------------------------------------

// Redis 서버 초기화 스크립트 (자동 실행 스크립트)
cd /tmp
wget https://raw.github.com/saxenap/install-redis-amazon-linux-centos/master/redis-server
sudo mv redis-server /etc/init.d
sudo chmod 755 /etc/init.d/redis-server
sudo vim /etc/init.d/redis-server
----------------------------------------------
redis="/usr/local/bin/redis-server" (확인)
----------------------------------------------

// 자동 실행 여부
sudo chkconfig --add redis-server
sudo chkconfig --level 345 redis-server on

※ redis-cli가 실행이 안되면 log파일의 권한을 확인해주거나 인스턴스를 재부팅해주면 된다.

2) MySQL 설치


[참고] Database - Practice - AWS EC2 인스턴스 Redis 설치 및 사용

[참고] Spring - Spring Boot - MySQL를 이용한 간단한 API 제작

[참고] blog.jiniworld.me/58 

[참고] techviewleo.com/how-to-install-mysql-8-on-amazon-linux-2/

 

 

 

3. Redis Cache 설정

1) build.gradle

plugins {
    id 'org.springframework.boot' version '2.4.1'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}

group = 'org.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
    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'
    implementation 'junit:junit:4.12'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'mysql:mysql-connector-java'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

//test {
//    useJUnitPlatform()
//}

2) application.yml

spring:
  cache:
    type: redis
    redis:
      time-to-live: 60000
      cache-null-values: true
  redis:
    host: [IP 주소]
    port: 6379


  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://[IP 주소]:3306/test?useSSL=false
    username: root
    password: [비밀번호]
    hikari:
      initialization-fail-timeout: 0
  jpa:
    database-platform: org.hibernate.dialect.MySQL5Dialect
    generate-ddl: true
    show-sql: true
    hibernate:
      ddl-auto: create

5) Redis 설정

Redis 연결을 위한 설정을 구현한다.


[참고] Spring - Spring Boot - Redis (Lettuce)를 이용한 간단한 API 제작


만약 T정보의 유효기간(TTL), 정보의 갱신 시점, 등의 기능을 사용하고 싶다면 별도의 설정이 필요하다. Spring Boot에서 제공하는 CachingConfigure 인터페이스를 구현해 놓은 CachingConfigureSupport 클래스를 상속받아 cacheManager를 오버라이딩을 하여 cacheManager에 필요한 속성값(Serializer, TTL, KeyPrefix)을 지정한다. 이 후에는 @Cacheable, @CachePut, @CacheEvict 어노테이션을 사용할 때 cacheManager를 지정하여 사용하면 된다.

package org.example.config.redis;

import lombok.RequiredArgsConstructor;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
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.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
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;

@RequiredArgsConstructor
@Configuration
@EnableCaching
@EnableRedisRepositories
public class RedisRepositoryConfig extends CachingConfigurerSupport {

    ... 

    @Override
    public CacheManager cacheManager() {
        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory());
        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .prefixKeysWith("prefix:")
                .entryTtl(Duration.ofHours(5L));

        builder.cacheDefaults(configuration);
        return builder.build();
    }
}

4) Application

@EnableCaching 어노테이션을 이용하여 Spring Boot에 캐시를 사용하겠다고 알려준다.

package org.example;

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

@EnableCaching
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

5) Controller

cacheManager를 지정하지 않을 경우, 해당 속성을 생략해주면 된다.

package org.example.controller;

import lombok.RequiredArgsConstructor;
import org.example.controller.dto.PostsListResponseDto;
import org.example.controller.dto.PostsResponseDto;
import org.example.controller.dto.PostsSaveRequestDto;
import org.example.controller.dto.PostsUpdateRequestDto;
import org.example.service.posts.PostsService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RestController
public class PostsController {

    private final PostsService postsService;

    @PostMapping("/api/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto) {
        return postsService.save(requestDto);
    }

    @PutMapping("/api/posts/{id}")
    public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto) {
        return postsService.update(id, requestDto);
    }

    @Cacheable(value = "post-single", key = "#id", cacheManager = "cacheManager")
    @GetMapping("/api/posts/{id}")
    public PostsResponseDto findById(@PathVariable Long id) {
        return postsService.findById(id);
    }

    @GetMapping("/api/posts")
    public List<PostsListResponseDto> findAllDesc() {
        return postsService.findAllDesc();
    }

    @CacheEvict(value = "post-single", key = "#id", cacheManager = "cacheManager")
    @DeleteMapping("/api/posts/{id}")
    public Long delete(@PathVariable Long id) {
        postsService.delete(id);
        return id;
    }
}

6) 간단한 API 제작

API 제작 방법은 기존의 방식과 동일하다. 단, 주의해야할 점은 redis는 데이터를 Hash에 저장하기 때문에 redis에 저장할 객체는 Serializable을 implement 해야한다. 따라서 redis에 저장할 객체(dto)를 Serializable을 implement하도록 변경한다.

package org.example.controller.dto;

import lombok.Getter;
import org.example.domain.posts.Posts;

import java.io.Serializable;

@Getter
public class PostsResponseDto implements Serializable {

    ...
}

7) 구현 화면

캐시를 하기전, 동일한 데이터에 대한 단일 조회를 반복적으로 할 때마다 조회 쿼리문이 사용되는 것을 확인할 수 있다. 반면, 캐시를 한 이후에는 동일한 데이터에 대한 단일 조회가 한번만 이루어지는 것을 확인할 수 있다.

캐시 사용 전
캐시 사용 후

redis-cli에 캐시가 되어있는 것을 확인할 수 있으며, 일정 시간이 지나면 캐시 데이터가 사라지는 것을 확인할 수 있다.

redis-cli 캐시 확인


[참고] github.com/ozofweird/SpringBoot_RedisCache

[참고] github.com/shameed1910/springboot-redis-cache/blob/master/pom.xml

[참고] 055055.tistory.com/75

[참고] yonguri.tistory.com/82

[참고] deveric.tistory.com/98

728x90