🏎 2주차 미션 - 자동차 경주 게임


2주차 미션은 자동차 경주 게임이었다.

구현 난이도가 그렇게 어려운 편은 아니었지만 요구사항이 추가됨과 더불어 객체를 활용해보는 첫 프로그램이였기 때문에 객체지향에 관한 다양한 지식을 배우고자 노력했다.

아래와 같이 작동을 할 수 있는 프로그램을 만드는 것이 미션의 궁극적인 목표였고 기능적인 요구사항 외에 다양한 요구사항들이 존재했다.

다양한 요구사항들 중 미션을 진행하면서 꾸준히 신경 썼던 프로그래밍 요구사항과 과제 진행 요구사항을 아래 정리해놨다.

💻 프로그래밍 실행 결과 예시


경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)
pobi,woni,jun
시도할 회수는 몇회인가요?
5

실행 결과
pobi : -
woni : 
jun : -

pobi : --
woni : -
jun : --

pobi : ---
woni : --
jun : ---

pobi : ----
woni : ---
jun : ----

pobi : -----
woni : ----
jun : -----

최종 우승자 : pobi, jun

🎱 프로그래밍 요구사항


  • 자바 코드 컨벤션을 지키면서 프로그래밍한다.
  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
  • 함수(또는 메소드)가 한 가지 일만 하도록 최대한 작게 만들어라.

추가된 요구사항

  • 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다.
  • else 예약어를 쓰지 않는다.
    • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
    • else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.

프로그래밍 요구사항 - Car 객체

  • 다음 Car 객체를 활용해 구현해야 한다.
  • Car 기본 생성자를 추가할 수 없다.
  • name, position 변수의 접근 제어자인 private을 변경할 수 없다.
  • 가능하면 setPosition(int position) 메소드를 추가하지 않고 구현한다.
public class Car {
    private final String name;
    private int position = 0;
 
    public Car(String name) {
        this.name = name;
    }
 
    // 추가 기능 구현
}

📈 과제 진행 요구사항


  • 기능을 구현하기 전에 java-baseball-precourse/README.md 파일에 구현할 기능 목록을 정리해 추가한다.
  • Git의 커밋 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가한다.

🖋 2주차 미션 후기


README 작성


1주차 피드백을 통해 README 의 개선점을 상당히 많이 찾을 수 있었다.

피드백을 받은 상황에서도 처음 기능 목록 정리를 완벽하게 하고자 하는 욕심 때문에 상당히 많은 시간을 사용했던 것 같다.

많은 시간을 사용하여 작성한 게 완벽하다고 생각했던 문서가 구현을 거듭할수록 나타나는 개선점들을 보고 기능 목록을 언제든지 바뀔 수 있다는 점을 알게 되었다.

피드백에서 언급한 살아있는 문서의 의미를 어느 정도 경험해 볼 수 있었다.

1주차에 작성한 README 를 보면 굉장히 세세하게 만들어 놓은 클래스들을 모두 설명하고 있는 모습을 볼 수 있었다.

피드백을 통해 클래스 이름, 메서드와 같은 것들은 언제든지 변경될 수 있기에 세세한 부분까지 정리하기보단 구현해야 할 기능 목록과 예외 상황을 정리하는 데 집중했다.

또한 README 의 본질은 해당 프로젝트를 소개하는 문서라는 것을 알게 되어 프로젝트 소개란 및 프로그램 흐름 설명을 추가하는 등 해당 프로젝트를 소개하기 위해 노력했다.

이 부분은 꾸준히 연습하여 익숙해질 필요가 있을 것 같다.

가독성


피드백을 받은 이후 1주차 과제를 뒤돌아보자 상당히 읽기 어려운 코드였다는 것을 알게되었다.

1주일이 지난 나 자신조차 코드를 어려워한다면 타인은 더욱 어려울 것임을 알 수 있었다.

이를 통해 해당 코드가 어떤 일을 하는지 한 번에 알 수 있는 것이 굉장히 중요하다는 것을 느꼈다.

따라서 이번 과제에서는 피드백을 바탕으로 읽기 좋은 코드를 작성하기 위해 노력했다.

클래스나 메서드의 이름으로 해당 코드의 의도를 나타낼 수 있도록 하였고 매직 넘버들을 상수화하여 해당 숫자들의 의도를 정확하게 드러낼 수 있도록 노력하였다.

메서드의 의도를 정확히 나타내려면 네이밍을 잘해야 했는데 이는 메서드가 한 가지 일만 하도록 만들면 된다는 것을 알 수 있었다.

메서드가 한 가지 일만 하도록 작성하면 나중에 중복될 수 있는 코드를 사전에 방지할 수 있었고 코드를 문장처럼 만드는 데 큰 도움이 되었다.

이런 식으로 코드를 작성하다 보니 2주차에 추가되었던 함수 라인 제한이나 else 예약어 지양과 같은 요구사항들은 저절로 지켜졌다.

메서드 구현 후 작성하게 된 커밋 메시지 역시 작성한 코드의 역할이 명확하다 보니 커밋 메시지의 의미도 뚜렷해지는 시너지도 발휘할 수 있었다.

validation.isValidCarNames(carNames);
validation.isValidRound(round);

의도를 나타내는 식으로 네이밍을 하다 보면 위와 같이 간혹 중복되는 문맥을 발견할 수 있었다.

