문제 해결 연습 | 신고 결과 받기

주싱·2023년 2월 1일
0

문제

링크 : 프로그래머스 > Level 1 > 신고 결과 받기

소요 시간

40분

최종 리팩토링된 코드

  • 방법 개선
    • A라는 사람이 반복해서 B를 신고하는 경우 신고 횟수를 1로 카운트 해야되는데 기존에는 조건문으로 이를 체크했습니다. 그러나 다시 보니 자료 구조 자체를 중복을 허용하지 않는 Set 자료구조를 사용하면 좋겠습니다. (기존에는 List을 사용하고 있었다) 실행 속도도 데이터가 많은 테스트 케이스에서 90msec 가량 줄었습니다.
  • 이름 개선
    • Map 자료구조의 이름은 keyAndValueMap 형태로 짓는게 코드를 읽고 이해하기 좋은 것 같습니다.
    • ReportUser 보다는 Reporter 라는 한 단어의 명사를 쓰는게 더 좋겠습니다.
    • String::split된 결과는 aAndB 처럼 인덱스 순서로 이름을 붙이는 것도 괜찮네요.
  • 코드 스타일
    • IntStream.range(…).forEach() 까지는 한 줄에 쓰는 것이 가독성이 더 좋은 것 같습니다. 탭이 중첩해서 등장하면 일단 눈에 잘 들어오지 않습니다.
public class GetReportResultTest {
    /**
     * 게시판의 사용자 목록(ID)과 신고 정보 목록 그리고 사용정지에 해당하는 신고횟수를 입력 받아서 각 사용자 별 신고 결과 알림을 받는 횟수를 반환합니다.
     * @param users 사용자 목록
     * @param report "A B"인 경우 A가 B를 신고한 정보를 뜻합니다. 신고 정보가 중복되는 경우 사용정지를 위한 통계에 포함시키지 않습니다. 자신이 자기를 신고하는 경우는 없습니다.
     * @param dropOutNum 해당 숫자 이상 신고를 받은 사용자는 사용정지 대상이 됩니다.
     * @return 사용정지된 사용자를 신고한 횟수
     */
		public int[] solution(String[] users, String[] report, int dropOutNum) {
        // 각 사용자를 신고한 사람 목록을 중복을 제외하고 수집합니다.
        HashMap<String, Set<String>> userAndReportersMap = new HashMap<>();
        IntStream.range(0, users.length)
                 .forEach(i -> userAndReportersMap.put(users[i], new HashSet<>()));

        IntStream.range(0, report.length).forEach(i -> {
            String[] reporterAndTargetUser = report[i].split(" ");
            String reporter = reporterAndTargetUser[0];
            String targetUser = reporterAndTargetUser[1];
            Set<String> reporters = userAndReportersMap.get(targetUser);
            reporters.add(reporter);
        });

        // 각 사용자의 사용중지될 사용자 신고 횟수를 카운트 합니다.
        HashMap<String, Integer> reporterAndAlarmCountMap = new HashMap<>();
        IntStream.range(0, users.length)
                 .forEach(i -> reporterAndAlarmCountMap.put(users[i], 0));
        userAndReportersMap.forEach( (user, reporters) -> {
            if (reporters.size() >= dropOutNum) {
                reporters.forEach(reporter -> {
                    int alarmNum = reporterAndAlarmCountMap.get(reporter);
                    alarmNum++;
                    reporterAndAlarmCountMap.put(reporter, alarmNum);
                });
            }
        });

        // 약속된 정수 배열로 반환합니다.
        int[] alarmNums = new int[users.length];
        IntStream.range(0, users.length)
                 .forEach(i -> alarmNums[i] = reporterAndAlarmCountMap.get(users[i]));

        return alarmNums;
    }
}

초기 문제 해결 코드

