-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: 데이터 조회 쿼리 최적화 진행 #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,30 +1,26 @@ | ||
| package com.spots.domain.ai.service; | ||
|
|
||
| import static java.math.BigDecimal.valueOf; | ||
| import static java.math.RoundingMode.HALF_UP; | ||
|
|
||
| import com.spots.domain.ai.dto.request.RecommendLLMRequest; | ||
| import com.spots.domain.ai.dto.request.RecommendLLMRequest.RecommendProgramData; | ||
| import com.spots.domain.ai.dto.request.UserInfoServiceRequest; | ||
| import com.spots.domain.ai.dto.response.WeeklyRecommendResponse; | ||
| import com.spots.domain.program.dto.response.ProgramDetailInfoResponse; | ||
| import com.spots.domain.program.dto.response.ProgramInfoResponse; | ||
| import com.spots.domain.program.dto.response.TransportData; | ||
| import com.spots.domain.program.dto.response.TransportDataRawWithFacility; | ||
| import com.spots.domain.program.entity.Program; | ||
| import com.spots.domain.program.repository.ProgramRepository; | ||
| import com.spots.domain.program.service.ProgramService; | ||
| import com.spots.domain.transport.repository.TransitRepository; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.concurrent.atomic.AtomicLong; | ||
| import java.util.function.Function; | ||
| import java.util.stream.Collectors; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.scheduling.annotation.Async; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
| import org.springframework.util.StopWatch; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
|
|
@@ -33,6 +29,7 @@ public class RecommendService { | |
|
|
||
| private final ProgramService programService; | ||
| private final ProgramRepository programRepository; | ||
| private final TransitRepository transitRepository; | ||
| private final RecommendLLMService recommendLLMService; | ||
|
|
||
| private static final double EARTH_RADIUS_KM = 6371.0; | ||
|
||
|
|
@@ -43,127 +40,60 @@ public CompletableFuture<WeeklyRecommendResponse> recommendWeeklyRoutine( | |
| UserInfoServiceRequest request | ||
| ) { | ||
|
|
||
| List<ProgramInfoResponse> programs = | ||
| programRepository | ||
| .searchPrograms( | ||
| request.toProgramInfoServiceRequest(), | ||
| 50L, | ||
| null, | ||
| null | ||
| ) | ||
| .getContent(); | ||
| log.info("programs size: {}", programs.size()); | ||
| List<RecommendProgramData> recommendProgramDataList = convertToRecommendProgramData(programs, request); | ||
| List<ProgramInfoResponse> programs = programRepository | ||
| .searchPrograms( | ||
| request.toProgramInfoServiceRequest(), | ||
| 50L, | ||
| null, | ||
| null | ||
| ) | ||
| .getContent(); | ||
|
|
||
| List<RecommendProgramData> recommendProgramDataList = convertToRecommendProgramData(programs); | ||
|
|
||
| RecommendLLMRequest llmRequest = RecommendLLMRequest.from(request, recommendProgramDataList); | ||
| WeeklyRecommendResponse response = recommendLLMService.createWeeklyPlan(llmRequest); | ||
|
|
||
| return CompletableFuture.completedFuture(response); | ||
| } | ||
|
|
||
| private List<RecommendProgramData> convertToRecommendProgramData( | ||
| List<ProgramInfoResponse> programs, | ||
| UserInfoServiceRequest request | ||
| ) { | ||
| // 성능 측정을 위한 스톱워치 생성 | ||
| StopWatch stopWatch = new StopWatch("DataConversionTask"); | ||
|
|
||
| // 1. ID 추출 | ||
| List<Long> programIds = programs.stream() | ||
| .map(ProgramInfoResponse::programId) | ||
| .toList(); | ||
|
|
||
| // 2. 배치 조회 측정 | ||
| stopWatch.start("Bulk DB Fetch (findAllById)"); | ||
| List<Program> programEntities = programRepository.findAllById(programIds); | ||
| stopWatch.stop(); | ||
|
|
||
| // Map 변환 | ||
| Map<Long, Program> programMap = programEntities.stream() | ||
| .collect(Collectors.toMap(Program::getId, Function.identity())); | ||
|
|
||
| // 3. 루프 처리 시간 측정 준비 | ||
| // stream 내부에서 시간을 누적하기 위해 AtomicLong 사용 | ||
| AtomicLong totalGetProgramTime = new AtomicLong(0); | ||
| AtomicLong totalCalcDistanceTime = new AtomicLong(0); | ||
|
|
||
| stopWatch.start("Stream Processing (Loop)"); | ||
|
|
||
| List<RecommendProgramData> result = programs.stream() | ||
| .map(programInfoResponse -> { | ||
|
|
||
| // A. 상세 정보 조회 시간 측정 (가장 의심되는 구간) | ||
| long startService = System.nanoTime(); | ||
| ProgramDetailInfoResponse programDetailInfoResponse = | ||
| programService.getProgram(programInfoResponse.programId()); | ||
| long endService = System.nanoTime(); | ||
| totalGetProgramTime.addAndGet(endService - startService); // 누적 | ||
|
|
||
| List<TransportData> transports = programDetailInfoResponse.transportData(); | ||
|
|
||
| Program program = programMap.get(programInfoResponse.programId()); | ||
|
|
||
| // B. 거리 계산 시간 측정 | ||
| double distance = 0.0; | ||
| if (program != null && program.getFacility() != null) { | ||
| long startCalc = System.nanoTime(); | ||
| distance = calculateDistance( | ||
| request.latitude(), | ||
| request.longitude(), | ||
| program.getFacility().getFcltyLa(), | ||
| program.getFacility().getFcltyLo() | ||
| ); | ||
| long endCalc = System.nanoTime(); | ||
| totalCalcDistanceTime.addAndGet(endCalc - startCalc); | ||
| } | ||
|
|
||
| return new RecommendProgramData( | ||
| programInfoResponse, | ||
| transports, | ||
| distance | ||
| ); | ||
| }) | ||
| .toList(); | ||
|
|
||
| stopWatch.stop(); | ||
|
|
||
| // 4. 결과 로그 출력 | ||
| log.info("================ 성능 측정 결과 ================"); | ||
| log.info(stopWatch.prettyPrint()); // 전체적인 요약 출력 | ||
| log.info(">> [상세] programService.getProgram() 총 소요 시간: {} ms", totalGetProgramTime.get() / 1_000_000); | ||
| log.info(">> [상세] 거리 계산 총 소요 시간: {} ms", totalCalcDistanceTime.get() / 1_000_000); | ||
| log.info("=============================================="); | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| private Double calculateDistance( | ||
| Double userLa, | ||
| Double userLo, | ||
| Double fcltyLa, | ||
| Double fcltyLo | ||
| private List<RecommendProgramData> convertToRecommendProgramData( | ||
| List<ProgramInfoResponse> programs | ||
| ) { | ||
| final double R = EARTH_RADIUS_KM; | ||
|
|
||
| double lat1 = Math.toRadians(userLa); | ||
| double lon1 = Math.toRadians(userLo); | ||
| double lat2 = Math.toRadians(fcltyLa); | ||
| double lon2 = Math.toRadians(fcltyLo); | ||
|
|
||
| double diffLat = lat2 - lat1; | ||
| double diffLon = lon2 - lon1; | ||
|
|
||
| double a = | ||
| Math.pow(Math.sin(diffLat / 2), 2) | ||
| + Math.cos(lat1) | ||
| * Math.cos(lat2) | ||
| * Math.pow(Math.sin(diffLon / 2), 2); | ||
|
|
||
| double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); | ||
|
|
||
| return valueOf(R * c) | ||
| .setScale(2, HALF_UP) | ||
| .doubleValue(); | ||
| List<Long> programIds = programs.stream() | ||
| .map(ProgramInfoResponse::programId) | ||
| .toList(); | ||
| List<Program> programEntities = programRepository.findAllWithFacility(programIds); | ||
| Map<Long, Program> programMap = programEntities.stream() | ||
| .collect(Collectors.toMap(Program::getId, Function.identity())); | ||
|
|
||
| List<Long> facilityIds = programEntities.stream() | ||
| .map(program -> program.getFacility().getId()) | ||
| .distinct() | ||
| .toList(); | ||
|
|
||
| Map<Long, List<TransportData>> transitMap = transitRepository | ||
|
Comment on lines
+71
to
+75
|
||
| .findTop2TransitByFacilityIds(facilityIds) | ||
| .stream() | ||
| .collect(Collectors.groupingBy( | ||
| TransportDataRawWithFacility::facilityId, | ||
| Collectors.mapping( | ||
| raw -> new TransportData( | ||
| raw.transportType(), | ||
| raw.transportName(), | ||
| raw.transportTime().longValue() | ||
| ), | ||
| Collectors.toList() | ||
| ) | ||
| )); | ||
|
|
||
| return programs.stream() | ||
| .map(info -> { | ||
| Program program = programMap.get(info.programId()); | ||
| Long facilityId = program.getFacility().getId(); | ||
| List<TransportData> transports = transitMap.getOrDefault(facilityId, List.of()); | ||
| return new RecommendProgramData(info, transports, info.distance()); | ||
| }) | ||
| .toList(); | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.spots.domain.program.dto.response; | ||
|
|
||
| public record TransportDataRawWithFacility( | ||
| Long facilityId, | ||
| String transportType, | ||
| String transportName, | ||
| Double transportTime | ||
| ) { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,16 @@ | ||
| package com.spots.domain.program.repository; | ||
|
|
||
| import com.spots.domain.program.entity.Program; | ||
| import java.util.List; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| @Repository | ||
| public interface ProgramRepository extends JpaRepository<Program, Long>, ProgramRepositoryCustom { | ||
|
|
||
| @Query("SELECT p FROM Program p JOIN FETCH p.facility WHERE p.id IN :ids") | ||
| List<Program> findAllWithFacility(@Param("ids") List<Long> ids); | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package com.spots.domain.transport.repository; | ||
|
|
||
| import com.spots.domain.program.dto.response.TransportDataRaw; | ||
| import com.spots.domain.program.dto.response.TransportDataRawWithFacility; | ||
| import com.spots.domain.transport.entity.FacilityTransit; | ||
| import java.util.List; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
@@ -12,7 +13,7 @@ | |
| public interface TransitRepository extends JpaRepository<FacilityTransit, Long> { | ||
|
|
||
| @Query(value = """ | ||
| SELECT DISTINCT ON (rank) | ||
| SELECT DISTINCT ON (rank) | ||
| pbtrnsp_fclty_sdiv_nm AS transportType, | ||
| bstp_subwayst_nm AS transportName, | ||
| (wlkg_mvmn_time / 60) + 1 AS transportTime | ||
|
|
@@ -22,4 +23,17 @@ SELECT DISTINCT ON (rank) | |
| """, | ||
| nativeQuery = true) | ||
| List<TransportDataRaw> findTop2Transit(@Param("facilityId") Long facilityId); | ||
|
|
||
| @Query(value = """ | ||
| SELECT DISTINCT ON (facility_id, rank) | ||
| facility_id AS facilityId, | ||
| pbtrnsp_fclty_sdiv_nm AS transportType, | ||
| bstp_subwayst_nm AS transportName, | ||
| (wlkg_mvmn_time / 60) + 1 AS transportTime | ||
| FROM facility_transit | ||
| WHERE facility_id IN (:facilityIds) AND rank IN (1, 2) | ||
| ORDER BY facility_id, rank, wlkg_mvmn_time ASC | ||
| """, | ||
|
Comment on lines
+32
to
+36
|
||
| nativeQuery = true) | ||
| List<TransportDataRawWithFacility> findTop2TransitByFacilityIds(@Param("facilityIds") List<Long> facilityIds); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ProgramServiceis no longer referenced in this class after the refactor, but it is still injected as a field. This adds an unnecessary dependency and can mislead future readers; remove the field (and corresponding constructor injection) if it’s not needed anymore.