-
Embedded Redis 구성하여 테스트 수행하기프로젝트/선착순 쿠폰 발급 시스템 2023. 5. 4. 00:01728x90
개요
로컬에서 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에서만 가능합니다..
드디어 모든 테스트가 성공적으로 수행됩니다.
참고자료
'프로젝트 > 선착순 쿠폰 발급 시스템' 카테고리의 다른 글
OpenAPI Specification으로 API-First 개발하기 (0) 2023.05.18 쿠폰 발급을 위한 Redis Set Document 읽기 (0) 2023.05.06 Event Driven Architecture란? (0) 2023.05.01 Spring Boot Flyway 적용하기(flyway, h2 DB test 에러 해결) (0) 2023.04.30 Database local dev환경 구성하기(postgreSQL with Docker) (0) 2023.04.29 - Spring Data Redis