[우아한테크코스 백엔드 4기]프리코스 2주차 "자동차 경주 게임" 회고

10717 단어 우테코우테코

⛳ 자동차 경주 게임

🚀 기능 요구사항

초간단 자동차 경주 게임을 구현한다.

  • 주어진 횟수 동안 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

⛳ 후기

🗒 원칙

  1. jdk 버전 8, intellij IDEA로 세팅했습니다.
    • checkstyle 등을 이용해 자바 코드 컨벤션을 꾸준히 확인했습니다.
    • crlflf로 전환했습니다.
  2. 커밋 메세지 컨벤션을 지키려 노력했습니다.
    • "타입(스코프): 내용" 형식으로, 내용은 한글로 작성했습니다.
    • 스코프에는 바뀐 파일/클래스 명을 썼고, 바뀐 파일이 3개 이상일 경우 비워뒀습니다.
  3. README.md 속 사항을 구현할 때마다 커밋을 남겼습니다.
    • 개괄적인 구현 이후엔 리팩토링 마다 커밋을 남겼습니다.
  4. MVC 구조를 사용했습니다.
    • /model : 데이터 저장 및 처리
    • /view : 인풋/아웃풋 관리
    • /controller : 프로그램 실행 로직 클래스
  5. indent = 1을 지키려 노력했습니다.
    • stream 메소드들을 공부하고 사용했습니다.
  6. 객체지향 생활 체조 원칙을 지키려 노력했습니다.
    • 메소드 당 기능 하나를 담당하도록 설계했습니다.
    • 메소드/변수명을 축약없이 설명적으로 적었습니다.
    • getter/setter 대신 상수 & 생성자를 쓰려 했습니다.
    • 필요한 곳에만 주석을 적었습니다.
    • 하드코딩 대신 상수 클래스를 사용했습니다.
    • else/switch 대신 if/continue를 사용했습니다.__
  7. 코드를 압축적으로 짜려 했습니다.
    • for/while 대신 람다/stream을 사용했습니다.
    • for/while 대신 재귀를 사용했습니다.
    • 단순 반복문은 IntStream.range(0,반복횟수)를 사용했습니다
    • 인풋 조건 확인 시 정규식을 사용하고 주석을 달았습니다.
  8. SOLID 원칙을 지키려 노력했습니다.
    • 단일 책임 원칙(SRP)을 적용시켜, 기능 별로 클래스를 분리시켰습니다.
    • 상위 클래스와 하위 클래스 사이의 일관된 책임관계를 주려 했습니다.
    • 응집도/결합도를 고려했습니다.

🖋 소감

지난 주차는 객체지향 및 클린코드의 기본 원칙들을 공부하는 시간이었다면, 이번 2주차는 해당 공부들을 적용시키고 더 나은 코드란 무엇인지 고민해볼 수 있는 시간이었습니다.

저는 이번 3가지 과제를

  1. 객체지향 체조원칙을 지키자
  2. SOLID 일부 원칙들을 적용시키자
  3. 코드 길이를 줄일 여러 방법들을 적용시키자

로 잡고 리팩토링을 해봤습니다. 이때 한꺼번에 모든 영역들을 리팩토링 하기보단, 리팩토링 가능한 영역 별로 작업해 커밋하는 것을 목표로 두었습니다.

  • 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 적용
  • 좋은 코드 구현방식을 계속 찾고 참고하기
  • 코딩 시간 줄이는 법 연구
  • 자주 쓰이는 자바 유틸 함수 구현/찾아보기

좋은 웹페이지 즐겨찾기