Home [우아한테크코스 4기 프리코스] 1주차 및 2주차 후기
Post
Cancel

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

이번 우아한테크코스 프리코스 4기에 함께 성장하는 방법을 배워보려 지원했다. 그러기 위해서는 함께 사용할 수 있는 코드(객체지향적 코드)를 작성해야 했고 이를 프리코스라는 과정을 통해 간접적으로 경험해볼 수 있었다.

1주차 과제는 숫자야구게임으로, 1주차 피드백만으로도 충분히 받아들일 수 있었고, 고통스럽게 하는 고민은 없었다. 다만, 2주차 과제에 고민이 있어 객체지향에 대해 고민을 더 많이 해본 선배님께 피드백을 받고 포스팅으로 고민을 공유하려한다. 1, 2주차 래포는 다음과 같다.

1주차

https://github.com/ChoiEungi/java-baseball-precourse

2주차

https://github.com/ChoiEungi/java-racingcar-precourse

1. InputRole의 멤버 변수에 대한 고민

본래 코드는 다음과 같았다. 본 코드에서 고민은 크게 2가지 였다.

  1. try- catch를 이용해 에러 헨들링을 진행해야 하는데, depth가 늘어나고, 코드의 중복 부분이 많아지고 이후 확장할 때 메서드의 길이가 15을 넘어갈 수 있을 것 같다.
  2. 꼭 nameList와 trialNumber가 class의 멤버 변수여야 하는가?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class InputRole {
		private String[] nameList;
    private Integer trialNmber;
		public void inputStart() {
        while (true) {
            try {
                inputNames();
                break;
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
        while (true) {
            try {
                inputTrialNumber();
                break;
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }
	
    public String[] getNameList() {
        return nameList;
    }
		
    public int getTrialNmber() {
        return trialNmber;
    }
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GameController {
	public void gameStart() {
      InputRole inputRole = new InputRole();
      OutputRole outputRole = new OutputRole();
      inputRole.inputStart();
      changeInputToCar(inputRole);

      outputRole.pirntResultInstruction();
      for (int i = 0; i < inputRole.getTrialNmber(); i++) {
          tryGameOnce();
          outputRole.printOneGame(carList);
      }
      findWinner();
      outputRole.printWinner(winnersList);
  }
}

우선 1번 고민에 대해서는 try-catch 문의 기능적으로 크게 좋은 솔루션이 없어, 단순히 메서드를 분리하려 했다. 이는 캡슐화가 제대로 이뤄진다고 보기는 어렵지만, 지금 현실적인 상황에서 가장 최선의 방법이었다. 그리고 인풋을 받는 것이 극단적인 스케일인 몇 만개 이런 식으로 늘어나는 변화는 현실적으로 어렵기 때문에, 단순히 메서드를 분리하는 것으로 해결했다. 그러다보니 class 멤버 변수를 크게 사용할 이유가 없었고 inputStart() 메서드에 구애 받기 보다는 Util로서의 Input을 설정했다.

InputRole 이 단순히 인스턴스로서 상태를 갖는 객체일 수 있는데, 어디서든 특정 역할에 대한 인풋을 받고 싶으면 상태를 갖는 것이 아니라, 필요할 때 마다 필요한 인풋을 받아서 인풋의 결과값을 출력해주면 여러 클래스에서 여러 인스턴스를 선언 하지 않고 사용할 수 있게 된다.

고민 후 수정한 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public String[] getNameList() {
        while (true) {
            try {
                return inputNames();
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }

    public Integer getTrialNmber() {
        while (true) {
            try {
                return inputTrialNumber();
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }

private String[] inputNames() {
        System.out.println(NAME_INPUT_INSTRUCTION);
        String inputNames = Console.readLine();
        String[] nameList = inputNames.split(",");
        for (String name : nameList) {
            checkNameWhiteSpaceValid(name);
            checkNameLengthValid(name);
        }
        return nameList;
    }

private Integer inputTrialNumber() {
        System.out.println(TRIAL_NUMBER_INPUT_INSTRUCTION);
        String inputNumber = Console.readLine();
        checkTrialNumberValid(inputNumber);
        return Integer.valueOf(inputNumber);
    }

...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class GameController {
    private static final InputRole inputRole = new InputRole();

    public void gameStart() {
        OutputRole outputRole = new OutputRole();
        changeInputToCar(inputRole.getNameList());

        outputRole.pirntResultInstruction();
        for (int i = 0; i < inputRole.getTrialNmber(); i++) {
            tryGameOnce();
            outputRole.printOneGame(carList);
        }
        findWinner();
        outputRole.printWinner(winnersList);
    }

이 코드를 보면 inputRole을 더 유연하게 사용할 수 있다는 점과 코드가 간결해진 점에서 크게 유용할 수 있었다.

2. Game 도메인 추가

  • GameController의 역할을 고민하다 보니 GameController의 역할이 너무 많다는게 느껴졌다. 내가 생각한 역할은 다음과 같다.
  1. InputRole에서 받아온 후 이를 게임을 진행한 후 OutputRole에 출력하는 역할
  2. Game을 진행한다.
    • 랜덤 숫자 생성
    • 게임을 진행한다.
    • 게임의 승자를 찾는다.

이러한 방식으로 역할이 나뉘었는데, 1번 2번은 같은 역할로서 볼 수 있겠지만, 3번은 충분히 분리해도 좋을 법할 것 같다는 생각을 해볼 수 있었다. 3번을 분리하기 전에 원래 코드를 보면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class GameController {
    private static final InputRole inputRole = new InputRole();
    private static final int MAX_PICK_NUMBER = 9;
    private static final int MIN_PICK_NUMBER = 0;
    private static final int MOVE_FORWARD_CONTION_NUMBER = 4;
    private final ArrayList<Car> carList = new ArrayList<>();
    private final ArrayList<String> winnersList = new ArrayList<>();

    public void gameStart() {
        OutputRole outputRole = new OutputRole();
        changeInputToCar(inputRole.getNameList());

        outputRole.pirntResultInstruction();
        for (int i = 0; i < inputRole.getTrialNmber(); i++) {
            tryGameOnce();
            outputRole.printOneGame(carList);
        }
        findWinner();
        outputRole.printWinner(winnersList);
    }

    private void changeInputToCar(String[] nameList) {
        for (String name : nameList) {
            this.carList.add(new Car(name));
        }
    }

    private int getRandomNumber() {
        int randomNumber = Randoms.pickNumberInRange(MIN_PICK_NUMBER, MAX_PICK_NUMBER);
        return randomNumber;
    }

    private boolean checkMoveForward(int randomNumber) {
        return randomNumber >= MOVE_FORWARD_CONTION_NUMBER;
    }

    private void tryGameOnce() {
        for (Car car : carList) {
            int randomNumber = getRandomNumber();
            if (checkMoveForward(randomNumber)) {
                car.moveForward();
            }
        }
    }

    private void findWinner() {
        int maxValue = findMaxInCarList(carList);
        for (Car car : carList) {
            if (car.getPosition() == maxValue) {
                winnersList.add(car.getName());
            }
        }
    }

    private int findMaxInCarList(ArrayList<Car> carList) {
        int maxValue = -1;
        for (Car car : carList) {
            if (maxValue < car.getPosition()) {
                maxValue = car.getPosition();
            }
        }
        return maxValue;
    }

코드가 너무나도 길다. 그리고 역할이 너무 많다. 이후 확장할 때 코드를 작성하는데 점점 부담이 커질 수 밖에 없는 구조인 것이다. 그렇기 때문에 이를 분리해보면 다음과 같이 코드를 깔끔하게 변경해볼 수 있었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Game {
    private static final int MOVE_FORWARD_CONTION_NUMBER = 4;
    private static final int MAX_PICK_NUMBER = 9;
    private static final int MIN_PICK_NUMBER = 0;

    public void startOnce(List<Car> carList) {
        for (Car car : carList) {
            int randomNumber = getRandomNumber();
            if (checkMoveForward(randomNumber)) {
                car.moveForward();
            }
        }
    }

    public List<String> winner(List<Car> carList) {
        int maxValue = findMaxInCarList(carList);
        List<String> winnersList = new ArrayList<>();
        for (Car car : carList) {
            if (car.getPosition() == maxValue) {
                winnersList.add(car.getName());
            }
        }
        return winnersList;
    }

    private int getRandomNumber() {
        int randomNumber = Randoms.pickNumberInRange(MIN_PICK_NUMBER, MAX_PICK_NUMBER);
        return randomNumber;
    }

    private boolean checkMoveForward(int randomNumber) {
        return randomNumber >= MOVE_FORWARD_CONTION_NUMBER;
    }

    private int findMaxInCarList(List<Car> carList) {
        int maxValue = -1;
        for (Car car : carList) {
            if (maxValue < car.getPosition()) {
                maxValue = car.getPosition();
            }
        }
        return maxValue;
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class GameController {
    private static final InputRole inputRole = new InputRole();
    private final List<Car> carList = new ArrayList<>();

    public void gameStart() {
        Game game = new Game();
        OutputRole outputRole = new OutputRole();
        changeInputToCar(inputRole.getNameList());
        Integer trialNumber = inputRole.getTrialNmber();

        outputRole.pirntResultInstruction();
        for (int i = 0; i < trialNumber; i++) {
            game.startOnce(carList);
            outputRole.printOneGame(carList);
        }
        outputRole.printWinner(game.winner(carList));
    }

    private void changeInputToCar(String[] nameList) {
        for (String name : nameList) {
            this.carList.add(new Car(name));
        }
    }
}

이렇게 역할(클래스)가 명확하게 분리돼 더 보기 좋고 확장성이 좋은 코드를 작성해볼 수 있었다. 리펙토링한 코드는 choieungi_refactor branch에서 확인해볼 수 있다.

추가적인 소감

객체지향적으로 코드를 변경하려 하니 클래스를 매개변수로 쓰는 것이 아닌, 클래스의 멤버 변수 하나를 매개변수로 쓸 수 있는 등 놓칠 수 있는 부분을 개선하게 되는 계기가 될 수 있었다. 이처럼 코드에 대해 고민을 계속하다보면 정말 필요한 코드만 작성할 수 있을 것이라 느낄 수 있었다.

코드 이외에는 2주차 피드백에서 다른 사람의 코드를 보고 함께 성장할 수 있기에 코드를 작성하면서 느낀 소감을 PR에 올리면 좋을 것 같다는 피드백이 굉장히 인상적이었고 다른 의미로 기뻤다. 나는 1, 2주차 PR에 모두 소감을 올렸는데, 내가 느낀 것들을 다른 사람도 볼 수 있으면 같이 성장할 수 있겠다 느꼈기 때문이다.

프리코스를 통해 단순히 시험의 과정이 아닌, 성장의 과정으로 느낄 수 있었던 뜻깊던 시간이 아닐까 싶다. 그렇기에 프리코스 과정이 굉장히 몰입감을 주고 재미도 있다. 다만, 자만은 하면 안된다. 아직 부족한 점이 많고, 생각해볼 여지가 많은 부분이 있는 코드이므로 3주차 과제에서는 더 큰 고통을 느껴보고 성장해나갈 것이다.

개인적으로는 피드백을 통해 고민해볼 수 있는 기회가 많아졌다. 그렇기 때문에 프리코스 과정 피드백에서 이런 부분을 구현해보면 어떻게 될까요? 와 같이 challenging해볼 수 있는 여지를 줘도 재밌을 것 같다는 느낌이 든다.

This post is licensed under CC BY 4.0 by the author.