설계 문제로 인한 테스트 불가, 도메인 설계 변경

Alex·2024년 7월 31일
0

Seoul_Nolgoat

목록 보기
8/11

이번 프로젝트에서 나는 정렬 기능을 담당한다.

이용자가 1차로 갈 곳, 2차로 갈 곳, 3차로 갈 곳의 가게 카테고리(한식, 술집, 고깃집 등등)를 정하면 각각의 조합을 짜서 정렬해야 한다.

가령, 1차 2차 3차 각각 100개의 가게들이 db에서 조회되면 100만개의 조합을 짜서 기준에 맞춰 정렬한다.

기준은 1)평점 2)거리다.

처음 구현할 때는 100만개의 조합을 정렬하고서 상위 10개 혹은 20개만을 다른 도메인으로 넘겼다.

거리 정렬에선 이 방식이 중요했다. 거리 정렬은 시작위치 -> 1차 가게 -> 2차 가게 -> 3차 가게의 좌표를 통해서 단순 거리를 구한다. 이는 실제 보행자 거리가 아니라 좌표로 계산한 물리적인 거리일 뿐이다.

그래서, TMap api를 호출해서 실제 보행자 거리를 구하고 이에 맞춰서 내림차순 정렬하는 방식으로 보완을 했다.

다만, TMap api는 하루에 1000번만 호출할 수 있다. 100만개 조합을 다 TMap으로 거리 보정을 할수가 없었다.

거리 순으로 100만개 조합을 정렬하고서 상위 20개만을 추리고, TMapi api로 보행자 거리를 구했다.

여기서 테스트가 어려워진다는 문제가 발생했다.

만약, 100만개 조합에서 평점 정렬을 하고 상위 20개만 넘기면, 내가 반환하는 결과물이 평점 기준으로 100만개의 조합중에서 상위 1등부터 20등까지인지 어떻게 확신할 수 있을까?

정렬 도메인에서는 상위 20개의 조합만 반환하는 public api밖에 없다.
이 api를 위해서 만든 메서드들은 private으로 감춰두었다.

테스트코드에서 public api가 반환하는 20개를 가지고, 결과물이 평점에 따라 내림차순 됐는지만 확인하면 되는걸까?

이렇게 되면 리팩토링을 할 때마다 정렬 도메인이 100만개를 정확히 평점, 거리순에 따라 내림차순을 했는지 확신할 수가 없게 된다.

당장 코드 하나만 바꿔도 결과물이 달라질 수 있는데?...

만약 상위 20개가 아니라 아무 조합 20개나 뽑아와서 그것만 내림차순하게끔 로직이 변경된다면??
테스트는 성공할 것이지만, 우리가 제공하고자 하는 기능은 본래의 목적을 이루지 못하게 된다.

여기서 고민이 생겼다.

1)private 메서드들을 public으로 변경한다. 즉, 100만개 조합을 내림차순 정렬하고 반환하는 api를 만들어놓고, 이를 public api에서 가져와서 호출하는 식으로 코드를 변경한다.

2)테스트를 할 때만 private 함수를 public으로 바꾸고 로직이 잘 돌아가는지 확인하고서 다시 private으로 변경한다.

(호눅스는 두가지 방법다 사용할 수는 있지만 설계에 문제가 있는건 아닌지 검토해보면 좋다고 조언해주셨다.)

정렬 도메인은 우리 서비스에서 핵심 도메인이다. 그만큼 유지보수가 중요할 것이라고 판단했다.
그래서, 코드를 변경할 때마다 계속 꼼꼼하게 테스트를 해야 한다고 생각했다.

그래서, 1번 방안을 쓰기로 했다.


public class SortService {

    private static final int TOP_FIRST = 0;
    private static final int TOP_TWENTIETH = 20;

    private final TMapService tMapService;

    public List<GradeSortCombinationDto> sortStoresByGrade(SortConditionDto<StoreForGradeSortDto> sortConditionDto) {
        int totalRounds = sortConditionDto.getTotalRounds();
        List<GradeSortCombinationDto> gradeCombinations = generateGradeCombinations(sortConditionDto, totalRounds);

        return sortCombinationsByGrade(gradeCombinations);
    }

