Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Step5]자동차 경주(리팩터링) #936

Open
wants to merge 15 commits into
base: choi-ys
Choose a base branch
from

Conversation

choi-ys
Copy link

@choi-ys choi-ys commented Nov 28, 2022

안녕하세요 리뷰어님! 교육생 최용석입니다 :)

5단계 자동차 경주(리팩터링) 미션 구현이 완료되어 리뷰 요청 드립니다.


이번 미션은 이전 단계에서 리뷰해 주신 내용을 다음과 같이 정리한 후,

아래와 같은 사항에 중점을 두어 피드백 사항을 개선해 보았습니다.

  • 역할이 명확하지 않은 Cars 일급컬렉션에 우승자 산출 역할을 부여하고, 불필요한 Winners 일급컬렉션 제거(객체에 메시지를 보내라)
  • 자동차의 속성으로 적절하지 않은 전체 라운드 만큼 생성된 난수 목록인 RandomNumbers 일급 컬렉션 객체 제거
  • 난수 생성 부 추상화를 통해 난수 생성 부와 결합된 Service 계층의 테스트가 어려운 부분 개선
    • 운영 환경과 테스트 환경에 적합한 구현체를 구현한 뒤 각 실행 환경에 따라 필요한 구현체를 주입
  • 테스트 표현 개선
    • 하드코딩된 검증부 작성을 통해 기존 도메인 로직을 활용한 검증 부가 테스트로써 적절한 역할을 수행할 수 없는 문제 개선
    • 테스트 픽스처 활용으로 인해 테스트 코드가 결합될 수 있는 현상 개선을 위해, 각 테스트에 필요한 fixture 생성 부를 해당 class의 private 메서드로 구성

이번 미션 역시 부족하거나 접근이 잘못된 부분이 있으면 한번 더 리뷰 부탁드립니다!


윤혁님, 지난 미션간 꼼꼼하게 리뷰해주신 덕분에, 더 많이 고민하고 여러 방법을 시도해 볼 수 있었습니다! 👍
그 덕분에 코틀린 뿐만 아니라, 간과하고 있었던 평소 안좋은 개발 습관도 자각할 수 있었습니다!

벌써 올해가 끝나가는데요,
올해 마무리 잘하시고, 항상 건강하세요! 😄😄

별도의 역할 없이 객체를 포장하고 있는 `Cars`일급 컬렉션과 참가한 자동차 목록으로 부터 우승자를 산출하는 `Winners` 일급 컬렉션 역할을 병합
난수 발생 부를 추상화하여 운영 환경에 주입되는 구현체와 테스트 환경에 주입되는 구현체를 별도로 구현한 뒤, 각 실행 환경에 맞는 구현체를 선택하여 주입함으로써 난수로 인해 테스트 하기 어려운 문제 개선
- RandomNumberGenerator : 운영 환경에서 1~9 범위의 난수를 발생하는 구현체
- TestRandomNumberGenerator : 테스트 환경에서 의도한 수를 반환하는 구현체
- 자동차 경주를 진행하는 `RacingService` 객체에 추상화된 난수 생성 부의 구현체를 주입하여 생성한 후, 각 라운드 진행 시 운영 환경에서는 실제 난수 생성 구현체가 동작하고, 테스트 환경에서는 난수 발생 시 의도한 수를 반환하는 구현체가 동작할 수 있도록 수정
- 난수 생성 부 추상화에 따른 `Car`객체에 자동차를 표현하기 어색한 `RandomNumbers` 필드 제거
- 난수 발생 부를 추상화하여 운영 환경에 주입되는 구현체와 테스트 환경에 주입되는 구현체를 별도로 구현한 뒤, 각 실행 환경에 맞는 구현체를 선택하여 주입함으로써 난수로 인해 테스트 하기 어려운 문제 개선
- 자료형과 반환형이 생략된 변수 및 함수에 자료형과 반환형 명시
- 각 차량에 발생한 난수를 저장하는 RandomNumbers 미사용 일급 컬렉션 제거
- 테스트 픽스처로 인해 테스트 코드가 결합되어 테스트 코드 유지보수가 복잡해지는 현상 개선을 위해, 각 테스트에 필요한 fixture 생성부를 private 메서드로 구성
- 테스트 검증 부가 도메인 로직을 참조하고 있음에 따라 로직 변경 시 테스트로서의 역할을 제대로 수행할 수 없는 문제점 개선을 위해 테스트 검증 부를 하드코딩된 값으로 변경
Copy link
Member

