본문 바로가기
Develop/Spring Framework

도커를 이용해서 Redis 사용 및 스프링 프로젝트에 적용하기

by 제이._ 2022. 11. 16.

오늘은 스프링 프로젝트에 Redis를 설정하고 어떻게 적용했는지에 대해 포스팅하겠습니다.

 

 

Redis 사용 준비

먼저 Redis 설치는 로컬에 하는 방법과, 도커를 이용하는 방법이 있습니다. 저는 Docker를 이용해서 설치해보도록 하겠습니다.

처음 시작하시는 분들이 있을 수 있기에 기본적인 Dockerfile도 같이 업로드하겠습니다.

혹시 도커가 설치 안 되신 분들은 설치하시고 진행하시면 됩니다.

 

 

Docker 세팅하기

Dockerfile

FROM openjdk:11
ARG JAR_FILE=*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

저는 Java11 버전으로 프로젝트를 만들었습니다.

 

 

docker-compose.yml

version: '3.8'
services:
  mysqldb:
    image: mysql
    restart: always
    environment:
      MYSQL_DATABASE: capstone
      MYSQL_ROOT_PASSWORD: "root"
    ports:
      - 3306:3306

  redis:
    image: redis
    ports:
      - 6379:6379

  backend:
    build: .
    restart: always
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://mysqldb:3306/capstone
      SPRING_DATASOURCE_USERNAME: "root"
      SPRING_DATASOURCE_PASSWORD: "root"
    ports:
      - 8080:8080
    depends_on:
      - mysqldb
      - redis

 

 

build.gradle과 application.yml 설정하기

build.gradle

build.gradl에 redis 디펜던시 설치를 위해 dependencies 안에 추가해주세요.

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

application.yml

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/capstone?useSSL=false&useUnicode=true&characterEncoding=utf8&allowPublicKeyRetrieval=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    host: redis
    port: 6379

  jpa:
    hibernate:
      ddl-auto: update
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
      use-new-id-generator-mappings: false
    database: mysql
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        use_sql_comments: true

application.yml도 본인에게 맞는 걸로 설정하면 됩니다. 다만 redis를 추가하기 위해서는 위에 redis 부분은 꼭 설정해주셔야 합니다.

 

 

마지막으로 스프링 프로젝트에서 값을 숨기기 위해서 secure.properties를 다음과 같이 설정해주었습니다.

redis.host=localhost
redis.port=6379

 

 

 

 

스프링 프로젝트에 Redis 적용하기

 

RedisConfig

package com.example.capstone.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
@PropertySource("classpath:secure.properties")
public class RedisConfig {
    @Value("${redis.host}")
    private String host;

    @Value("${redis.port}")
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<?, ?> redisTemplate() {
        RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }
}

RedisConfig를 만들어서 위에와 같은 코드를 입력해줍니다. 이는 Redis를 사용하기 위한 기본 설정이라고 보시면 됩니다.

이제 바로 적용해보겠습니다.

 

 

Redis 적용 상황 분석

먼저 저는 프로젝트에서 같이 먹으면 안 되는 약을 조회하는 API를 만들고 있었습니다.

약A, 약B를 쿼리 파라미터로 받아서 데이터베이스에서 이걸 이용해서 병용금기 엔터티를 조회하고, 금기 이유를 리턴해주는 API입니다.

 

문제는 병용금기 내역의 데이터가 너무 많기 때문에 조회 시 너무 헤비 하다는 단점이 있었습니다.

이를 보완하기 위해서 생각한 방법이 유저들이 자주 검색하는 병용금기 약에 대해 view_count 즉 조회수를 두고, 일정 조회수를 넘어가면 이 병용금기 내역을 redis 데이터베이스에 추가하는 방법으로 진행하고자 했습니다.

 

package com.example.capstone.service.contraindicate;

import com.example.capstone.entity.contraindicate.Contraindicate;
import com.example.capstone.exception.ContraindicateNotFoundException;
import com.example.capstone.repository.contraindicate.ContraindicateRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class ContraindicateService {
    private final RedisTemplate<String, String> redisTemplate;
    private final ContraindicateRepository contraindicateRepository;

    @Transactional
    public Object findContraindicate(String pillA, String pillB) {
        if (getRedis(pillA, pillB) == null) {
            // 레디스에서 조회 했는데 없는 경우
            // 1. 디비 조회
            Contraindicate contraindicate = contraindicateRepository.findByPillAAndPillB(pillA, pillB)
                    .orElseThrow(ContraindicateNotFoundException::new);

            // 2. viewCount 증가 (if >= 3 추가)
            contraindicate.addViewCount();
            if (contraindicate.getViewCount() >= 3) {
                setRedis(pillA, pillB, contraindicate.getSymptom());
            }
            return contraindicate.getSymptom();
        }
        // 레디스에서 조회 했는데 있는 경우
        // 출력
        return getRedis(pillA, pillB);
    }

    public void setRedis(String key, String pillVal, String symptom) {
        HashOperations<String, Object, Object> hashOperations = redisTemplate.opsForHash();
        hashOperations.put(key, pillVal, symptom);
    }

    public Object getRedis(String key, String pillVal) {
        HashOperations<String, Object, Object> hashOperations = redisTemplate.opsForHash();
        Object symptom = hashOperations.get(key, pillVal);
        return symptom;
    }
}

약A에 대해 약B, 약C 등등 여러 가지의 병용금기 약이 있고 그에 따른 symptom도 있기 때문에 저는 위와 같이 리스트로 구현했습니다.

HashMap으로 해도 상관은 없을 듯합니다.

 

RedisTemplate에는 여러 가지 타입을 넣을 수 있으니 다들 상황에 맞게 잘 구현하시면 될 것 같습니다 :)

 

 

++ 병용 금기 내용은 변하지 않으므로, MySQL과 Redis 사이에서 데이터의 정합성의 문제로부터 벗어날 수 있다고 판단하고, 영속성과 동시성 문제는 추후에 각각 캐시파일로 만들고, 코드를 리팩토링하면서 해결해야할 것 같습니다.

 

 

 

Redis에 들어가기 전 MySQL 조회 시간 257ms

 

Redis에 들어간 후 조회 시간 44ms