JAVA/우아한테크코스 4기

[211129] 우아한테크코스 프리코스 1주차 후기

kth990303 2021. 11. 29. 20:15
반응형

우아한테크코스 1차 심사에 합격하여 3주동안 프리코스에 참여할 수 있는 좋은 기회를 얻었다.

코딩테스트 합격 발표가 난 차주 수요일에 위와 같은 메일이 발송됐다.

프리코스 1주차 안내 메일이었다.

 

이번 프리코스 1주차 주제는, 지난 기수와 동일하게 '숫자야구'였다.

https://github.com/woowacourse/java-baseball-precourse 여기에 조건들과 룰이 적혀있다. 깃허브의 숫자야구 슈도코드를 fork해서 clone한 후, 깃허브 컨벤션에 맞게 1기능 1커밋 후 PR을 날리라는 미션이었다.

 

깃허브 관련 안내 링크는 https://github.com/woowacourse/woowacourse-docs/tree/master/precourse 여기서 확인할 수 있다.


숫자야구 조건

평범한 숫자야구와 룰은 같다.

서로 다른 세 자리의 수를 랜덤으로 컴퓨터가 만들어내면, 사용자가 그 수를 힌트를 통해 맞춰가는 게임이었다.

주의점 및 핵심 포인트는 아래와 같다.

  • 입력을 받을 때, 그리고 컴퓨터의 세 자리 수는 주어진 api를 사용하도록 한다.
  • indent는 2 이하로 한다. 즉, 이중for문 안에 if문이 들어있는 경우는 indent가 3이므로 불가능하다.
  • 사용자가 입력에 맞지 않는 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션을 종료한다.
  • 3항 연산자를 쓰지 않는다.
  • 자바 코드 컨벤션, 깃허브 코드 컨벤션을 지킨다.
  • 주어진 테스트 코드를 실패할 경우, 0점 처리한다.

 

 

코드를 직접 짜기 전, 자바 코드 컨벤션을 간단하게 쭉 훑어봤는데,

if문과 else if문에서 바로 엔터치지 말고 반드시 중괄호로 감싸줘야 한다는 점과 

같은 줄에 여러 변수를 선언하지 말아야 한다는 점을 명심하면서 코드를 작성했다. (그동안 ps에 너무 익숙해져버린 나..)


객체지향 경험이 처음이었던 나에게

이번 프리코스는 긴장되면서도 좋은 설계를 통해 내가 스스로 무언가를 개발한다는 설렘을 느낄 수 있었던 경험이었다.

사실 처음에는 C++로 절차지향적인 알고리즘 문제풀이만 해왔었기 때문에 잘하지 못할까 걱정이 더 앞섰던 상황이었다. 

 

그리고 무엇보다, 나는 자바프로그래밍 수업을 들은 적이 없다.

스프링 웹개발 독학을 하기 위해 그전에 자바 문법을 좀 익힌 것이 전부이다. 객체지향은 학교수업으로 들은 C++프로그래밍 과목으로 잠깐 맛만 익혔는데, 그땐 굉장히 무지성으로 설계했었다. 이러한 점도 좀 걱정스러운 부분이었다.

따라서 나는 프리코스 시작 전부터 조금씩조금씩 자바 문법과 객체지향 공부를 진행했으며, 백준 브론즈~실버 하위 문제들을 통해 stream, lambda를 익혀보는 시간을 가졌다.


생성자는 생성자의 기능만. 하나의 메소드는 최소단위의 기능만 하도록 분리하자

이번 프리코스를 통해 얻었던 가장 큰 객제지향적 교훈이다.

다른 사람들이 보기엔 별거 아닌 것처럼, 그리고 당연한 소리처럼 여겨질 수 있다.

물론 나도 이러한 이론 자체는 알고 있었지만, 이렇게 몸소 겪으면서 와닿았던 적이 처음(...맞겠지?) 이기 때문에 작성해본다.

 

맨 처음 작성했었던 BallInputNumber 클래스

public class BallInputNumber {
    static final int NUMBER_LENGTH = 3;
    static final int RANGE_START = 1;
    static final int RANGE_END = 9;

    private List<Integer> clientNumber;

