이번 포스팅에서는 Java에서 Spring Data Redis를 이용하여 Redis와 통신하는 방법에 대해서 알아보자.
Redis에 대한 개념은 이전글를 참고하자.
Java의 Redis Client는 크게 두 가지가 있다.
Jedis와 Lettuce이며, 원래 Jedis를 많이 사용했으나
Lettuce와 비교했을 때 TPS/CPU/응답속도 등 모두 Lettuce가 월등히
성능이 좋기 때문에 추세가 넘어가고 있었다.
그러다 결국 Spring boot 2.0 부터 Jedis가 기본 클라이언트에서 deprecated 되고 Lettuce가 탑재되었다.
더 자세한 내용은 Spring Session에서 Jedis 대신 Lettuce를 사용하는 이유를 참고하자.
Spring Boot에서 Redis를 사용하는 방법은 RedisRepository와 RedisTemplate 두 가지가 있다.
그전에 아래와 같이 설정이 필요하다.
build.gradle에 아래 라이브러리를 추가한다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
또한, application.yml에 host와 port를 설정한다.
spring:
redis:
host: localhost
port: 6379
마지막으로 Configuration에서 Bean에 등록해준다.
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
}
Spring Data Redis의 Redis Repository를 이용하면 간단하게 Domain Entity를
Redis Hash로 만들 수 있다.
다만 트랜잭션을 지원하지 않기 때문에 만약 트랜잭션을 적용하고
싶다면 RedisTemplate을 사용해야 한다.
@Getter
@RedisHash(value = "people", timeToLive = 30)
public class Person {
@Id
private String id;
private String name;
private Integer age;
private LocalDateTime createdAt;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
this.createdAt = LocalDateTime.now();
}
}
@RedisHash
를 붙이면 된다.
keyspace와 합쳐져서 레디스에 저장된 최종 키 값은 keyspace:id가 된다.
public interface PersonRedisRepository extends CrudRepository<Person, String> {
}
@SpringBootTest
public class RedisRepositoryTest {
@Autowired
private PersonRedisRepository repo;
@Test
void test() {
Person person = new Person("Park", 20);
// 저장
repo.save(person);
// `keyspace:id` 값을 가져옴
repo.findById(person.getId());
// Person Entity 의 @RedisHash 에 정의되어 있는 keyspace (people) 에 속한 키의 갯수를 구함
repo.count();
// 삭제
repo.delete(person);
}
}
@Indexed 어노테이션을 사용해서 id값 외에 다른 필드로 조회할 수 있도록 SecondIndex를 지원한다.
@Getter
@RedisHash(value = "people", timeToLive = 30)
public class Person {
@Id
private String id;
@Indexed // 필드 값으로 데이터 찾을 수 있게 하는 어노테이션
private String name;
...
}
public interface PersonRedisRepository extends CrudRepository<Person, String> {
Optional<Person> findByName(String name);
}
redisTemplate에는 redis가 제공하는 list, set, sorted set, hash... 와 같은
다양한 command를 지원하기 위한 opsFor* method가 있다.
사용하고자 하는 redis command에 대응되는 method를 호출하여 사용하면 된다.
해당 method를 호출하면 각 redis command에 대응된 operation 객체가 반환된다.
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
@SpringBootTest
class RedisControllerTest {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Test
void testHash() {
// given
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
String key = "stringKey";
// when
valueOperations.set(key, "hello");
// then
String value = valueOperations.get(key);
assertThat(value).isEqualTo("hello");
}
}
@Test
void testSet() {
// given
SetOperations<String, String> setOperations = redisTemplate.opsForSet();
String key = "setKey";
// when
setOperations.add(key, "h", "e", "l", "l", "o");
// then
Set<String> members = setOperations.members(key);
Long size = setOperations.size(key);
assertThat(members).containsOnly("h", "e", "l", "o");
assertThat(size).isEqualTo(4);
}
@Test
void testHash() {
// given
HashOperations<String, Object, Object> hashOperations = redisTemplate.opsForHash();
String key = "hashKey";
// when
hashOperations.put(key, "hello", "world");
// then
Object value = hashOperations.get(key, "hello");
assertThat(value).isEqualTo("world");
Map<Object, Object> entries = hashOperations.entries(key);
assertThat(entries.keySet()).containsExactly("hello");
assertThat(entries.values()).containsExactly("world");
Long size = hashOperations.size(key);
assertThat(size).isEqualTo(entries.size());
}
@Test
void testGeo() {
GeoOperations<String, String> geoOperations = redisTemplate.opsForGeo();
String key = "geopoints";
Point point1 = new Point(127.0753893256187439, 37.62959205066435686);
Point point2 = new Point(127.07614034414291382, 37.62974666865508055);
geoOperations.add(key, point1, "Union Coffee");
geoOperations.add(key, point2, "CU");
Point point = new Point(127, 38);
Metric metric = RedisGeoCommands.DistanceUnit.KILOMETERS;
Distance distance = new Distance(200, metric);
Circle circle = new Circle(point, distance);
// 경도, 위도(127, 38) 기준으로 반경 200 km를 찾고,
// 가장 가까운 순서로 5개 위치정보 추출하기
// 거리와 좌표정보 같이 출력
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands
.GeoRadiusCommandArgs
.newGeoRadiusArgs()
.includeDistance()
.includeCoordinates()
.sortAscending()
.limit(5);
GeoResults<RedisGeoCommands.GeoLocation<String>> radius = geoOperations
.radius(key, circle, args);
if (radius != null) {
radius.forEach(geoLocationGeoResult -> {
RedisGeoCommands.GeoLocation<String> content = geoLocationGeoResult.getContent();
//member name such as tianjin
String name = content.getName();
// Corresponding latitude and longitude coordinates
Point pos = content.getPoint();
// Distance from the center point
Distance dis = geoLocationGeoResult.getDistance();
System.out.println(name);
System.out.println(pos);
System.out.println(dis);
});
}
// CU에 대한 좌표 정보 삭제
//geoOperations.remove(key, "CU");
}
위의 코드를 살펴보면, opsForGeo()를 이용하여 Geospatial 자료구조를 만들었다.
해당 자료구조에 CU, Union Coffee의 좌표 정보를 추가했고,
이제 좌표정보(127, 38)을 기준으로 반경 200 km 에 들어있는 가장 가까운
좌표 정보를 5개 찾는다.
또한, 찾은 데이터의 거리와 좌표정보도 같이 출력하게 된다.
Reference
https://bcp0109.tistory.com/328
https://sabarada.tistory.com/105