validation.isValid(carNames);
validation.isValid(round);

이를 방지하기 위해 위와 같이 비슷한 역할을 하는 메서드는 오버로딩을 적용하여 가독성을 향상하고 중복되는 문맥 역시 제거할 수 있었다.

코드 컨벤션을 지키는 것 역시 가독성을 향상 시키는 데 상당한 도움이 됐다.

구현 순서를 지킴으로써 해당 객체가 아는 것과 하는 것에 대한 분별력이 높아졌고 어떤 객체와 상호작용하는지도 쉽게 알아낼 수 있었다.

MVC 패턴 적용


1주차 미션을 진행하면서 볼 수 있었던 타 참가자들의 코드를 보면 클래스 분리뿐만 아니라 패키지 구조 설계 역시 적용하여 구현하던 사람들의 코드를 많이 볼 수 있었다.

이와 같은 구조 설계 방법이 MVC 패턴을 적용한 것임을 알게 되면서 자연스럽게 이번 주차에는 MVC 패턴을 적용하여 미션을 수행해보고자 노력했다.

먼저 객체지향의 개념에 대해 더 정확히 공부하기 위해 [우아한테크세미나] 190620 우아한객체지향 by 우아한형제들 개발실장 조영호님객체지향 프로그래밍이 뭔가요? 와 같은 영상들을 참고하고 [SOLID] 단일 책임 원칙(SRP)이란? 과 같은 객체지향 프로그래밍에 대한 개념을 공부했다.

이후 [10분 테코톡] 🧀 제리의 MVC 패턴 을 참고하면서 MVC 패턴의 간단한 개념을 배우고 코드를 작성했다.

처음엔 getter 를 통해 객체의 상태에 접근하는 방식으로 구현했었는데 [OOP] 캡슐화(Encapsulation)란? 을 통해 캡슐화를 공부하게 되면서 무분별한 getter 사용은 보안 측면에서 굉장히 취약하며 객체 간 상호작용에 있어 부적절한 코드였다는 것을 알게 되었다.

getter를 사용하는 대신 객체에 메시지를 보내자 를 보고 객체가 아는 것과 하는 것을 바탕으로 객체 간 메시지를 보내 상호작용하도록 코드를 재구성하니 각각의 객체들이 소통하는 프로그램을 만들 수 있었다.

private void moveCars(Car[] cars) {
	for (Car car : cars) {
		car.move();
		printResult(car);
	}
}
 
private void printResult(Car car) {
	System.out.print(car.getName() + COLON);
	printDistance(car.getPosition());
	System.out.println();
}

위와 같이 Car 객체에 직접적으로 접근해서 해당 자동차의 이름과 위치를 getter 를 통해 가져오는 코드를 아래와 같이 move() 함수가 이동 후 이름과 위칫값을 리스트로 반환하여 결과를 출력하도록 코드를 재구성했다.

private void move(Car[] cars) {
	for (Car car : cars) {
		ArrayList<String> result = car.move();
		printResult(result.get(CAR_NAME), result.get(CAR_POSITION));
	}
}
 
private void printResult(String name, String position) {
	System.out.print(name + COLON);
	printDistance(Integer.parseInt(position));
	System.out.println();
}

또한 아래와 같이 직접적으로 Car 객체로부터 위치를 가져와 최고 기록을 구하는 것보다

private void getHighScore() {
	for (Car car : cars) {
		highScore = Math.max(car.getPosition(), highScore);
	}
}
private void getHighScore() {
	for (Car car : cars) {
		highScore = car.updateHighScore(highScore);
	}
}

위와 같이 Car 객체에 최고 기록인지 물어보는 방식으로 메시지를 전달함으로써 객체들이 역할을 뚜렷이 할 수 있다.

또한 해당 과제를 진행하면서 JVM 메모리 구조를 공부하게 되었고 static 의 위험성에 대해서 배우게 되어 개인적으로 static 사용을 지양하는 제한사항을 두고 프로그램을 구현하였다.

지금 와서 드는 생각이지만 view 패키지는 인스턴스를 생성할 필요 없이 static 으로 두어 controller 에서 바로 사용하도록 설계 하는 게 더 적합했을 것 같다.

캡슐화를 통한 객체의 은닉과 더불어 메모리의 효율성과 객체 간의 의존성을 고려하며 설계하고 프로그래밍할 수 있는 시간이 되었다.

앞으로 이런 구조 설계에 대해 더 공부하면서 적절한 코드를 작성하는 연습을 꾸준히 해야겠다고 생각하게 되었다.

예외 처리


이번 미션에서는 부적절한 입력 발생 시 에러를 던지고 프로그램을 종료시키는 것이 아닌 예외를 처리함으로써 프로그램이 정상 종료될 수 있도록 해야 했다.

피드백에서 언급되었던 예외 발생 시나리오를 생각하면서 생각할 수 있는 예외 상황들을 모두 메서드로 구현하였고 각 메서드는 해당 예외 상황에 맞는 에러 메시지를 출력하도록 구현했다.

해당 경험을 통해 정상적인 기능뿐만 아니라 예외적인 상황도 동시에 고려해야 한다는 점에서 앞서 언급했던 README 가 완벽할 수 없다는 점을 다시 한번 알 수 있었다.