    public BallInputNumber() {
        clientNumber = inputNumberByClient();
        InputNumberValidator inputNumberValidator = new InputNumberValidator(clientNumber);
        if (!inputNumberValidator.inputNumberExceptionCheck()) {
            throw new IllegalArgumentException();
        }
    }
}

처음에는 생성자에 생성자의 역할 뿐만 아니라 별별 기능을 다 넣었었다.

이 때 당시에는 "사용자가 매번 세자리수를 결정해야 되니까 그냥 생성자에 집어넣는게 깔끔하지 않을까?" 라는 생각으로 작성했던 것 같다.

 

그러나, 코드를 짜면 짤수록 불필요한 생성자 호출이 많아져, 인스턴스를 생성할 일이 지나치게 많아지게 되었고,

생성자를 아래와 같이 수정한 결과 다른 클래스들에서도 아래와 같은 변화를 가져올 수 있었다.

 

수정한 BallInputNumber 클래스 생성자

 public BallInputNumber() {
 	clientNumber = new ArrayList<>();
 }

이 생성자뿐만 아니라, 다른 생성자들도 최소한의 기능만 하도록 수정해준 결과 아래와 같은 변화를 가져올 수 있었다.

 

Game 클래스

public static void main(String[] args) {
  // Game game;
  Game game = new Game();
  do {
    // game = new Game();
    game.playBaseball();
  } while (game.askRestart());
}

그 전에는 매번 게임이 리플레이될 때마다 game 객체를 새로 생성하면서 낭비가 있었는데, 

좀 더 깔끔해진 모습을 볼 수 있다.

이는 생성자는 말그대로 생성자 역할만 하고, 게임 진행, 특정 기능의 기능을 함수분리를 해주었기 덕분에 얻을 수 있는 이점이다.


java stream API와 lambda를 익혀두자

또한 이번 프리코스는 스트림과 람다를 익혀보면서 사용법 및 장점을 간단하게나마 알 수 있게 된 계기가 됐다.

이는 indent 제약조건이 2였기 때문에 알 수 있게 된 것이다.

 

InputNumberValidator 클래스

/**
 * 사용자에게 입력받은 수가 올바른지 검증하는 클래스
 * 올바르지 않을 경우 예외를 발생시키고 종료시킨다.
 */
public class InputNumberValidator {
    static final int NUMBER_LENGTH = 3;
    static final int RANGE_START = 1;
    static final int RANGE_END = 9;

    public static boolean inputNumberExceptionCheck(List<Integer> inputNumberList) {
        return lengthCheck(inputNumberList)
                && rightRangeCheck(inputNumberList)
                && distinctCheck(inputNumberList);
    }

    public static boolean lengthCheck(List<Integer> inputNumberList) {
        return inputNumberList.size() == NUMBER_LENGTH;
    }

    public static boolean rightRangeCheck(List<Integer> inputNumberList) {
        return inputNumberList.stream()
                .filter(number -> RANGE_START <= number && number <= RANGE_END)
                .count() == NUMBER_LENGTH;
    }

    public static boolean distinctCheck(List<Integer> inputNumberList) {
        return inputNumberList.stream()
                .distinct()
                .count() == NUMBER_LENGTH;
    }
}

indent가 0으로 깔끔하게 검증 작업을 거칠 수 있었다.

원래는 하나의 메소드에 중복체크, 올바른길이체크, 수범위체크를 모두 집어넣으려 했으나, 하나의 메소드는 하나의 기능만 하도록 깔끔하게 작성했고, 결과도 만족스러웠다.

 

참고로 위 클래스에 대한 고민을 되게 많이 했다.

static 메소드를 사용할지, 그냥 메소드를 사용할지 굉장히 고민이 많았는데, 검증 작업은 어차피 BallInputNumber 클래스를 생성할 때 필수작업이므로 static 메소드로 해주지 않으면, 매번 new로 생성작업을 해줘야 한다고 판단했다.

때문에 static을 통해 인스턴스 생성을 하지 않고 사용할 수 있도록 하였다.

객체지향적으로 올바른 설계인가?는 의문이 들지만, 불필요한 소모를 줄이기 위해 그렇게 판단했다.

 

Game 클래스의 getScore 메소드: 몇볼 몇 스트라이크인지 알아내는 메소드

