Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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,6 +21,7 @@

import java.time.Duration;

@Slf4j
@Repository
public class TrafficRedisRepository {

Expand All @@ -38,78 +40,91 @@ public TrafficRedisRepository(RedisTemplate<Object,Object> redisTemplate){
}

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 데이터 저장
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());

// 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());
hashOperations.put(KEY_HASH, trafficId.toString(), trafficData);
Copy link
Collaborator

Choose a reason for hiding this comment

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

public void putStateCache(Long crossroadId, CrossroadStateResponse response) {
        ValueOperations<Object, Object> operations = redisTemplate.opsForValue();

        int minTimeLeft = response.minTimeLeft();
        minTimeLeft *= 100; // 1/10초 단위를 1/1000(ms)로 변환

        operations.set(STATE_PREFIX + crossroadId, response, minTimeLeft, TimeUnit.MILLISECONDS);
    }

이 코드처럼 DTO 자체를 직렬화 시켜서 저장시킬 수 있습니다. 이때 저장하려는 DTO는 Serializable을 상속받으면 돼요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

오...변경해서 다시올려볼게요! 감사합니다!


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

// GEO와 HASH 모두에 TTL 설정
redisTemplate.expire(KEY_GEO, TTL);
redisTemplate.expire(KEY_HASH, TTL);
// GEO와 HASH 모두에 TTL 설정
redisTemplate.expire(KEY_GEO, TTL);
redisTemplate.expire(KEY_HASH, TTL);

}

public List<TrafficResponse> findNearbyTraffics(double lat, double lng, double radius) {
public List<TrafficResponse> findNearbyTraffics(double lat, double lng, double kiloRadius) {

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

List<GeoResult<GeoLocation<Object>>> geoResults;

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))
);

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

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();
}
if (geoResults.isEmpty()) {
log.info("redis 내부에 데이터 없음");
return Collections.emptyList();
}

if (geoResults.isEmpty()) {
return Collections.emptyList();
}

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

for (GeoResult<GeoLocation<Object>> result : geoResults) {
String trafficId = result.getContent().getName().toString(); // GEO에서 가져온 ID
log.info("redis GEO 데이터 검색 성공");
for (GeoResult<GeoLocation<Object>> result : geoResults) {
String trafficId = result.getContent().getName().toString();

TrafficResponse response = findById(Long.valueOf(trafficId));
TrafficResponse response = findById(Long.valueOf(trafficId));

trafficResponses.add(response);
}
trafficResponses.add(response);
}

return trafficResponses;
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);

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(); // 경도
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 @@ -22,53 +22,69 @@
@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"));
boolean exists = Boolean.TRUE.equals(redisTemplate.hasKey(TRAFFIC_REDIS_KEY));

if (exists) {
double kiloRadius = (double) radius/1000;
return trafficRedisRepository.findNearbyTraffics(lat, lng, kiloRadius);
List<TrafficResponse> responseRedis = trafficRedisRepository.findNearbyTraffics(lat, lng, kiloRadius);

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

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

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

log.info("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