본문 바로가기

우아한테크코스 6기/프리코스

우아한테크코스 6기 프리코스 2주차

728x90

1. 1주차 공통 피드백

 

해당 피드백 중 나에게 의미 있었던 것들만 언급하겠다.

 

1) 커밋 메시지를 의미 있게 작성하라

 

1주차에서는 커밋 메시지를 작성할 때 그냥 생각나는대로 커밋 메시지를 작성했는데 사실 적으면서도 맞게 하는 건지에 대한 의문이 생겼다. 1주차 공통 피드백에는 "커밋 메시지에 해당 커밋에서 작업한 내용에 대한 이해가 가능하도록 작성한다." 이렇게만 적혀있었는데 그래서 어떻게 작성하는 거지라는 생각이 들었다. 일단은 피드백을 읽고 과제 진행 요구 사항을 읽던 중 새롭게 추가된 내용이 눈에 들어왔다. 

 

[그림 1-1] 과제 진행 요구 사항 중 새롭게 추가된 부분

 

[그림 1-1] 커밋 메시지 컨벤션 예시부터 봤는데 맙소사 생각보다 엄청 길게 메시지를 작성해야 하는 건가라는 생각부터 들었다. 일단 커밋 메시지도 형식이 따로 있구나 정도로만 생각하고 숫자 야구 피드백 강의를 보았다. 여기서 [그림 1-1]의 "커밋은 기능 목록 단위로 한다"라는 말을 이해할 수 있었다. 강의에서 나왔던 몇 가지 예시를 들어보면 다음과 같다.

 

feat(baseball): create skeleton code
feat(baseball): 서로 다른 임의의 수 3개를 생성하는 기능 구현
test(baseball): 숫자 비교 테스트
refactor(baseball): 테스트 중복 제거

 

아까 어렴풋이 보았던 예시들의 첫 줄이었다. 이렇게 작성하면 되겠구나 생각하고 [그림 1-1] 커밋 메시지 컨벤션 중 Subject Line 부분을 중점적으로 해석해보았다.

 

<type> (<scope>): <subject>
...

커밋 메시지의 어떠한 라인도 100자를 넘어선 안된다! ...

주제 라인 (Subject line)
주제 라인은 변경의 간결한 묘사를 포함하고 있다.

허용된 <type>
feat (기능)
fix (버그 고침)
docs (문서) => docs.README와 같은 문서가 아닐까 싶다.
style (형식, 세미 콜론 빠진 경우, ...) => 줄 바꿈, 공백, 인덴팅 등과 함께 ;를 빠뜨린 경우 문법적인 오류도 포함인 것 같다.
refactor
test (빠진 테스트 넣을 때)
chore (유지)  => 이해가 안 가 사이트를 참고 했는데 명확하게 이해가 되진 않는다.

허용된 <scope>범위는 커밋을 한 곳 어디든 상관이 없다. 예를 들어, $location, $browser, $compile, $rootScope, ngHref, ngClick, ngView, 기타 등등 ... => 강의 예시 중 baseball은 package, $location이었다.

<subject> 텍스트
명령조의 현재 시제를 사용하여라: "changed", "changes"가 아니라 "change"
첫번째 글자는 대문자화하지 말 것
마침표 붙이지 말 것
...

 

[추가] 1주차 때는 생각나는대로 메시지를 적어 커밋 해보았다면 이번 주차에서는 다음과 같은 방식으로 커밋을 구현해보았다. 

 

https://github.com/hakie2kim/java-racingcar-6/commits/hakie2kim

 

GitHub - hakie2kim/java-racingcar-6

Contribute to hakie2kim/java-racingcar-6 development by creating an account on GitHub.

github.com

 

  • 틀린 부분도 있지만 커밋 메시지에서 <scope> 부분은 앞으로 파일 이름을 제외한 모든 파일 경로를 쓰기로 결정했다. 만약 여러개인 경우 '|'을 통해 구분한다.
  • 1주차와 달리 자주 커밋하니까 해당 커밋을 클릭했을 때 어떤 변화가 있었는지 한 눈에 파악하기 쉬웠다. 

 

2) 이름을 통해 의도를 드러내라 & 축약하지 않는다

 

