본문 바로가기

우테코 프리코스

프리코스 1주차 미션

10월 19일 오후 3시.. 드디어 프리코스 미션이 공개되었다.

미션은 숫자 야구였고 기능 요구사항, 프로그래밍 요구 사항, 과제 진행 요구 사항 이렇게 세 가지로 구성되어 있었다.

 

먼저 프로그래밍 요구 사항에서 JDK 17버전에서 실행이 가능해야 하는 조건이 있었다.

이에 따라 기존의 19 버전을 17버전으로 다운그레이드했다.

오랜만에 환경변수를 설정해서 터미널에서 올바르게 17버전이 뜨도록 했고 인텔리제이에서도 설정하여 인텔리제이 내부 터미널에서도 17버전이 뜨도록 했다.

 

그리고 눈길이 가는 요구 사항이 2가지가 있었다.

첫째는 자체 제공하는 라이브러리에서 제공하는 Randoms 및 Console API를 사용하여 Random 값 추출 및 사용자가 입력하는 값을 구현하라는 점이었다.

Randoms 의 경우 예시 코드가 주어져있어서 이 코드를 보며 이해를 하던 중 이 예시 코드가 이번 미션에서 그대로 쓰일 수 있음을 알게됐다. 스스로 코드를 작성해볼 기회가 없어져서 아쉽기도 했지만 미션을 조금이라도 해결한 거 같아 다행이라는 생각도 들었다.

Console의 경우 평소 BufferReader를 사용했기에 새로운 라이브러리는 조금 당황스러웠다. 

BufferReader의 경우 객체 생성 후 메소드를 호출하기에 여기에 익숙해져있다가 이번 Console의 경우 생성자가 private이기도 하고 아예 비워져있어서 사용하지 않는 다는 것으로 보였고, Console 파일에 직접 접근해서 메소드를 호출했다.

 

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str = br.readLine();

 

최근 알고리즘 문제만 풀며 위와 같은 경우에 너무 익숙해져서 어찌보면 당연한 아래와 같은 경우를 바로 생각하지 못했다.

 

String input = Console.readLine();

 

본격적인 코드 작성 적인데도 기계적인 알고리즘 풀이만 했던 자신에 대해 벌써 반성하게 되었다.

 

또 하나의 눈길이 가는 요구사항은 java 코드 컨벤션 가이드를 준수한다는 것이었다. 

평소 그냥 적당히 보기 좋은 정도로 두루뭉실하게 코드를 구현하고 최종 수정했던 나에게 좋은 기준점이 등장하였다.

아직은 많이 익숙하지 않고 완벽하게 컨벤션을 적용하지도 못한다. 하지만 컨벤션을 읽어보며 좋은 코드란 것에 대해 생각해보게 되었고 코드 리팩토링에 있어서 지속적으로 신경쓰게 되었다.

비슷하게 객체지향 생활체조 9원칙이라는게 있었는데 그래도 9가지라서 컨벤션 가이드에 비해 많이 축약되어서 읽기 좋았다. 처음에는 이 9원칙을 중심으로 코드 컨벤션 가이드에서 조금씩 적용해보려 한다.

아래는 각각 공부할 때 들여다봤던 컨벤션 가이드와 생활체조 9원칙이다.

https://github.com/woowacourse/woowacourse-docs/tree/main/styleguide/java

https://jamie95.tistory.com/99

 

[Java] 객체지향 생활 체조 원칙 9가지 (from 소트웍스 앤솔러지)

