본문 바로가기
생각정리

[우아한테크코스 5기] 프리코스 온보딩 미션 학습 내용 및 회고

by greeng00se 2022. 11. 2.

1주 차

첫 번째 주의 온보딩 미션은 추후에 진행될 프리코스 미션을 위한 발판이 되어줄 수 있는 주차라고 생각했고 시작하기 전 요구사항을 만족하는 것 이외에 세 가지의 목표를 기준으로 삼았습니다.

  • 하나의 메서드가 하나의 기능을 간결하게 표현하는 것
  • 읽기 쉬운 코드를 작성하는 것
  • 의도가 담긴 커밋 메시지를 남기는 것

클래스를 분리하고 책임을 가진 객체들이 협력하는 방법으로 프로그래밍하지 않고, 단일 클래스에서 하나의 메서드가 하나의 기능을 간결하게 표현하는 것에 집중을 했습니다.

메서드를 분리하는 연습을 한 것은 추후에 객체지향 프로그래밍을 할 때도 도움이 될 꺼라 생각합니다.

리팩토링 부분에서 부족함이 많이 보였던 1주차였던 것 같습니다. 객체지향 프로그래밍 미션이 나왔을 때 더욱 파이팅 하겠습니다!

Problem 1

1번 문제는 예외 상황에 대한 검증 그리고 매직 넘버를 상수로 선언하고 점수를 계산하는 로직에 대한 부분을 고민 한 후 문제를 풀었습니다.

처리한 예외 상황은 다음과 같습니다.

  • 페이지가 연속되는가?
  • 입력 값이 1 ~ 400의 범위내에 들어오는가?
  • 왼쪽 페이지는 홀수, 오른쪽 페이지는 짝수번호 인가?
private static Integer calculateMaxScore(List<Integer> pages) {
    return pages.stream()
            .map(page -> Integer.max(calculateSumOfDigits(page), calculateProductOfDigits(page)))
            .max(Comparator.naturalOrder())
            .orElseThrow(IllegalArgumentException::new);
}

private static Integer calculateSumOfDigits(Integer page) {
    int result = 0;

    while (page != 0) {
        result += page % 10;
        page /= 10;
    }

    return result;
}

private static Integer calculateProductOfDigits(Integer page) {
    int result = 1;

    while (page != 0) {
        result *= page % 10;
        page /= 10;
    }

    return result;
}

점수를 구하는 부분은 문자열을 이용하는 방법과 나머지 연산을 이용해서 구하는 방법 두 가지를 생각했었고 기존의 입력값이 Integer였기 때문에 형변환 없이 그대로 사용하는 게 더 좋아보여서 나머지 연산을 이용해 점수를 구하는 쪽으로 구현했습니다.

Problem 2

2번 문제의 경우 처음에는 Stack을 사용하여 중복된 문자를 구하고 중복된 문자가 아닌 경우 결과에 추가하는 형식으로 풀었습니다.

추후에 정규 표현식을 이용하는 방법이 더 깔끔하다고 생각되어 리팩토링 하였습니다.

// aa, bb, ccc 같은 연속되는 중복 문자
private static final Pattern CONSECUTIVE_DUPLICATE_LETTERS = Pattern.compile("(.)\\1+");
private static final String EMPTY_STRING = "";

public static String solution(String cryptogram) {
    return decrypt(cryptogram);
}

private static String decrypt(String cryptogram) {
    String result = removeConsecutiveDuplicateLetters(cryptogram);

    if (result.equals(cryptogram)) {
        return result;
    }

    return decrypt(result);
}

private static String removeConsecutiveDuplicateLetters(String cryptogram) {
    return CONSECUTIVE_DUPLICATE_LETTERS.matcher(cryptogram).replaceAll(EMPTY_STRING);
}

Pattern의 경우 이펙티브 자바 Item 6를 참고하여 상수화하여 재사용했습니다.

또한 클린코드에서 언급한 정보를 제공하는 주석이 필요하다고 생각되어 주석을 추가했습니다.

Problem 3

1번 문제와 유사한 문제인 것 같습니다. 마찬가지로 숫자마다 박수 갯수를 구하는 메서드를 정의하고 스트림을 이용하여 합계를 계산했습니다.

public static int solution(int number) {
    return IntStream.range(START_NUMBER, number + 1)
            .map(Problem3::countClap)
            .sum();
}

private static int countClap(int number) {
    int result = 0;

    while (number != 0) {
        int digit = number % 10;
        if (includeOneOf369(digit)) {
            result += 1;
        }
        number /= 10;
    }

    return result;
}

private static boolean includeOneOf369(int digit) {
    return digit == CLAP_THREE || digit == CLAP_SIX || digit == CLAP_NINE;
}

Problem 4

알파벳 대문자는 알파벳 대문자로, 알파벳 소문자는 알파벳 소문자로 변환하는 문제입니다.

HashMap을 이용하여 문제를 풀었습니다.

문제에서 주어진 요구사항대로 알파벳 외의 문자를 변환하지 않기 위해서 getOrDefault() 메서드를 사용했습니다.

분기를 하나 추가하지 않아도 된다는 부분이 장점이라고 생각하였고 의도를 명확하게 표현하는 게 좋다고 생각하여 의도를 표현하는 주석을 추가하였습니다.

private static final int ALPHABETS_COUNT = 26;

