Test

Embedded Redis로 외부 환경에서 Redis Test 하기

KAispread 2023. 10. 18. 18:27
728x90
반응형

개요

프로젝트를 진행하면서 다양한 테스트 코드를 작성하고 있다. 우리 팀은 각종 인증번호, Refresh Token 저장용도로 Redis 를 사용하고 있었다. Redis를 사용하는 API에 대한 테스트를 작성하고 PR을 올렸는데 CI 에서 테스트가 실패하는 문제가 발생했다.

테스트 실패 (PR을 올리면 Actions에서 자동으로 테스트가 수행되도록 구축해놓은 상태이다.)

에러 로그를 통해 Redis를 사용하는 테스트가 모두 실패했다는 것을 확인할 수 있었다. 테스트 실패 원인에 대해 생각해보니 Actions에서 연결할 Redis 노드가 없다는 것을 알아차리게 되었다.

 

문제 상황

다음은 프로젝트의 환경별 데이터베이스 의존성에 대해 나타낸 것이다.

프로젝트에서 사용하고 있는 데이터베이스는 Redis와 RDBMS 이다.

로컬 환경에서는 In-Memory DB 인 H2와 Docker로 Redis 컨테이너를 띄워 사용하고 있었고 개발, 운영환경에서는 AWS의 ElastiCacheRDS를 사용하고 있었다.

그림과 같이 Actions Runner 환경에서는 연결할 수 있는 Redis 노드가 없었고 당연히 Redis와 연결할 수 없는 환경이기 때문에 테스트가 실패한 것이다.

처음에는 Actions Runner에서 일일이 Redis 를 띄워줘야하나.. 라고 생각했지만 너무 비효율적인 방법인 것 같아 관련된 내용을 찾아보기 시작했다. 다행히 H2 처럼 In-Memory에서 동작하는 Embedded Redis의 존재를 알게 되었고 이를 적용하고자 하였다.

 

Embedded Redis 설정

build.gradle 에 다음의 의존성을 추가한다.

dependencies {
    // Redis
    implementation 'org.springframework.boot:spring-boot-starter-data-redis:3.1.0'
    
    // Embedded Redis
    testImplementation 'it.ozimov:embedded-redis:0.7.2'
}

 

Test 패키지에서 적당한 곳에 다음의 코드를 추가한다.

/*
* Test 용 EmbeddedRedis / profile 이 local, default 일때만 활성화
* */
@Slf4j
@Profile({"local", "default"})
@Configuration
public class EmbeddedRedisConfig {

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

    private RedisServer redisServer;

    @PostConstruct
    public void redisServer() throws IOException {
        // Application Context 가 2번 이상 실행될 때 포트 충돌 문제 해결
        int port = isRedisRunning() ? findAvailablePort() : redisPort;

        redisServer = new RedisServer(port);
        redisServer.start();
    }

    @PreDestroy
    public void stopRedis() {
        if (redisServer != null) {
            redisServer.stop();
        }
    }

    /**
     * Embedded Redis가 현재 실행중인지 확인
     */
    private boolean isRedisRunning() throws IOException {
        return isRunning(executeGrepProcessCommand(redisPort));
    }

    /**
     * 현재 PC/서버에서 사용가능한 포트 조회
     */
    public int findAvailablePort() throws IOException {

        for (int port = 10000; port <= 65535; port++) {
            Process process = executeGrepProcessCommand(port);
            if (!isRunning(process)) {
                return port;
            }
        }

        throw new IllegalArgumentException("Not Found Available port: 10000 ~ 65535");
    }

    /**
     * 해당 port를 사용중인 프로세스 확인하는 sh 실행
     */
    private Process executeGrepProcessCommand(int port) throws IOException {
        String command = String.format("netstat -nat | grep LISTEN|grep %d", port);
        String[] shell = {"/bin/sh", "-c", command};
        return Runtime.getRuntime().exec(shell);
    }

    /**
     * 해당 Process가 현재 실행중인지 확인
     */
    private boolean isRunning(Process process) {
        String line;
        StringBuilder pidInfo = new StringBuilder();

        try (BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()))) {

            while ((line = input.readLine()) != null) {
                pidInfo.append(line);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        return StringUtils.hasText(pidInfo.toString());
    }
}

 

간단하게 port나 Max memory size와 같은 설정값들을 지정해주고 RedisServer 인스턴스를 생성하여 Embedded Redis를 띄워주면 된다.

주의해야할 점은 스프링 컨테이너가 여러번 띄워질 때 Port 충돌이 발생하므로, 사용하고있지 않은 port를 찾아 넣어주어야한다.

 

Test 수행

테스트 환경에서 Embedded Redis가 잘 적용되는지 간단한 테스트를 작성했다.

@SpringBootTest
class RedisConnectionTest extends AbstractIntegrationTest {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @DisplayName("Redis 에서 데이터를 조회하는데 성공한다.")
    @Test
    void load() {
        final String key = "01083215123";
        final String value = "123456";

        ValueOperations<String, String> operation = redisTemplate.opsForValue();
        operation.set(key, value);

        String fromRedis = operation.get(key);
        assertThat(fromRedis).isEqualTo(value);
    }

    @DisplayName("Redis 에 존재하지 않는 키값을 조회하면 Null이 반환된다.")
    @Test
    void isNull() {
        final String key = "01083215123";
        final String value = "1234568";

        ValueOperations<String, String> operation = redisTemplate.opsForValue();
        operation.set(key, value);

        String fromRedis = operation.get("123");
        assertThat(fromRedis).isNull();
    }

    @DisplayName("중복된 키값을 저장하면 덮어쓰기 된다.")
    @Test
    void duplicate() {
        // given
        final String key = "01083215123";
        final String value = "123456";
        final String secondValue = "987643";

        // when
        ValueOperations<String, String> operation = redisTemplate.opsForValue();
        operation.set(key, value);
        operation.set(key, secondValue);

        // then
        String fromRedis = operation.get(key);
        assertThat(fromRedis).isEqualTo(secondValue);
    }

}

테스트를 돌려보면?

 

다음과 같이 테스트 환경에서 잘 동작하는 모습을 볼 수 있다.

 

Actions의 CI 작업도 성공하는 모습을 확인할 수 있었다.

728x90
반응형