문제 해결 연습 : 성격 유형 검사하기

주싱·2023년 1월 25일
0

문제

링크 : 프로그래머스 > Level 1 > 성격 유형 검사하기

소요 시간

70분

해결과정 회고

  • 처음에 문제의 조건을 제대로 이해하지 못하고 문제를 풀었다. choices 입력을 질문지에 대한 응답자의 선택이 아니라 점수라고 잘못 이해했다. 잘못 이해한 것이 변수 및 메서드의 이름에 확연히 드러나며 문제 해결을 방해했다.
  • 긴밀하게 관련되어 함께 실행되어야 하는 것을 나누면 사용자에게 규칙에 맞게 호출해야 할 책임을 전가하는 것이 될 수 있다. 이럴 때는 적절히 캡슐화 하는 것이 좋겠다. 그러나 반대로 필연적인 연관성이 없는 것을 합쳐두는 것은 도리어 비지니스 로직을 코드에서 가리게 되는 역효과를 낳는다. 사용자의 선택에서 연관된 성격 유형을 얻는 것과 점수를 얻는 것은 분리하는 것이 좋았다.
    Optional<String> indicator = relatedIndicator(choices[i], twoIndicators);
    int score = mappedScore(choices[i]);
  • Enum 대신 문자열을 특정 상태를 지칭하는데 사용하면 컴파일 시점에 오류를 인지할 수 없는 단점이 있다.
  • 비지니스 로직이 메서드 이름에 전혀 드러나지 않게 지나치게 추상화를 한 것은 좋지 않았다. (getYourIndicator (X) → selectPersonalityWithHighScore (O))
  • 문제 해결을 위한 자료구조를 잘 초기화 해주면 로직에서 예외 처리를 제거해 코드를 단순화 할 수 있구나! 아래와 같이 해 둠으로 특정 성격 유형에 대한 질문이 없을 때 null 체크하는 코드를 제거할 수 있었다.
    private void initSheet(HashMap<String,Integer> database) {
        database.put("R", 0);
        database.put("T", 0);
        database.put("C", 0);
        database.put("F", 0);
        database.put("J", 0);
        database.put("M", 0);
        database.put("A", 0);
        database.put("N", 0);
    }
  • switch-case 문 대신 정수 배열을 사용해 사용자의 선택에 대한 점수를 단번에 매치시킬 수 있구나!
    private int mappedScore(int choice) {
        int[] scores = { -1, 3, 2, 1, 0, 1, 2, 3 };
        return scores[choice];
    }
  • 복잡한 비지니스 로직을 리팩토링 내성 지표 향상을 위해 블랙박스 테스트에만 의존하려고 하면 경우의 수가 기하급수적으로 늘어나 어려움을 겪는다. 이럴 때는 적절히 단계 단계를 잘 구분하고 각 단계를 완전하게 검증해 나가는 것도 좋은 방안 같다.
  • 복잡한 비지니스 로직을 이해하게 하는 변수, 메서드 네이밍은 매우 중요하다.
  • 리팩토링을 할 때 무엇을 할 것인지 커밋 메시지를 작성하고 하나씩 단계적으로 하는게 좋겠다.

잘한점

  • 우아한 방법을 몰라 아는 것으로 어떻게든 동작하게 만들려고 시도한 점이 좋았다. (Stream에서 제공하는 reduce를 활용해 문자를 문자열로 이어 붙이는 기능을 몰라서 foreach와 StringBuilder 활용해 처리함)
  • 테스트 케이스 실패로부터 예외적인 조건을 식별해서 추가해 나간 점이 좋았다.
  • 공개되지 않은 테스트 케이스 실패 시 문제의 조건을 다시 읽고 내 오류를 찾은 점이 좋았다.

