Kotlin + Spring Boot에서 DB 캐시를 Redis로 시도

소개



Kotlin + Spring-Boot 애플리케이션에서 Redis를 사용하여 DB (MySQL) 데이터를 캐시 해 보았습니다.

환경



Spring Boot 2.2.6
Kotlin 1.3.71
gradle 6.3

프로젝트의 병아리 만들기



Spring Initializr에서 병아리를 만듭니다.
병아리의 설정은 이런 느낌입니다.
h tps : // s rt. sp 링 g. 이오/


MySQL과 Redis 설정



MySQL과 Redis는 설치되어 있다고 가정합니다.
아래와 같이 application.properties 를 기술합니다.
포트는 모두 기본값입니다.

application.properties
#MySQL
spring.jpa.hibernate.ddl-auto=update
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo
spring.datasource.username=demouser
spring.datasource.password=password
#Redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=null
spring.redis.database=0

Entity 및 Repository 만들기



예로서 이러한 Entity와 Repository를 만듭니다.

User.kt
package com.example.demo.domain.entity

import java.io.Serializable
import java.util.*
import javax.persistence.*

@Entity
data class User (
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id : Int = 0,
    var name : String,
    var age : Int
) : Serializable

UserRepository.kt
package com.example.demo.domain.repository

import com.example.demo.domain.entity.User
import org.springframework.data.jpa.repository.JpaRepository

interface UserRepository : JpaRepository<User, Int> {
}

서비스 만들기



DB와 상호 작용하는 클래스를 정의합니다.
동시에 캐시를 보고 히트하면 캐시에서 데이터를 검색합니다.

UserService.kt
package com.example.demo.app.service

import com.example.demo.domain.repository.UserRepository
import com.example.demo.domain.entity.User
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.*

@Service
@Transactional
class UserService{
    @Autowired
    lateinit var userRepository: UserRepository

    fun existsById(id : Int) : Boolean {
        return userRepository.existsById(id)
    }

    @Cacheable(cacheNames = ["User"], key = "'User:' + #id")//キャッシュの参照とミスヒット時の登録
    fun findById(id : Int) : Optional<User> {
        return userRepository.findById(id)
    }
}
@Cacheable 어노테이션이 추가 된 함수는 return하기 전에 캐시를 참조합니다. 지정된 키(ex; User::User:1)의 캐쉬가 존재하면 캐쉬로부터 값을 취득해, return 합니다. 캐시가 없으면 return할 값을 지정된 키에 캐시합니다.
이 예제에서는 설명하지 않지만 @CachePut 어노테이션이 추가된 함수는 return 시 return할 값으로 캐시를 업데이트합니다. 예를 들어 DB의 컬럼을 편집할 때 동시에 캐시를 갱신하거나 하는 용도에 사용합니다.
또한 @cacheEvict는 return시 지정된 키의 캐시를 삭제합니다. DB의 열을 삭제하고 동시에 캐시도 삭제하고 싶을 때 등에 사용합니다.
이 예제에서는 id를 지정하고 해당 열을 검색할 때 캐시를 참조하고 등록하는 함수를 설명합니다.

컨트롤러 작성



UserController.kt
package com.example.demo.app.controller

import com.example.demo.app.service.UserService
import com.example.demo.domain.entity.User
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.ResponseBody
import java.util.*
import kotlin.system.measureTimeMillis

@Controller
class UserController{
    @Autowired
    lateinit var userService : UserService

    @GetMapping("/users/{id}")
    @ResponseBody
    fun getUser(@PathVariable id: Int) : String {
        lateinit var user: Optional<User>
        if (userService.existsById(id)) {
            val time = measureTimeMillis {
                user = userService.findById(id)
            }
            var name = user.get().name
            var age = user.get().age
            return "name=$name, age=$age, time=$time(ms)"
        }
        return "does not exist id=$id"
    }
}
/users/{id}에 요청하면 id가 있으면 DB 또는 캐시에서 데이터를 검색합니다. 그 때에 걸린 시간(time)을 계측해 둡니다. 응답으로서 User의 name, age와 계측한 시간 time를 포함한 캐릭터 라인을 돌려줍니다. id가 존재하지 않으면 존재하지 않는다는 것을 알려주는 문자열을 반환합니다.

Redis config 클래스 만들기



RedicConfig.kt
package com.example.demo.app.config

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.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer
import org.springframework.data.redis.serializer.StringRedisSerializer

@Configuration
@EnableCaching
class RedisConfig{
    @Bean
    fun redisConnectionFactory(): LettuceConnectionFactory {
        return LettuceConnectionFactory()
    }

    @Bean
    fun redisTemplateSet()  : RedisTemplate<String, Object> {
        var redisTemplate = RedisTemplate<String, Object>()
        redisTemplate.setConnectionFactory(redisConnectionFactory())
        redisTemplate.keySerializer = StringRedisSerializer()
        redisTemplate.valueSerializer = JdkSerializationRedisSerializer()
        redisTemplate.afterPropertiesSet()
        return redisTemplate
    }

    @Bean
    fun redisCacheManager(lettuceConnectionFactory : LettuceConnectionFactory) : RedisCacheManager {
        var redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(lettuceConnectionFactory)
                                .cacheDefaults(redisCacheConfiguration).build();
    }
}

이 예에서 key : "User::User:1"의 경우 value는 id : 1의 user 데이터 클래스 (id, name, age)를 캐시합니다. 즉 Object를 저장합니다.

데이터베이스 작성 및 테이블 초기화



init.sql
create database if not exists demo;

grant all on demo.* to 'demouser'@'%';

create table if not exists demo.user(
       id INT(11) AUTO_INCREMENT not null primary key,
       name varchar(30) not null,
       age INT(3) not null
);       
insert into demo.user(name, age)values('A-san', 20);
mysql -u root -p < init.sql

실행해보기



응용 프로그램을 시작합니다.
./gradlew bootRun

다른 터미널에서 요청을 보냅니다.
curl localhost:8080/users/1
#> name=A-san, age=20, time=268(ms)

첫 번째는 캐시되지 않고 DB에서 직접 데이터를 검색하므로 268ms로 약간의 시간이 걸립니다.
그대로 다시 동일한 명령을 입력합니다.
curl localhost:8080/users/1
#> name=A-san, age=20, time=9(ms)

두 번째는 캐시가 효과적이기 때문에 상당히 빠르게 데이터를 얻을 수 있습니다.

마지막으로



Kotlin + Spring Boot2에서 redis를 사용하여 DB 캐시했습니다.
이 조합으로 redis를 사용하여 DB 캐시를하려고, 특히 RedisConfig.kt에 기술한 내용으로 빠져 버렸으므로, 참고가 되면 다행입니다. 나도 아직 RedisConfig.kt의 내용을 잘 이해하지 못했기 때문에 이것에 대해 뭔가 교수해 주시면 감사하겠습니다.
여기까지 읽어 주셔서 감사합니다!

좋은 웹페이지 즐겨찾기