1. 한 메서드에 오직 한 단계의 들여쓰기만 한다. 한 메서드에 들여쓰기가 여러 개 존재한다면, 해당 메서드는 여러가지 일을 하고 있다고 봐도 무관하다. 메서드는 맡은 일이 적을수록(잘게 쪼

jamie95.tistory.com

 

과제 진행 요구사항에서는 지정된 저장소에서의 Fork & Clone이 시작이었다. 주어진 미션을 해결하기 위해서 Git의 기본에대해 다시 한번 공부해보는 시간을 가졌다. 친절하게도 스크린 샷 첨부와 함께 해당 git repository에 설명되어 있어서 기본 세팅을 하는데에 있어서는 큰 어려움은 없었다.

또한 기능 구현 전 README 파일에 구현할 기능 목록을 정리하라는 요구 사항이 있어서 비록 단순한 숫자 야구일지라도 입력부터 작동을 거쳐 출력까지, 그리고 예외 처리까지 세밀하게 작성 후 기능 구현 개발 준비를 완료했다.

 

마지막으로 기능 요구 사항에서 숫자 야구의 규칙에 대한 설명과 입출력 요구 사항이 작성되어 있었다.

일단 먼저 냅다 구현을 했다. 그리고나서 기능별로 아래와 같이 각 클래스를 새로운 파일로 분리했다.

게임 재시작 여부가 포함된 메인 게임 클래스

정답 생성 클래스

입력값이 올바른지에 대한 여부 판단 클래스

스트라이크, 볼 여부 판단 클래스

스트라이크, 볼 여부 출력 클래스

그리고 상수들을 하나의 파일에 묶어서 저장함으로서 가독성을 향상시키고, 특정 값이 변경되어야 할때 한 곳에서만 수정하면 되므로 유지보수성이 올라가게 했다. 일일이 하나씩 바꾸게 된다면 오류가 발생할 가능성이 높아진다.

 

그 결과 아래와 같이 완성되었다.

 

1. 게임 재시작 여부가 포함된 메인 게임 클래스 (BaseballGame.java)

 

package baseball;

import camp.nextstep.edu.missionutils.Console;
import java.util.List;

public class BaseballGame {
    public static void startGame() {
        System.out.println("숫자 야구 게임을 시작합니다.");
        while (playGame()) {
            System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.");
            String reGame = Console.readLine();
            if (reGame.equals(Constants.QUIT_GAME)) {
                return;
            }
            if (!reGame.equals(Constants.RESTART_GAME)) {
                throw new IllegalArgumentException("올바른 입력이 아닙니다.");
            }
        }
    }

    private static boolean playGame() {
        List<Integer> answer = AnswerGenerator.generateAnswer();
        while (true) {
            System.out.print("숫자를 입력해주세요 : ");
            String input = Console.readLine();
            List<Integer> guess = InputValidator.validateInput(input);
            int[] result = StrikeBallCalculator.calculate(guess, answer);
            String message = GameResultMessage.generateResultMessage(result);
            System.out.println(message);

            if (message.equals("3스트라이크")) {
                System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료");
                return true;
            }
        }
    }
}

 

 

2. 정답 생성 클래스 (AnswerGenerator.java)

 

package baseball;

import camp.nextstep.edu.missionutils.Randoms;
import java.util.ArrayList;
import java.util.List;

public class AnswerGenerator {
    /*
     * 정답 생성
     * 중복되지 않은 세개의 숫자
     * 1 ~ 9 범위에 해당되는 숫자
     */
    public static List<Integer> generateAnswer() {
        List<Integer> answer = new ArrayList<>();
        while (answer.size() < 3) {
            int randomNumber = Randoms.pickNumberInRange(1, 9);
            if (!answer.contains(randomNumber)) {
                answer.add(randomNumber);
            }
        }
        return answer;
    }
}

 

 

3. 입력값이 올바른지에 대한 여부 판단 클래스 (InputValidator.java)

 

package baseball;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class InputValidator {
    public static List<Integer> validateInput(String input) {
        validateInputLength(input);
        List<Integer> guess = convertInputToIntegerList(input);
        validateInputRange(guess);
        validateInputForDuplicates(guess);
        return guess;
    }

    private static void validateInputLength(String input) {
        if (input.length() != Constants.NUMBER_COUNT) {
            throw new IllegalArgumentException("입력은 반드시 세 자리여야 합니다.");
        }
    }

    private static List<Integer> convertInputToIntegerList(String input) {
        List<Integer> guess = new ArrayList<>();
        for (int i = 0; i < Constants.NUMBER_COUNT; i++) {
            char c = input.charAt(i);
            guess.add(Character.getNumericValue(c));
        }
        return guess;
    }

    private static void validateInputRange(List<Integer> guess) {
        for (int num : guess) {
            if (num < Constants.MIN_NUMBER || num > Constants.MAX_NUMBER) {
                throw new IllegalArgumentException("입력은 1에서 9 사이의 숫자여야 합니다.");
            }
        }
    }

    private static void validateInputForDuplicates(List<Integer> guess) {
        Set<Integer> set = new HashSet<>();
        for (int number : guess) {
            if (!set.add(number)) {
                throw new IllegalArgumentException("중복된 숫자는 허용되지 않습니다.");
            }
        }
    }

}

 

 

4. 스트라이크, 볼 여부 판단 클래스 (StrikeBallCalculator.java)

 

package baseball;

import java.util.List;

public class StrikeBallCalculator {
    /*
     * 스트라이크, 볼 여부를 판단
     */
    public static int[] calculate(List<Integer> guess, List<Integer> answer) {
        int[] result = new int[2]; // ball, strike 순서

        for (int i = 0; i < Constants.NUMBER_COUNT; i++) {
            if (guess.get(i).equals(answer.get(i))) {
                result[1]++; // 스트라이크 증가
            } else {
                for (int j = 0; j < Constants.NUMBER_COUNT; j++) {
                    if (i != j && guess.get(i).equals(answer.get(j))) {
                        result[0]++; // 볼 증가
                    }
                }
            }
        }

        return result;
    }
}

 

 

5. 스트라이크, 볼 여부 출력 클래스 (GameResultMessage.java)

 

package baseball;

public class GameResultMessage {
    public static String generateResultMessage(int[] result) {
        /*
         * 스트라이크, 볼 여부 출력
         */
        String message = "";
        if (result[0] == 0 && result[1] == 0) {
            message = "낫싱";
        }
        if (result[1] == 3) {
            message = "3스트라이크";
        }
        if (result[0] > 0) {
            message += result[0] + "볼 ";
        }
        if (result[1] > 0 && result[1] != 3) {
            message += result[1] + "스트라이크";
        }

        return message;
    }
}

 

 

6. 상수 클래스 (Constants.java)

 

package baseball;

public class Constants {
    public static final int NUMBER_COUNT = 3;
    public static final int MIN_NUMBER = 1;
    public static final int MAX_NUMBER = 9;

    public static final String RESTART_GAME = "1";
    public static final String QUIT_GAME = "2";

}

 

완성 코드를 보니 그래도 처음에 비해 괜찮다는 생각이 들었다. 아직 많이 부족하지만 단순 문제 풀이가 아니라 무언가를 해냈다는 느낌이 들었다. 처음에 냅다 다 짜고나서 정답 생성 기능 부분을 따로 클래스화에서 파일을 만드려고 하는 거부터 애를 먹었다. 캡스톤 프로젝트의 과거는 잊어버리고 매번 하나의 파일에서 모든 걸 해결하는 알고리즘 풀이에서 비롯되었다고 생각했고 또 한번 알고리즘 풀이만 하던 자신을 반성하게 되었다.

 

실수와 고민들...

public class AnswerGenerator {
    /*
     * 정답 생성
     * 중복되지 않은 세개의 숫자
     * 1 ~ 9 범위에 해당되는 숫자
     */
    List<Integer> answer = new ArrayList<>();
    while (answer.size() < 3) {
        int randomNumber = Randoms.pickNumberInRange(1, 9);
        if (!answer.contains(randomNumber)) {
            answer.add(randomNumber);
        }
    }
}

 

처음 위와 같이 작성했을 때 오류가 났다.

while루프를 클래스 바로 아래에 두었기 때문에 발생한 오류라고 한다.

Java 클래스 내부에 클래스의 멤버로서 선언된 변수나 메서드와 같은 구조적 요소가 클래스의 구조를 정의하고, 실행 가능한 코드 블록은 메서드 내에 위치해야 하기 때문이다. 클래스는 데이터와 메서드의 집합으로 이해할 수 있으며, 클래스 내부에 직접 실행 가능한 코드를 두는 것은 클래스의 기본 구조에 어긋난다고 한다.

메인 함수에서는 항상

 

public static void main(String[] args)

 

이 존재하기 때문에 바로 구현부터 가능했던 점을 간과하고 다른 파일에서 그냥 구현부터 시작했던 것이었다.

(일부 분들께서는 경악할 만한 기초적인 실수이기도 하지만 다시는 안하고자 이렇게 작성한다)

과거에도 알고 있었고 사용했던 기본 개념인데 이걸 망각하고 문제풀이에만 적응해버린 자신이 너무 속상하고 안타까웠다. 우테코 프리코스를 통해 잊었던 기본 개념들을 다시 한번 복기하고 적용해보는 시간을 가져야겠다.

아래는 수정되어 가능한 코드와 Java 클래스 내부에 올 수 있는 요소들에 대한 설명이다.

 

public class AnswerGenerator {
    /*
     * 정답 생성
     * 중복되지 않은 세개의 숫자
     * 1 ~ 9 범위에 해당되는 숫자
     */
    public static List<Integer> generateAnswer() {
        List<Integer> answer = new ArrayList<>();
        while (answer.size() < 3) {
            int randomNumber = Randoms.pickNumberInRange(1, 9);
            if (!answer.contains(randomNumber)) {
                answer.add(randomNumber);
            }
        }
        return answer;
    }
}

 

Java 클래스 내부에는 다음과 같은 요소가 올 수 있다 ->

  1. 필드(멤버 변수): 클래스의 속성을 나타내며, 객체의 상태를 저장한다.
  2. 메서드: 클래스가 수행하는 작업을 정의하고, 동작을 구현한다.
  3. 생성자: 객체의 초기화를 담당하는 특별한 메서드다.
  4. 내부 클래스: 클래스 내부에 다른 클래스를 정의할 수 있다.

그리고 입력값의 중복 여부를 검사할 때 이중포문 vs HashSet 중에 고민했다.

작은 규모의 입력값이므로 성능차이는 미미하다고 판단, 가독성이 더 좋은 hashset을 선택했다.

 

private static void validateInputForDuplicates(int[] guess) {
    for (int i = 0; i < 3; i++) {
        for (int j = i + 1; j < 3; j++) {
            if (guess[i] == guess[j]) {
                throw new IllegalArgumentException("중복된 숫자는 허용되지 않습니다.");
            }
        }
    }
}

 

private static void validateInputForDuplicates(List<Integer> guess) {
    Set<Integer> set = new HashSet<>();
    for (int number : guess) {
        if (!set.add(number)) {
            throw new IllegalArgumentException("중복된 숫자는 허용되지 않습니다.");
        }
    }
}

 

만약 입력값의 규모가 커지면 가독성보다는 먼저 성능 문제로 인해 이중 포문을 선택하는게 맞다고 보는게 지금의 나의 생각이다.

 

또한 사용자에게 입력받는 숫자를 정답과 다르게 리스트가 아닌 배열에 저장했다. 입력받는 숫자의 개수가 일정하고 추가적인 삽입 및 삭제가 없기 때문에 탐색 측면에서 더 빠른 배열이 좋지 않을까 라는 생각을 해봤다.

 

마치며..

첫 주차 미션이라서 그런지 구현 자체에는 큰 어려움이 없었다. Git 사용과 앞으로 있을 프리코스와 최종 코딩테스트를 위한 환경 세팅 등을 위해, 그리고 어쩌면 많은 지원자가 처음 경험해볼 수 있는 좋은 코드에 대한 자세한 방법 등을 읽어보고 익히고 한 번 적용해보라는 배려가 있었기 때문이 아닐까 생각해본다. 최근 알고리즘 문제 풀이만 진행하던 나는 오랜만에 Git에 대해 다시 공부해보고 환경세팅을 해보았다. 숫자 야구 구현도 단순 문제 풀이였다면 그냥 구현만 하고 끝냈을 것을 생활체조 9원칙과 코드 컨벤션을 생각해보며 리팩토링에 대해 고민도 해보고 클래스 분리 등 리팩토링 과정에서 새로운 클래스 파일에 메서드를 선언하지않고 바로 구현 부분 코드를 옮겨서 오류가 뜨는, 누구는 경악할 만한 기본적인 실수로 인해 애를 먹기도 하고 알고리즘 풀이였다면 그냥 작동만 하면 다 된다고 생각했을 배열 vs 리스트 고민도 해보게 되었다. 또한 평소라면 아무 생각없이 사용했을 이중 for문에 대해 다시 한번 생각해 보기도 했다. 과거 캡스톤 프로젝트를 진행했을때도 어느정도 리팩토링을 해가며 프로젝트를 진행했지만 어디까지나 기준 없이 적당히 보기좋게 두루뭉실하게 했던 걸 생각하면 간단한 숫자 야구일지라도 발전한게 아닐까 하는 생각도 해본다. 비록 단순한 게임이지만 정말 최소한의 일을 맡도록 메서드를 쪼개보니 보기에도 더 좋고 추후 수정할 때도 더 편하다는게 한 눈에 느껴졌다. 기능을 쪼갤 수 있을 때까지 쪼개는게 아직은 익숙하지 않고, 좋은 코드라는 것에 대해 아직 많이 부족하고 생각할게 많으며, 하나의 메서드에서 사소한 변화까지 다 commit 하는게 너무 많이 하나 싶기도 해 어색하기까지 하지만 이러한 과정들이 모두 내가 지금보다 단 1이라도 좀 더 나은 개발자로 성장 중이며, 좋은 개발자로 성장할 가능성을 올려주는 값진 과정이라고 생각한다. 남은 2,3,4 주차 미션을 더 몰입해서 내 안의 잠재력을 깨우쳐 그것이 곧 결과로도 보이는 시간이 되었으면 좋겠다. 더 나아가 최종 코딩 테스트까지 도달해서 우아한 형제들에 가서 시험을 치르는 경험을 꼭 해보고 싶다.