-
Notifications
You must be signed in to change notification settings - Fork 94
[ 자동차 경주 - 초간단 애플리케이션 ] 미션 제출 #134
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
base: qkrcodus
Are you sure you want to change the base?
Conversation
…hold 구현체 사용하도록 변경
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
채연님 미션 잘 구현해주셨네요👍
코멘트 남겨두었으며, 아래는 질문에 대한 제 생각입니다!
1. 책임 분리
숫자가 4 이상이면 자동차는 1칸 전진, 아니면 전진하지 않는다
이 요구사항만 보고 바로 "기준 비교"와 "판단"이라는 두 역할을 나누어야겠다고 판단하는 게 익숙하지 않았습니다. 처음부터 책임 분리를 잘하기 위해 어떤 기준을 가져야하는지 궁금합니다.
좋은 질문이네요! PR 메시지에 객체별로 어떤 책임을 갖는지 적어주신 부분이 있는데, 저도 예전에 미션을 할때 책임들을 나열해보고 어떤 객체에 책임을 주는게 좋을까 리스트 형식으로도 나타내고 다이어그램도 그려보면서 고민했던 것 같아요.
일단 저는 "기준비교"와 "판단"이라고 처음부터 나눌 것 같진 않습니다. 요구사항을 보면서 좀 큰 단위로 책임을 나눠보고 객체에 이를 부여해보고 전반적인 큰 그림을 보면서 조절해나가는 과정을 거칠 것 같아요. 다른 관점에서 생각해보면 Car는 단순히 본인의 위치 상태를 관리하고 이동하는 기능만 가질 수 있고, CarMover라는 친구가 있어서 여기서는 특정 전략에 따라 움직임 여부를 판단하고 Car에게 움직이라는 명령을 할 수 도 있죠!
답은 없으니 여러가지 시도를 해보셨으면 좋겠고, 처음에는 최대한 간단한 방법부터 시작해서 점점 필요해질때마다 구체적으로 들어가보면 좋을 것 같아요.
2. 인터페이스, 클래스 둘 중 뭘 사용할지 판단하기
- '어떻게' 역할을 하는지 수정 가능한 경우
- 역할만 존재하고 상태를 갖지 않는 경우
일단 질문을 먼저 드릴게요!
인터페이스를 사용하는 목적이 뭘까요? 왜 상태를 갖으면 안된다고 생각하셨나요?
추가적으로 policy 패키지를 다른곳으로 이동시켜보면 좋을 것 같아요~
### 개인적 목표 | ||
1. 주석이 없어도 읽기 좋은 코드를 짜보자. | ||
2. 메서드가 한가지 기능만 갖도록 분리하다가 따로 관리하는게 좋을 것 같으면 클래스로 구현하자. | ||
3. 단위 테스트 핵심 원칙을 따르자. | ||
4. 개념에 머무르고 있었던 객체 지향적 개념을 활용하여 코드를 작성해보자. | ||
5. 발생할 수 있는 예외 케이스를 고려하여 방어적인 코드를 작성하고, 이를 테스트 코드로 확인하자. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
개인적인 목표도 적어주셨군요👍
|
||
### 3단계 | ||
- 자동차에 이름 부여가능하고, 전진하는 자동차 출력할 때 자동차 이름을 같이 출력한다. | ||
- 자동차 이름은 , 기준으로 구분하며 이름은 5자 이하만 가능하다. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요구사항이 충족되지 않은것으로 보여요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추가적으로 현재는 자동차 이름이 중복될 수 있는데요. 고유한 자동차 이름을 갖게 변경해보면 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
InputView 에서 5자 초과 이름, 중복되는 자동차 입력 시 예외를 던지게 수정하였고, InputViewTest에서 이를 확인했습니다.
@@ -14,6 +14,7 @@ dependencies { | |||
testImplementation platform('org.assertj:assertj-bom:3.25.1') | |||
testImplementation('org.junit.jupiter:junit-jupiter') | |||
testImplementation('org.assertj:assertj-core') | |||
developmentOnly('org.springframework.boot:spring-boot-devtools') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
어떤 이유로 추가해주섰나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
잘 기억이 나지 않습니다만 아마 Spring Boot DevTools is not configured. 문구가 떠서 추가한 것 같아요!
// scanner는 공유자원 | ||
private static final Scanner scanner = new Scanner(System.in); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(1) 왜 주석을 달아주셨나요?
(2) 신규 생성할 필요 없는 객체를 static으로 선언하여 재사용하는것 좋습니다! (어떤 장점이 있을까요?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(1) 공부하다가 달아둔거라 별 다른 의미 없습니다! scanner가 상태를 갖고 변경되는 객체라고 생각하였는데, 내부 커서를 유지하며 한번 읽은 내용은 다시 읽지 않기 때문에 private static final 으로 선언 가능하다는 점을 배웠습니다.
(2) 생성자를 통해 객체를 만들면 힙에 객체가 할당되고, 일정 시간이 지나면 가비지 컬렉션가 이를 관리해준다고 알고 있습니다. static으로 선언해준다면 같은 인스턴스를 재사용하기 때문에 관리할 대상이 줄어드는 점이 장점입니다.
|
||
public static List<String> readCarNames() { | ||
System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); | ||
String[] inputNames = scanner.nextLine().split(","); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
매직넘버/리터럴을 상수로 선언하여 이름을 부여해보는 연습을 해보면 어떨까요?
private int findMaxDistance(){ | ||
int max=0; | ||
for (Car car : cars) { | ||
max=Math.max(max,car.getMovedDistance()); | ||
} | ||
return max; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
위와 같습니다!
import java.util.List; | ||
import java.util.stream.Collectors; | ||
|
||
public class WinnerFinder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
미사용되는걸로 보여요 👀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
실수로 2단계를 건너뛰고 3단계 구현을 ( RacingGame ) 먼저 해뒀기 때문에 해당 클래스는 2단계 구현 내용을 보여주기 위해 별도로 작성한 클래스입니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 클래스를 테스트해보기 위한 WinnerFindTest 클래스도 있는데 둘 다 삭제할까요!
@@ -0,0 +1,13 @@ | |||
package racinggame.policy.evaluator; | |||
|
|||
public class GreaterThanOrEqualThresholdEvaluator implements ThresholdEvaluator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
비교로직이 바뀔때마다 구현체를 만들어줘야할것 같은데, 어떠한 장단점이 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
새로운 비교 로직 ( GreaterThan 이나 LessThan )은 새 클래스만 추가하면 되어 이점이 있겠지만, 패키지 구조가 너무 복잡해질 수 있을 것 같습니다.
src/test/java/CarTest.java
Outdated
@DisplayName("랜덤값이 조건을 만족할 경우 자동차는 1칸 전진한다") | ||
@Test | ||
void 움직일_수_있을_때_1칸_전진() { | ||
// given | ||
RandomGenerator fixedGenerator = () -> 7; | ||
ThresholdEvaluator evaluator=new GreaterThanOrEqualThresholdEvaluator(4); | ||
MoveDecider decider= new ThresholdBaseMoveDecider(evaluator); | ||
MoveStrategy strategy = new OneStepMoveStrategy(fixedGenerator, decider); | ||
Car car = new Car("car1", strategy); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
하나의 테스트로 ThresholdEvaluator, MoveDecider, MoveStrategy, Car 의 기능을 모두 테스트하는걸로 보여요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MoveStrategy 구현을 위해 RandomGenerator와 MoveDecider 인스턴스를 일일이 생성해야 한다고 생각했는데, 인터페이스에 람다를 넣어 예측 가능한 구현체를 만들어줬습니다. 기능별 테스트 코드도 작성해뒀습니다!
src/test/java/RacingGameTest.java
Outdated
@Test | ||
@DisplayName("0이하 값을 이동 횟수로 입력하면 표준 예외를 출력한다.") | ||
void invalidInputMoveCount(){ | ||
RandomGenerator fixedGenerator = () -> 7; | ||
ThresholdEvaluator evaluator=new GreaterThanOrEqualThresholdEvaluator(4); | ||
MoveDecider decider= new ThresholdBaseMoveDecider(evaluator); | ||
MoveStrategy strategy = new OneStepMoveStrategy(fixedGenerator, decider); | ||
assertThatThrownBy(() -> new RacingGame(List.of("car1", "car2"), 0,strategy)) | ||
.isInstanceOf(IllegalArgumentException.class) | ||
.hasMessage("moveCount는 0보다 큰 값이여야 합니다."); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트코드도 가독성 있게 작성하는게 중요해요. 적절한 단위로 개행을 넣어주면 어떨까요?
코드리뷰 후 회고
미션을 구현하며 새로운 책임이 보일 때 마다 이를 인터페이스로 만들어줬습니다.
인터페이스 사용 이유가 뭘까 고민해봤습니다.
하지만, 기능을 세분화하다 보니 패키지 구조가 오히려 복잡해졌고, 각 기능에 대한 테스트 코드도 따로 작성해야해서 번거로움이 있었습니다.
큰 책임 단위로 나눈 뒤, 필요할 때 구체화하는 방식을 다음 미션때 생각하며 진행하겠습니다.
Car 가 move 하는 테스트 코드 작성했을 때 MoveStrategy 구현을 위해 RandomGenerator와 MoveDecider 인스턴스를 일일이 생성해야 한다고 생각했는데, 추상 메서드를 하나 갖고 있는 MoveStrategy 인터페이스에 람다를 넣어 예측 가능한 구현체를 만들어줬습니다.
-> 큰 책임으로 나누다보면 , 람다를 넣는 방식으로 테스트 코드를 만들긴 어려워보이는데 ( 인터페이스 내 메서드가 여러개 필요할 것 같아서 ) 맞을까요??
이미 InputView에서 사용자 입력을 검증해주지만, 도메인 객체 자체가 항상 올바른 상태가 유지 되도록 2차 검증을 해줄 수 있다는 것도 배웠습니다.
String[ ] , int[ ], list ,set을 Stream< T >로 바꿔주면 다양한 메소드를 사용할 수 있다는 점도 배웠습니다.
아쉬운 점은, 오프라인에서 controller는 콘솔, API 등 제공 방식에 따라 구현이 달라지는 부분이어서 최대한 비즈니스 로직을 넣지 않는게 좋다고 하셨는데 이를 이번 미션에 반영하지 못했습니다. 다음 미션에선 이를 염두하며 mvc 패턴을 구현하겠습니다.
1단계
개념에 머무르고 있었던 객체 지향 원칙을 적용해보려고 노력했습니다.
1. 요구 사항 해석하기 ( 주체 + 책임 관점으로 )
[ 고민한 부분 ]
초반 설계 - 주체 : 책임
구현하다 보니
MoveDecider
가 두 가지 책임을 갖고 있어이 책임을 분리하고자 아래처럼 리팩토링했습니다
[ 질문 ]
이 요구사항만 보고 바로 "기준 비교"와 "판단"이라는 두 역할을 나누어야겠다고 판단하는 게 익숙하지 않았습니다. 처음부터 책임 분리를 잘하기 위해 어떤 기준을 가져야하는지 궁금합니다.
2. 주체의 메소드( 역할 )와 필드( 상태 )엔 뭐가 필요할지 생각하기
Car
MoveStrategy
MoveDecider
ThresholdEvaluator
RandomGenerator
3. 인터페이스, 클래스 둘 중 뭘 사용할지 판단하기
[ 기준 ]
주체의 역할을 인터페이스로 분리할지 여부는 다음 두 가지 기준을 고려하여 결정하였습니다.
1. '어떻게' 역할을 하는지 수정 가능한 경우
�ex ) move 가 2씩 움직인다 , 0-9까지가 아닌 0-99 숫자들 중 generate, 항상 can move 한다, 5이상일때 is satisfied )
2. 역할만 존재하고 상태를 갖지 않는 경우
결과는 다음과 같습니다

[ 질문 ]
너무 과하게 역할을 인터페이스로 분리한 것이 아닌가 싶은데, 앨리님은 어떻게 생각하시나요? 제 판단 기준이 적절해 보이는지도 궁금합니다.
4. 객체들 협력 관계 생각하기
[ 고민한 부분 ]
어떤 객체가 어떤 객체를 필요로 하는지 생각할 때 너무 헷갈렸습니다.
또한 협력 관계를 구현할때 구현체인 클래스가 아니라 인터페이스를 통해 하며 외부에서 구현체를 생성자를 통해 주입 받는 방식, 개념으로만 알고 있었던 DIP 를 경험해볼 수 있어서 좋았습니다.
[ 질문 ]
앨리님은 협력 관계를 설계하실 때 어떤 기준이나 사고 과정을 따르시나요?
요구 사항 분석 -> 인터페이스 설계 -> 클래스 구현하며 협력 관계 생각 -> 타깃 객체 (Car) 구현하며 협력 관계 생각 순서로 구현하는게 좋아보이는데 이 방법이 맞을지도 궁금합니다.
2단계
[ 고민한 부분 ]
공동 우승자나 단일 우승자 여부를 테스트하려면, 자동차가 어떻게 움직일지 결과를 예측할 수 있어야 하는데,
car.move()는 내부적으로 MoveStrategy의 구현체인 OneStepMoveStrategy를 사용하기 때문에 고정된 이동 결과를 반환하는 FixedStrategy 를 테스트 클래스 내부에 별도로 구현하였습니다.
3 4단계
[ 고민한 부분 ]
발생할 수 있는 예외 상황과 테스트 가능한 상황들을 생각해보았습니다.
2단계에선 FixedStrategy 를 통해 인자로 받는 distance 만큼 무조건 움직이게 해뒀지만 아눈 내부의 threshold 판단 로직을 건너뛰기 때문에 OneStepMoveStrategy을 사용하되, 랜덤값을 고정하기 위해 RandomGenerator의 구현체로 fixedGenerator를 주입하여 테스트했습니다.
ps. 제가 실수로 2단계를 건너뛰고 3단계를 먼저 구현했습니다 😢