[추가]  "클래스와 메서드 이름을 한 두 단어로 유지하려고 노력하고 문맥을 중복하는 이름을 자제" 이 문구를 유념해 다음과 같은 클래스, 변수, 메서드의 이름은 리뷰 후 의도가 명확하지 않고 중복된 거 같아 바꾸었다.

 

  • Car class의 toString() 메서드 중 지역 변수 distanceDash의 뜻을 유추해보면 그냥 거리, 대쉬 이 뜻밖에 되지 않아 distanceInDash, "거리를 대쉬로" 라는 의미가 되게 in을 넣어 주었다.
  • CarRacingGame 클래스에서 멤버 변수 car은 경주할 자동차 인스턴스를 담고 있는 List이다. 따라서 car 보다는 복수의 의미를 담게 cars로 변경해주었다. 같은 의미로 인스턴스 메서드 setCar도 setCars로 변경해주었다. isDistinct에서도 통일성을 위해 stringList,를 strings로 바꿨다.
  • trial 또한 numberOfAttempts, setTrialInput 또한 어차피 set이 입력값을 설정하는 의미를 포함하고 있기에 setNumberOfAttempts로 바꾸어주었다.
  • 비슷하게 List 타입의 변수는 모두 s를 붙여주었다.

 

3) Java에서 제공하는 API를 적극 활용한다 & 배열 대신 Java Collection을 사용한다

 

1주차에서는 플레이어의 숫자 입력값을 char[] 배열로 변환했는데 이렇게 하면 구현하고 싶은 기능의 알고리즘을 일일히 다 생각해야 한다. Collection을 사용하면 대부분의 구현하고 싶은 기능은 다양한 API(미리 구현된 기능들)를 사용하면 되기 때문에 굳이 수고로울 필요가 없다. 

 

[추가] 2주차에서는 다음과 같이 Java Collection을 사용해 제공하는 API를 활용했다.

 

[그림 1-2]

 

각 자동차의 거리가 Car라는 객체 안의 멤버이기 때문에 기존의 배열으로는 길게 코드를 짜야할 것을 44-46 이 세 줄로 구현할 수 있었다. 54번째 줄은 최대 거리를 주행한 다른 자동차 중 이름이 겹치지 않는 자동차를 우승자에 List에 추가하는 조건이다.

 

[그림 1-3]

 

해당 메서드의 경우 List에 string 요소를 하나씩 추가하며 다음 요소가 만약 기존 List에 담겨 있으면 (contains) 예외를 발생한다.


2. 과제 진행 요구 사항

 

✅ 미션은 java-racingcar-6 저장소를 Fork & Clone해 시작한다.

 

✅ 기능을 구현하기 전 docs/README.md에 구현할 기능 목록을 정리해 추가한다.

 

1주차와 같이 처음에는 클래스, 메서드(기능) 후보들을 추리는 것을 유지하되, 숫자 야구 피드백 강의와 같이 해당 기능에 대한 설명 각 메서드 첫 줄에 좀 더 상세하게 추가했다.


✅  Git의 커밋 단위는 앞 단계에서 docs/README.md에 정리한 기능 목록 단위로 추가한다.

✅  커밋 메시지 컨벤션 가이드를 참고해 커밋 메시지를 작성한다.

 

1-1)에 언급

 

✅ 과제 진행 및 제출 방법은 프리코스 과제 제출 문서를 참고한다.


3. 프로그래밍 요구 사항

 

✅ JDK 17 버전에서 실행 가능해야 한다. JDK 17에서 정상적으로 동작하지 않을 경우 0점 처리한다.

✅ 프로그램 실행의 시작점은 Application의 main()이다.

✅ build.gradle 파일을 변경할 수 없고, 외부 라이브러리를 사용하지 않는다.

 

✅ Java 코드 컨벤션 가이드를 준수하며 프로그래밍한다.

 

4.2 블럭 들여쓰기: +4 스페이스

CarRacingGameTest 클래스에서 indentation이 +8가 돼있는 것을 보고 +4로 바꾸어주었다. (깃허브 링크)

 

4.1.3 빈 블록 : 간결 할 수 있음

