본문 바로가기
Spring/SpringBoot

[SpringBoot] Redis 테스트 환경 구축하기 (1) - Embedded

by J4J 2024. 7. 7.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 redis 테스트 환경 구축하기 첫 번째인 embedded에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

관련 글

 

[SpringBoot] Redis 사용하기 (1) - Redis란 무엇인가?

[SpringBoot] Redis 사용하기 (2) - Redis Repository 사용하기

[SpringBoot] Redis 사용하기 (3) - Redis Template 사용하기

[SpringBoot] Redis 사용하기 (4) - Redis Cache Manager 사용하기

 

 

반응형

 

 

Redis Embedded란 ?

 

redis embedded는 redis 서버를 별도로 필요로 하지 않고 애플리케이션 내부에서 직접 redis 서버가 동작되는 환경을 의미합니다.

 

redis embedded를 사용하는 이유는 다양하게 존재하지만, 해당 글에서는 제목에 보이는 것처럼 테스트 환경을 구축하기 위해 사용합니다.

 

redis embedded가 테스트하는 환경에서 사용될 수 있는 이유는 다음과 같이 존재합니다.

 

  • 테스트마다 독립적인 redis 환경을 구축하여 이전 테스트가 영향받지 않음
  • 실제 redis 서버의 연결 없이 테스트를 동작할 수 있음
  • 배포 파이프라인 동작을 수행할 때 네트워크에 대해 고민하지 않도록 도와줌

 

 

 

다시 말하면 redis embedded가 없어도 테스트 환경을 구축하는데 문제는 없을 수 있습니다.

 

다만, redis embedded를 활용한다면 위와 같은 이점을 얻을 수 있기에 redis를 이용한 테스트 환경을 더 효율적으로 개선할 수 있습니다.

 

 

 

 

Redis Embedded 설정 전 테스트

 

redis embedded를 설정하기 전에는 다음과 같이 간단하게 테스트 코드를 작성해 볼 수 있습니다.

 

// user entity
package com.jforj.redisembedded.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private String id;
    private String name;
    private int age;
}


// user repository
package com.jforj.redisembedded.repository;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jforj.redisembedded.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class UserRepository {
    private final RedisTemplate<String, String> redisTemplate;
    private final ObjectMapper objectMapper;

    public void createUser(String id, String name, int age) {
        User user = User
                .builder()
                .id(id)
                .name(name)
                .age(age)
                .build();

        try {
            redisTemplate.opsForValue().set(id, objectMapper.writeValueAsString(user));
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    public User getUser(String id) {
        try {
            return objectMapper.readValue(redisTemplate.opsForValue().get(id), User.class);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}


// user repository test
package com.jforj.redisembedded.repository;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jforj.redisembedded.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Repository;

import static org.assertj.core.api.Assertions.assertThat;

@DataRedisTest(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class))
@Import(ObjectMapper.class)
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void getUserTest() {
        // given
        String id = "j4j";
        String name = "jforj";
        int age = 123;

        userRepository.createUser(id, name, age);

        // when
        User user = userRepository.getUser(id);

        // then
        assertThat(user).isNotNull();
        assertThat(user.getName()).isEqualTo(name);
    }
}

 

 

 

 

redis 사용 환경이 기본으로 설정되어 있는 상태에서 위와 같이 코드를 작성한다면 다음과 같은 테스트 결과를 확인할 수 있습니다.

 

테스트 동작 결과

 

 

 

다만 위의 테스트는 redis 서버에 직접 연결되어 테스트가 되는 것입니다.

 

그래서 다음과 같이 redis 서버 연결이 필요하며 또한 테스트의 결과가 redis 서버에 영향을 줄 수 있습니다.

 

테스트 동작 후 redis 서버 적재 상태

 

 

 

 

Redis Embedded 설정 방법

 

[ 1. dependency 추가 ]

 

// build.gradle
dependencies {
    // redis embedded
    implementation 'it.ozimov:embedded-redis:0.7.2'
}

 

 

 

[ 2. redis embedded 설정 추가 ]

 

redis embedded가 테스트를 수행할 때만 사용되기에 설정을 위한 클래스 파일의 경로를 /test 하위에 배치하면 됩니다.

 

package com.jforj.redisembedded.config;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.util.StringUtils;
import redis.embedded.RedisServer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

@TestConfiguration
public class RedisEmbeddedConfig {
    private static final int REDIS_PORT = 6379;
    private RedisServer redisServer;

    @PostConstruct
    public void configRedisServer() throws IOException {
        int port = REDIS_PORT;
        if (isProcessRunning(getProcess(port))) {
            port = getAvailablePort();
        }

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

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

    /**
     * 사용 가능한 port getter
     *
     * @return 사용 가능한 port
     * @throws IOException 미 사용 port 확인 불가
     */
    private int getAvailablePort() throws IOException {
        for (int port = 10000; port <= 65535; port++) {
            Process process = getProcess(port);
            if (!isProcessRunning(process)) {
                return port;
            }
        }

        throw new RuntimeException("available port is not exist between 10000 and 65535.");
    }

    /**
     * port로 동작되는 process getter
     *
     * @param port port 정보
     * @return port 동작 process
     * @throws IOException shell runtime 실행 exception
     */
    private Process getProcess(int port) throws IOException {
        String os = System.getProperty("os.name").toLowerCase();

        // window인 경우
        if (os.contains("win")) {
            String command = String.format("netstat -ano | find \"LISTEN\" | find \"%d\"", port);
            String[] shell = {"cmd.exe", "/y", "/c", command};
            return Runtime.getRuntime().exec(shell);
        }

        // window가 아닌 경우
        String command = String.format("netstat -nat | grep LISTEN | grep %d", port);
        String[] shell = {"/bin/sh", "-c", command};
        return Runtime.getRuntime().exec(shell);
    }

    /**
     * process 동작 여부 검증
     *
     * @param process 동작 검증될 process
     * @return process 동작 여부
     */
    private boolean isProcessRunning(Process process) {
        String line;
        StringBuilder stringBuilder = new StringBuilder();

        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line);
            }
        } catch (Exception e) {
            throw new RuntimeException("process running read fail.");
        }

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

 

redis embedded 설정 위치

 

 

 

 

[ 3. redis embedded 설정 테스트에 추가 ]

 

redis embedded 동작이 원하는 테스트에 위에서 설정한 클래스 파일을 import 해주면 됩니다.

 

package com.jforj.redisembedded.repository;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jforj.redisembedded.config.RedisEmbeddedConfig;
import com.jforj.redisembedded.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Repository;

import static org.assertj.core.api.Assertions.assertThat;

@DataRedisTest(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class))
@Import({ObjectMapper.class, RedisEmbeddedConfig.class}) // redis embedded 클래스 추가
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void getUserTest() {
        // given
        String id = "j4j";
        String name = "jforj";
        int age = 123;

        userRepository.createUser(id, name, age);

        // when
        User user = userRepository.getUser(id);

        // then
        assertThat(user).isNotNull();
        assertThat(user.getName()).isEqualTo(name);
    }
}

 

 

 