public static String solution(String word) {
    Map<Character, Character> frogMap = generateFrogMap();
    return transform(frogMap, word);
}

private static Map<Character, Character> generateFrogMap() {
    HashMap<Character, Character> frogMap = new HashMap<>();

    char upperCaseLetterA = 'A';
    char upperCaseLetterZ = 'Z';
    char lowerCaseLetterA = 'a';
    char lowerCaseLetterZ = 'z';

    for (int i = 0; i < ALPHABETS_COUNT; i++) {
        frogMap.put(upperCaseLetterA++, upperCaseLetterZ--);
        frogMap.put(lowerCaseLetterA++, lowerCaseLetterZ--);
    }

    return frogMap;
}

private static String transform(Map<Character, Character> frogMap, String word) {
    StringBuilder result = new StringBuilder();

    for (char c : word.toCharArray()) {
        // 생성한 FrogMap에 없는 문자인 경우 문자 그대로 결과값에 더한다.
        result.append(frogMap.getOrDefault(c, c));
    }

    return result.toString();
}

추가로 문자열 연결하는 하는 경우 + 연산자와 StringBuilder를 사용하는 방법 중 StringBuilder를 사용하는 것이 좋은 방법이라고 판단되어 StringBuilder를 사용하기로 정했습니다.

Problem 5

전형적인 그리디 문제입니다.

큰 수부터 금액을 나눈다음에 결과값에 추가하는 방법을 이용하여 문제를 풀었습니다.

금액을 표현하는 부분이 명확해야 된다고 생각하여 enum을 생성하여 문제를 해결했습니다.

enum Money {
    _50000_WON(50_000),
    _10000_WON(10_000),
    _5000_WON(5_000),
    _1000_WON(1_000),
    _500_WON(500),
    _100_WON(100),
    _50_WON(50),
    _10_WON(10),
    _1_WON(1);

    private final int value;

    Money(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public static List<Money> highestOrder() {
        return Arrays.stream(values())
                .sorted((o1, o2) -> o2.value - o1.value)
                .collect(Collectors.toList());
    }
}

Problem 6

시간 복잡도에 대한 고민을 오히려 많이 한 문제였던 것 같습니다.

HashMap<분리된 닉네임, Email>을 사용하여 O(NM) 정도의 시간복잡도로 문제를 풀 수 있는 것 같습니다.

다음의 로직을 통해 HashMap을 생성하는 동시에 허용되지 않는 이메일을 구했습니다.

처음에 HashMap을 생성할 때 허용되지 않은 메일이 포함되더라도 충돌이 생긴 사람이 존재하는 경우 두 사람의 이메일이 모두 허용되지 않은 이메일 리스트에 포함되어 반환되도록 구현했습니다.

private static List<String> getInvalidEmails(HashMap<String, String> separatedNameToEmail, List<String> form) {
    String email = getEmail(form);
    String nickname = getNickname(form);

    if (!isValidDomain(email)) {
        return List.of();
    }

    List<String> result = new ArrayList<>();

    for (int i = 0; i < nickname.length() - 1; i++) {
        String separatedNickName = nickname.substring(i, i + 2);
        String invalidEmail = separatedNameToEmail.putIfAbsent(separatedNickName, email);
        if (invalidEmail != null) {
            result.add(invalidEmail);
        }
    }

    if (!result.isEmpty()) {
        result.add(email);
    }

    return result;
}

개개인 마다 위 메서드를 돌리고 반환값을 모으고 결과를 반환하는 부분은 Stream을 사용하여 구현했습니다.

public static List<String> solution(List<List<String>> forms) {

    HashMap<String, String> separatedNameToEmail = new HashMap<>();

    return forms.stream()
            .map(form -> getInvalidEmails(separatedNameToEmail, form))
            .flatMap(Collection::stream)
            .distinct()
            .sorted()
            .collect(toList());
}

Problem 7

HashMap을 사용하여 사용자의 친구의 친구들을 10점주고 방문자에게 1점을 주는 방식으로 문제를 풀었습니다.

결과에 포함하지 말아야하는 자기 자신과 친구, 0점인 사람의 경우 점수를 추가할 때 제외하지 않고 마지막에 Stream을 이용하여 필터링 했습니다.

또한 사용자의 친구를 제외하는 부분에서 기존에 생성했던 List<String>을 이용하여 HashSet을 새로 생성해 Contains의 시간 복잡도를 O(n)에서 O(1)로 줄일 수 있었습니다.

private static List<String> convertToResult(Map<String, Integer> scoreMap, HashSet<String> usersFriend, String user) {
    return scoreMap.keySet().stream()
            .filter(friend -> scoreMap.getOrDefault(friend, ZERO_POINT) != ZERO_POINT)
            .filter(friend -> !usersFriend.contains(friend))
            .filter(friend -> !friend.equals(user))
            .sorted(sortByHighScoreAndLexicographicalOrder(scoreMap))
            .limit(RESULT_LIMIT)
            .collect(Collectors.toList());
}

private static Comparator<String> sortByHighScoreAndLexicographicalOrder(Map<String, Integer> scoreMap) {
    return (friend1, friend2) -> {
        int difference = scoreMap.get(friend2) - scoreMap.get(friend1);
        if (difference == 0) {
            return friend1.compareTo(friend2);
        }
        return difference;
    };
}

참고 자료