ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Embedded Redis 구성하여 테스트 수행하기
    프로젝트/선착순 쿠폰 발급 시스템 2023. 5. 4. 00:01
    728x90

    개요

    로컬에서 redis를 테스트하기 위해 embedded redis를 구축하고자 합니다.

    h2와 비슷하게 embedded redis를 활용하면 프로젝트를 수행하기 위해 특별한 작업 없이 git clone만 받으면 바로 로컬/개발을 할 수 있게 됩니다.

     

     

    사용 기술

    • Spring Data Redis
      • Redis를 JPA Repository를 사용하듯이 인터페이스 제공
    • Lettuce
      • Redis Java Client
      • Jedis는 업데이트가 거의 되지 않음
    • Embedded Redis
      • H2와 같은 내장 Redis

     

    build.gradle.kts

    //redis
    implementation("it.ozimov:embedded-redis:0.7.2")
    implementation("org.springframework.boot:spring-boot-starter-data-redis")

    예전에는 kstyrc.embedded-redis를 사용했지만 업데이트가 이루어지지 않아 이를 fork 하여 만들어진 모듈이 바로 it.ozimov.embedded-redis입니다.

     

    LocalRedisConfig

    @Profile("local") //profile이 local일때만 활성화
    @Configuration
    class LocalRedisConfig(
        @Value("\${spring.redis.port:6379}") private val redisPort: Int
    ) {
        private val redisServer: RedisServer = RedisServer(redisPort)
    
        @PostConstruct
        private fun startRedis(){
            redisServer.start()
        }
    
        @PreDestroy
        private fun stopRedis(){
            redisServer.stop()
        }
    }

    @Profile은 application의 환경이 특정환경일 때만 활성화시킴을 의미합니다.

    spring.redis.port를 application.yml에서 주입받으며 기본값은 6379로 설정합니다.

    이후 @PostConstruct, @PreDestory를 활용하여 redisServer를 start, stop 시킵니다.

     

    RedisRepositoryConfig

    @Configuration
    @EnableRedisRepositories
    class RedisRepositoryConfig(
        @Value("\${spring.redis.host:localhost}") private val redisHost: String,
        @Value("\${spring.redis.port:6379}") private val redisPort: String,
    ) {
    
        @Bean
        fun redisConnectionFactory(): RedisConnectionFactory{
            return LettuceConnectionFactory(redisHost, redisPort.toInt())
        }
    
        @Bean
        fun redisTemplate(): RedisTemplate<ByteArray,ByteArray>{
            val redisTemplate = RedisTemplate<ByteArray, ByteArray>()
            redisTemplate.setConnectionFactory(redisConnectionFactory())
            return redisTemplate
        }
    }

     

    RedisConnectionFactory 인터페이스 하위 클래스에는 Lettuce와 Jedis가 존재합니다.

    Lettuce의 성능이 더 좋기 때문에 Lettuce를 사용합니다.

     

    RedisTemplate은 Redis 서버에 커맨드를 수행하기 위한 추상화를 제공하며 Object 직렬화 Connection Management를 수행합니다.

     

    이를 통해 Stirng, List, Set, Sorted Set, Hash 등의 다양한 자료구조를 사용할 수 있습니다.

     

    RedisTest

    @RedisHash("redis-test")
    class RedisTest(
        @Id
        private val id: String,
        var refreshTime: LocalDateTime,
    ) : Serializable {
    
        fun refresh(refreshTime: LocalDateTime) {
            if(refreshTime.isAfter(this.refreshTime)){
                this.refreshTime = refreshTime
            }
        }
    }

    @RedisHash는 Hash Collection을 명시하며 Jpa의 Entity에 해당하는 애노테이션과 유사합니다.

    value 값은 key를 만들 때 사용하며 Hash의 Key는 value + @Id로 형성됩니다.

    @Id는 key를 식별할 때 사용하는 고유한 값으로 @RedisHash와 결합하여 key를 생성합니다.

     

    RedisTestRepository

    public interface PointRedisRepository extends CrudRepository<Point, String> {
    }

     

    src/test/resources/application.yml

    spring:
      config:
        activate:
          on-profile: local
    
      redis:
        port: 6379
        host: 127.0.0.1

     

    테스트코드

    @Import(LocalRedisConfig::class)
    @DataRedisTest
    @ActiveProfiles("local")
    class RedisTestTest {
    
        @Autowired
        private lateinit var redisTestRepository: RedisTestRepository
    
        @Test
        fun `기본 등록 조회기능`(){
            //given
            val id = "junuuu"
            val refreshTime = LocalDateTime.of(2023,2,20,0,0)
            val redisTest = RedisTest(
                id = id,
                refreshTime = refreshTime,
            )
    
            //when
            redisTestRepository.save(redisTest)
    
            //then
            val result = redisTestRepository.findById(id).get()
            assertThat(result.refreshTime).isEqualTo(refreshTime)
        }
    }

    @DataRedisTest를 통해 테스트하기 때문에 LocalRedisConfig.class를 읽어오지 못한다.

    따로 @Import(LocalRedisConfig::class)를 해주어야 한다.

     

    테스트를 위한 SpringBootApplication 지정

    이대로 테스트를 돌리면 다음과 같은 Exception이 발생합니다.

    Unable to find a @SpringBootConfiguration,
    you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test

     

    현재 모듈에서 @SprngBootApplication 어노테이션이 존재하지 않는 infra-cache와 같은 모듈이라면 다음과 같이 테스트시 다음과 같은 클래스를 추가해주어야 합니다.

    @SpringBootApplication(scanBasePackages = ["com.your.package.path"])
    class SpringBootApplication {
    }

     

     

    ./gradlew build 수행

    테스트를 수행하는 환경에서 fail이 발생합니다.

    org.gradle.parallel=true

    테스트를 병렬로 수행하는 옵션을 켜두었고 redis의 6379 포트들끼리 충돌이 발생합니다.

     

     

    LocalRedisConfig를 다음과 같이 수정합니다.

    @Profile("local")
    @Configuration
    class LocalRedisConfig(
        @Value("\${spring.redis.port:6379}") private var redisPort: Int
    ) {
        private var redisServer: RedisServer = RedisServer(redisPort)
    
        @PostConstruct
        private fun startRedis() {
            if(isRedisRunning()){
                redisPort = findAvailablePort()
            }
            redisServer = RedisServer(redisPort)
            redisServer.start()
        }
    
        @PreDestroy
        private fun stopRedis() {
            redisServer.stop()
        }
    
        /**
         * Checks if the embedded Redis is currently running.
         */
        private fun isRedisRunning(): Boolean {
            return isRunning(executeGrepProcessCommand(redisPort))
        }
    
        /**
         * Finds an available port on the current PC/server.
         */
        @Throws(IllegalArgumentException::class, IOException::class)
        fun findAvailablePort(): Int {
            for (port in 10000..65535) {
                val process = executeGrepProcessCommand(port)
                if (!isRunning(process)) {
                    return port
                }
            }
    
            throw IllegalArgumentException("Not Found Available port: 10000 ~ 65535")
        }
    
        /**
         * Executes a shell command to check if a given port is being used by a process.
         */
        @Throws(IOException::class)
        private fun executeGrepProcessCommand(port: Int): Process {
            val command = String.format("netstat -nat | grep LISTEN|grep %d", port)
            val shell = arrayOf("/bin/sh", "-c", command)
            return Runtime.getRuntime().exec(shell)
        }
    
        /**
         * Checks if a given process is currently running.
         */
        private fun isRunning(process: Process): Boolean {
            var line: String?
            val pidInfo = StringBuilder()
    
            BufferedReader(InputStreamReader(process.inputStream)).use { input ->
                while (input.readLine().also { line = it } != null) {
                    pidInfo.append(line)
                }
            }
    
            return !pidInfo.toString().isNullOrEmpty()
        }
    }

    redisPort가 실행가능하지 않다면 새로운 포트를 찾아서 구성합니다.

    mac, linux에서만 가능합니다..

    드디어 모든 테스트가 성공적으로 수행됩니다.

     

     

     

     

    참고자료

    https://jojoldu.tistory.com/297

    http://arahansa.github.io/docs_spring/redis.html

    댓글

Designed by Tistory.