![[Spring] Redis 테스트 환경 구축하기(Embedded Redis)](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdVum8F%2FbtsLCFvrnea%2Fv6IzHV7DU5z7V8chqyUWHK%2Fimg.png)
현재 상황
현재 진행하는 프로젝트에서 환자의 위도와 경도 값을 저장하기 위해 Redis를 사용하고 있습니다.
프로젝트의 테스트 코드를 작성하는 과정에서 Redis를 사용하는 기능은 어떻게 테스트 코드를 작성해야 할지 고민이었습니다.
현재 저희 서비스의 아키텍처는 NCP 서버에 Docker를 활용해 Redis를 운영하고 있습니다.
처음에는 로컬 PC에서도 운영 서버와 동일한 환경을 Docker를 사용하여 구축하고, 테스트를 진행하는 방법을 고려했습니다.
하지만, 이 방식은 테스트를 실행할 때마다 Docker Container를 생성해야 하며, 무엇보다 Github에서 프로젝트를 클론한 후 바로 테스트를 실행할 수 없는 문제가 있었습니다. 테스트 코드를 실행하기 위해서는 PC에 Redis 데몬을 설치해야 하는 문제가 있었습니다.
Redis 테스트 코드는 어떻게 작성해야 할까?
Spring에서 Redis를 테스트하는 방법을 찾아본 결과 아래 두 가지 방법을 찾았습니다.
- Embedded Redis
- TestContainers
먼저, TestContainers에 대해서 알아본 결과, TestContainers는 테스트 환경에서 Docker 컨테이너를 실행할 수 있는 라이브러리로 운영 환경에 가까운 테스트를 할 수 있다는 장점이 있었습니다. Docker만 설치되어 있다면 별도의 환경 구축이 필요 없다는 장점을 가지고 있지만, 컨테이너를 생성하고 삭제하는 과정이 포함되어 있어 테스트가 느려진다는 단점이 있었습니다. 반면, Embedded Redis는 운영 환경과 Docker 환경을 설정할 필요도 없으며, 테스트를 빠르게 진행할 수 있다는 장점이 있었습니다.
이러한 이유로, 로컬에서 Docker를 설치하는 번거로움을 피하고, 별도의 설정 없이 간편하게 사용할 수 있는 Embedded Redis를 사용하기로 하였습니다.
Embedded Redis
Embedded Redis는 Github 설명에 따르면 Java 통합 테스트를 위한 애플리케이션에 내장되어 있는 Redis 임베디드 서버라고 합니다.
https://github.com/ozimov/embedded-redis
GitHub - ozimov/embedded-redis: Redis embedded server
Redis embedded server. Contribute to ozimov/embedded-redis development by creating an account on GitHub.
github.com
Embedded Redis를 사용하면, Redis가 얼마큼의 메모리를 사용하는지 궁금하였고, 확인해 보니 1,228K로 메모리 사용량이 굉장히 적은 것을 확인할 수 있었습니다.
또한, @Profile("test")를 통해 'test' 환경에서만 내장 Redis 서버가 동작하게 하여, 운영 환경에서는 내장 Redis서버가 동작하지 않아 서버 성능에 영향을 주지 않습니다.
@Profile("test")
@Configuration
public class EmbeddedRedisConfig {
구현
build.gradle 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'it.ozimov:embedded-redis:0.7.2'
EmbeddedRedisConfig
@Profile("test")
@Configuration
public class EmbeddedRedisConfig {
@Value("${spring.data.redis.port}")
private int redisPort;
private RedisServer redisServer;
@PostConstruct
public void redisServer() throws IOException {
int port = isRedisRunning()? findAvailablePort() : redisPort;
redisServer = new RedisServer(redisPort);
redisServer.start();
}
@PreDestroy
public void stopRedis() {
if (redisServer != null) {
redisServer.stop();
}
}
private boolean isRedisRunning() throws IOException {
return isRunning(executeNetstatCommand(redisPort));
}
public int findAvailablePort() throws IOException {
for (int port = 10000; port <= 65535; port++) {
Process process = executeNetstatCommand(port);
if (!isRunning(process)) {
return port;
}
}
throw new IllegalArgumentException("Not Found Available port: 10000 ~ 65535");
}
private Process executeNetstatCommand(int port) throws IOException {
String command = String.format("netstat -ano | findstr LISTENING | findstr %d", port);
return Runtime.getRuntime().exec(command);
}
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) {
}
return !StringUtils.isEmpty(pidInfo.toString());
}
}
@Profile("test")를 통해 EmbeddedRedisConfig 클래스가 'test' 환경에서만 동작하도록 설정하였습니다.
그렇기에 운영 환경에서는 동작하지 않습니다. 즉, 내장 Redis 서버가 동작하지 않습니다.
isRedisRunning(), findAvailablePort(), executeNetstatCommand(), isRunning() 메서드의 경우 여러 스프링 테스트 콘텍스트가 실행됨에 따라 발생하는 EmbeddedRedis 포트 충돌을 방지하기 위해 필요합니다.
executeNetstatCommand의 경우는 로컬 PC의 환경을 고려하여 수정해야 합니다.(저는 Window 환경입니다)
EmbeddedRedisConfig 외의 제 Redis 설정은 아래와 같습니다.
RedisConfig
@Configuration
@EnableRedisRepositories
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private String port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(Integer.parseInt(port));
return new LettuceConnectionFactory(redisStandaloneConfiguration);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setDefaultSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
RedisCacheConfig
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public CacheManager rcm(RedisConnectionFactory cf) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofMinutes(3L));
return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(cf)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
RedisUtil
@RequiredArgsConstructor
@Service
public class RedisUtils {
private final RedisTemplate<String, Object> redisTemplate;
public void deleteData(String key) {
redisTemplate.delete(key);
}
public void saveLocationInRedis(Long patient, LocationRequestDto locationRequestDto) {
String latKey = "patient:location:" + patient + ":latitude";
String lonKey = "patient:location:" + patient + ":longitude";
redisTemplate.opsForValue().set(latKey, locationRequestDto.getLatitude());
redisTemplate.opsForValue().set(lonKey, locationRequestDto.getLongitude());
}
public PatientResponseDto.Location getLocationByPatientId(Long patientId) {
String latKey = "patient:location:" + patientId + ":latitude";
String lonKey = "patient:location:" + patientId + ":longitude";
String latitude = (String)redisTemplate.opsForValue().get(latKey);
String longitude = (String)redisTemplate.opsForValue().get(lonKey);
if (latitude == null || longitude == null) {
return null;
}
return PatientResponseDto.Location.builder()
.latitude(latitude)
.longitude(longitude)
.build();
}
}
테스트 코드
@ActiveProfiles("test")
@SpringBootTest
class PatientControllerTest {
@Autowired
private RedisUtils redisUtils;
@DisplayName("환자의 마지막 위치를 Redis에 저장한다.")
@Test
void savePatientLastLocation() {
// given
Long patientId = 1L;
LocationRequestDto locationRequestDto = LocationRequestDto.builder()
.latitude("37.5665")
.longitude("126.987")
.build();
// when
redisUtils.saveLocationInRedis(patientId, locationRequestDto);
// then
PatientResponseDto.Location location = redisUtils.getLocationByPatientId(1L);
assertThat(location.getLatitude()).isEqualTo("37.5665");
assertThat(location.getLongitude()).isEqualTo("126.987");
}
}
결과
트러블 슈팅
LoggerFactory is not a Logback LoggerContext but Logback is on the classpath
해결
it.ozimov:embedded-redis:0.7.3 버전의 경우, org.slf4j를 포함하고 있습니다.
그렇기에 SpringBoot 내장 slf4j와 충돌해서 발생한 에러였습니다.
it.ozimov:embedded-redis:0.7.2로 버전을 낮춰서 문제를 해결했습니다.
또는, 컴파일 빌드 시 slf4j를 제외하여 해결할 수 있습니다.
compile ('it.ozimov:embedded-redis:0.7.3') {
exclude group: "org.slf4j", module: "slf4j-simple"
}
참고자료
[Redis] SpringBoot Data Redis 로컬/통합 테스트 환경 구축하기
안녕하세요? 이번 시간엔 SpringBoot Data Redis 로컬 테스트 예제를 진행해보려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다. (공부한 내용을 정리하는 Git
jojoldu.tistory.com
'Spring' 카테고리의 다른 글
[Spring] 놀멍 서비스 개발 일지 - 지도 화면 개발하기2(공간 인덱스 적용) (0) | 2025.01.07 |
---|---|
[Spring] 놀멍 서비스 개발 일지 - 지도 화면 개발하기1 (0) | 2025.01.05 |
[Spring] URL 이미지 리사이징 후, S3에 업로드 (3) | 2024.07.28 |
[Spring] Google STT(Speech-to-Text) 서비스 사용하기 (2) | 2024.06.30 |
[Spring] Spring Data JPA 페이징 처리 알아보기 (2) | 2023.10.03 |
느리더라도 단단하게 성장하고자 합니다!
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!