    public List<DistanceSortCombinationDto> sortStoresByDistance(SortConditionDto<StoreForDistanceSortDto> sortConditionDto) {
        int totalRounds = sortConditionDto.getTotalRounds();
        List<DistanceSortCombinationDto> distanceCombinations = generateAndSortDistanceCombinations(
                sortConditionDto,
                totalRounds
        ).subList(TOP_FIRST, TOP_TWENTIETH);

        return fetchDistancesFromTMapApi(sortConditionDto.getStartCoordinate(), distanceCombinations, totalRounds);
    }

    // 테스트를 위해 접근제어자를 public으로 변경
    public List<DistanceSortCombinationDto> generateAndSortDistanceCombinations(
            SortConditionDto<StoreForDistanceSortDto> sortConditionDto,
            int totalRounds) {
        if (totalRounds == 3) {
            return createDistanceCombinationsForThreeRounds(
                    sortConditionDto.getFirstFilteredStores(),
                    sortConditionDto.getSecondFilteredStores(),
                    sortConditionDto.getThirdFilteredStores(),
                    sortConditionDto.getStartCoordinate()
            ).stream()
                    .sorted(Comparator.comparingDouble(DistanceSortCombinationDto::getTotalDistnace))
                    .toList();
        }
        if (totalRounds == 2) {
            return createDistanceCombinationsForTwoRounds(
                    sortConditionDto.getFirstFilteredStores(),
                    sortConditionDto.getSecondFilteredStores(),
                    sortConditionDto.getStartCoordinate()
            ).stream()
                    .sorted(Comparator.comparingDouble(DistanceSortCombinationDto::getTotalDistnace))
                    .toList();
        }
        if (totalRounds == 1) {
            return createDistanceCombinationsForOneRound(
                    sortConditionDto.getFirstFilteredStores(),
                    sortConditionDto.getStartCoordinate()
            ).stream()
                    .sorted(Comparator.comparingDouble(DistanceSortCombinationDto::getTotalDistnace))
                    .toList();
        }
        throw new RuntimeException();
    }

sort 도메인에서는

1)평점의 경우 조합 100만개를 그냥 바로 반환한다. 이를 테스트에서 내림차순 정렬됐는지 바로 확인할 수 있다.

2)거리 정렬은 마지막에 TMap 호출을 해야해서 (1) 단순 거리만을 계산하고서 100만개를 반환하는 public api (2)는 (1) api를 활용해서 100만개를 가져오고 이를 상위 20개로 자른 다음 tmap api를 호출해서 내림차순 정렬을 한다.

@SpringBootTest
class StoreSorterTest {

    @Autowired
    StoreRepository storeRepository;

    @Autowired
    SortService sortService;

    @Test
    void 평점_정렬_성공_카카오() {
        List<Long> firstIds = new ArrayList<>();
        Random random = new Random();

        // 랜덤 ID 생성
        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            firstIds.add(randomId);
        }