@malibinYun malibinYun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5단계 미션 고생 많으셨습니다 🔥
피드백 문서도 정리하시고, 반영하려 노력하시는 모습들이 코드에 녹아있네요 👍
제 리뷰로 더 많은 고민을 하게 되시고, 다양한 방법들을 시도해보셨다니 정말 뿌듯합니다 😊
이번에도 코멘트 남겨두었으니 확인 부탁드릴게요!
확인해보시고 다시 리뷰 요청 바랍니다 :)
언제든 궁금한 게 있으시다면 코멘트도 좋고, 슬랙 DM으로 물어봐 주셔도 좋아요~ 💪🙂

import step3.racingcar.view.InputView.Companion.inputJoinerCarsGuideMessagePrinter
import step3.racingcar.view.InputView.Companion.inputRoundCountGuideMessagePrinter

class RacingCarController {
private val racingCarService: RacingCarService = RacingCarService()
private val racingCarService: RacingCarService = RacingCarService(RandomNumberGenerator())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service 하위의 모든 객체에서 이제 생성되는 숫자에 대해 개발자가 제어할 수 있겠네요 :) 👍👍

@@ -4,14 +4,10 @@ private const val CAR_ID_DELIMITER = "-"
private const val MOVE_CRITERIA = 4

class Car(val name: String) {
private var randomNumbers: RandomNumbers = RandomNumbers()
var distance = 0
var distance: Int = 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

distance는 외부에서 접근해서 값을 변경할 수 있겠네요!
외부에서 변경하지 못하게 막아보는 것은 어떨까요?

Comment on lines 3 to 4
class Cars private constructor(private val elements: List<Car>) {
fun elements(): List<Car> = elements
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elements 프로퍼티를 공개하는 것과 element() 메서드를 따로 만드는 것은 큰 차이가 없어보이네요 :)

Comment on lines 10 to 11
fun winnerNames(): String =
findWinnerNames().joinToString(WINNER_NAME_JOINING_SEPARATOR)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

승자들의 이름들을 , 로 묶어내는 일은 뷰의 일로 보이네요 :)