학습한 것

  • 메서드를 활용한 파라미터화된 테스트 케이스 입출력 활용 (JUnit @ParameterizedTest, @MethodSource)
  • Stream의 reduce 기능 활용하여 문자를 문자열로 조립하기 (참조 → Guide to Stream.reduce())
    @Test
    void reduceCharsToOneString() {
        Stream<Character> chars = Stream.of('A', 'B', 'C');
        String result = chars.map(String::valueOf)
                .reduce("", (partialString, element) -> partialString + element);
        Assertions.assertThat(result).isEqualTo("ABC");
    }
  • 문자열에서 문자 하나하나를 간단하게 문자열 배열로 분리하는 방법 (String::split(””))
  • 문자열 배열 알파벳 순으로 정렬하기
    @Test
    void sortStringArray() {
        String[] array = { "A", "C", "B" };
        List<String> strings = Arrays.stream(array).sorted().toList();
        Assertions.assertArrayEquals(new String[]{"A", "B", "C"}, strings.toArray());
    }
  • 알파벳 크기 비교 (String::compareTo)

최종 리팩토링된 코드

import java.util.HashMap;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

@SuppressWarnings("DynamicRegexReplaceableByCompiledPattern")
public class PersonalTypeTest {

    static Stream<Arguments> testCaseSupplier() {
        return Stream.of(
                Arguments.arguments(new String[] {"AN", "CF", "MJ", "RT", "NA"}, new int[] {5, 3, 2, 7, 5}, "TCMA"),
                Arguments.arguments(new String[] {"TR", "RT", "TR"}, new int[] {7, 1, 3}, "RCJA"),
                Arguments.arguments(new String[] {"TR", "RT", "AN", "NA", "MJ", "JM"}, new int[] {7, 7, 1, 1, 3, 3}, "RCJA")
        );
    }

    @ParameterizedTest
    @MethodSource("testCaseSupplier")
    void testSolution(String[] survey, int[] choices, String expected) {
        String result = solution(survey, choices);
        Assertions.assertEquals(expected, result);
    }

    // 응답자는 어떤 질문에 대해 1~7번 답을 선택합니다. 질문은 예를 들면 AB 두 유형 중 어느 유형에 가까운지를 검사합니다.
    // 1~3번은 A에 가까운 정도를 나타내며, 5~7번은 B에 가까운 정도를 나타냅니다.
    // 1번으로 갈수록 A에 매우 가까운 정도이며, 7번으로 갈 수록 B에 매우 가까운 정도를 나타냅니다.
    // AB 두 유형이 획득한 점수가 동점인 경우 A,B중 알파벳 순서에 앞서는 유형이 선택됩니다.
    public String solution(String[] survey, int[] choices) {
        HashMap<String, Integer> scoreSheet = new HashMap<>(); // Key: 성격 유형, Value: 획득한 점수
        initSheet(scoreSheet);

        // 각 유형 별로 획득한 점수를 집계합니다.
        IntStream.range(0, survey.length).forEach(i -> {
            String[] twoIndicators = survey[i].split("");
            Optional<String> indicator = relatedIndicator(choices[i], twoIndicators);
            int score = mappedScore(choices[i]);

            if (indicator.isPresent()) {
                Integer sumOfScore = scoreSheet.get(indicator.get());
                sumOfScore += score;
                scoreSheet.put(indicator.get(), sumOfScore);
            }
        });

        // 집계한 데이터로부터 1~4번 지표의 성격 유형을 결정합니다.
        String[][] personalityTypes = { { "R", "T" }, { "C", "F" }, { "J", "M" }, { "A", "N" }};
        return Stream.of(personalityTypes)
                     .map(candidates -> selectPersonalityWithHighScore(candidates, scoreSheet))
                     .collect(Collectors.joining());
    }

    private void initSheet(HashMap<String,Integer> database) {
        database.put("R", 0);
        database.put("T", 0);
        database.put("C", 0);
        database.put("F", 0);
        database.put("J", 0);
        database.put("M", 0);
        database.put("A", 0);
        database.put("N", 0);
    }

    private Optional<String> relatedIndicator(int choice, String[] indicators) {
        if (choice >= 1 && choice <= 3) {
            return Optional.of(indicators[0]);
        } else if (choice >= 5 && choice <= 7) {
            return Optional.of(indicators[1]);
        } else {
            return Optional.empty();
        }
    }

    private int mappedScore(int choice) {
        int[] scores = { -1, 3, 2, 1, 0, 1, 2, 3 };
        return scores[choice];
    }

    private String selectPersonalityWithHighScore(String[] candidates, HashMap<String, Integer> scoreSheet) {
        Integer score0 = scoreSheet.get(candidates[0]);
        Integer score1 = scoreSheet.get(candidates[1]);

        if (score0 > score1) {
            return candidates[0];
        } else if (score0 < score1) {
            return candidates[1];
        } else {
            return getPrecedingAlphabet(candidates[0], candidates[1]);
        }
    }