public void getScore() {
        String inputString = inputNumberList.stream()
                .map(String::valueOf)
                .collect(Collectors.joining());
        String answerString = answerNumberList.stream()
                .map(String::valueOf)
                .collect(Collectors.joining());

        for (int i = 0; i < BALL_SIZE; i++) {
            int index = inputString.indexOf(answerString.charAt(i));
            if (index == i) {
                strike++;
            } else if (index != -1) {
                ball++;
            }
        }
    }

볼인지 확인하기 위해선, 이중for문과 if문으로 indent 3이상을 피해갈 수 없었다.

따라서 고민하다가 stream 과 lambda를 통해 String형으로 바꿔준 후, for문으로 비교해주었다.

덕분에 indent 2로 마무리할 수 있었다. indent 1로 하고 싶었지만, 잘 생각이 나지 않을 뿐더러, 가독성 면에서도 지금 코드가 딱 적당한 것 같아 만족하며 마무리하였다.

 

원래는 getBall() 메소드, getStrike() 메소드를 따로 작성했다.

그러나 getBall() 메소드를 작성하다가, getStrike()와 유사한 작업이 반복해서 쓰였기 때문에 getScore()로 합쳤다.

덕분에 불필요한 반복을 줄일 수 있었다.


메시지는 상수로

 static final String RESTART = "1";
 static final String EXIT = "2";
 static final int BALL_SIZE = 3;

static final String BALL_HINT_MESSAGE = "볼 ";
static final String STRIKE_HINT_MESSAGE = "스트라이크";
static final String NOTHING_HINT_MESSAGE = "낫싱";
static final String VICTORY_MESSAGE = "3개의 숫자를 모두 맞히셨습니다! 게임 종료";
static final String RESTART_MESSAGE = "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.";

나중에 메시지를 수정해야 할 때,

바로바로 수정할 수 있도록 상수로 따로 분리하였다.

 

아예 상수들만 모아놓은 클래스를 만들까 고민하다가, 메시지들이 어디 클래스에서 쓰이는지 일일이 찾아봐야 할 고충을 줄이기 위해, 상수 사용 빈도가 높은 클래스 상단부에 상수를 선언하는 방식을 택하였다.


깃허브 컨벤션 적용

feat(기능 구현), fix(버그 수정), refactor(리팩토링), style(format, 코드스타일 수정) 과 같이 앞쪽에 키워드를 붙이면서 커밋하는 것을 처음으로 진행해봤다.

또한, 그동안은 1완성 1커밋 방식으로 했었는데, 1기능 1커밋으로 적용해보는 것이 훨씬 더 좋은 듯하다. 버그 생길 때 되돌리기도 편하고, 깃허브 메시지만으로 어떤 작업을 거쳤는지도 알 수 있기 때문이다.

 

다만, 컨벤션 적용이 처음이라 그런지, fix와 refactor를 혼동하여 잘못 쓴 경우도 있었고 ㅠㅠ

1기능 1커밋이라곤 하지만, 실제론 2~3기능 1커밋인 경우도 있었던 듯하다.

2주차와 3주차 때는 보다 나아질 수 있도록 노력해야 될 듯하다.


느낀 점

처음엔 걱정이 앞섰지만, 진행하면 할수록 굉장히 재밌었고 얻는 것도 생각보다 많아 만족스러웠다.

제출 소감문에도 쓴 내용이긴 한데, 함수 분리를 철저히 하면서 예술적으로 설계하는 과정 속에서 마치 스타크래프트 게임을 하는 듯한 느낌을 받아 더 재밌게 진행할 수 있었다. (소감문 그대로 쓴 내용...ㅎㅎ)

숫자야구는 굉장히 간단한 게임이지만, 자바 코드 컨벤션, indent 조건, 그리고 객체지향적 설계를 지키면서 진행하려다보니 많이 배우는 것 같다.

 

그리고 다른 분들의 코드를 참고하는 과정 속에서도 굉장히 많이 배운 것 같다.

우테코 예비 4기분들이 PR 날린 코드들을 살펴봤는데 정말 잘하시는 분들이 많은 것 같다.

2주차와 3주차에서도 지금처럼 많은 것을 얻고 성장해나가는 내 자신을 마주하고 싶다.

반응형