[우아한테크코스 백엔드 4기]프리코스 2주차 "자동차 경주 게임" 회고
⛳ 자동차 경주 게임
🚀 기능 요구사항
초간단 자동차 경주 게임을 구현한다.
- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다.
- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.
- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.
- 사용자가 잘못된 값을 입력할 경우
IllegalArgumentException
를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다. - 아래의 프로그래밍 실행 결과 예시와 동일하게 입력과 출력이 이루어져야 한다.
✍🏻 입출력 요구사항
⌨️ 입력
- 경주 할 자동차 이름(이름은 쉼표(,) 기준으로 구분)
pobi,woni,jun
- 시도할 회수
5
🖥 출력
- 각 차수별 실행 결과
pobi : --
woni : ----
jun : ---
- 단독 우승자 안내 문구
최종 우승자 : pobi
- 공동 우승자 안내 문구
최종 우승자 : pobi, jun
- 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 [ERROR] 로 시작해야 한다.
[ERROR] 시도 횟수는 숫자여야 한다.
🔗 관련 링크
2주차 프리코스 깃허브 링크
작성한 코드
프리코스 참고자료 노션 정리
구현 기능 목록
✅ 자동차 이름 인풋&세팅
-
"경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)" 출력
-
인풋 받기
- [예외] 인풋이 없는 경우
- [예외] 인풋에 문자,숫자,쉼표 외에 다른 것이 들어온 경우
-
자동차 이름 목록을 문자열 리스트로 저장
- [예외] 자동차 이름 길이가 0인 경우
- [예외] 자동차 이름 길이가 5자를 넘는 경우
- [예외] 같은 이름이 있는 경우
-
예외발생 시 처음부터 실행
✅ 시도 회수 인풋&세팅
-
"시도할 회수는 몇회인가요?" 출력
-
인풋 받기
- [예외] 인풋이 없는 경우
- [예외] 인풋에 숫자 외에 다른 것이 들어온 경우
-
인풋을 정수로 저장
-
예외발생 시 처음부터 실행
✅ 자동차 생성
- 자동차 이름 생성하기
- 자동차 이동거리 생성하기
- 자동차 전진 함수 생성하기
- 0에서 9 사이에서 무작위 값을 구하기
- 무작위 값이 4 이상일 경우 이동거리 + 1
✅ 자동차 목록 생성
- 자동차 목록 생성하기
- 자동차 목록에 자동차 추가하기
- 모든 자동차 라운드 결과 알려주는 함수 생성하기
- 우승자 목록 알려주는 함수 생성하기
✅ 레이싱 게임 실행
-
자동차 목록 생성
- 자동차 생성
-
차수 수 만큼 반복
- 자동차 전진시키기
- 차수별 실행 결과 출력
-
우승자 목록 생성
- 우승자 목록 출력
- 우승자가 한명일 경우 그대로 출력
- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분해 출력
- 우승자 목록 출력
🔍구현 로직
-
Main diagram
-
Util diagram
⛳ 후기
🗒 원칙
jdk 버전 8
,intellij IDEA
로 세팅했습니다.- checkstyle 등을 이용해
자바 코드 컨벤션
을 꾸준히 확인했습니다. crlf
를lf
로 전환했습니다.
- checkstyle 등을 이용해
커밋 메세지 컨벤션
을 지키려 노력했습니다."타입(스코프): 내용"
형식으로, 내용은 한글로 작성했습니다.스코프
에는 바뀐 파일/클래스 명을 썼고, 바뀐 파일이 3개 이상일 경우 비워뒀습니다.
README.md
속 사항을 구현할 때마다 커밋을 남겼습니다.- 개괄적인 구현 이후엔 리팩토링 마다 커밋을 남겼습니다.
MVC 구조
를 사용했습니다./model
: 데이터 저장 및 처리/view
: 인풋/아웃풋 관리/controller
: 프로그램 실행 로직 클래스
indent = 1
을 지키려 노력했습니다.stream
메소드들을 공부하고 사용했습니다.
- 객체지향 생활 체조 원칙을 지키려 노력했습니다.
메소드 당 기능 하나
를 담당하도록 설계했습니다.- 메소드/변수명을 축약없이 설명적으로 적었습니다.
getter/setter
대신 상수 & 생성자를 쓰려 했습니다.- 필요한 곳에만 주석을 적었습니다.
- 하드코딩 대신
상수 클래스
를 사용했습니다. else/switch
대신if/continue
를 사용했습니다.__
- 코드를 압축적으로 짜려 했습니다.
for/while
대신람다/stream
을 사용했습니다.for/while
대신 재귀를 사용했습니다.- 단순 반복문은 IntStream.range(0,반복횟수)를 사용했습니다
- 인풋 조건 확인 시 정규식을 사용하고 주석을 달았습니다.
- SOLID 원칙을 지키려 노력했습니다.
- 단일 책임 원칙(SRP)을 적용시켜, 기능 별로 클래스를 분리시켰습니다.
- 상위 클래스와 하위 클래스 사이의 일관된 책임관계를 주려 했습니다.
- 응집도/결합도를 고려했습니다.
🖋 소감
지난 주차는 객체지향 및 클린코드의 기본 원칙들을 공부하는 시간이었다면, 이번 2주차는 해당 공부들을 적용시키고 더 나은 코드란 무엇인지 고민해볼 수 있는 시간이었습니다.
저는 이번 3가지 과제를
- 객체지향 체조원칙을 지키자
- SOLID 일부 원칙들을 적용시키자
- 코드 길이를 줄일 여러 방법들을 적용시키자
로 잡고 리팩토링을 해봤습니다. 이때 한꺼번에 모든 영역들을 리팩토링 하기보단, 리팩토링 가능한 영역 별로 작업해 커밋하는 것을 목표로 두었습니다.
- indent 1로 줄이기(함수 분리)
- else 없애기
- 메소드가 한가지 일만 하도록 하기
- (반복문 여러개 생겨도 ㄱㅊ)
- static 가능하면 util함수처럼 바꾸기
- 로컬변수가 한번만 쓰이면 걍 합치기
- 메소드 당 라인수를 15라인 → 10라인 까지 줄여나가기
- 일급콜력션 많이 사용(콜렉션 변수 하나만 있는 클래스, wrapper같은 느낌)
- 클래스의 인스턴스 변수 2개까지
의 순서로 리팩토링을 하는 것을 목표로 잡았습니다! 그런데 막상 리팩토링을 시작하니 해당 구간별로 커밋을 끊어 진행하는 게 쉽지 않다는 걸 느꼈습니다. TODO 작성도 미뤄지기 마련이었습니다. 앞으로 계속 연습해나가야 할 것 같아요.
1. README
초기 README 속 구현사항을 플랫하게 짜는 것이 목표였습니다! 그런데 막상 구현사항을 쓰면서 예외처리나 클래스의 분리 등을 고려하면서 시간을 꽤 썼습니다.
결과적으론 해당 구현사항에서 변경된 부분이 거의 없이 기본 구현을 완료하게 되었습니다.
구현기능 별로 README를 체크해나가는 건 깔끔하게 진행되었지만, 살아있는 README 문서를 만들지는 못한 것 같아 아쉽습니다.
변명하자면, 아직 구현사항이 어렵지 않아 변경된 지점이 많지 않은 것일 수도 있습니다. 이후 3주차와 최종 과제에서는 어쩔 수 없이 라도 변경들이 많을 것이라 생각합니다.
2. 코드 길이 줄이기
우선 for/while문 -> 람다(forEach)/stream으로 대부분 교체했습니다.
Stream은 지난 과제 때에도 사용했지만 온전하게 활용하지 못했던 것 같아요. 이번엔 람다와 스트림 중 어느 방식의 코드가 더 효율적일지 생각해 적용했습니다. 추가적인 stream 함수들도 배웠습니다. (range라던가...) 코드를 한줄이라도 더 줄이기 위해 if문 순서를 바꿔 return을 없애기도 했습니다.
그리고 Arraylist로 구현했을 때 람다식 등 다양한 함수를 쓸 수 있어 더 편리하더라고요. 싹 교체했습니다!
3. 응집도/결합도
코드의 응집도/결합도에 대해서도 신경을 썼습니다.
클래스 안 메소드에서 동등한 레벨로 모여있는 것이 좋다는 얘기를 듣고 어떻게 레벨을 균등하게 유지할지 고민했습니다. 결국 하위 레벨의 메소드를 Util 클래스로 따로 빼는 방식을 택했습니다.
이게 응집도는 높이지만 동시에 결합도를 높이는 방향인 것 같습니다.
앞으로 더 나은 선택지가 무엇일 지 고민해봐야 할 것 같습니다.
대신, 리팩토링 때 파라미터에 다른 클래스의 메소드 값을 넣으며 객체지향을 침범하는 일을 줄이려 했습니다.
Car 객체에 getName(), getPosition() 등을 호출하는 방식을 지양했고, 상위 클래스인 RacingCars 객체와만 소통할 수 있게 했습니다.
최종 컨트롤 타워인 GameController 클래스에서도 파라미터 없이 RacingCars 안에서 입출력이 이뤄지도록 했습니다.
정답인지는 아직 잘 모르겠습니다. 그만큼 RacingCars 클래스 안에서 많은 일들을 진행해야 했기 때문입니다.
4. 정규식
함수를 계속해서 쪼개다가 한계를 느껴 정규식
을 적용시켜보기도 했습니다.
처음엔 정규식을 validator 클래스 인스턴스 내부에서 계속 생성했습니다. 그러다가 하드코딩이기도 하고, 정규식 생성 이후 테스트 결과가 느려지는 것 같아 패턴 자체를 상수 클래스로 이동했습니다.
테스트 시간이 8초에서 3초로 줄어드는 기적을 맛봤습니다.
적절한 곳에 static 상수를 쓰는 것의 성능적 중요성을 깨닫는 시간이었습니다.
5. 아쉬운 점
이번 과제 때 TDD를 적용시켜보는 것이 목표였습니다. 그러나 동시에 2차 코딩테스트 시간인 5시간 내에 코드를 완전히 구현하는 것 역시 목표로 삼았습니다. 테스트 코드 짜는 법을 충분히 연습하지 못한 채 시간에 쫒겼습니다. 결국 TDD보단 최종 기능 구현을 목표로 코드를 짜게 되어 아쉽습니다.
다음 주차 과제부터는 테스트 코드를 먼저 짠 뒤, 구현해나가는 연습을 해봐야 할 것 같습니다.
이때 테스트 코드의 방향성에 대해서도 고민해볼 생각입니다.
(프로그램 실행 로직에 따라 짤지, 구현할 클래스 기능을 기준으로 짤지...)
또한 인터페이스/상속/추상클래스 없이 코드를 짰는데 옳은 선택인지 확신이 들지 않습니다.
InputController 속 두 메소드는 모두 예외처리 함수로 구현 방식이 비슷합니다. 그래서 오버로딩, 한 함수-파라미터 사용 등의 방법을 고민했는데, 막상 안에서 사용하는 외부 메소드들의 성질이 다른 것 같아 따로 구현했습니다.
두 Validator 클래스 역시 같은 validate란 이름의 함수를 갖고 있습니다. 그래서 인터페이스를 따로 구현해야 할지 고민했었는데, 이 역시 validate 속 예외처리할 사항들에 중복이 없기 때문에 다른 함수일 수밖에 없고, 오히려 간단한 프로그램의 복잡도만 증가시키는 거 아닐까 싶어 그대로 냅뒀습니다.
하지만 분명 중복을 줄일 방법이 있을 것 같은데... 계속 고민해봐야 할 것 같습니다.
마지막으로, 다른 클래스 간의 같은 함수이름 사용이 괜찮은지 궁금합니다.
예를 들어,Car 클래스와 RacingCars 클래스 모두 getPosition(s) 메소드가 있는데, 이게 좋은 메소드 작명법인지 아직 모르겠네요. 다행히 두 메소드 다 두 클래스 이외에서 쓰여 헷갈릴 일은 없지만, 앞으로 이런 중복되는 이름들이 응집도를 높이는 일일지, 아니면 결합도를 높이는 일일지 찾아봐야 겠습니다.
📚 TODO
- 테스트 코드 작성 연습
- TDD 적용
- 좋은 코드 구현방식을 계속 찾고 참고하기
- 코딩 시간 줄이는 법 연구
- 자주 쓰이는 자바 유틸 함수 구현/찾아보기
Author And Source
이 문제에 관하여([우아한테크코스 백엔드 4기]프리코스 2주차 "자동차 경주 게임" 회고), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@betterfuture4/우아한-테크코스-백엔드-4기프리코스-2주차-자동차-경주-게임-회고저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)