Car 클래스의 stop() 메서드는 빈 블록이므로 간결하게 void stop() {} 와 같이 바꾸어주었다. (깃허브 링크)

 

6.1 @Override: 항상 사용

Car 클래스의 toString() 메서더는 Object 클래스의 것을 오버라이딩 한 것이므로 @Override를 추가해준다. (깃허브 링크)


✅ 프로그램 종료 시 System.exit()를 호출하지 않는다.
✅ 프로그램 구현이 완료되면 ApplicationTest의 모든 테스트가 성공해야 한다. 테스트가 실패할 경우 0점 처리한다.
✅ 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다.

 

1) 추가된 요구 사항

 

✅ indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
  예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
  힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.

 

함수 자체는 들여쓰기로 보지 않았다.

 

[그림 2-1]

 

 

[그림 2-2]

 

[그림 2-1]의 인덴트가 3이므로 [그림 2-2]와 같이 기본 제공 API를 이용해 다시 리팩토링했다. 훨씬 가독성이 좋고 코드가 갖고 있는 의미도 더 살아났다.

 

✅ 3항 연산자를 쓰지 않는다.

✅ 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.

 

[그림 2-3]

 

[그림 2-4]

 

[그림 2-3]과 [그림 2-4]는 원래 경주할 자동차를 설정하는 메서드 setCars에 포함돼 있었는데 메서드가 한 가지 일만 하도록 최대한 작게 만들었다.


✅ JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
  테스트 도구 사용법이 익숙하지 않다면 test/java/study를 참고하여 학습한 후 테스트를 구현한다.

 

1주차 소감문에 작성한 "3. 더 고민해야 할 부분"에 해당 내용을 적었었는데 추가된 요구 사항에 나와서 왠지 모르게 뿌듯했다. 

 

다음과 같이 예시로 나온 테스트 도구 사용법을 정리 후 테스트 코드를 작성하였다.

 

AssertJ는 assertion(주장)을 제공하는 라이브러리다. 

 

AssertJ is a Java library that provides a rich set of assertions and truly helpful error messages, improves test code readability, and is designed to be super easy to use within your favorite IDE.

출처 - https://assertj.github.io/doc/

 

도대체 이 문장이 무슨 말인지 해서 Oracle 공식 문서에서 무슨 뜻인지 찾아보았다. 정리하자면 assertion은 프로그램에 적힌 가정들이 맞는지 아닌지 확인하는데 도움을 주는 라이브러리이다. 

 

An assertion is a statement in the Java programming language that enables you to test your assumptions about your program.

출처 - https://docs.oracle.com/javase/8/docs/technotes/guides/language/assert.html#:~:text=An%20assertion%20is%20a%20statement,than%20the%20speed%20of%20light.

 

다음 AssertJ 깃허브 공식 문서의 간단한 예시를 통해서 test/java/study에 나온 예시들의 의미를 알 수 있었다.

 

[그림 2-5]

출처 - https://assertj.github.io/doc/#assertj-core-assertions-guide

 

[그림 2-6]

 

테스트할 객체를 assertThat()의 매개 변수로 집어 넣는다. [그림 2-5]의 4와 같이 assertion을 확인하고 싶은만큼 연결한다. 문서의 Checking iterables/arrays content를 보면 contains는 배열이 인자로 주어진 값들을 포함하고 있는지만 확인하는 것이고, containsExactly는 순서까지 확인하는 것이다. 15, 16의 결과가 모두 true이기 때문에 해당 테스트는 통과한다.

 

[그림 2-7]

 

isEqualTo()는 두 객체의 값을 서로 비교한다. 하지만, 사용자 정의 객체의 경우 객체의 주소를 비교하기 때문에 isEqualToComparingFieldByFieldRecursively()를 사용해야 한다. String 또한 객체로 알고 있는데 그렇다면 isEqualToComparingFieldByFieldRecursively()를 사용해야 하는 것이 아닌가 하는 의문이 든다.

 

[그림 2-8]

 

