From f6d2d5b36536cc524fe4e55b4eeb44d79b03a974 Mon Sep 17 00:00:00 2001 From: Byungil Oh Date: Wed, 30 Apr 2025 03:06:47 +0900 Subject: [PATCH 01/10] =?UTF-8?q?test:=20CustomTrafficRepoImpl=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/CustomTrafficRepoImplTest.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/CustomTrafficRepoImplTest.java 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..7b8579b3 --- /dev/null +++ b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/CustomTrafficRepoImplTest.java @@ -0,0 +1,69 @@ +package org.programmers.signalbuddyfinal.domain.trafficSignal.repository; + +import static org.mockito.Mockito.when; + +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.programmers.signalbuddyfinal.global.support.RepositoryTest; + +@ExtendWith(MockitoExtension.class) +public class CustomTrafficRepoImplTest extends RepositoryTest { + + @Mock + private CustomTrafficRepository customTrafficRepository; + + private List expected; + + @BeforeEach + void setUp() { + + expected = List.of( + TrafficResponse.builder() + .district("강남구") + .signalType("보행등") + .lat(37.5000) + .lng(127.0300) + .address("강남대로 123") + .build(), + + TrafficResponse.builder() + .district("서초구") + .signalType("보행등") + .lat(37.4950) + .lng(127.0200) + .address("서초대로 456") + .build() + ); + + } + + @Test + @DisplayName("주변 보행등 정보 DB 검색 테스트") + void findNearestTrafficsTest(){ + + // Given + double lat = 37.4950; + double lng = 127.0200; + int radius = 1000; + + when(customTrafficRepository.findNearestTraffics(lat,lng,radius)).thenReturn(expected); + + // When + List results = customTrafficRepository.findNearestTraffics(lat, lng, radius); + + // Then + Assertions.assertNotNull(results); + Assertions.assertEquals(2, results.size()); + Assertions.assertEquals("강남구",results.get(0).getDistrict()); + + } + + +} From 5cd28e29ab5ffa6983d0593090d7a6453cc86a3f Mon Sep 17 00:00:00 2001 From: Byungil Oh Date: Wed, 30 Apr 2025 03:07:26 +0900 Subject: [PATCH 02/10] =?UTF-8?q?test:=20TrafficController=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TrafficControllerTest.java | 59 ++++++++----------- 1 file changed, 26 insertions(+), 33 deletions(-) rename src/test/java/org/programmers/signalbuddyfinal/domain/{traffic => trafficSignal}/controller/TrafficControllerTest.java (53%) 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() + ) )); } } From b50c8d38106d6024f9adbf8ff4a3453a40a382a6 Mon Sep 17 00:00:00 2001 From: Byungil Oh Date: Wed, 30 Apr 2025 03:08:24 +0900 Subject: [PATCH 03/10] =?UTF-8?q?test:=20TrafficCsvService=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/TrafficCsvServiceTest.java | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) rename src/test/java/org/programmers/signalbuddyfinal/domain/{traffic => trafficSignal}/service/TrafficCsvServiceTest.java (66%) 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); - - } } From ac3e9d313053fbdeb171a5ec7f73ee9fd32a35a4 Mon Sep 17 00:00:00 2001 From: Byungil Oh Date: Wed, 30 Apr 2025 03:10:14 +0900 Subject: [PATCH 04/10] =?UTF-8?q?refactor:=20Traffic=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=A1=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8E=99=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/TrafficRedisRepository.java | 95 +++++++++++-------- .../repository/TrafficRepository.java | 1 - .../service/TrafficCsvService.java | 63 +++++++----- .../trafficSignal/service/TrafficService.java | 28 ++++-- 4 files changed, 117 insertions(+), 70 deletions(-) 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..c6f384c0 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 @@ -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; @@ -20,6 +21,7 @@ import java.time.Duration; +@Slf4j @Repository public class TrafficRedisRepository { @@ -38,78 +40,91 @@ public TrafficRedisRepository(RedisTemplate 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 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 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); - 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 findNearbyTraffics(double lat, double lng, double radius) { + public List findNearbyTraffics(double lat, double lng, double kiloRadius) { + + log.debug("redis 캐싱 데이터 검색 - lat = {}, lng = {}, kiloRadius = {}", lat, lng, kiloRadius); + + 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)) + ); + + 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) { + log.debug("redis 캐싱 데이터 id로 검색 - id = {}", id); + String trafficId = String.valueOf(id); 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(); // 경도 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..25433dbd 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 @@ -22,6 +22,8 @@ @Transactional(readOnly = true) public class TrafficService { + private static final String TRAFFIC_REDIS_KEY = "traffic:info"; + private final CustomTrafficRepositoryImpl customTrafficRepository; private final TrafficRedisRepository trafficRedisRepository; private final TrafficRepository trafficRepository; @@ -29,46 +31,60 @@ public class TrafficService { 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")); + boolean exists = Boolean.TRUE.equals(redisTemplate.hasKey(TRAFFIC_REDIS_KEY)); + if (exists) { double kiloRadius = (double) radius/1000; - return trafficRedisRepository.findNearbyTraffics(lat, lng, kiloRadius); + List 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); } From 586eed40cb683d7c1bd09022f46cd89c6e5bfeb7 Mon Sep 17 00:00:00 2001 From: Byungil Oh Date: Wed, 30 Apr 2025 03:10:56 +0900 Subject: [PATCH 05/10] =?UTF-8?q?test:=20TrafficRedisRepository=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TrafficRedisRepositoryTest.java | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepositoryTest.java 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..e9abf430 --- /dev/null +++ b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepositoryTest.java @@ -0,0 +1,162 @@ +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.HashMap; +import java.util.List; +import java.util.Map; +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.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; + +@ExtendWith(MockitoExtension.class) +public class TrafficRedisRepositoryTest { + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private HashOperations hashOperations; + + @Mock + private GeoOperations geoOperations; + + private static final String KEY_HASH = "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.opsForHash()).thenReturn(hashOperations); + 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(){ + + // When + trafficRedisRepository.save(expected.get(0)); + + // Then + verify(geoOperations).add( + eq(KEY_GEO), + eq(point), + eq( String.valueOf(id) ) + ); + + Map expectedTrafficData = new HashMap<>(); + expectedTrafficData.put("serialNumber", expected.get(0).getSerialNumber().toString()); + expectedTrafficData.put("district", expected.get(0).getDistrict()); + expectedTrafficData.put("signalType", expected.get(0).getSignalType()); + expectedTrafficData.put("address", expected.get(0).getAddress()); + + verify(hashOperations).put(eq(KEY_HASH), eq(id.toString()), eq(expectedTrafficData)); + verify(redisTemplate).expire(KEY_GEO, TTL); + verify(redisTemplate).expire(KEY_HASH, TTL); + } + + @Test + @DisplayName("주변 보행등 redis 데이터 반환") + void trafficNearByTestReturnTrafficList(){ + + //Given + 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)); + + Map expectedTrafficData = new HashMap<>(); + expectedTrafficData.put("serialNumber", expected.get(0).getSerialNumber().toString()); + expectedTrafficData.put("district", expected.get(0).getDistrict()); + expectedTrafficData.put("signalType", expected.get(0).getSignalType()); + expectedTrafficData.put("address", expected.get(0).getAddress()); + + doReturn(expectedTrafficData).when(hashOperations).get(eq(KEY_HASH), eq(id.toString())); + + //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; + }) + ); + + } +} From 1b941f0d49556c67033b15ca24266b0042bbe291 Mon Sep 17 00:00:00 2001 From: Byungil Oh Date: Wed, 30 Apr 2025 03:12:00 +0900 Subject: [PATCH 06/10] =?UTF-8?q?test:=20TrafficService=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ++ refactor 사항 추가 crossroad 리펙토링 진행하며 작업 진행 --- .../traffic/service/TrafficServiceTest.java | 61 --------- .../service/TrafficServiceTest.java | 129 ++++++++++++++++++ 2 files changed, 129 insertions(+), 61 deletions(-) delete mode 100644 src/test/java/org/programmers/signalbuddyfinal/domain/traffic/service/TrafficServiceTest.java create mode 100644 src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficServiceTest.java 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/trafficSignal/service/TrafficServiceTest.java b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficServiceTest.java new file mode 100644 index 00000000..35ce8957 --- /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(redisTemplate.hasKey(TRAFFIC_REDIS_KEY)).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(); + }*/ +} From e87d4223153c6971abc08cfff624b9ba2fbceabe Mon Sep 17 00:00:00 2001 From: Byungil Oh Date: Wed, 30 Apr 2025 03:13:11 +0900 Subject: [PATCH 07/10] =?UTF-8?q?docs:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=ED=95=98=EB=A9=B0=20=EB=8D=94=EB=AF=B8=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=9E=AC=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/response/seoul_traffic_light.csv} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{main/resources/static/file/seoul_traffic_light_test.csv => test/resources/response/seoul_traffic_light.csv} (100%) 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 From 0dffb17ae562aa06c6f1f206b70a5099843f7879 Mon Sep 17 00:00:00 2001 From: Byungil Oh Date: Wed, 30 Apr 2025 11:52:40 +0900 Subject: [PATCH 08/10] =?UTF-8?q?refactor:=20pr=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 리뷰 수정사항 - service log info-> debug로 수정 - 불필요한 Map 객체 이용 -> dto로 바로 받게 변경 - repository와 service에서 redis 중복 호출 -> repository에서만 호출하도록 분리 - 테스트 코드 수정 -> 변경된 내용으로 mock 수정 --- .../trafficSignal/dto/TrafficResponse.java | 2 ++ .../repository/TrafficRedisRepository.java | 26 ++++++++----------- .../trafficSignal/service/TrafficService.java | 13 +++------- .../TrafficRedisRepositoryTest.java | 17 +++--------- .../service/TrafficServiceTest.java | 2 +- 5 files changed, 22 insertions(+), 38 deletions(-) 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 c6f384c0..4ffebf60 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,7 @@ 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; @@ -26,7 +24,7 @@ public class TrafficRedisRepository { private final RedisTemplate redisTemplate; - private final HashOperations> hashOperations; + private final HashOperations hashOperations; private final GeoOperations geoOperations; private static final String KEY_HASH = "traffic:info"; @@ -39,6 +37,10 @@ public TrafficRedisRepository(RedisTemplate redisTemplate){ this.geoOperations = redisTemplate.opsForGeo(); } + public boolean isExist(){ + return hashOperations.hasKey(KEY_HASH, KEY_GEO); + } + public void save(TrafficResponse trafficResponse) { Long trafficId = trafficResponse.getTrafficSignalId(); @@ -50,13 +52,7 @@ public void save(TrafficResponse trafficResponse) { ); // 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()); - - hashOperations.put(KEY_HASH, trafficId.toString(), trafficData); + hashOperations.put(KEY_HASH, trafficId.toString(), trafficResponse); // GEO와 HASH 모두에 TTL 설정 redisTemplate.expire(KEY_GEO, TTL); @@ -110,7 +106,7 @@ public TrafficResponse findById(Long id) { String trafficId = String.valueOf(id); - Map data = hashOperations.get(KEY_HASH, trafficId); + TrafficResponse data = hashOperations.get(KEY_HASH, trafficId); if (data == null) { log.info("redis에 데이터 없음"); @@ -131,10 +127,10 @@ public TrafficResponse findById(Long id) { 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/service/TrafficService.java b/src/main/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficService.java index 25433dbd..c5b45096 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 @@ -27,32 +25,29 @@ 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_REDIS_KEY)); - - if (exists) { + if (trafficRedisRepository.isExist()) { double kiloRadius = (double) radius/1000; List responseRedis = trafficRedisRepository.findNearbyTraffics(lat, lng, kiloRadius); - log.info("redis 주변 보행등 데이터 : redis data 갯수 = {} ", responseRedis.size()); + log.debug("redis 주변 보행등 데이터 : redis data 갯수 = {} ", responseRedis.size()); return responseRedis; } try { responseDB = customTrafficRepository.findNearestTraffics(lat, lng, radius); - log.info("주변 보행등 정보 캐싱 : DB data 갯수 = {} ", responseDB.size()); + log.debug("주변 보행등 정보 캐싱 : DB data 갯수 = {} ", responseDB.size()); for (TrafficResponse response : responseDB) { trafficRedisRepository.save(response); } - log.info("DB 주변 보행등 데이터 캐싱 성공"); + log.debug("DB 주변 보행등 데이터 캐싱 성공"); return responseDB; } catch (Exception e) { 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 index e9abf430..e1023baf 100644 --- a/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepositoryTest.java +++ b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepositoryTest.java @@ -58,6 +58,7 @@ public class TrafficRedisRepositoryTest { @BeforeEach void setUp() { + when(redisTemplate.opsForHash()).thenReturn(hashOperations); when(redisTemplate.opsForGeo()).thenReturn(geoOperations); @@ -96,13 +97,7 @@ void trafficSaveTest(){ eq( String.valueOf(id) ) ); - Map expectedTrafficData = new HashMap<>(); - expectedTrafficData.put("serialNumber", expected.get(0).getSerialNumber().toString()); - expectedTrafficData.put("district", expected.get(0).getDistrict()); - expectedTrafficData.put("signalType", expected.get(0).getSignalType()); - expectedTrafficData.put("address", expected.get(0).getAddress()); - - verify(hashOperations).put(eq(KEY_HASH), eq(id.toString()), eq(expectedTrafficData)); + verify(hashOperations).put(eq(KEY_HASH), eq(id.toString()), eq(expected.get(0))); verify(redisTemplate).expire(KEY_GEO, TTL); verify(redisTemplate).expire(KEY_HASH, TTL); } @@ -132,13 +127,9 @@ void trafficNearByTestReturnTrafficList(){ when(geoOperations.position(KEY_GEO, id.toString())).thenReturn(List.of(point)); - Map expectedTrafficData = new HashMap<>(); - expectedTrafficData.put("serialNumber", expected.get(0).getSerialNumber().toString()); - expectedTrafficData.put("district", expected.get(0).getDistrict()); - expectedTrafficData.put("signalType", expected.get(0).getSignalType()); - expectedTrafficData.put("address", expected.get(0).getAddress()); - doReturn(expectedTrafficData).when(hashOperations).get(eq(KEY_HASH), eq(id.toString())); + + doReturn(expected.get(0)).when(hashOperations).get(eq(KEY_HASH), eq(id.toString())); //When trafficRedisRepository.save(expected.get(0)); 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 index 35ce8957..31b5d8bc 100644 --- a/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficServiceTest.java +++ b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/service/TrafficServiceTest.java @@ -61,7 +61,7 @@ void testSearchAndSaveTrafficRedisExists() { // Given when(trafficRedisRepository.findNearbyTraffics(lat, lng, 1.0)).thenReturn(expected); - when(redisTemplate.hasKey(TRAFFIC_REDIS_KEY)).thenReturn(true); + when(trafficRedisRepository.isExist()).thenReturn(true); // When List result = trafficService.searchAndSaveTraffic(lat, lng, 1000); From df847bf56f4ac298ff7273a391ecca397f596b12 Mon Sep 17 00:00:00 2001 From: Byungil Oh Date: Tue, 6 May 2025 02:49:41 +0900 Subject: [PATCH 09/10] =?UTF-8?q?refactor:=20pr=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 리뷰 수정사항 - TrafficRedisRepository hash->value로 변경 - TrafficService 불필요한 코드제거 - CustomTrafficRepoImplTest - RepositoryTest로 변경 --- .../repository/TrafficRedisRepository.java | 23 +++---- .../trafficSignal/service/TrafficService.java | 2 - .../repository/CustomTrafficRepoImplTest.java | 61 +++++++++---------- .../TrafficRedisRepositoryTest.java | 12 ++-- 4 files changed, 45 insertions(+), 53 deletions(-) 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 4ffebf60..ac6837a1 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 @@ -15,6 +15,7 @@ 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; @@ -24,25 +25,26 @@ 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_INFO = "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 boolean isExist(){ - return hashOperations.hasKey(KEY_HASH, KEY_GEO); + return redisTemplate.hasKey(KEY_INFO); } public void save(TrafficResponse trafficResponse) { Long trafficId = trafficResponse.getTrafficSignalId(); + String trafficKey = KEY_INFO + trafficId; // GEO 데이터 저장 redisTemplate.opsForGeo().add( @@ -51,13 +53,8 @@ public void save(TrafficResponse trafficResponse) { trafficId.toString() ); - // HASH 데이터 저장 - hashOperations.put(KEY_HASH, trafficId.toString(), trafficResponse); - - // GEO와 HASH 모두에 TTL 설정 + valueOperations.set(trafficKey, trafficResponse, TTL); redisTemplate.expire(KEY_GEO, TTL); - redisTemplate.expire(KEY_HASH, TTL); - } public List findNearbyTraffics(double lat, double lng, double kiloRadius) { @@ -101,12 +98,12 @@ public List findNearbyTraffics(double lat, double lng, double k public TrafficResponse findById(Long id) { + String trafficId = String.valueOf(id); + String trafficKey = KEY_INFO + trafficId; log.debug("redis 캐싱 데이터 id로 검색 - id = {}", id); - String trafficId = String.valueOf(id); - - TrafficResponse data = hashOperations.get(KEY_HASH, trafficId); + TrafficResponse data = (TrafficResponse) valueOperations.get(trafficKey); if (data == null) { log.info("redis에 데이터 없음"); 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 c5b45096..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 @@ -20,8 +20,6 @@ @Transactional(readOnly = true) public class TrafficService { - private static final String TRAFFIC_REDIS_KEY = "traffic:info"; - private final CustomTrafficRepositoryImpl customTrafficRepository; private final TrafficRedisRepository trafficRedisRepository; private final TrafficRepository trafficRepository; 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 index 7b8579b3..7dfa8726 100644 --- a/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/CustomTrafficRepoImplTest.java +++ b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/CustomTrafficRepoImplTest.java @@ -1,47 +1,47 @@ package org.programmers.signalbuddyfinal.domain.trafficSignal.repository; -import static org.mockito.Mockito.when; - +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.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.PrecisionModel; +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; -@ExtendWith(MockitoExtension.class) +@Import({CustomTrafficRepositoryImpl.class}) public class CustomTrafficRepoImplTest extends RepositoryTest { - @Mock - private CustomTrafficRepository customTrafficRepository; + @Autowired + private CustomTrafficRepositoryImpl customTrafficRepository; - private List expected; + @Autowired + private EntityManager entityManager; @BeforeEach void setUp() { - expected = List.of( - TrafficResponse.builder() - .district("강남구") - .signalType("보행등") - .lat(37.5000) - .lng(127.0300) - .address("강남대로 123") - .build(), + TrafficFileResponse response = TrafficFileResponse.builder() + .serial(1L) + .district("용산구") + .signalType("보행신호등") + .address("서울특별시 용산구 한강대로 405") + .lat(37.5546) + .lng(126.9706) + .build(); - TrafficResponse.builder() - .district("서초구") - .signalType("보행등") - .lat(37.4950) - .lng(127.0200) - .address("서초대로 456") - .build() - ); + TrafficSignal signal = new TrafficSignal(response); + entityManager.persist(signal); + entityManager.flush(); } @Test @@ -49,19 +49,16 @@ void setUp() { void findNearestTrafficsTest(){ // Given - double lat = 37.4950; - double lng = 127.0200; - int radius = 1000; - - when(customTrafficRepository.findNearestTraffics(lat,lng,radius)).thenReturn(expected); + double lat = 37.5546; + double lng = 126.9706; + int radius = 10000; // When List results = customTrafficRepository.findNearestTraffics(lat, lng, radius); // Then Assertions.assertNotNull(results); - Assertions.assertEquals(2, results.size()); - Assertions.assertEquals("강남구",results.get(0).getDistrict()); + 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 index e1023baf..820e22e9 100644 --- a/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepositoryTest.java +++ b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepositoryTest.java @@ -31,6 +31,7 @@ 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; @ExtendWith(MockitoExtension.class) public class TrafficRedisRepositoryTest { @@ -39,12 +40,12 @@ public class TrafficRedisRepositoryTest { private RedisTemplate redisTemplate; @Mock - private HashOperations hashOperations; + private ValueOperations valueOperations; @Mock private GeoOperations geoOperations; - private static final String KEY_HASH = "traffic:info"; + private static final String KEY_INFO = "traffic:info"; private static final String KEY_GEO = "traffic:geo"; private static final Duration TTL = Duration.ofMinutes(5); @@ -59,7 +60,7 @@ public class TrafficRedisRepositoryTest { @BeforeEach void setUp() { - when(redisTemplate.opsForHash()).thenReturn(hashOperations); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); when(redisTemplate.opsForGeo()).thenReturn(geoOperations); trafficRedisRepository = new TrafficRedisRepository(redisTemplate); @@ -97,9 +98,8 @@ void trafficSaveTest(){ eq( String.valueOf(id) ) ); - verify(hashOperations).put(eq(KEY_HASH), eq(id.toString()), eq(expected.get(0))); + verify(valueOperations).set(eq(KEY_INFO + id.toString()), eq(expected.get(0)), eq(TTL)); verify(redisTemplate).expire(KEY_GEO, TTL); - verify(redisTemplate).expire(KEY_HASH, TTL); } @Test @@ -129,7 +129,7 @@ void trafficNearByTestReturnTrafficList(){ - doReturn(expected.get(0)).when(hashOperations).get(eq(KEY_HASH), eq(id.toString())); + doReturn(expected.get(0)).when(valueOperations).get(eq(KEY_INFO + id.toString())); //When trafficRedisRepository.save(expected.get(0)); From 25000eabb918bfc92a515aaeae26650bbe3173d7 Mon Sep 17 00:00:00 2001 From: Byungil Oh Date: Tue, 6 May 2025 17:51:36 +0900 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20build=20error=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 원인 KEY_INFO가 Redis 예약어로 설정되어있어 충돌이 나는 문제 해결 KEY_INFO->KEY_VALUE로 수정 --- .../repository/TrafficRedisRepository.java | 10 +++++----- .../repository/CustomTrafficRepoImplTest.java | 4 ---- .../repository/TrafficRedisRepositoryTest.java | 12 ++++++------ 3 files changed, 11 insertions(+), 15 deletions(-) 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 ac6837a1..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 @@ -28,7 +28,7 @@ public class TrafficRedisRepository { private final ValueOperations valueOperations; private final GeoOperations geoOperations; - private static final String KEY_INFO = "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); @@ -39,15 +39,15 @@ public TrafficRedisRepository(RedisTemplate redisTemplate){ } public boolean isExist(){ - return redisTemplate.hasKey(KEY_INFO); + return redisTemplate.hasKey(KEY_VALUE); } public void save(TrafficResponse trafficResponse) { Long trafficId = trafficResponse.getTrafficSignalId(); - String trafficKey = KEY_INFO + trafficId; + String trafficKey = KEY_VALUE + trafficId; // GEO 데이터 저장 - redisTemplate.opsForGeo().add( + geoOperations.add( KEY_GEO, new Point(trafficResponse.getLng(),trafficResponse.getLat()), trafficId.toString() @@ -99,7 +99,7 @@ public List findNearbyTraffics(double lat, double lng, double k public TrafficResponse findById(Long id) { String trafficId = String.valueOf(id); - String trafficKey = KEY_INFO + trafficId; + String trafficKey = KEY_VALUE + trafficId; log.debug("redis 캐싱 데이터 id로 검색 - id = {}", id); 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 index 7dfa8726..5f3be8a5 100644 --- a/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/CustomTrafficRepoImplTest.java +++ b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/CustomTrafficRepoImplTest.java @@ -6,10 +6,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.geom.Point; -import org.locationtech.jts.geom.PrecisionModel; import org.programmers.signalbuddyfinal.domain.trafficSignal.dto.TrafficFileResponse; import org.programmers.signalbuddyfinal.domain.trafficSignal.dto.TrafficResponse; import org.programmers.signalbuddyfinal.domain.trafficSignal.entity.TrafficSignal; 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 index 820e22e9..19e2eb1b 100644 --- a/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepositoryTest.java +++ b/src/test/java/org/programmers/signalbuddyfinal/domain/trafficSignal/repository/TrafficRedisRepositoryTest.java @@ -10,9 +10,7 @@ import java.time.Duration; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -29,7 +27,6 @@ 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.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; @@ -45,7 +42,7 @@ public class TrafficRedisRepositoryTest { @Mock private GeoOperations geoOperations; - private static final String KEY_INFO = "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); @@ -87,6 +84,7 @@ void setUp() { @Test @DisplayName("보행등 저장 테스트") void trafficSaveTest(){ + String trafficKey = KEY_VALUE + id; // When trafficRedisRepository.save(expected.get(0)); @@ -98,7 +96,7 @@ void trafficSaveTest(){ eq( String.valueOf(id) ) ); - verify(valueOperations).set(eq(KEY_INFO + id.toString()), eq(expected.get(0)), eq(TTL)); + verify(valueOperations).set(eq(trafficKey), eq(expected.get(0)), eq(TTL)); verify(redisTemplate).expire(KEY_GEO, TTL); } @@ -107,6 +105,8 @@ void trafficSaveTest(){ void trafficNearByTestReturnTrafficList(){ //Given + String trafficKey = KEY_VALUE + id; + double radius = 1; // result set @@ -129,7 +129,7 @@ void trafficNearByTestReturnTrafficList(){ - doReturn(expected.get(0)).when(valueOperations).get(eq(KEY_INFO + id.toString())); + doReturn(expected.get(0)).when(valueOperations).get(eq(trafficKey)); //When trafficRedisRepository.save(expected.get(0));