        List<Long> secondIds = new ArrayList<>();

        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            secondIds.add(randomId);
        }

        List<Long> thirdIds = new ArrayList<>();

        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            thirdIds.add(randomId);
        }

        List<Store> storeList1 = storeRepository.findAllById(firstIds);
        List<Store> storeList2 = storeRepository.findAllById(secondIds);
        List<Store> storeList3 = storeRepository.findAllById(thirdIds);

        List<StoreForGradeSortDto> firstStores = storeList1.stream().map(i -> new StoreForGradeSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()),
                i.getKakaoAverageGrade())).toList();
        List<StoreForGradeSortDto> secondStores = storeList2.stream().map(i -> new StoreForGradeSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()),
                i.getKakaoAverageGrade())).toList();
        List<StoreForGradeSortDto> thirdStores = storeList3.stream().map(i -> new StoreForGradeSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()),
                i.getKakaoAverageGrade())).toList();

        SortConditionDto<StoreForGradeSortDto> sortConditionDto = new SortConditionDto<>(
                new CoordinateDto(37.4979, 127.0276),
                firstStores,
                secondStores,
                thirdStores);

        List<GradeSortCombinationDto> combination = sortService.sortStoresByGrade(sortConditionDto);
        for (int i = 0; i < combination.size() - 1; i++) {
            assertThat(plusRate(combination.get(i)) >=
                    plusRate(combination.get(i + 1))).isTrue();
            System.out.println(plusRate(combination.get(i)));
        }
    }

    @Test
    void 평점_정렬_실패_카카오() {
        List<Long> firstIds = new ArrayList<>();
        Random random = new Random();

        // 랜덤 ID 생성
        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            firstIds.add(randomId);
        }

        List<Long> secondIds = new ArrayList<>();

        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            secondIds.add(randomId);
        }

        List<Long> thirdIds = new ArrayList<>();

        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            thirdIds.add(randomId);
        }

        List<Store> storeList1 = storeRepository.findAllById(firstIds);
        List<Store> storeList2 = storeRepository.findAllById(secondIds);
        List<Store> storeList3 = storeRepository.findAllById(thirdIds);

        List<StoreForGradeSortDto> firstStores = storeList1.stream().map(i -> new StoreForGradeSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()),
                i.getKakaoAverageGrade())).toList();
        List<StoreForGradeSortDto> secondStores = storeList2.stream().map(i -> new StoreForGradeSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()),
                i.getKakaoAverageGrade())).toList();
        List<StoreForGradeSortDto> thirdStores = storeList3.stream().map(i -> new StoreForGradeSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()),
                i.getKakaoAverageGrade())).toList();

        SortConditionDto<StoreForGradeSortDto> sortConditionDto = new SortConditionDto<>(
                new CoordinateDto(37.4979, 127.0276),
                firstStores,
                secondStores,
                thirdStores);

        List<GradeSortCombinationDto> combination = sortService.sortStoresByGrade(sortConditionDto);
        for (int i = 0; i < combination.size() - 1; i++) {
            assertThat(plusRate(combination.get(i)) <
                    plusRate(combination.get(i + 1))).isFalse();
            System.out.println(plusRate(combination.get(i)));
        }
    }

    @Test
    void 거리_TMAP_정렬_REPO_테스트_티맵_없이() {
        List<Long> firstIds = new ArrayList<>();
        Random random = new Random();

        // 랜덤 ID 생성
        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            firstIds.add(randomId);
        }

        List<Long> secondIds = new ArrayList<>();

        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            secondIds.add(randomId);
        }

        List<Long> thirdIds = new ArrayList<>();

        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            thirdIds.add(randomId);
        }

        List<Store> storeList1 = storeRepository.findAllById(firstIds);
        List<Store> storeList2 = storeRepository.findAllById(secondIds);
        List<Store> storeList3 = storeRepository.findAllById(thirdIds);

        List<StoreForDistanceSortDto> firstStores = storeList1.stream().map(i -> new StoreForDistanceSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()))).toList();
        List<StoreForDistanceSortDto> secondStores = storeList2.stream().map(i -> new StoreForDistanceSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()))).toList();
        List<StoreForDistanceSortDto> thirdStores = storeList3.stream().map(i -> new StoreForDistanceSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()))).toList();

        SortConditionDto<StoreForDistanceSortDto> sortConditionDto = new SortConditionDto<>(
                new CoordinateDto(37.4979, 127.0276),
                firstStores,
                secondStores,
                thirdStores);

        List<DistanceSortCombinationDto> combination = sortService.generateAndSortDistanceCombinations(sortConditionDto, sortConditionDto.getTotalRounds());
        for (int i = 0; i < combination.size() - 1; i++) {
            assertThat(combination.get(i).getTotalDistnace() <=
                    combination.get(i + 1).getTotalDistnace()).isTrue();
            System.out.println(combination.get(i).getTotalDistnace());
        }
    }

    @Test
    void 거리_TMAP_정렬_REPO_테스트_티맵_활용() {
        List<Long> firstIds = new ArrayList<>();
        Random random = new Random();

        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            firstIds.add(randomId);
        }

        List<Long> secondIds = new ArrayList<>();

        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            secondIds.add(randomId);
        }

        List<Long> thirdIds = new ArrayList<>();

        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            thirdIds.add(randomId);
        }

        List<Store> storeList1 = storeRepository.findAllById(firstIds);
        List<Store> storeList2 = storeRepository.findAllById(secondIds);
        List<Store> storeList3 = storeRepository.findAllById(thirdIds);

        List<StoreForDistanceSortDto> firstStores = storeList1.stream().map(i -> new StoreForDistanceSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()))).toList();
        List<StoreForDistanceSortDto> secondStores = storeList2.stream().map(i -> new StoreForDistanceSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()))).toList();
        List<StoreForDistanceSortDto> thirdStores = storeList3.stream().map(i -> new StoreForDistanceSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()))).toList();

        SortConditionDto<StoreForDistanceSortDto> sortConditionDto = new SortConditionDto<>(
                new CoordinateDto(37.4935, 127.0142),
                firstStores,
                secondStores,
                thirdStores);


        long startTime = System.nanoTime();
        List<DistanceSortCombinationDto> combination = sortService.sortStoresByDistance(sortConditionDto);
        long endTime = System.nanoTime();
        long duration = endTime - startTime;

        System.out.println("걸린 시간: " + duration + " nanoseconds");
        SoftAssertions softAssertions = new SoftAssertions();
        for (int i = 0; i < combination.size() - 1; i++) {
            softAssertions.assertThat(combination.get(i).getWalkRouteInfoDto().getTotalDistance() <=
                    combination.get(i + 1).getWalkRouteInfoDto().getTotalDistance()).isTrue();
            System.out.println(combination.get(i).getTotalDistnace());
        }

        for (int i = 0; i < combination.size() - 1; i++) {
            softAssertions.assertThat(combination.get(i).getWalkRouteInfoDto().getTotalTime() <=
                    combination.get(i + 1).getWalkRouteInfoDto().getTotalTime()).isTrue();
            System.out.println(combination.get(i).getWalkRouteInfoDto().getTotalTime());
        }
    }

    @DisplayName("총 거리+Tmap 동선 기준으로 내림차순 정렬한다.(실패)")
    @Test
    void 거리_TMAP_정렬_테스트2() {
        List<Long> firstIds = new ArrayList<>();
        Random random = new Random();

        // 랜덤 ID 생성
        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            firstIds.add(randomId);
        }

        List<Long> secondIds = new ArrayList<>();

        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            secondIds.add(randomId);
        }

        List<Long> thirdIds = new ArrayList<>();

        for (int i = 0; i < 20; i++) {
            long randomId = 3400L + random.nextInt(120000 - 3400 + 1);
            thirdIds.add(randomId);
        }

        List<Store> storeList1 = storeRepository.findAllById(firstIds);
        List<Store> storeList2 = storeRepository.findAllById(secondIds);
        List<Store> storeList3 = storeRepository.findAllById(thirdIds);

        List<StoreForDistanceSortDto> firstStores = storeList1.stream().map(i -> new StoreForDistanceSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()))).toList();
        List<StoreForDistanceSortDto> secondStores = storeList2.stream().map(i -> new StoreForDistanceSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()))).toList();
        List<StoreForDistanceSortDto> thirdStores = storeList3.stream().map(i -> new StoreForDistanceSortDto(
                i.getId(),
                i.getName(),
                new CoordinateDto(i.getLatitude(), i.getLongitude()))).toList();

        SortConditionDto<StoreForDistanceSortDto> sortConditionDto = new SortConditionDto<>(
                new CoordinateDto(37.4979, 127.0276),
                firstStores,
                secondStores,
                thirdStores);
        long startTime = System.nanoTime();
        List<DistanceSortCombinationDto> combination = sortService.sortStoresByDistance(sortConditionDto);
        long endTime = System.nanoTime();
        long duration = endTime - startTime;

        System.out.println("걸린 시간: " + duration + " nanoseconds");
        SoftAssertions softAssertions = new SoftAssertions();
        for (int i = 0; i < combination.size() - 1; i++) {
            softAssertions.assertThat(combination.get(i).getWalkRouteInfoDto().getTotalDistance() >
                    combination.get(i + 1).getWalkRouteInfoDto().getTotalDistance()).isFalse();
        }

        for (int i = 0; i < combination.size() - 1; i++) {
            softAssertions.assertThat(combination.get(i).getWalkRouteInfoDto().getTotalTime() >
                    combination.get(i + 1).getWalkRouteInfoDto().getTotalTime()).isFalse();
        }
    }


    private double plusRate(GradeSortCombinationDto gradeSortCombinationDto) {

        return gradeSortCombinationDto.getFirstStore().getAverageGrade() +
                gradeSortCombinationDto.getSecondStore().getAverageGrade() +
                gradeSortCombinationDto.getThirdStore().getAverageGrade();

    }
}

이제 코드를 리팩토링할 때마다 전체 테스트를 돌리면서 안심할 수 있게 됐다.

profile
답을 찾기 위해서 노력하는 사람

0개의 댓글