40분이라는 시간동안 테스트 케이스를 통과하도록 작성한 날 것 그대로의 코드입니다.

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class GetReportResultTest {

    /**
     * 게시판의 사용자 목록(ID)과 신고 정보 목록 그리고 사용정지에 해당하는 신고횟수를 입력 받아서 각 사용자 별 신고 결과 알림을
     * 받는 횟수를 반환합니다.
     * @param users 사용자 목록
     * @param report "A B"인 경우 A가 B를 신고한 정보를 뜻합니다. 신고 정보가 중복되는 경우 사용정지를 위한 통계에 포함시키지 않습니다. 자신이 자기를 신고하는 경우는 없습니다.
     * @param dropOutNum 해당 숫자 이상 신고를 받은 사용자는 사용정지 대상이 됩니다.
     * @return 사용정지된 사용자를 신고한 횟수
     */
    public int[] solution(String[] users, String[] report, int dropOutNum) {
        // "A B" (A가 B를 신고) 형식의 리포트를 분석해서 누가, 누구를 신고했는지 정보를 모읍니다.
        // 사용자 별로 자신을 신고한 다른 사용자 목록을 알고 있어야 합니다.
        HashMap<String, List<String>> myReportUserMap = new HashMap<>();
        IntStream.range(0, users.length)
                 .forEach(i -> myReportUserMap.put(users[i], new ArrayList<>()));

        IntStream.range(0, report.length)
                .forEach(i -> {
                    String[] aReportB = report[i].split(" ");
                    List<String> myReportUser = myReportUserMap.get(aReportB[1]);
                    if (!myReportUser.contains(aReportB[0])) {
                        myReportUser.add(aReportB[0]);
                    }
                });

        // 결국 사용 중지 당한 사용자를 신고한 사람을 카운팅 해야합니다.
        HashMap<String, Integer> alarmNumMap = new HashMap<>();
        IntStream.range(0, users.length)
                 .forEach(i -> alarmNumMap.put(users[i], 0));

        myReportUserMap.forEach( (user, myReportUsers) -> {
            if (myReportUsers.size() >= dropOutNum) {
                myReportUsers.forEach(reportUser -> {
                    int alarmNum = alarmNumMap.get(reportUser);
                    alarmNum++;
                    alarmNumMap.put(reportUser, alarmNum);
                });
            }
        });

        // 최종적으로 사용자 별로 사용정지된 사용자를 신고한 횟수를 반환합니다.
        int[] alarmNums = new int[users.length];
        IntStream.range(0, users.length)
                 .forEach(i -> alarmNums[i] = alarmNumMap.get(users[i]));

        return alarmNums;
    }
}

개선할 점

  • 비지니스 로직의 제약사항이 코드에 모두 반영되었는지 추적이 되지 않아 코드에 확신을 가질 수 없었습니다. 제약사항을 주석에 정리하고, 코드에 반영되었는지 추적하는 노력이 필요하겠습니다.
  • 비지니스 로직을 코드에 녹여 내기 쉽게 적절한 변수 및 메서드 이름을 짓는 것이 매우 중요한 것 같습니다.
  • 비지니스 로직에서 중요한 차이를 가지는 두 객체를 reporter, reported와 같이 같은 단어의 형태만 바꾸어 사용하는 것은 코드를 이해하는데 방해가 됩니다. reporter, targetUser와 같이 확실히 티가 나게 네이밍하는 것이 나은 것 같습니다.

잘한점

  • 최종 결과를 얻기 위해 어떤 데이터를 수집해 나가야 하는지, 또 그 데이터를 모으기 위해서 어떤 데이터를 모아야 하는지 반복해서 접근한 것이 문제를 논리적으로 해결하는데 도움이 되었습니다.
  • 자료구조를 잘 초기화해 둠으로 로직이 자연스럽고 이해하기 쉽게 작성된 것 같습니다.

학습한 것

  • 다른 사람의 코드를 보니 Map::getOrDefault 메서드와 중복 제거를 위해 Stream::distinct 를 사용해 문제를 해결하는 모습이 인상깊습니다. 학습 테스트 코드를 작성해 봅니다.
    @Test
    void getOrDefault() {
        // Given
        Map<String, Integer> userAndAgeMap = new HashMap<>();
        userAndAgeMap.put("Joo", 40);
        userAndAgeMap.put("Yang", 39);
    
        // Expected
        Assertions.assertEquals(10, userAndAgeMap.getOrDefault("SSa", 10));
        Assertions.assertEquals(40, userAndAgeMap.getOrDefault("Joo", 10));
        Assertions.assertEquals(39, userAndAgeMap.getOrDefault("Yang", 10));
    }
    
    @Test
    void distinctRemovesDuplicatedItems() {
        // When
        List<Integer> integers = Stream.of(1, 2, 3, 4, 5, 5, 4, 3, 2, 1)
                                       .distinct()
                                       .toList();
    
        // Expected
        Assertions.assertArrayEquals(new Integer[] { 1, 2, 3, 4, 5 }, integers.toArray());
    }

느낀점

혼자 문제를 풀고 리팩토링해 보는데 아쉬운 마음이 듭니다. 다른 사람들과 같이 리뷰도 하고, 내 머리 밖에 있는 더 좋은 방법들에 대해 교류하면 좋겠다는 생각이 듭니다.

profile
소프트웨어 엔지니어, 일상

0개의 댓글