diff --git a/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/dto/TrafficResponse.java b/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/dto/TrafficResponse.java index d62e8a13..a2772239 100644 --- a/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/dto/TrafficResponse.java +++ b/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/dto/TrafficResponse.java @@ -1,5 +1,6 @@ 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; @@ -7,6 +8,7 @@ @Getter @Builder +@SerializableAutoValue @AllArgsConstructor @NoArgsConstructor public class TrafficResponse { diff --git a/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepository.java b/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepository.java index 17781e4c..9585ff02 100644 --- a/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepository.java +++ b/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepository.java @@ -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; @@ -16,110 +15,119 @@ import org.springframework.data.redis.core.GeoOperations; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Repository; import java.time.Duration; +@Slf4j @Repository public class TrafficRedisRepository { private final RedisTemplate redisTemplate; - private final HashOperations> hashOperations; + private final ValueOperations valueOperations; private final GeoOperations geoOperations; - private static final String KEY_HASH = "traffic:info"; + private static final String KEY_VALUE = "traffic:info"; private static final String KEY_GEO = "traffic:geo"; private static final Duration TTL = Duration.ofMinutes(5); public TrafficRedisRepository(RedisTemplate redisTemplate){ this.redisTemplate = redisTemplate; - this.hashOperations = redisTemplate.opsForHash(); + this.valueOperations = redisTemplate.opsForValue(); this.geoOperations = redisTemplate.opsForGeo(); } - public void save(TrafficResponse trafficResponse) { - - Long trafficId = trafficResponse.getTrafficSignalId(); + public boolean isExist(){ + return redisTemplate.hasKey(KEY_VALUE); + } - // GEO 데이터 저장 - redisTemplate.opsForGeo().add( - KEY_GEO, - new Point(trafficResponse.getLng(),trafficResponse.getLat()), - trafficId.toString() - ); + public void save(TrafficResponse trafficResponse) { + Long trafficId = trafficResponse.getTrafficSignalId(); + String trafficKey = KEY_VALUE + trafficId; + + // GEO 데이터 저장 + geoOperations.add( + KEY_GEO, + new Point(trafficResponse.getLng(),trafficResponse.getLat()), + trafficId.toString() + ); + + valueOperations.set(trafficKey, trafficResponse, TTL); + redisTemplate.expire(KEY_GEO, TTL); + } - // HASH 데이터 저장 - Map 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()); + public List findNearbyTraffics(double lat, double lng, double kiloRadius) { - hashOperations.put(KEY_HASH, trafficId.toString(), trafficData); + log.debug("redis 캐싱 데이터 검색 - lat = {}, lng = {}, kiloRadius = {}", lat, lng, kiloRadius); - // GEO와 HASH 모두에 TTL 설정 - redisTemplate.expire(KEY_GEO, TTL); - redisTemplate.expire(KEY_HASH, TTL); + List>> geoResults; - } + log.info("redis kiloRadius 내 GEO 데이터 조회 - kiloRadius = {}", kiloRadius); + if (geoOperations != null) { + GeoResults> geoResult = geoOperations.radius( + KEY_GEO, + new Circle(new Point(lng, lat), new Distance(kiloRadius, Metrics.KILOMETERS)) + ); - public List findNearbyTraffics(double lat, double lng, double radius) { + geoResults = (geoResult != null) ? geoResult.getContent() : List.of(); + } else { + log.info("redis 내부에 데이터 없음"); + return List.of(); + } - List>> geoResults; - // 반경 내 GEO 데이터 조회 - if (geoOperations != null) { - GeoResults> 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 trafficResponses = new ArrayList<>(); + List trafficResponses = new ArrayList<>(); - for (GeoResult> result : geoResults) { - String trafficId = result.getContent().getName().toString(); // GEO에서 가져온 ID + log.info("redis GEO 데이터 검색 성공"); + for (GeoResult> 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) { - String trafficId = String.valueOf(id); + String trafficKey = KEY_VALUE + trafficId; + + log.debug("redis 캐싱 데이터 id로 검색 - id = {}", id); + + TrafficResponse data = (TrafficResponse) valueOperations.get(trafficKey); - Map data = hashOperations.get(KEY_HASH, trafficId); if (data == null) { + log.info("redis에 데이터 없음"); return null; } List 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(); diff --git a/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRepository.java b/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRepository.java index 44cb99b5..7dd9366d 100644 --- a/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRepository.java +++ b/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRepository.java @@ -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; diff --git a/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficCsvService.java b/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficCsvService.java index fc0c667b..045bebd7 100644 --- a/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficCsvService.java +++ b/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficCsvService.java @@ -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("경로 탐색 시도 감지됨"); } @@ -50,6 +60,7 @@ public void saveCsvData(String fileName) throws IOException { List entityList = new ArrayList<>(); + log.info("csvtoBean-opencsv (csv파일 객체화)"); CsvToBean csvToBean = new CsvToBeanBuilder(reader) .withType(TrafficFileResponse.class) .withIgnoreLeadingWhiteSpace(true) @@ -57,38 +68,41 @@ public void saveCsvData(String fileName) throws IOException { List 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 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 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); } } @@ -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; } } diff --git a/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficService.java b/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficService.java index fc408783..085d8795 100644 --- a/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficService.java +++ b/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficService.java @@ -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 @@ -25,50 +23,61 @@ public class TrafficService { private final CustomTrafficRepositoryImpl customTrafficRepository; private final TrafficRedisRepository trafficRedisRepository; private final TrafficRepository trafficRepository; - private final RedisTemplate redisTemplate; public List searchAndSaveTraffic(Double lat, Double lng, int radius){ + log.debug("주변 보행등 정보 - lat = {}, lng = {}, radius = {}", lat, lng, radius); List 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 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); } diff --git a/src/test/java/org/programmers/signalbuddyfinal/domain/traffic/service/TrafficServiceTest.java b/src/test/java/org/programmers/signalbuddyfinal/domain/traffic/service/TrafficServiceTest.java deleted file mode 100644 index 4ddca1bc..00000000 --- a/src/test/java/org/programmers/signalbuddyfinal/domain/traffic/service/TrafficServiceTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.programmers.signalbuddyfinal.domain.traffic.service; - - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.programmers.signalbuddyfinal.domain.trafficSignal.dto.TrafficResponse; -import org.programmers.signalbuddyfinal.domain.trafficSignal.repository.TrafficRedisRepository; -import org.programmers.signalbuddyfinal.domain.trafficSignal.service.TrafficService; -import org.programmers.signalbuddyfinal.global.support.ServiceTest; -import org.springframework.data.redis.core.RedisTemplate; - -public class TrafficServiceTest extends ServiceTest { - - @InjectMocks - private TrafficService trafficService; - - @Mock - private RedisTemplate redisTemplate; - - @Mock - private TrafficRedisRepository trafficRedisRepository; - - private List expected; - - @BeforeEach - void setUp() { - - expected = List.of( - TrafficResponse.builder() - .trafficSignalId(1L) - .signalType("1") - .district("강남구") - .address("강남구 대변로 29") - .lat(37.4777135) - .lng(126.9153603) - .build() - ); - } - - @Test - void testSearchAndSaveCrossroadRedisExists() { - - Double lat = expected.get(0).getLat(); - Double lng = expected.get(0).getLng(); - - when(trafficRedisRepository.findNearbyTraffics(lat, lng, 1.0)).thenReturn(expected); - when(redisTemplate.hasKey("traffic:info")).thenReturn(true); - - // When - List result = trafficService.searchAndSaveTraffic(lat, lng, 1000); - - // Then - assertThat(result).isEqualTo(expected); - } -} diff --git a/src/test/java/org/programmers/signalbuddyfinal/domain/traffic/controller/TrafficControllerTest.java b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/controller/TrafficControllerTest.java similarity index 53% rename from src/test/java/org/programmers/signalbuddyfinal/domain/traffic/controller/TrafficControllerTest.java rename to src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/controller/TrafficControllerTest.java index 96e27800..8252af19 100644 --- a/src/test/java/org/programmers/signalbuddyfinal/domain/traffic/controller/TrafficControllerTest.java +++ b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/controller/TrafficControllerTest.java @@ -1,4 +1,4 @@ -package org.programmers.signalbuddyfinal.domain.traffic.controller; +package org.programmers.signalbuddyfinal.domain.trafficSignal.controller; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; @@ -6,7 +6,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.programmers.signalbuddyfinal.domain.trafficSignal.controller.TrafficController; import org.programmers.signalbuddyfinal.domain.trafficSignal.service.TrafficCsvService; import org.programmers.signalbuddyfinal.global.config.WebConfig; import org.programmers.signalbuddyfinal.global.support.ControllerTest; @@ -16,14 +15,8 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.ResultActions; -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Map; - import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.doNothing; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -35,46 +28,46 @@ @Import(WebConfig.class) public class TrafficControllerTest extends ControllerTest { - private final String tag = "Traffic API"; + private static final String tag = "Traffic API"; @MockitoBean private TrafficCsvService trafficCsvService; @Test @DisplayName("데이터 저장") - void saveTrafficData() throws Exception { + void saveTrafficDataTest() throws Exception { + + // Given + String fileName = "seoul_traffic_light.csv"; - // given - String fileName = "seoul_traffic_light_test.csv"; - // when doNothing().when(trafficCsvService).saveCsvData(fileName); + // When ResultActions result = mockMvc.perform( - post("/api/traffic/save") - .param("fileName", fileName) - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - ).andExpect(status().isOk()) - .andExpect(jsonPath("$.data").value("파일이 성공적으로 저장되었습니다.")); + post("/api/traffic/save") + .param("fileName", fileName) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + ).andExpect(status().isOk()) + .andExpect(jsonPath("$.data").value("파일이 성공적으로 저장되었습니다.")); - - // then + // Then result.andExpect(status().isOk()).andDo( document("csv 파일 저장", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource(ResourceSnippetParameters.builder() - .tag(tag) - .formParameters( - parameterWithName("fileName").description("CSV 파일 이름") - ) - .responseFields( - fieldWithPath("status").description("성공 여부"), - fieldWithPath("data").description("응답 데이터"), - fieldWithPath("message").description("음답 메세지") - ) - .build() + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .tag(tag) + .formParameters( + parameterWithName("fileName").description("CSV 파일 이름") + ) + .responseFields( + fieldWithPath("status").description("성공 여부"), + fieldWithPath("data").description("응답 데이터"), + fieldWithPath("message").description("음답 메세지") ) + .build() + ) )); } } diff --git a/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/CustomTrafficRepoImplTest.java b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/CustomTrafficRepoImplTest.java new file mode 100644 index 00000000..5f3be8a5 --- /dev/null +++ b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/CustomTrafficRepoImplTest.java @@ -0,0 +1,62 @@ +package org.programmers.signalbuddyfinal.domain.trafficSignal.repository; + +import jakarta.persistence.EntityManager; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.programmers.signalbuddyfinal.domain.trafficSignal.dto.TrafficFileResponse; +import org.programmers.signalbuddyfinal.domain.trafficSignal.dto.TrafficResponse; +import org.programmers.signalbuddyfinal.domain.trafficSignal.entity.TrafficSignal; +import org.programmers.signalbuddyfinal.global.support.RepositoryTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; + +@Import({CustomTrafficRepositoryImpl.class}) +public class CustomTrafficRepoImplTest extends RepositoryTest { + + @Autowired + private CustomTrafficRepositoryImpl customTrafficRepository; + + @Autowired + private EntityManager entityManager; + + @BeforeEach + void setUp() { + + TrafficFileResponse response = TrafficFileResponse.builder() + .serial(1L) + .district("용산구") + .signalType("보행신호등") + .address("서울특별시 용산구 한강대로 405") + .lat(37.5546) + .lng(126.9706) + .build(); + + TrafficSignal signal = new TrafficSignal(response); + + entityManager.persist(signal); + entityManager.flush(); + } + + @Test + @DisplayName("주변 보행등 정보 DB 검색 테스트") + void findNearestTrafficsTest(){ + + // Given + double lat = 37.5546; + double lng = 126.9706; + int radius = 10000; + + // When + List results = customTrafficRepository.findNearestTraffics(lat, lng, radius); + + // Then + Assertions.assertNotNull(results); + Assertions.assertNotEquals(0, results.size()); + + } + + +} diff --git a/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepositoryTest.java b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepositoryTest.java new file mode 100644 index 00000000..19e2eb1b --- /dev/null +++ b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepositoryTest.java @@ -0,0 +1,153 @@ +package org.programmers.signalbuddyfinal.domain.trafficSignal.repository; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.programmers.signalbuddyfinal.domain.trafficSignal.dto.TrafficResponse; +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.Metrics; +import org.springframework.data.geo.Point; +import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.data.redis.core.GeoOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +@ExtendWith(MockitoExtension.class) +public class TrafficRedisRepositoryTest { + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ValueOperations valueOperations; + + @Mock + private GeoOperations geoOperations; + + private static final String KEY_VALUE = "traffic:info"; + private static final String KEY_GEO = "traffic:geo"; + private static final Duration TTL = Duration.ofMinutes(5); + + private TrafficRedisRepository trafficRedisRepository; + private List expected; + + private Point point; + private Long id; + private double lat; + private double lng; + + @BeforeEach + void setUp() { + + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(redisTemplate.opsForGeo()).thenReturn(geoOperations); + + trafficRedisRepository = new TrafficRedisRepository(redisTemplate); + + expected = List.of( + TrafficResponse.builder() + .trafficSignalId(1L) + .serialNumber(1L) + .signalType("1") + .district("강남구") + .address("강남구 대변로 29") + .lat(37.4777135) + .lng(126.9153603) + .build() + ); + + id = expected.get(0).getTrafficSignalId(); + lat = expected.get(0).getLat(); + lng = expected.get(0).getLng(); + + point = new Point(lng, lat); + } + + @Test + @DisplayName("보행등 저장 테스트") + void trafficSaveTest(){ + String trafficKey = KEY_VALUE + id; + + // When + trafficRedisRepository.save(expected.get(0)); + + // Then + verify(geoOperations).add( + eq(KEY_GEO), + eq(point), + eq( String.valueOf(id) ) + ); + + verify(valueOperations).set(eq(trafficKey), eq(expected.get(0)), eq(TTL)); + verify(redisTemplate).expire(KEY_GEO, TTL); + } + + @Test + @DisplayName("주변 보행등 redis 데이터 반환") + void trafficNearByTestReturnTrafficList(){ + + //Given + String trafficKey = KEY_VALUE + id; + + double radius = 1; + + // result set + List>> geoResults = new ArrayList<>(); + RedisGeoCommands.GeoLocation geoLocation = new RedisGeoCommands.GeoLocation<>(id.toString(),point); + GeoResult> geoResult + = new GeoResult<>(geoLocation, new Distance(1, Metrics.KILOMETERS)); + + geoResults.add(geoResult); + + GeoResults> mockGeoResults = mock(GeoResults.class); + when(mockGeoResults.getContent()).thenReturn(geoResults); + + when(geoOperations.radius( + eq(KEY_GEO), + any(Circle.class) + )).thenReturn(mockGeoResults); + + when(geoOperations.position(KEY_GEO, id.toString())).thenReturn(List.of(point)); + + + + doReturn(expected.get(0)).when(valueOperations).get(eq(trafficKey)); + + //When + trafficRedisRepository.save(expected.get(0)); + List result = trafficRedisRepository.findNearbyTraffics(lat, lng, radius); + + //Then + Assertions.assertNotNull(result); + Assertions.assertEquals(expected.size(), result.size()); + Assertions.assertEquals(id, result.get(0).getTrafficSignalId()); + + verify(geoOperations).radius( + eq(KEY_GEO), + argThat(circle -> { + return circle.getCenter().getX() == lng && + circle.getCenter().getY() == lat && + circle.getRadius().getValue() == radius; + }) + ); + + } +} diff --git a/src/test/java/org/programmers/signalbuddyfinal/domain/traffic/service/TrafficCsvServiceTest.java b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficCsvServiceTest.java similarity index 66% rename from src/test/java/org/programmers/signalbuddyfinal/domain/traffic/service/TrafficCsvServiceTest.java rename to src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficCsvServiceTest.java index cd2da88e..fc87c6cb 100644 --- a/src/test/java/org/programmers/signalbuddyfinal/domain/traffic/service/TrafficCsvServiceTest.java +++ b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficCsvServiceTest.java @@ -1,46 +1,66 @@ -package org.programmers.signalbuddyfinal.domain.traffic.service; +package org.programmers.signalbuddyfinal.domain.trafficSignal.service; import com.opencsv.bean.CsvToBean; import com.opencsv.bean.CsvToBeanBuilder; +import java.net.URL; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.locationtech.jts.io.WKBWriter; +import org.mockito.InjectMocks; +import org.mockito.Mock; import org.programmers.signalbuddyfinal.domain.trafficSignal.dto.TrafficFileResponse; -import org.programmers.signalbuddyfinal.domain.trafficSignal.repository.TrafficRepository; -import org.programmers.signalbuddyfinal.domain.trafficSignal.service.TrafficCsvService; import org.programmers.signalbuddyfinal.global.support.ServiceTest; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.transaction.annotation.Transactional; import java.io.*; import java.net.URISyntaxException; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; @Transactional public class TrafficCsvServiceTest extends ServiceTest { - @Autowired - private TrafficRepository trafficRepository; - - @Autowired + @InjectMocks private TrafficCsvService trafficCsvService; + @Mock + private NamedParameterJdbcTemplate namedJdbcTemplate; + private File testCsvFile; + private String fileName; @BeforeEach - void setUp() throws URISyntaxException, NullPointerException { - testCsvFile = new File(getClass() - .getClassLoader() - .getResource("static/file/seoul_traffic_light_test.csv") - .toURI()); + void setUp() throws URISyntaxException { + + fileName = "seoul_traffic_light.csv"; + + URL resource = getClass().getClassLoader().getResource("response/"+fileName); + assertThat(resource).isNotNull(); + testCsvFile = new File(resource.toURI()); + } + + @Test + void csvSaveMethodTest(){ + doReturn(new int[]{1, 1}) + .when(namedJdbcTemplate) + .batchUpdate(anyString(), any(MapSqlParameterSource[].class)); + + trafficCsvService.saveCsvData(fileName); + + verify(namedJdbcTemplate, atLeastOnce()).batchUpdate(anyString(), any(MapSqlParameterSource[].class)); } @Test - @DisplayName("데이터 랜더 처리 검증") + @DisplayName("데이터 리더 처리 검증 및 메소드 호출") void csvParsingTest() throws IOException { // given Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream(testCsvFile))); @@ -72,7 +92,7 @@ void csvParsingTest() throws IOException { void csvParsingDtoTest() { // given String csvContent = """ - serial, district, signalType, lat, lng, address + serial,district,signalType,lat,lng,address 123, 서울, 교차로, 37.5665, 126.9780, 서울시 중구 124, 부산, 신호등, 35.179, 129.0756, 부산시 해운대구 """; @@ -94,18 +114,4 @@ void csvParsingDtoTest() { } - - @Test - @DisplayName("보행등 정보 저장 검증") - void saveTrafficInfo() throws IOException { - - String fileName = testCsvFile.getName(); - - //when - trafficCsvService.saveCsvData(fileName); - - //then - assertThat(trafficRepository.count()).isGreaterThan(1L); - - } } diff --git a/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficServiceTest.java b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficServiceTest.java new file mode 100644 index 00000000..31b5d8bc --- /dev/null +++ b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficServiceTest.java @@ -0,0 +1,129 @@ +package org.programmers.signalbuddyfinal.domain.trafficSignal.service; + + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.programmers.signalbuddyfinal.domain.trafficSignal.dto.TrafficResponse; +import org.programmers.signalbuddyfinal.domain.trafficSignal.repository.CustomTrafficRepositoryImpl; +import org.programmers.signalbuddyfinal.domain.trafficSignal.repository.TrafficRedisRepository; +import org.programmers.signalbuddyfinal.global.support.ServiceTest; +import org.springframework.data.redis.core.RedisTemplate; + +public class TrafficServiceTest extends ServiceTest { + + @InjectMocks + private TrafficService trafficService; + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private TrafficRedisRepository trafficRedisRepository; + + @Mock + private CustomTrafficRepositoryImpl customTrafficRepository; + + private List expected; + private static final String TRAFFIC_REDIS_KEY = "traffic:info"; + + private Double lat; + private Double lng; + + @BeforeEach + void setUp() { + expected = List.of( + TrafficResponse.builder() + .trafficSignalId(1L) + .signalType("1") + .district("강남구") + .address("강남구 대변로 29") + .lat(37.4777135) + .lng(126.9153603) + .build() + ); + + lat = expected.get(0).getLat(); + lng = expected.get(0).getLng(); + } + + @Test + @DisplayName("주변 보행등 정보 redis 테스트") + void testSearchAndSaveTrafficRedisExists() { + + // Given + when(trafficRedisRepository.findNearbyTraffics(lat, lng, 1.0)).thenReturn(expected); + when(trafficRedisRepository.isExist()).thenReturn(true); + + // When + List result = trafficService.searchAndSaveTraffic(lat, lng, 1000); + + // Then + assertThat(result).isEqualTo(expected); + } + + @Test + @DisplayName("주변 보행등 정보 캐싱 테스트") + void testSearchAndSaveTrafficRedisNotExists() { + + // Given + when(redisTemplate.hasKey(TRAFFIC_REDIS_KEY)).thenReturn(false); + doNothing().when(trafficRedisRepository).save(any(TrafficResponse.class)); + + when(customTrafficRepository.findNearestTraffics(lat,lng,1000)).thenReturn(expected); + + // When + List result = trafficService.searchAndSaveTraffic(lat, lng, 1000); + + // Then + assertThat(result).isEqualTo(expected); + } + + @Test + @DisplayName("ID값 redis 테스트") + void testTrafficFindByIdRedisExists() { + + // Given + Long id = expected.get(0).getTrafficSignalId(); + + when(trafficRedisRepository.findById(id)).thenReturn(expected.get(0)); + when(redisTemplate.hasKey(TRAFFIC_REDIS_KEY)).thenReturn(true); + + // When + TrafficResponse result = trafficService.trafficFindById(id); + + // Then + assertThat(result).isEqualTo(expected.get(0)); + + } + + /** + * TODO: TrafficService 코드 refactor 필요 + * -> findById() + * Servive 레이어에서 entity에 접근하는 repository를 직접 사용하지 않게 + */ +/* @Test + @DisplayName("보행등 ID 캐싱 테스트") + void testTrafficFindByIdRedisNotExists() { + + // Given + Long id = expected.get(0).getTrafficSignalId(); + + when(redisTemplate.hasKey(TRAFFIC_REDIS_KEY)).thenReturn(false); + when(trafficRepository.findById(id)).thenReturn(expected.get(0)); + + // When + TrafficResponse result = trafficService.trafficFindById(id); + + // Then + assertThat(result).isNotNull(); + }*/ +} diff --git a/src/main/resources/static/file/seoul_traffic_light_test.csv b/src/test/resources/response/seoul_traffic_light.csv similarity index 100% rename from src/main/resources/static/file/seoul_traffic_light_test.csv rename to src/test/resources/response/seoul_traffic_light.csv