Skip to content

[Refactor] 보행등 코드 커버리지 높이기 #229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package org.programmers.signalbuddyfinal.domain.trafficSignal.dto;

import com.google.auto.value.extension.serializable.SerializableAutoValue;
import lombok.*;
import org.locationtech.jts.geom.Point;
import org.programmers.signalbuddyfinal.domain.trafficSignal.entity.TrafficSignal;
import org.programmers.signalbuddyfinal.global.util.PointUtils;

@Getter
@Builder
@SerializableAutoValue
@AllArgsConstructor
@NoArgsConstructor
public class TrafficResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.programmers.signalbuddyfinal.domain.trafficSignal.dto.TrafficResponse;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
Expand All @@ -20,11 +19,12 @@

import java.time.Duration;

@Slf4j
@Repository
public class TrafficRedisRepository {

private final RedisTemplate<Object, Object> redisTemplate;
private final HashOperations<Object, Object, Map<String,String>> hashOperations;
private final HashOperations<Object, Object, TrafficResponse> hashOperations;
private final GeoOperations<Object,Object> geoOperations;

private static final String KEY_HASH = "traffic:info";
Expand All @@ -37,89 +37,100 @@ public TrafficRedisRepository(RedisTemplate<Object,Object> redisTemplate){
this.geoOperations = redisTemplate.opsForGeo();
}

public boolean isExist(){
return hashOperations.hasKey(KEY_HASH, KEY_GEO);
}

public void save(TrafficResponse trafficResponse) {
Long trafficId = trafficResponse.getTrafficSignalId();

Long trafficId = trafficResponse.getTrafficSignalId();
// GEO 데이터 저장
redisTemplate.opsForGeo().add(
KEY_GEO,
new Point(trafficResponse.getLng(),trafficResponse.getLat()),
trafficId.toString()
);

// GEO 데이터 저장
redisTemplate.opsForGeo().add(
KEY_GEO,
new Point(trafficResponse.getLng(),trafficResponse.getLat()),
trafficId.toString()
);
// HASH 데이터 저장
hashOperations.put(KEY_HASH, trafficId.toString(), trafficResponse);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redis에 Hash 구조로 저장된 데이터를 삭제할 때, 관련 데이터가 전부 삭제되는 것을 확인하셨을까요?

그리고 ValueOperations 를 사용하시면 객체가 통째로 직렬화가 되어 저장됩니다. 역직렬화도 마찬가지로 가능합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

삭제할 때가 TTL이 모두 소진됐을 때를 말씀하시는거 맞죠? 그럼 삭제는 잘 됩니다!
ValueOperations 그래서 쓰는거군여..! 저도 그럼 코드 변경해볼게요! 감사합니다 ㅎㅎ


// HASH 데이터 저장
Map<String, String> trafficData = new HashMap<>();
trafficData.put("serialNumber", String.valueOf(trafficResponse.getSerialNumber()));
trafficData.put("district", trafficResponse.getDistrict());
trafficData.put("signalType", trafficResponse.getSignalType());
trafficData.put("address", trafficResponse.getAddress());
// GEO와 HASH 모두에 TTL 설정
redisTemplate.expire(KEY_GEO, TTL);
redisTemplate.expire(KEY_HASH, TTL);

hashOperations.put(KEY_HASH, trafficId.toString(), trafficData);
}

// GEO와 HASH 모두에 TTL 설정
redisTemplate.expire(KEY_GEO, TTL);
redisTemplate.expire(KEY_HASH, TTL);
public List<TrafficResponse> findNearbyTraffics(double lat, double lng, double kiloRadius) {

}
log.debug("redis 캐싱 데이터 검색 - lat = {}, lng = {}, kiloRadius = {}", lat, lng, kiloRadius);

public List<TrafficResponse> findNearbyTraffics(double lat, double lng, double radius) {
List<GeoResult<GeoLocation<Object>>> geoResults;

List<GeoResult<GeoLocation<Object>>> geoResults;
// 반경 내 GEO 데이터 조회
if (geoOperations != null) {
GeoResults<GeoLocation<Object>> geoResult = geoOperations.radius(
KEY_GEO,
new Circle(new Point(lng, lat), new Distance(radius, Metrics.KILOMETERS))
);
geoResults = (geoResult != null) ? geoResult.getContent() : List.of();
} else {
return List.of();
}
log.info("redis kiloRadius 내 GEO 데이터 조회 - kiloRadius = {}", kiloRadius);
if (geoOperations != null) {
GeoResults<GeoLocation<Object>> geoResult = geoOperations.radius(
KEY_GEO,
new Circle(new Point(lng, lat), new Distance(kiloRadius, Metrics.KILOMETERS))
);

if (geoResults.isEmpty()) {
return Collections.emptyList();
}
geoResults = (geoResult != null) ? geoResult.getContent() : List.of();
} else {
log.info("redis 내부에 데이터 없음");
return List.of();
}

if (geoResults.isEmpty()) {
log.info("redis 내부에 데이터 없음");
return Collections.emptyList();
}

List<TrafficResponse> trafficResponses = new ArrayList<>();

for (GeoResult<GeoLocation<Object>> result : geoResults) {
String trafficId = result.getContent().getName().toString(); // GEO에서 가져온 ID
List<TrafficResponse> trafficResponses = new ArrayList<>();

TrafficResponse response = findById(Long.valueOf(trafficId));
log.info("redis GEO 데이터 검색 성공");
for (GeoResult<GeoLocation<Object>> result : geoResults) {
String trafficId = result.getContent().getName().toString();

trafficResponses.add(response);
}
TrafficResponse response = findById(Long.valueOf(trafficId));

return trafficResponses;
trafficResponses.add(response);
}

return trafficResponses;
}


public TrafficResponse findById(Long id) {

log.debug("redis 캐싱 데이터 id로 검색 - id = {}", id);

String trafficId = String.valueOf(id);

Map<String, String> data = hashOperations.get(KEY_HASH, trafficId);
TrafficResponse data = hashOperations.get(KEY_HASH, trafficId);

if (data == null) {
log.info("redis에 데이터 없음");
return null;
}

List<Point> positions = geoOperations.position(KEY_GEO, trafficId);

if (positions == null || positions.isEmpty()) {
log.info("redis에 데이터 없음");
return null;
}

log.info("redis data 검색 성공");
Point point = positions.get(0);
double savedLat = point.getY(); // 위도
double savedLng = point.getX(); // 경도

return TrafficResponse.builder()
.trafficSignalId(id)
.serialNumber(Long.valueOf(data.get("serialNumber")))
.district(data.get("district"))
.signalType(data.get("signalType"))
.address(data.get("address"))
.serialNumber(data.getSerialNumber())
.district(data.getDistrict())
.signalType(data.getSignalType())
.address(data.getAddress())
.lat(savedLat)
.lng(savedLng)
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.programmers.signalbuddyfinal.domain.trafficSignal.repository;

import org.programmers.signalbuddyfinal.domain.trafficSignal.dto.TrafficResponse;
import org.programmers.signalbuddyfinal.domain.trafficSignal.entity.TrafficSignal;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,24 @@ public class TrafficCsvService {
private final NamedParameterJdbcTemplate namedJdbcTemplate;

@Transactional
public void saveCsvData(String fileName) throws IOException {
public void saveCsvData(String fileName) {

log.debug("보행등 파일 데이터 저장 - fileName = {}", fileName);

WKBWriter wkbWriter = new WKBWriter();
String sql = "INSERT INTO traffic_signals(serial_number, district, signal_type, address, coordinate) VALUES (:serialNumber, :district, :signalType, :address, ST_GeomFromWKB(:coordinate))";
String sql = "INSERT INTO traffic_signals(serial_number, district, signal_type, address, coordinate) "
+ "VALUES (:serialNumber, :district, :signalType, :address, ST_GeomFromWKB(:coordinate))";
/*
WKBWriter 처리
WKB (Well-Known Binary) 는 공간 데이터를 이진 형식으로 표현하는 표준
-> JPA + Spatial로 DB 저장시 binary형식으로 저장되는데 WKB형식과 동일
-> 좌표값을 WKB형식으로 변환해서 저장이 필요
*/

try {

if (!isValidFileName(fileName)) {
log.error("SecurityException - fileName = {}", fileName);
throw new SecurityException("경로 탐색 시도 감지됨");
}

Expand All @@ -50,45 +60,49 @@ public void saveCsvData(String fileName) throws IOException {

List<TrafficSignal> entityList = new ArrayList<>();

log.info("csvtoBean-opencsv (csv파일 객체화)");
CsvToBean<TrafficFileResponse> csvToBean = new CsvToBeanBuilder<TrafficFileResponse>(reader)
.withType(TrafficFileResponse.class)
.withIgnoreLeadingWhiteSpace(true)
.build();

List<TrafficFileResponse> traffics = csvToBean.parse();

log.info("DTO -> Entity로 데이터 변환");
for (TrafficFileResponse trafficRes : traffics) {
entityList.add(new TrafficSignal(trafficRes));
}

//Bulk Insert
if(!entityList.isEmpty()) {
int batchSize = 1000; // 배치 크기
for (int i = 0; i < entityList.size(); i += batchSize) {
List<TrafficSignal> batch = entityList.subList(i, Math.min(i+batchSize, entityList.size()));

System.out.println("데이터 값 : " + Math.min(i+batchSize, entityList.size()));

MapSqlParameterSource[] batchParams = batch.stream()
.map(entity -> new MapSqlParameterSource()
.addValue("serialNumber", entity.getSerialNumber())
.addValue("district", entity.getDistrict())
.addValue("signalType", entity.getSignalType())
.addValue("address", entity.getAddress())
.addValue("coordinate", wkbWriter.write(entity.getCoordinate())))
.toArray(MapSqlParameterSource[]::new);

namedJdbcTemplate.batchUpdate(sql, batchParams);
int batchSize = 1000; // 배치 크기
for (int i = 0; i < entityList.size(); i += batchSize) {
List<TrafficSignal> batch = entityList.subList(i, Math.min(i+batchSize, entityList.size()));
log.info("배치 범위 - i = {}, i+ batchSize = {}", i, batchSize+i);

MapSqlParameterSource[] batchParams = batch.stream()
.map(entity -> new MapSqlParameterSource()
.addValue("serialNumber", entity.getSerialNumber())
.addValue("district", entity.getDistrict())
.addValue("signalType", entity.getSignalType())
.addValue("address", entity.getAddress())
.addValue("coordinate", wkbWriter.write(entity.getCoordinate())))
.toArray(MapSqlParameterSource[]::new);

namedJdbcTemplate.batchUpdate(sql, batchParams);
}

log.info("csv파일 데이터 저장 완료");
}
}
} catch (FileNotFoundException e){
log.error("File Not Found : {}", e.getMessage(), e);
log.error("File Not Found : {}", e.getMessage());
throw new BusinessException(TrafficErrorCode.FILE_NOT_FOUND);
} catch (DataIntegrityViolationException e) {
log.error("Data Integrity Violation: {}", e.getMessage(), e);
log.error("Data Integrity Violation: {}", e.getMessage());
throw new BusinessException(TrafficErrorCode.ALREADY_EXIST_TRAFFIC_SIGNAL);
} catch (Exception e) {
log.error(e.getMessage());
log.error("error message = {}",e.getMessage());
throw new RuntimeException("파일 처리 중 예외 발생", e);
}

}
Expand All @@ -97,7 +111,10 @@ public void saveCsvData(String fileName) throws IOException {
private boolean isValidFileName(String fileName) {
String regex = "^[a-zA-Z0-9._-]+$";
Pattern pattern = Pattern.compile(regex);
return pattern.matcher(fileName).matches();
boolean matches = pattern.matcher(fileName).matches();

log.info("파일 이름 검증 - match = {}", matches);
return matches;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@
import org.programmers.signalbuddyfinal.domain.trafficSignal.repository.CustomTrafficRepositoryImpl;
import org.programmers.signalbuddyfinal.domain.trafficSignal.repository.TrafficRepository;
import org.programmers.signalbuddyfinal.global.exception.BusinessException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.programmers.signalbuddyfinal.domain.trafficSignal.repository.TrafficRedisRepository;

import java.util.ArrayList;
import java.util.List;

@Slf4j
Expand All @@ -22,53 +20,66 @@
@Transactional(readOnly = true)
public class TrafficService {

private static final String TRAFFIC_REDIS_KEY = "traffic:info";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용하지 않는 변수는 이제 필요없으니 삭제하면 좋을 거 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 그러게요..! 바로 삭제할게요!


private final CustomTrafficRepositoryImpl customTrafficRepository;
private final TrafficRedisRepository trafficRedisRepository;
private final TrafficRepository trafficRepository;
private final RedisTemplate<Object, Object> redisTemplate;

public List<TrafficResponse> searchAndSaveTraffic(Double lat, Double lng, int radius){

log.debug("주변 보행등 정보 - lat = {}, lng = {}, radius = {}", lat, lng, radius);
List<TrafficResponse> responseDB;

boolean exists = Boolean.TRUE.equals(redisTemplate.hasKey("traffic:info"));
if (exists) {
if (trafficRedisRepository.isExist()) {
double kiloRadius = (double) radius/1000;
return trafficRedisRepository.findNearbyTraffics(lat, lng, kiloRadius);
List<TrafficResponse> responseRedis = trafficRedisRepository.findNearbyTraffics(lat, lng, kiloRadius);

log.debug("redis 주변 보행등 데이터 : redis data 갯수 = {} ", responseRedis.size());
return responseRedis;
}

try {
responseDB = customTrafficRepository.findNearestTraffics(lat, lng, radius);

log.debug("주변 보행등 정보 캐싱 : DB data 갯수 = {} ", responseDB.size());
for (TrafficResponse response : responseDB) {
trafficRedisRepository.save(response);
}

log.debug("DB 주변 보행등 데이터 캐싱 성공");
return responseDB;

} catch (NullPointerException e) {
log.error("❌ traffic Not Found : {}", e.getMessage(), e);
} catch (Exception e) {
log.error("주변 보행등 조회 실패 : lat = {}, lng = {}, radius = {}, error = {}", lat, lng, radius, e.getMessage());
throw new BusinessException(TrafficErrorCode.NOT_FOUND_TRAFFIC);
}
}

public TrafficResponse trafficFindById(Long id) {

log.debug("보행등 세부정보 찾기 - id = {}", id);

TrafficResponse responseRedis = trafficRedisRepository.findById( id );

if(responseRedis != null) {
log.info("redis 보행등 세부정보 : redis data = {} ", responseRedis);
return responseRedis;
}

try{

TrafficResponse responseDB = new TrafficResponse(trafficRepository.findByTrafficSignalId(id));

log.info("보행등 세부정보 : DB data = {} ", responseDB);

trafficRedisRepository.save(responseDB);

log.info("보행등 세부정보 캐싱 성공");
return responseDB;

} catch (NullPointerException e) {
log.error("❌ traffic Not Found : {}", e.getMessage(), e);
} catch (Exception e) {
log.error("보행등 세부정보 조회 실패 : {}", e.getMessage(), e);
throw new BusinessException(TrafficErrorCode.NOT_FOUND_TRAFFIC);
}

Expand Down
Loading