[ 4. 테스트 ]

 

위와 같이 설정을 했다면 테스트를 해보겠습니다.

 

먼저, 상황에 따라 해당 설정을 하시는 분들은 env 설정 값이 구분되어 있을 수 있는데 만약 test를 위한 application-test 파일을 따로 관리하시는 분들이라면 해당 설정 값에 redis 설정을 제외해 주시면 됩니다.

 

그리고 저는 redis를 로컬에서 동작시키고 있었는데 테스트를 위해 redis를 종료시키고 테스트를 해보면 다음과 같이 redis의 동작 없이도 테스트가 수행되는 결과를 확인할 수 있습니다.

 

redis 종료 확인

 

redis embedded 설정 후 테스트 동작 결과

 

 

 

 

GitLab 파이프라인 전/후 비교

 

위의 결과를 확인하면 redis embedded 설정으로 테스트가 동작하는 것을 볼 수 있습니다.

 

추가적으로 배포 파이프라인에 대해서도 유용하게 사용되는지에 대해 확인하려고 합니다.

 

 

 

먼저, redis embedded 설정을 하기 전 소스 코드를 다음과 같이 .gitlab-ci.yml을 설정하여 gitlab에 push 하면 다음과 같은 파이프라인 결과를 얻을 수 있습니다.

 

// .gitlab-ci.yml
stages:
  - build
  - test

build:
  image: gradle:8.8.0-jdk17
  stage: build
  script:
    - chmod +x gradlew
    - ./gradlew bootJar
  artifacts:
    paths:
      - build/libs/*.jar
    expire_in: 1 days

test:
  image: gradle:8.8.0-jdk17
  stage: test
  script:
    - chmod +x gradlew
    - ./gradlew test

 

redis embedded 설정 전 배포 파이프라인

 

 

 

다음으로 redis embedded 설정을 한 이후 동일한 환경에서 소스 코드를 업로드해보겠습니다.

 

그러면 redis 서버가 애플리케이션 내부에서 동작되기 때문에 connection 생성에 문제가 없게 되어 다음과 같이 올바르게 파이프라인이 동작되는 것을 볼 수 있습니다.

 

redis embedded 설정 후 배포 파이프라인

 

redis embedded 설정 후 배포 파이프라인

 

 

 

 

 

 

 

 

이상으로 redis 테스트 환경 구축하기 첫 번째인 embedded에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글