사내에서 Springboot를 사용하면서 Reactive Redis를 쓰고 있습니다.
이번 포스트에서는 config는 어떻게 설정하는지, 테스트 코드 작성방법, Generic하게 클래스에 맵핑해서 꺼낼 수 있는방법에 대해 정리해보고자 합니다.
Dependency
springboot 3.2.2, java17을 사용하고 있으며, dependency는 아래와 같습니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
testImplementation "org.junit.jupiter:junit-jupiter:5.8.1"
//test-container
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:testcontainers:1.19.4'
}
Configuration
Redis에 연결하고 관련 서비스를 만들기 위해서, redis.core의 ReativeRedisOperation이 필요합니다.
아래 Config는 해당 Bean을 등록해주는 과정이라고 생각해주시면 됩니다.
@Configuration
@EnableAutoConfiguration(exclude={RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class})
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public ReactiveRedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public ReactiveRedisOperations<String, Object> redisTemplate() {
ReactiveRedisConnectionFactory rrcf = redisConnectionFactory();
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
RedisSerializationContext.RedisSerializationContextBuilder<String, Object> builder = RedisSerializationContext
.newSerializationContext(new StringRedisSerializer());
RedisSerializationContext<String, Object> context = builder.value(serializer).hashValue(serializer)
.hashKey(serializer).build();
return new ReactiveRedisTemplate<>(rrcf, context);
}
}
- @EnableAutoConfiguration(exclude={...})
내가 지정한 host, port, serializer를 등록하기 위한 부분입니다. 이 부분이 없으면 아래와 같은 에러를 보게 될 것입니다.
The bean 'redisConnectionFactory', defined in class path resource [org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.class], could not be registered. A bean with that name has already been defined in class path resource [com/example/demo/RedisConfig.class] and overriding is disabled.
- Jackson2JsonRedisSerializer<Object> serializer
JSON 형식의 데이터를 Redis에 저장하기 위한 직렬화 및 역직렬화를 담당하는 Jackson2JsonRedisSerializer를 생성합니다. 이 때, Object.class를 전달하여 어떠한 객체 타입이라도 처리할 수 있도록 합니다. 이후 데이터를 가져올 때, 알맞은 Dto와 맵핑하는 부분은 아래 RedisService에서 구현하겠습니다.
그리고 application.yml에 아래처럼 세팅해주었습니다.
spring:
data:
redis:
host: localhost
password:
port: 6379
redis test - TestContainer
1. 테스트 컨테이너 생성
레디스를 테스트하는 것에는 embeded redis 혹은 testContainer를 이용하는 2가지 방법이 있습니다.
저는 아래와 같은 이유로 testContainer를 이용하기로 결정하였습니다. 아래코드는 테스트컨테이너 홈페이지를 참조했습니다.
- git clone만 받으면 따로 local에 redis설치 없이도 테스트를 돌릴 수 있다.
- 테스트의 독립성을 보장할 수 있다.
- 포트 충돌을 고려할 필요 없다.(참고링크)
우선, 아래와 같이 Redis Test Container를 생성해줍니다. 아래 클래스는 BeforeAllCallback의 구현 클래스입니다.
테스트 코드 내에서 @BeforeAll안에서 사용하지 않고 이렇게 빼는 이유는 아래에서 설명하겠습니다.
package com.example.demo;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
public class RedisContainer implements BeforeAllCallback{
private static final String REDIS_DOCKER_IMAGE = "redis:5.0.3-alpine";
@Override
public void beforeAll(ExtensionContext context) throws Exception {
GenericContainer<?> REDIS_CONTAINER = new GenericContainer<>(DockerImageName.parse(REDIS_DOCKER_IMAGE))
.withExposedPorts(6379).withReuse(true);
REDIS_CONTAINER.start();
System.setProperty("spring.data.redis.host", REDIS_CONTAINER.getHost());
System.setProperty("spring.data.redis.port", REDIS_CONTAINER.getMappedPort(6379).toString());
}
}
위 코드가 의미하는 부분은 간단합니다.
가장 먼저 "redis:5.0.3-alpine" 라는 이미지를 사용하여 REDIS_CONTAINER 내부에서 redis default port인 6379를 expose하도록 세팅합니다.
그리고 실제로 해당 컨테이너를 띄운 후, 해당 컨테이너의 호스트와 컨테이너 내부 포트 6379와 매핑되는 외부포트를 받아와 SystemProperty에 세팅해줍니다.
2. 테스트코드 작성
테스트 코드를 작성해보겠습니다.
여기에서 주의하여 볼 점은 @BeforeAll 메소드를 사용하는 것이 아니라, RedisContainer를 따로 뺀 뒤, ExtendWith를 통해 CoffeeTest클래스 생성전에 레디스 컨테이너연결을 하도록 했다는 점입니다.
package com.example.demo;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.testcontainers.junit.jupiter.Testcontainers;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@ExtendWith(RedisContainer.class)
@Testcontainers
@Import(RedisService.class)
@SpringBootTest
public class CoffeeTest {
@Autowired
private RedisService redisService;
@Test
void testCoffee() {
String TESTKEY = "TEST_KEY";
String TESTVALUE = "TEST_VALUE";
redisService.setValue(TESTKEY, TESTVALUE).subscribe();
Mono<String> storedValue = redisService.getCacheValueGeneric(TESTKEY, String.class);
StepVerifier.create(storedValue)
.assertNext(value -> assertThat(value).isEqualTo(TESTVALUE))
.verifyComplete();
}
}
위 테스트를 실행시키면, 아래처럼 testContainer가 실제로 뜨는 것을 확인 할 수 있습니다.
redis 서비스 - Generic redis service
이번에는 redis service를 만들어 보겠습니다. 처음 말씀드린 것처럼 Object로 직렬화 시켜 저장시킨 데이터를 다시 역직렬화 시키면서 원하는 DTO에 매핑하는 부분을 ObjectMapper와 Generic 함수를 이용해 getCacheValueGeneric를 구현해 보겠습니다.
package com.example.demo;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono;
import lombok.RequiredArgsConstructor;
@Component
@RequiredArgsConstructor
public class RedisService {
private final ReactiveRedisOperations<String, Object> redisOps;
private final ObjectMapper objectMapper;
public Mono<String> getValue(String key) {
return redisOps.opsForValue().get(key).map(String::valueOf);
}
public Mono<Boolean> setValue(String key, Object value) {
return redisOps.opsForValue().set(key, value);
}
public <T> Mono<T> getCacheValueGeneric(String key, Class<T> clazz) {
try {
return redisOps.opsForValue().get(key)
.switchIfEmpty(Mono.error(new RuntimeException("No Datas for key: " + key)))
.flatMap(value -> Mono.just(objectMapper.convertValue(value, clazz)));
} catch (Exception e) {
e.getStackTrace();
return Mono.error(new RuntimeException("error occured!" + e.getMessage()));
}
}
}
실제코드와 연동
만약, 위 서비스 코드를 test-container가 아닌, 실제 redis와 연결해 이용하고 싶다면 redis를 설치 후 redis-server를띄운 뒤, 해당 서비스와 매핑되는 controller 코드를 구현하여 실행시키면 될 것입니다.
1. redis 설치
brew install redis
2. redis-server 실행
redis-server
3. Controller 구현
간단하게 만든 controller코드는 아래와 같습니다.
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Mono;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
@RestController
@RequiredArgsConstructor
public class CoffeeController {
private final RedisService redisService;
@GetMapping("/coffees")
public Mono<Coffee> getCoffee(@RequestParam("id") String coffeeId) {
return redisService.getCacheValueGeneric(coffeeId, Coffee.class);
}
@PostMapping("/coffee")
public Mono<Boolean> postMethodName(@RequestBody Coffee coffee) {
return redisService.setValue(coffee.getId(), coffee);
}
}
'java,springboot' 카테고리의 다른 글
[Java] Java에서 JSON 다루기(mapper, converter) (0) | 2024.04.04 |
---|---|
[성능개선]KMP알고리즘 활용하여 50% 성능개선 해보기 (1) | 2024.03.11 |
try-with-resources 사용법 및 주의점 (1) | 2023.12.05 |
브라우저에서 RTSP프로토콜 스트리밍 하기 (1) | 2023.10.30 |
[R2DBC] Batch Insert 성능 테스트 및 개선 (17배 향상) (0) | 2023.09.05 |