    private String getPrecedingAlphabet(String str1, String str2) {
        return str1.compareTo(str2) > 0 ? str2 : str1;
    }
}

초기 문제 해결 코드

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

import java.util.HashMap;
import java.util.Optional;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class PersonalTypeTest {

    @Test
    void testSolution() {
        String result = solution(new String[]{"TR", "RT", "TR"},
                              new int[]{7, 1, 3});
        Assertions.assertEquals("RCJA", result);
    }

    enum ChoiceType {
        POSITIVE, NONE, NEGATIVE
    }

    /**
     * N개의 질문 각각이 조사하는 두 가지 성격 유형을 문자열 배열로 주어집니다. 그리고 그에 대한 설문자의 답변이 1~7번까지 주어집니다.
     * 이 때 질문자의 성격 유형(유형에는 번호가 부여되어 있음)을 번호 순서대로 결정하여 반환합니다. 한 지표에서 두 가지 성격 유형이
     * 동점인 경우에는 사전 순으로 빠른 유형을 선택합니다.
     */
    public String solution(String[] survey, int[] choices) {
        HashMap<Character, Integer> database = new HashMap<>();

        // 각 질문지가 조사하는 두 성격 유형에 대한 조사자의 답변 데이터를 수집합니다.
        IntStream.range(0, survey.length)
                .forEach(i -> {
                    int choiceScore = choices[i];
                    ChoiceType choiceType = matchChoiceType(choiceScore);
                    String twoSurveyType = survey[i];
                    Optional<Character> matchedType = matchPersonalType(twoSurveyType, choiceType);
                    int score = matchChoiceScore(choiceScore);
                    matchedType.ifPresent( type -> {
                        Integer prevScore = database.get(matchedType.get());
                        if (prevScore == null) {
                            prevScore = 0;
                        }
                        database.put(matchedType.get(), prevScore + score);
                    });
                });

        // 데이터로부터 1~4번 지표의 성격 유형을 판단합니다.
        StringBuilder answer = new StringBuilder();
        Stream.of("RT", "CF", "JM", "AN")
              .map(twoTypeAlphabetic -> toSelectedType(database, twoTypeAlphabetic))
              .forEach(answer::append);

        return answer.toString();
    }

    private Character toSelectedType(HashMap<Character, Integer> database, String twoTypeAlphabetic){
        Integer firstScore = database.get(twoTypeAlphabetic.charAt(0));
        Integer secondScore = database.get(twoTypeAlphabetic.charAt(1));
        if (firstScore == null && secondScore == null) {
            return twoTypeAlphabetic.charAt(0);
        }
        if (firstScore == null) {
            return twoTypeAlphabetic.charAt(1);
        }
        if (secondScore == null) {
            return twoTypeAlphabetic.charAt(0);
        }
        if (firstScore >= secondScore) {
            return twoTypeAlphabetic.charAt(0);
        } else {
            return twoTypeAlphabetic.charAt(1);
        }
    }

    private int matchChoiceScore(int selection) {
        HashMap<Integer, Integer> database = new HashMap<>();
        database.put(7, 3);
        database.put(6, 2);
        database.put(5, 1);
        database.put(4, 0);
        database.put(3, 1);
        database.put(2, 2);
        database.put(1, 3);
        return database.get(selection);
    }

    private ChoiceType matchChoiceType(int selection) {
        if (selection >= 1 && selection <= 3) {
            return ChoiceType.NEGATIVE;
        } else if (selection >= 5 && selection <= 7) {
            return ChoiceType.POSITIVE;
        } else {
            return ChoiceType.NONE;
        }
    }

    private Optional<Character> matchPersonalType(String twoSurveyType, ChoiceType choiceType) {
        if (choiceType == ChoiceType.NEGATIVE) {
            return Optional.of(twoSurveyType.charAt(0));
        } else if (choiceType == ChoiceType.POSITIVE) {
            return Optional.of(twoSurveyType.charAt(1));
        } else {
            return Optional.empty();
        }
    }
}
profile
소프트웨어 엔지니어, 일상

0개의 댓글