assertThatThrownBy의 예를 보니 catchThrowable의 대안이고 좀 더 가독성 있다고 생각하면 사용하라고 적혀있었다. [그림 2-8]의 예에서 input의 5번째 인덱스를 접근하려고 하면 IndexOutOfBoundsException의 예외가 발생하게 된다. 해당 예외는 StringIndexOutOfBoundsException로 형변환이 가능하다. 그리고 접근하려는 요소의 인덱스가 범위 밖인지 알려준 메시지가 출력됐는지 hasMessageContaining을 통해 확인한다.

 

테스트 만드는 방법은 인텔리제이 문서를 참고했다. 클래스 이름에 커서를 놓고 Alt + Enter를 입력하면 된다.

 

[그림 2-9]

 

[그림 2-10]

 

[그림 2-11] Ctrl을 누른 채로 [그림 2-10]의 assertRandomNumberInRangeTest를 클릭한다.

 

[그림 2-12] 출처 - https://www.javatpoint.com/arrow-operator-in-java

 

우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분하는 기능을 테스트할 메서드[그림 2-9]를 만들 때가 제일 막막했다. 시도 횟수가 많아지면 많아질수록 나올 수 있는 실행 결과의 경우의 수가 너무 많았고, 전진 조건이 랜덤으로 나오는 숫자여서 누가 우승자일지도 확신하기 힘들었기 때문이다.

 

문득 전진_후진 테스트 메서드 예시가 생각났고 이를 살펴보았다. assertRandomNumberInRangeTest는 우아한테크코스가 제공한 API인데 ([그림 2-11]) 솔직히 말하면 봐도 잘 모르겠다. [그림 2-10]을 유추를 해보려고 노력한 결과, "() -> {}" 처음 보는 이 표현은 기존의 익명 클래스(한 번만 사용하는 클래스)를 간단하게 표현한 람다식인 것 같았다 ([그림 2-12]). [그림 2-10] 메서드 이름을 보면 "assert범위안랜덤숫자를테스트"라는 것과, 상수 MOVING_FORWARD, STOP에 각각 4와 3의 값(0~9 안 범위다)을 인자로 주는 것, 실행 결과 pobi의 거리가 1, woni의 거리가 0인 것으로 보아 람다식 안 두 문장의 뜻은 pobi는 전진 woni는 멈춤인 거 같았다. [그림 2-11]을 보면 마지막 매개변수가 Integer...와 같이 가변 인자로 돼 있는 것을 확인할 수 있다. 따라서 전진과 후진을 판단하는 0부터 9까지의 2개 이상 정수를 인자로 줄 수 있기에 [그림 2-9]의 테스트 메서드에서 jun, camel, poro가 모두 전진하도록 5, 6, 8의 값을 인자로 주었다. 해당 테스트 메서드가 통과했기에 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분하는 기능도 만족함을 알 수 있었다.

 

2) 라이브러리

 

✅ JDK에서 제공하는 Random 및 Scanner API 대신 camp.nextstep.edu.missionutils에서 제공하는 Randoms 및 Console API를 사용하여 구현해야 한다.
✅ Random 값 추출은 camp.nextstep.edu.missionutils.Randoms의 pickNumberInRange()를 활용한다.
✅ 사용자가 입력하는 값은 camp.nextstep.edu.missionutils.Console의 readLine()을 활용한다.
✅ 사용 예시

  0에서 9까지의 정수 중 한 개의 정수 반환
  Randoms.pickNumberInRange(0,9);

 

1주차와 다르지 않은 라이브러리라 사용하는데 어려움은 없었다.


3. 기능 요구 사항

 

1주차와 같이 클래스, 메서드(기능) 후보들을 추리는 방법대로 요구 사항을 정리하고 구현할 기능 목록을 docs/README.md에 저장했다.

 

[그림 3-1]

 

노란색은 객체, 분홍색은 기능, 연두색은 기능과 관련된 조건이다.


4. 진행 방식

 

✅ 미션은 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항 세 가지로 구성되어 있다.
✅ 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.

 

2. 과제 진행 요구 사항에 언급


✅ 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.

 

1주차 미션 기능 요구 사항에는 "게임을 종료한 후 게임을 다시 시작하거나 완전히 종료할 수 있다."가 있었지만 이번에는 빠져서 살짝 의아했다. 분명 1주차와 마찬가지로 2주차 미션도 게임을 다시 시작하고 완전히 종료할 수 있는 기능이 필요했기 때문이다. 하지만 이 기능을 넣고 ApplicationTest를 돌리면 테스트가 실패하기 때문에 넣지 않았다.

 