Comment on lines 3 to 4
interface RandomNumber {
fun value(): Int
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하위 구현체가 "랜덤 숫자 생성기" 이니, 상위 인터페이스는, "숫자 생성기" 정도가 어떨까요?
그러면 다른 구현체를 만들 때, 다른 종류의 숫자 생성기를 만들어볼 수도 있겠네요 🙂

fun play(playInfo: PlayInfo) {
repeat(playInfo.totalRound) {
playEachRound(it, playInfo.cars)
}
printWinner(Winners.of(playInfo.cars))
printWinner(playInfo.cars)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

도메인 로직을 수행하는 Service는 View의 일을 가지고 있네요!
해당 로직은 controller로 옮기는 것은 어떨까요?

Comment on lines 19 to 21
cars.elements().forEach {
it.race(currentRoundIndex)
val randomNumber = randomNumber.value()
it.race(randomNumber)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cars에서 직접 값을 꺼내서 메서드를 호출하기 보다, cars에게 메시지를 던져보는 것은 어떨까요?

Comment on lines 7 to 11
given("경주에 참가하는 자동차 한대에 4이상의 숫자가 주어지고") {
val currentRoundIndex = 0
val givenCar = CarFixtureGenerator.난수를_가지는_차량_생성("참가 차량", 4)
val givenCar = Car("참가 차량")

`when`("경주를 진행하면") {
givenCar.race(currentRoundIndex)
givenCar.race(4)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given과 when이 조금 섞여있어 보여요.
given에는 필요한 차량만을 만들고 있고, when에서 4이상의 숫자를 넣고 있는데, when에 대한 내용이 given에 적혀있네요!

Comment on lines 8 to 10
val 첫_번째_차량 = 경주를_완료한_차량_생성("첫 번째 차량", 1, 1, 1, 4, 4)
val 두_번째_차량 = 경주를_완료한_차량_생성("두 번째 차량", 1, 1, 4, 4, 4)
val 세_번째_차량 = 경주를_완료한_차량_생성("세 번째 차량", 4, 4, 4, 1, 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Car의 생성자에 distance 초기값을 넣게 만들어보는 것은 어떨까요?
그렇다면, 일일이 race를 호출하지 않고도, 원하는 위치에 존재하는 Car 객체를 만들어내서 손쉽게 테스트해볼 수 있겠네요 :)

- variable로 선언된 distance(누적 주행 거리) 속성의 외부 변경을 제한하기 위해 누락된 private 접근 제어자 추가
- 미사용 상수 제거 및 distance 속성의 초기값 상수 추가
- 경주가 완료된 차량의 용이한 테스트를 위해 `Car` 도메인의 `distance` 속성의 초기값 설정이 가능하도록 수정
- 우승자 산출 방식 변경 : 해당 차량의 누적 주행거리가 최대값인지를 확인하는 함수 작성
  - AS-IS : 우승자 산출을 위해 경주에 기존 `Cars` 객체에서 각 원소(Car)를 순회하며 distance 속성에 접근하여 최대 누적 주행거리를 비교
  - TO-BE : `Cars` 객체에서 각 원소(Car)를 순회하며 누적 주행거리가 최대값인 원소를 추출
- 우승자 산출을 위한 Cars 도메인 TC의 Car.race() 호출 부 개선
  - 우승자 산출 테스트를 위해 Car.race()를 호출하여 누적 주행거리를 설정하는 테스트 방식을 Car 도메인의 distance 속성 초기값 설정을 통해 제거하여 테스트 간소화
  - 경주를 완료한 차량 생성을 위해 더 이상 불필요한 test fixture 생성 함수 제거
- 생성자의 `elements` 프로퍼티를 반환하는 elements() 함수를 제거하고, 해당 프로퍼티를 public 으로 변경
- ','를 기준으로 우승자 이름을 묶어내는 `Cars` 도메인의 함수를 View 계층으로 이동
- Service 계층에서 Cars 도메인의 각 원소(Car)에 접근하여 경주를 진행하는 방식에서 Cars 도메인에 메세지를 보내어 각 원소(Car)의 race() 함수를 호출함으로써 도메인이 메세지를 받아 직접 로직을 처리 하도록 수정
- AS-IS : `Cars` 도메인에서 직접 각 라운드 결과를 출력하고, `RacingService` 계층에서 우승자를 출력
- TO-BE :
  - 각 `Car` 객체가 해당 라운드의 경주를 완료 하면 각 라운드 결과를 기록한 객체(RoundResult)를 반환
  - `Cars` 객체는 `Car` 객체로부터 각 라운드 결과 객체를 반환받아 합산한 후, 전체 라운드 결과 객체(RoundResults)를 반환
  - [x] `RacingService` 계층은 반환받은 전체 라운드 결과 객체를 출력부에 전달하여 로직과 출력부를 분리
- 각 라운드 결과 객체에 현재 `Car` 객체의 상태값을 저장하기 위한 DeepCopy 함수 구현
- Cars 도메인 계층에서 출력부를 분리함에 따라, 출력부에서 접근하고 있던 `Cars` 도메인의 `elements` 프로퍼티의 접근 제한자를 `private`에서 `public`으로 변경
- 개선 사항 :
  - 출력을 위해 public 으로 열려있는 도메인 계층의 프로퍼티의 접근 제한자를 private 으로 변경
  - 서비스 계층과 도메인 계층은 자동차 경주만을 위한 코드가 작성되고, 출력 부는 로직이 완료 된 후 반환된 객체를 넘겨 받아 결과를 출력함으로써 출력부에 대한 로직 실행부의 의존성 제거
- AS-IS : raceRoundResult()
- TO-BE : race
@choi-ys
Copy link
Author

choi-ys commented Nov 30, 2022

안녕하세요 리뷰어님, 교육생 최용석입니다!

리뷰해주신 내용을 다음과 같이 정리하고,

아래와 같은 사항에 중점을 두고 수정해 보았습니다.


서비스 계층과 도메인 계층에 결합된 출력 로직 분리를 위해 아래와 같이 로직 개선 계획을 세우고 수정하였습니다.

  • Car 객체는 해당 라운드의 경주를 완료 한 후, RoundReuslt 객체에 해당 라운드의 전진/멈춤 결과가 누적된 Car 객체를 deep copy 하여 기록한 후 반환
  • Cars 객체는 전체 라운드가 종료될 때 까지 경주에 참여하는 Car객체로 부터 각 라운드 결과(RoundResult)를 반환받아 전체 라운드 결과를 기록하는 RoundResults 일급 컬렉션에 합산한 후 반환
  • RacingService 계층은 반환 받은 전체 라운드 결과(RoundResults)를 출력부에 전달하여 로직과 출력부를 분리

로직과 출력부 분리를 통해 아래와 같은 개선 효과를 얻을 수 있었습니다.

  • 출력을 위해 public 으로 열려있는 도메인 계층의 프로퍼티의 접근 제한자를 private 으로 변경
  • 도메인과 서비스 계층에는 자동차 경주만을 위한 코드가 작성되고, 로직이 완료 된 후 출력 부로 객체를 반환하여 결과를 출력함으로써 로직 실행부의 출력부에 대한 의존성 제거

외부에서 객체 속성에 접근하여 로직을 처리하는 방식에서 객체에 메시지를 보내어 객체가 직접 처리할 수 있도록 수정

  • AS-IS : RacingService 계층에서 Cars 일급컬렉션의 원소인 참가 차량 목록(List)를 조회한 후, 목록을 순회하며 Car.race()를 호출하여 각 차량의 경주를 진행
  • TO-BE : Cars 일급컬렉션에 메시지를 보내 각 차량의 경주를 진행

getter를 이용한 외부 처리 방식에서 객체 내부로 메시지를 전달하는 처리 방식으로 인해 얻을 수 있는 효과를 고민한 후 아래와 정리해 보았습니다.

  • 관련 도메인이 직접 로직을 처리함에 따라 외부 계층으로 로직이 분산되지 않고 도메인에 응집되어 유지 보수성을 향상 시킬 수 있고, 무분별한 Getter 사용을 억제할 수 있다.
  • 도메인으로 로직 처리를 이동함에 따라 서비스 계층의 의존성 통제(Mocking, Sttubing) 없이 테스트가 가능해졌다.

이번 피드백 역시 많은 생각을 가지고 여러 해결 방법과 그로 인해 얻을 수 있는 효과를 고민해보며 구현해보는 좋은 시간이었습니다! 👍👍
부족한 부분이 있거나 더 좋은 방안이 있다면 한번 더 피드백 부탁드립니다! 😄


어제는 눈도 오고⛄ 날씨가 많이 추워졌네요!
항상 감기 조심하시고, 즐거운 주말 보내세요!

Copy link
Member

@malibinYun malibinYun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자동차 경주 미션 정말 고생 많으셨어요!! 🔥
정말 많은 생각을 하셨다는 게 온라인을 통해서도 잘 느껴졌어요!
매 단계마다 나은 코드를 작성했다는 것을 체감하셨으면 하는 바람이에요 😊
몇 가지 코멘트 남겨두었으니 확인 부탁 드릴게요!
더 시도해보고 싶으신 내용이 있으시다면 다시 리뷰 요청 부탁드리고, 여기서 마무리 짓고 싶으시다면 슬랙 DM으로 메시지 부탁드릴게요!
긴 기간 동안 자동차 경주 완주 정말 고생 많으셨습니다~! 😊

Comment on lines +62 to +95
## 2차 코드 리뷰 피드백 내용 정리
* Car 도메인 수정
* [x] `variable`로 선언된 `distance`(누적 주행 거리) 속성의 외부 변경을 제한하기 위해 누락된 `private` 접근 제어자 추가
* [x] 미사용 상수 제거 및 `distance` 속성의 초기값 상수 추가
* [x] 경주가 완료된 차량의 용이한 테스트를 위해 `Car` 도메인의 `distance` 속성의 초기값 설정이 가능하도록 수정
* [x] 우승자 산출 방식 변경 : 해당 차량의 누적 주행거리가 최대값인지를 확인하는 함수 작성
* AS-IS : 우승자 산출을 위해 경주에 기존 `Cars` 객체에서 각 원소(Car)를 순회하며 distance 속성에 접근하여 최대 누적 주행거리를 비교
* TO-BE : `Cars` 객체에서 각 원소(Car)를 순회하며 누적 주행거리가 최대값인 원소를 추출
* [x] 우승자 산출을 위한 `Cars` 도메인 TC의 Car.race() 호출 부 개선
* 우승자 산출 테스트를 위해 Car.race()를 호출하여 누적 주행거리를 설정하는 테스트 방식을 `Car` 도메인의 `distance` 속성 초기값 설정을 통해 제거하여 테스트 간소화
* 경주를 완료한 차량 생성을 위해 더 이상 불필요한 `test fixture` 생성 함수 제거

* Cars 도메인 수정
* [x] private 생성자의 `elements` 프로퍼티를 반환하는 getter 제거하고, 해당 프로퍼티를 public 으로 변경
* [x] ','를 기준으로 우승자 이름을 묶어내는 `Cars` 도메인의 함수를 View 계층으로 이동
* [x] Service 계층에서 `Cars` 도메인의 각 원소(Car)에 접근하여 경주를 진행하는 방식에서 Cars 도메인에 메세지를 보내어 각 원소(Car)의 race() 함수를 호출함으로써 도메인이 메세지를 받아 직접 로직을 처리 하도록 수정

* [x] 명확하지 않은 난수 생성기 인터페이스 이름 변경
* AS-IS : RandomNumber
* TO-BE : NumberGenerator

* [x] 명확하지 않은 Given/When 테스트 표현 개선

* 도메인 계층과 서비스 계층에 구현된 출력 부를 View 계층으로 이동
* AS-IS : `Cars` 도메인에서 직접 각 라운드 결과를 출력하고, `RacingService` 계층에서 우승자를 출력
* TO-BE :
* [x] 각 `Car` 객체가 해당 라운드의 경주를 완료 하면 각 라운드 결과를 기록한 객체(RoundResult)를 반환
* [x] `Cars` 객체는 `Car` 객체로부터 각 라운드 결과 객체를 반환받아 합산한 후, 전체 라운드 결과 객체(RoundResults)를 반환
* [x] `RacingService` 계층은 반환받은 전체 라운드 결과 객체를 출력부에 전달하여 로직과 출력부를 분리
* [x] 각 라운드 결과 객체에 현재 `Car` 객체의 상태값을 저장하기 위한 DeepCopy 함수 구현
* [x] Cars 도메인 계층에서 출력부를 분리함에 따라, 출력부에서 접근하고 있던 `Cars` 도메인의 `elements` 프로퍼티의 접근 제한자를 `private`에서 `public`으로 변경
* 개선 사항 :
* 출력을 위해 public 으로 열려있는 도메인 계층의 프로퍼티의 접근 제한자를 private 으로 변경
* 서비스 계층과 도메인 계층은 자동차 경주만을 위한 코드가 작성되고, 출력 부는 로직이 완료 된 후 반환된 객체를 넘겨 받아 결과를 출력함으로써 출력부에 대한 로직 실행부의 의존성 제거
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

피드백 내용들을 정말 열심히 곱씹으시며 많은 고민을 하신 것들이 잘 느껴지는 대목이네요 😊👍👍

Comment on lines 5 to +6

class Car(val name: String) {
private var randomNumbers: RandomNumbers = RandomNumbers()
var distance = 0

fun addRandomNumber(randomNumber: Int) = randomNumbers.add(randomNumber)

fun race(currentRoundIndex: Int) {
val randomNumberByCurrentRound: Int = randomNumbers[currentRoundIndex]
if (isMove(randomNumberByCurrentRound)) {
class Car(val name: String, var distance: Int = INITIAL_DISTANCE) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여전히 외부에서 distance 값을 변형시킬 수 있습니다 😥
생성자에 반드시 멤버변수를 동시에 선언할 필요는 없답니다 :)

아래 처럼 작성해볼 수 있어요! 참고 정도로만 봐주시면 좋겠습니다 🙂

class Car(
    val name: String,
    initialDistance: Int = 0,
) {
    var distance: Int = initialDistance
        private set

    ...
}

Comment on lines 12 to +13

fun isMaximumDistance(maxDistance: Int): Boolean = distance == maxDistance
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 함수명은 내용과 사뭇 달라 보입니다.
car의 거리가 파라미터와 같은 지 물어보는 내용이기 때문에,
hasDistance(distance) 정도는 어떨까요?
distance 값 정도를 ==로 비교하는 것은 비교적 간단한 연산이기 때문에, 외부에서 꺼내서 비교해도 괜찮다고 생각해요 :)

Comment on lines +3 to +5
interface NumberGenerator {
fun value(): Int

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kotlin에서 1개 함수만 있는 인터페이스를 람다로 사용하고 싶을때는,
fun interface 키워드를 사용하면 SAM(Single Abstract Method) 기능을 사용할 수 있어요! :)

Comment on lines +3 to +9
class RoundResult {
private val elements: MutableList<Car> = mutableListOf()

operator fun get(index: Int): Car = elements[index]
fun add(car: Car) = elements.add(car.copy())
fun size(): Int = elements.size
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

각 라운드 별 결과에서는 직접 Car를 가지지 않고, 라운드에 대한 정보만 가져보는 것은 어떨까요?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants