이런 이유 중에 멘토님에게 들은 것은 일단 DB를 소중히 다뤄줘야 한다는 이유와 빠르다는 것. 그게 레디스를 쓰게 된 가장 큰 이유이다.
쨋든, 구현해야되는 기능이 지도에서 자신의 위치를 기준으로 주변에 있는 사람을 띄워야 되는 기능인데, 이러려면 애초에 다른 기존 DB와는 다른 DB를 사용했어야 했다. 더불어서 현제 하고 있는 프로젝트에서는 서로 매칭이 되어있는 그러니까 채팅방이 만들어져있고 그 채팅방이 활성화 되어있는 사람들만 탐색기능에서 떠야한다. 그래서 생각한 것은 이거다.
이렇게 4가지 단계를 거쳐가면서, 최종적으로 원하는 값만을 따오려고 했다.
우선 삽질했다.
기존 sql을 쓸 때는 Between을 써서 그 위경도 값에 있는 사람들을 JPA를 써서 가져오려고 했는데 이게 웬결, Redis는 Between을 쓰지 못했다. 그래서 검색 검색을 하다가 RediSQL까지 알게 되었는데, '이게 맞나,,'라는 생각이 들었다. 그래서 결국 나온 방법은 Redis의 'GeoOperation' 이것이다. 멘토님께서 추천을 받아서 검색을 통해서 공부를 해봤는데, 정말 딱 필요하던 기능이었다.
1. 의존성
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
2. GeoOperations<String,String> Bean 등록
@Bean
public GeoOperations<String, String> geoOperations(RedisTemplate<String, String> template){
return template.opsForGeo();
}
이렇게 되면 일단 준비는 완료이다.
3. 값을 넘길 Dto인 LocationDto를 만든다.
@Data
public class LocationDto {
private String name;
private Double lat;
private Double lng;
}
4. Redis에 키값으로 "이름:id" 밸류로 위경도를 넘긴다.
public NameDto add(String email, LocationDto locationDto) {
User findUser = userRepository.findByEmail(email).orElseThrow();
String key = findUser.getNickname() + ":" + findUser.getId();
Point point = new Point(locationDto.getLng(), locationDto.getLat());
geoOperations.add(VENUS_VISITED, point, key);
return new NameDto(findUser.getNickname());
}
5. 컨트롤러를 통해서 서비스를 호출한다.
@PostMapping("/location")
public ResponseEntity<NameDto> addLocation(@RequestBody LocationDto location, HttpServletRequest request) {
String email = jwtUtil.getEmail(request);
NameDto add = userService.add(email, location);
return new ResponseEntity<>(add,HttpStatus.OK);
}
NameDto는 String name만 들어가있는 클라이언트를 위한 Dto다.
이렇게 레디스에 자신의 위경도를 넣어두고
6. 이름을 통해서 레디스에 있는 값을 기준으로 근처 위도에 있는 데이터들을 가져오고 채팅방이 활성화되어있는 유저들만 가져올 수 있는 서비스를 만든다.
public List<ResponseDto> nearByVenues(String email) {
User findUser= userRepository.findByEmail(email).orElseThrow();
String key = findUser.getNickname() + ":" + findUser.getId();
Point myPosition = Objects.requireNonNull(geoOperations.position(VENUS_VISITED, key)).get(0);
List<ResponseDto> dto = new ArrayList<>();
Circle circle = new Circle(new Point(myPosition.getX(), myPosition.getY()), new Distance(3, Metrics.KILOMETERS));
GeoResults<RedisGeoCommands.GeoLocation<String>> res = geoOperations.radius(VENUS_VISITED, circle);
assert res != null;
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = res.getContent();
for (GeoResult<RedisGeoCommands.GeoLocation<String>> c : content) {
ResponseDto responseDto = new ResponseDto();
List<Point> position = geoOperations.position(VENUS_VISITED, c.getContent().getName());
assert position != null;
for (Point point : position) {
responseDto.setLat(point.getX());
responseDto.setLng(point.getY());
responseDto.setName(c.getContent().getName());
dto.add(responseDto);
}
}
List<List<Chatroom>> chatrooms = new ArrayList<>();
chatrooms.add(findUser.getLikeUser());
chatrooms.add(findUser.getLikedUser());
List<Chatroom> chatrooms1 = chatrooms.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
Set<User> userList = new HashSet<>();
List<User> like = chatrooms1.stream()
.filter(Chatroom::getActive)
.map(Chatroom::getLikeUser)
.collect(Collectors.toList());
userList.addAll(like);
List<User> liked = chatrooms1.stream()
.filter(Chatroom::getActive)
.map(Chatroom::getLikedUser)
.collect(Collectors.toList());
userList.addAll(liked);
List<ResponseDto> result = new ArrayList<>();
for (ResponseDto d : dto) {
String[] split = d.getName().split(":");
for (User u : userList) {
if (!findUser.getNickname().equals(split[0])) {
if (u.getNickname().equals(split[0])) {
d.setName(split[0]);
Long id = Long.valueOf(split[1]);
d.setUserId(id);
result.add(d);
}
}
}
}
return result;
}
여기서 알게돼서 신기했던 코드는
Circle circle = new Circle(new Point(myPosition.getX(), myPosition.getY()), new Distance(3, Metrics.KILOMETERS));
이 코드이다. 사실 기존 DB에는 Double을 이용해서 근처의 유저를 JPA를 사용해서 긁어오는 방식을 선택했고 그 과정에서 이제 Between을 썼던 것이다. 근데 레디스를 쓰다보니 이렇게 어떤 위경도를 기점으로 근처 몇 킬로미터안에 있는 위경도를 계산해서 알아서 찾아준다는 것을 알았다. 너무 신기했다. 쨋든 이 과정을 통해서 기준을 정하고
GeoResults<RedisGeoCommands.GeoLocation<String>> res = geoOperations.radius(VENUS_VISITED, circle);
이 코드를 통해서 circle을 그려서 그 안에 있는 유저 정보를 가져온다. 신기했다. 이렇게 가져온 값들을 통해서 값을 담았다. 이 과정에서 애먹었던 부분이 바로 레디스에 저장되어있는 레디스값을 가져와야 한다는 것이다. 근데 공부를 했던 레퍼런스에서는 위경도를 계산된 값으로 나오게 되어있었다. 그러다보니, 위경도 값을 가져오는데 어떻게 해야되나 싶었다. 그래서 다른 자료구조를 써야되나 다른 방식은 없나 검색을 하다가 이 코드를 알게 되었다.
geopos 이걸 통해서 redis-cli에서는 위 경도 값을 계산하지 않고 가져온다는 것을 알게되었다. 그걸 스프링에서 써보니, Position으로 버전이 바뀌었나보다.
List<Point> position = geoOperations.position(VENUS_VISITED, c.getContent().getName());
이런식으로 위경도를 Point 형태로 가져온다. 이걸 몰라서 굉장히 해맸었다..
쨋든 이걸로 값을 담고, 레디스에서 가져온 값을 통해서 채팅방이 활성화 되어있는 유저들과 비교를 통해서 그 유저들의 값만 클라이언트 단에 넘기는 로직을 만들었다.
7. 컨트롤러에 맞춰준다.
@GetMapping("/location/nearby")
public ResponseEntity<List<ResponseDto>> locations(HttpServletRequest request) {
String email = jwtUtil.getEmail(request);
List<ResponseDto> responseDtos = userService.nearByVenues(email);
return ResponseEntity.ok(responseDtos);
}
8. 사용자가 탐색 기능을 쓸 때만 본인과 상대방이 지도에 떠야하므로 삭제 기능도 필요하다.
public void deleteRedisData(LocationDto locationDto){
User findUser = userRepository.findByNickname(locationDto.getName());
String key = findUser.getNickname() + ":" + findUser.getId();
geoOperations.remove(VENUS_VISITED, key);
}
9. 위 서비스도 연결해줄 컨트롤러를 만들어준다.
@DeleteMapping("/location/delete")
public ResponseEntity<?> deleteRedisData(@RequestBody LocationDto locationDto) {
userService.deleteRedisData(locationDto);
return new ResponseEntity<>(HttpStatus.OK);
}
이런식으로 레디스에 있는 값을 삭제하는 api도 만들어두었다.
이렇게해서 우선 기능적인 부분은 해결한 것 같다.
코드도 많이 지저분하고, 또 레디스에 대한 지식이 깊지 않다는 것을 알지만, 현재 공부하고 있는 시점에서 이런 기능을 구현했다는 것부터 기록으로 남겨두고 싶었고, 지금 블로그에 정리하지 않으면 지금처럼 생생하게 뭐가 어려웠고 해맸는지 쓰기 어려울 것 같아서 우선 썼다.
앞으로 레디스에 대해서 더 공부를 해봐야 될 것 같다. 아래는 제일 도움을 많이 받은 레퍼런스들이다.
참고자료
https://wonyong-jang.github.io/bigdata/2021/05/12/BigData-Redis-Geospatial.html
[Redis] Geospatial 자료구조 - SW Developer
이번 글에서는 redis에서 geospatial data를 저장하고 위치 정보를 활용하는 방법에 대해서 살펴볼 예정이다. geospatial 자료구조를 이해하기 위해서는 redis 개념과 sorted set을 이해하고 있어야 한다. Geo
wonyong-jang.github.io
https://egkatzioura.com/2022/11/15/use-redis-geohash-with-spring-boot/
Use Redis GeoHash with Spring boot
One very handy Data Structure when it comes to Redis is the GeoHash Data structure. Essentially it is a sorted set that generates a score based on the longitude and latitude. We will spin up a Redi…
egkatzioura.com