원래는 사용자의 경주할 자동차 이름의 입력값을 받을 때 쉼표가 없는 경우와 이름이 5자 초과일 때만 고려했다. 하지만 경주할 자동차 이름들이 모두 같은 경우 각각의 자동차를 구별할 수 없기 때문에 이를 구별하는 조건 또한 추가해주었다. 

 

시도 횟수를 설정할 때 다음과 같이 예외를 발생하게 했다. 

 

[그림 4-1]

 

사용자가 숫자가 아닌 값을 입력할 상황이 있을 수도 있기 때문에 우선 Integer 클래스의 parseInt 메서드를 사용해 변환이 안될 경우 NumberFormatException (IllegalArgumentException의 자손 클래스)을 발생하도록 했다. 정수값을 입력했다 하더라도 0이하의 값을 입력하게 되면 해당 게임의 의미가 무색해지기 때문에 이와 같은 예외를 발생하도록 했다. 


5. 미션 제출 방법

 

✅ 미션 구현을 완료한 후 GitHub을 통해 제출해야 한다.
✅ GitHub을 활용한 제출 방법은 프리코스 과제 제출 문서를 참고해 제출한다.
✅ GitHub에 미션을 제출한 후 우아한테크코스 지원 사이트에 접속하여 프리코스 과제를 제출한다.
✅ 자세한 방법은 제출 가이드 참고
✅ Pull Request만 보내고 지원 플랫폼에서 과제를 제출하지 않으면 최종 제출하지 않은 것으로 처리되니 주의한다.


6. 과제 제출 전 체크 리스트 - 0점 방지

 

✅ 기능 구현을 모두 정상적으로 했더라도 요구 사항에 명시된 출력값 형식을 지키지 않을 경우 0점으로 처리한다.
✅ 기능 구현을 완료한 뒤 아래 가이드에 따라 테스트를 실행했을 때 모든 테스트가 성공하는지 확인한다.
✅ 테스트가 실패할 경우 0점으로 처리되므로, 반드시 확인 후 제출한다.

 

1) 테스트 실행 가이드

 

✅ 터미널에서 java -version을 실행하여 Java 버전이 17인지 확인한다. Eclipse 또는 IntelliJ IDEA와 같은 IDE에서 Java 17로 실행되는지 확인한다.
✅ 터미널에서 Mac 또는 Linux 사용자의 경우 ./gradlew clean test 명령을 실행하고, Windows 사용자의 경우 gradlew.bat clean test 또는 ./gradlew.bat clean test 명령을 실행할 때 모든 테스트가 아래와 같이 통과하는지 확인한다.

BUILD SUCCESSFUL in 0s

7. 그 외

 

저번 소감문 중 더 고민해야 할 부분에서 제어자에 대해 언급하겠다.

 

CarRacingGame 클래스

  • execute()를 제외한 모든 메서의 접근 제어자가 private인데 Application은 CarRacingGame에서 execute()만 실행하면 되기 때문이다. 또한 인스턴스 멤버를 사용하고 있지 않은 메서드에 대해서는 추가로 static을 붙여주었다.
  • execute를 작성하던 도중 어차피 사용자의 입력값은 CarRacingGame만 사용하는데 굳이 User (사용자) 클래스를 만들 필요가 있을까라고 생각해 입력값을 받는 set 메서드를 이 클래스 안에 선언, 구현했다.
  • 원래는 예외가 발생하는 메서드에 throws IllegalArgumentException을 선언해주었는데 RuntimeException과 같은 unchecked 예외는 하지 않는 게 관례라 하여 제거해주었다.

 

 

Car 클래스

접근 제어자는 CarRacingGame 클래스가 Car 클래스를 같은 패키지 내에서는 접근할 수 있어야 하기 때문에 멤버 변수, toString()을 제외하고는 모두 (default)이다. toString()의 Object 클래스 내에서의 접근 제어자는 public이기 때문에 자손의 접근 제어자는 조상의 것보다 범위가 좁을 수 없다.

반응형