Skip to content

Commit

Permalink
refactor: 틈틈 위치 기반 로직 수정 및 테스트 (#139)
Browse files Browse the repository at this point in the history
* refactor: redis 관련 클래스 리팩토링 (#124)

* feat: 요청에 따른 100 m 이내 6명 조회 로직 구현 (#124)

* refactor: 위치 기반 DTO, VO 관련 리팩토링 (#124)

* refactor: 위치 기반 DTO, VO 관련 리팩토링 (#124)

* test: RedisRepository 리팩토링 및 추가 구현 (#124)

* test: 위치 기반 관련 테스트 수정 (#124)

* fix: sonarCloud 에러 수정 (#124)
  • Loading branch information
choidongkuen authored Jan 22, 2024
1 parent 05dcd4c commit a7bf5b0
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 152 deletions.
7 changes: 3 additions & 4 deletions src/main/java/net/teumteum/core/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ public RedisConnectionFactory redisConnectionFactory() {
}

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
redisTemplate.setDefaultSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
50 changes: 46 additions & 4 deletions src/main/java/net/teumteum/core/security/service/RedisService.java
Original file line number Diff line number Diff line change
@@ -1,30 +1,72 @@
package net.teumteum.core.security.service;

import static java.util.Objects.requireNonNull;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import java.io.IOException;
import java.time.Duration;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import net.teumteum.teum_teum.domain.UserLocation;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class RedisService {


private static final String HASH_KEY = "userLocation";
private final RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper;
private ValueOperations<String, String> valueOperations;

@PostConstruct
void init() {
valueOperations = redisTemplate.opsForValue();
}

public String getData(String key) {
return redisTemplate.opsForValue().get(key);
return valueOperations.get(key);
}

public void setData(String key, String value) {
redisTemplate.opsForValue().set(key, value);
valueOperations.set(key, value);
}

public void setDataWithExpiration(String key, String value, Long duration) {
Duration expireDuration = Duration.ofSeconds(duration);
redisTemplate.opsForValue().set(key, value, expireDuration);
valueOperations.set(key, value, expireDuration);
}

public void deleteData(String key) {
redisTemplate.delete(key);
valueOperations.getOperations().delete(key);
}

public void setUserLocation(UserLocation userLocation, Long duration) {
String key = HASH_KEY + userLocation.id();
String value;
try {
value = objectMapper.writeValueAsString(userLocation);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
valueOperations.set(key, value, duration);
}

public Set<UserLocation> getAllUserLocations() {
Set<String> keys = redisTemplate.keys(HASH_KEY + ":*");
return requireNonNull(keys).stream().map(key -> {
String value = valueOperations.get(key);
try {
return objectMapper.readValue(value, UserLocation.class);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}).collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,19 @@ public class TeumTeumController {
@ResponseStatus(HttpStatus.OK)
public UserAroundLocationsResponse getUserAroundLocations(
@Valid @RequestBody UserLocationRequest request) {
return teumTeumService.processingUserAroundLocations(request);
return teumTeumService.saveAndGetUserAroundLocations(request);
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorResponse handleMethodArgumentNotValidException(
MethodArgumentNotValidException methodArgumentNotValidException) {
Sentry.captureException(methodArgumentNotValidException);

BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();
List<ObjectError> errors = bindingResult.getAllErrors();

return ErrorResponse.of(errors.get(0).getDefaultMessage());
@ExceptionHandler({IllegalArgumentException.class, MethodArgumentNotValidException.class})
public ErrorResponse handleException(Exception exception) {
Sentry.captureException(exception);
if (exception instanceof MethodArgumentNotValidException methodArgumentNotValidException) {
BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();
List<ObjectError> errors = bindingResult.getAllErrors();
return ErrorResponse.of(errors.get(0).getDefaultMessage());
} else {
return ErrorResponse.of(exception);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package net.teumteum.teum_teum.domain;


public record UserData(
public record UserLocation(
Long id,
Double latitude,
Double longitude,
String name,
String jobDetailClass,
Long characterId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import net.teumteum.teum_teum.domain.UserData;
import net.teumteum.teum_teum.domain.UserLocation;

public record UserLocationRequest(
@NotNull(message = "경도는 필수 입력값입니다.")
Double longitude,
@NotNull(message = "위도는 필수 입력값입니다.")
Double latitude,
@NotNull(message = "id 는 필수 입력값입니다.")
Long id,
@NotNull(message = "위도는 필수 입력값입니다.")
Double latitude,
@NotNull(message = "경도는 필수 입력값입니다.")
Double longitude,
@NotBlank(message = "이름은 필수 입력값입니다.")
String name,
@NotBlank(message = "직무는 필수 입력값입니다.")
Expand All @@ -19,7 +19,7 @@ public record UserLocationRequest(
Long characterId
) {

public UserData toUserData() {
return new UserData(id, name, jobDetailClass, characterId);
public UserLocation toUserLocation() {
return new UserLocation(id, latitude, longitude, name, jobDetailClass, characterId);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package net.teumteum.teum_teum.domain.response;

import java.util.List;
import net.teumteum.teum_teum.domain.UserData;
import net.teumteum.teum_teum.domain.UserLocation;

public record UserAroundLocationsResponse(
List<UserAroundLocationResponse> userLocations
List<UserAroundLocationResponse> aroundUserLocations
) {

public static UserAroundLocationsResponse of(List<UserData> userData) {
public static UserAroundLocationsResponse of(List<UserLocation> userData) {
return new UserAroundLocationsResponse(
userData.stream()
.map(UserAroundLocationResponse::of)
Expand All @@ -23,13 +23,13 @@ public record UserAroundLocationResponse(
) {

public static UserAroundLocationResponse of(
UserData userData
UserLocation userLocation
) {
return new UserAroundLocationResponse(
userData.id(),
userData.name(),
userData.jobDetailClass(),
userData.characterId()
userLocation.id(),
userLocation.name(),
userLocation.jobDetailClass(),
userLocation.characterId()
);
}
}
Expand Down
110 changes: 33 additions & 77 deletions src/main/java/net/teumteum/teum_teum/service/TeumTeumService.java
Original file line number Diff line number Diff line change
@@ -1,103 +1,59 @@
package net.teumteum.teum_teum.service;

import static java.lang.System.currentTimeMillis;
import static java.time.Duration.ofMinutes;
import static java.lang.Math.atan2;
import static java.lang.Math.sin;
import static java.lang.Math.sqrt;
import static java.lang.Math.toRadians;
import static java.util.Comparator.comparingDouble;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.teumteum.teum_teum.domain.UserData;
import net.teumteum.core.security.service.RedisService;
import net.teumteum.teum_teum.domain.UserLocation;
import net.teumteum.teum_teum.domain.request.UserLocationRequest;
import net.teumteum.teum_teum.domain.response.UserAroundLocationsResponse;
import net.teumteum.teum_teum.domain.response.UserAroundLocationsResponse.UserAroundLocationResponse;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
import org.springframework.data.redis.core.GeoOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.domain.geo.Metrics;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class TeumTeumService {

private static final String KEY = "userLocation";
private static final int SEARCH_LIMIT = 6;
private static final Duration LOCATION_EXPIRATION = ofMinutes(1);

private final ObjectMapper objectMapper;
private final RedisService redisService;

private final RedisTemplate<String, Object> redisTemplate;

public UserAroundLocationsResponse processingUserAroundLocations(UserLocationRequest request) {
GeoOperations<String, Object> geoValueOperations = redisTemplate.opsForGeo();

String userDataJson = null;
try {
userDataJson = objectMapper.writeValueAsString(
request.toUserData()) + ":" + currentTimeMillis();
} catch (JsonProcessingException e) {
log.error("JsonProcessingException Occurred!");
}

geoValueOperations.add(KEY, new Point(request.longitude(), request.latitude()), userDataJson);

return getUserAroundLocations(geoValueOperations, request.longitude(), request.latitude());
public UserAroundLocationsResponse saveAndGetUserAroundLocations(UserLocationRequest request) {
redisService.setUserLocation(request.toUserLocation(), 60L);
return getUserAroundLocations(request);
}

private UserAroundLocationsResponse getUserAroundLocations(GeoOperations<String, Object> geoValueOperations,
Double longitude, Double latitude) {
private UserAroundLocationsResponse getUserAroundLocations(UserLocationRequest request) {
Set<UserLocation> allUserLocations = redisService.getAllUserLocations();

GeoResults<GeoLocation<Object>> geoResults
= geoValueOperations.radius(KEY,
new Circle(new Point(longitude, latitude), new Distance(100, Metrics.METERS)));
List<UserLocation> aroundUserLocations = allUserLocations.stream()
.filter(userLocation -> !userLocation.id().equals(request.id()))
.filter(userLocation -> calculateDistance(request.latitude(), request.longitude(),
userLocation.latitude(), userLocation.longitude()) <= 100)
.sorted(comparingDouble(userLocation
-> calculateDistance(request.latitude(), request.longitude(),
userLocation.latitude(), userLocation.longitude()))
).limit(SEARCH_LIMIT)
.toList();

return getUserAroundLocationsResponse(geoResults);
return UserAroundLocationsResponse.of(aroundUserLocations);
}

private UserAroundLocationsResponse getUserAroundLocationsResponse(GeoResults<GeoLocation<Object>> geoResults) {

List<UserAroundLocationResponse> userAroundLocationResponses = new ArrayList<>();

long currentTime = currentTimeMillis();
int count = 0;

for (GeoResult<GeoLocation<Object>> geoResult : Objects.requireNonNull(geoResults)) {
String userSavedTime = String.valueOf(geoResult.getContent().getName()).split(":")[5];
long timestamp = Long.parseLong(userSavedTime);

if (currentTime - timestamp < LOCATION_EXPIRATION.toMillis()) {
String savedUserLocation = String.valueOf(geoResult.getContent().getName());
String userDataJson = savedUserLocation.substring(savedUserLocation.lastIndexOf(":") + 1);

UserData userData = null;
try {
userData = objectMapper.readValue(userDataJson, UserData.class);
} catch (JsonProcessingException e) {
log.error("JsonProcessingException Occurred!");
}

UserAroundLocationResponse userAroundLocationResponse
= UserAroundLocationResponse.of(Objects.requireNonNull(userData));

userAroundLocationResponses.add(userAroundLocationResponse);
count++;

if (count >= SEARCH_LIMIT) {
break;
}
}
}
return new UserAroundLocationsResponse(userAroundLocationResponses);
private double calculateDistance(double latitude1, double longitude1, double latitude2, double longitude2) {
final int earthRadius = 6371;
double latDistance = toRadians(latitude2 - latitude1);
double lonDistance = toRadians(longitude2 - longitude1);
double a = sin(latDistance / 2) * sin(latDistance / 2)
+ Math.cos(toRadians(latitude1)) * Math.cos(toRadians(latitude2))
* sin(lonDistance / 2) * sin(lonDistance / 2);
double c = 2 * atan2(sqrt(a), sqrt(1 - a));
return earthRadius * c * 1000;
}
}
Loading

0 comments on commit a7bf5b0

Please sign in to comment.