-
Notifications
You must be signed in to change notification settings - Fork 60
[그리디] 하수한 사다리 미션 제출합니다. #83
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
Conversation
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.
안녕하세요 수한님! 미션 하시느라 고생 많으셨어요😃
앞으로 함께 많은 이야기 나눌 수 있으면 좋겠습니다. 잘 부탁드립니다!
우선 PR에 각 클래스별로 코드의 구조와 흐름이 꼼꼼하게 정리되어 있어서 읽기 편했어요.
추가로, 저는 개인적으로 README에 코드에서 사용되는 용어나 클래스 또는 변수명 등을 정리해두면 다른 분들이 이해하기 더 쉽더라구요. 나중에 한 번 적용해보셔도 좋을 것 같습니다!
로직 전반적으로 객체가 잘 분리되어 있고 VO도 적절하게 활용하신 점이 깔끔하니 아주 좋았습니다!
입력값에 대한 검증 로직이 조금 더 보강되면 더 견고한 코드가 될 것 같아요. 미션 요구사항에 명시되지 않았더라도, 수한님께서 필요하다고 판단되는 검증은 자유롭게 추가해보셔도 좋을 것 같습니다 :)
이번 리뷰에서는 Controller의 흐름을 따라가며, 비즈니스 로직을 제외한 부분을 중심으로 피드백을 남겨보았습니다. 다음 리뷰에서는 비즈니스 로직을 위주로 읽어보겠습니다!
🤔 질문사항 답변
아래는 질문사항에 대한 답변입니다
- 이번 미션에서는 함수형 프로그래밍 문법을 적극적으로 사용해보았는데, 사용 과정에서 로직이 복잡해질수록 depth 자체는 감소할 수 있어도 가독성 측면에서는 오히려 알아보기가 힘들어진다는 느낌을 받았습니다. (e.g., LadderController::handleOutcomeQuery) 따라서 이러한 부분에 대해 어느 정도 trade-off가 존재한다고 생각하는데, 해윤님은 어떠한 생각을 가지고 계신지 궁금합니다..!
새로운 방식을 시도해보신 점이 정말 좋네요👍
저는 함수형 프로그래밍 방식을 도입했더니 코드의 가독성이 좋아지지 않았다면, 다른 방법도 고안해볼 것 같아요! 예를 들어 depth를 줄이기 위해, stream등의 문법을 사용했는데 가독성이 안좋아졌다면, 다른 방법으로 depth를 줄일 수 있는 방법을 생각해보는거죠.
또한 어떤 로직이 너무 길고 복잡해보인다 하면, 한 메서드 또는 절이 하나의 역할만 하는가? 를 먼저 고민해보면 좋을 것 같아요. 하나의 절이 많은 역할을 수행하게되면 읽기 힘든 복잡한 코드가 되기 쉬우니까요!
말씀해주신 LadderController의 handleOutcomeQuery 메서드를 보도록하겠습니다. 해당 메서드는 입력 + 반복 제어 + 결과 조회 + 출력의 역할을 하고있어요. 근데 네가지 기능을 하나의 stream절에서 실행하고 있으니 딱 봤을 때 흐름이 눈에 쉽게 들어오지 않는 것 같아요. 이러한 점들을 고려하여 리팩토링 진행해보시면 좋을 것 같습니다.
결론은 읽기 쉬운 코드를 작성하는 것은 매우 중요하기 때문에, 가독성에서의 문제를 느꼈다면 사용하지 말자(=대안을 찾자)! 였습니다
- 이번 미션의 경우에도 depth가 2를 넘지 않게 구현하도록 하는 조건이 명시되어 있었습니다. LadderFactory::createConnections() 메소드의 경우 고민을 해보았으나 도무지 depth를 여기서 더 줄일 방법이 떠오르지 않았습니다 ㅠ 혹시 해윤님은 이와 관련해서 다른 아이디어가 있으신지 궁금합니다!
생각나는 방법은
- Early return
for (int i = 0; i < width - 1; i++) {
if (!shouldConnect(random)) continue;
connections.add(new Connection(i, i + 1));
i += 1; // skip the right next line
}
'어떤 조건을 만족한다면 ~~해라' 가 아닌, 기본적으로 ~~를 하지만 '어떤 조건을 만족하지 않는다면 그 전에 반환 또는 break 또는 continue'를 하는 방법입니다.
조금 억지같지만 depth는 1로 만들 수 있을 것 같아요!
- 메서드 분리
if (shouldConnect(random)) {
connections.add(new Connection(i, i + 1));
i += 1; // skip the right next line
}
가장 많이 사용해 보셨을 방법인데요, 위 로직을 메서드로 분리하면 depth가 줄어들게 됩니다. 이렇게 되면 반복과, 연결 생성을 별도의 메서드로 분리할 수 있다는 장점도 있겠네요!
| public class Participant { | ||
| public static final int PARTICIPANT_NAME_MAX_LENGTH = 5; | ||
| private final String name; | ||
|
|
||
| public Participant(String name) { | ||
| if (name.length() > PARTICIPANT_NAME_MAX_LENGTH) { | ||
| throw new IllegalArgumentException("참가자의 이름은 최대 " + PARTICIPANT_NAME_MAX_LENGTH + "자만 가능합니다."); | ||
| } | ||
|
|
||
| this.name = name; | ||
| } | ||
|
|
||
| public String getName() { | ||
| return name; | ||
| } | ||
| } |
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.
수정할 필요는 없지만 같은 단순한 값을 나타내는 객체를 VO라고 하는데요, 값 객체는 class가아닌 record로 사용해도 좋습니다!
record는 모든 필드값이 불변 객체로 선언되고, equals(), hashCode(), toString()등의 코드가 자동 생성된다는 장점이 있습니다.
이러한 개념이 있다고 알고 넘어가면 좋을 것 같아요~
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.
앗 저번 스터디에서도 잠깐 언급되었던 내용이었던 것 같은데, 아무래도 적용해주는편이 반복적인 getter 메소드도 줄일 수 있어 훨씬 좋은 것 같습니다!
f1244e4 커밋에 반영했습니다~
| // method chaining으로 구성하려고 하였으나 Input 검증으로 인해 각각 따로 받음 | ||
| GameConfigurationBuilder builder = new GameConfigurationBuilder(); | ||
|
|
||
| List<Participant> participants = InputView.getParticipants().stream().map(Participant::new).toList(); |
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.
입력받은 String을 List로 변환하는 과정을 두 단계로 나누어
string을 List으로 변환하는 로직은 inputView에 위치하고,
List을 List와 같이 객체들의 리스트로 저장하는 로직은 컨트롤러에 작성하셨네요!
해당 로직을 모두 controller에서 진행하지 않고, 단계 별로 계층을 다르게 한 이유가 궁금합니다!
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.
추가로 participant 입력에 대한 검증 로직이 없어
,또는,,(,만으로 이루어진 문자열) 입력한 경우- 공백 혹은 아무것도 입력하지 않은 경우
에 예외를 던지지 않고 다음 단계 입력 로직이 실행되네요! 적절한 위치에 검증 로직을 추가해 위와 같은 경우에도 예외문을 던지면 좋을 것 같은데, 어디에 위치시키면 좋을 것 같나요?
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.
입력받은 String을 List로 변환하는 과정을 두 단계로 나누어 string을 List으로 변환하는 로직은 inputView에 위치하고, List을 List와 같이 객체들의 리스트로 저장하는 로직은 컨트롤러에 작성하셨네요!
해당 로직을 모두 controller에서 진행하지 않고, 단계 별로 계층을 다르게 한 이유가 궁금합니다!
해당 방식의 경우 InputView에서는 정말 기본적인 입력값에 대한 처리(e.g., 목록의 경우 split 후 List로 변환)만 담당하고, 이후 해당 값들을 의미있는 객체로 변환하는 과정은 컨트롤러와 같이 관련 있는 로직에서 담당하는 것이 더 좋다고 판단하여 두 단계로 나누어서 이루어질 수 있도록 작성하였습니다!
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.
추가로 participant 입력에 대한 검증 로직이 없어
,또는,,(,만으로 이루어진 문자열) 입력한 경우- 공백 혹은 아무것도 입력하지 않은 경우
에 예외를 던지지 않고 다음 단계 입력 로직이 실행되네요! 적절한 위치에 검증 로직을 추가해 위와 같은 경우에도 예외문을 던지면 좋을 것 같은데, 어디에 위치시키면 좋을 것 같나요?
제가 테스트 해보았을때는 두 케이스(,, ,,) 모두 정상적으로 검증 로직(GameConfigurationBuilder::participants())이 실행되는데, 혹시 제가 놓친 부분이 있을까요?
공백 입력의 경우 #83 (comment) 에서 이어서 다루도록 하겠습니다!!
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.
제가 테스트 해보았을때는 두 케이스(,, ,,) 모두 정상적으로 검증 로직(GameConfigurationBuilder::participants())이 실행되는데, 혹시 제가 놓친 부분이 있을까요?
다시 실행해보니 정상 작동하네요😂
| public static List<String> getParticipants() { | ||
| System.out.println("\n참여할 사람 이름을 입력하세요. (이름은 " + INPUT_DELIMITER + "로 구분하세요)"); | ||
|
|
||
| return Arrays.stream(scanner.next().split(INPUT_DELIMITER)).toList(); |
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.
저라면 해당 로직을 이렇게 작성할 것 같아요!
return Arrays.asList(scanner.nextLine().split(INPUT_DELIMITER));
저는 입력을 받을 때 한 줄씩 받기 위해 scanner.nextLine으로 받는 편인데 scanner.next를 선택한 이유가 있을까요?
또한 고정된 리스트인 Arrays.asList()대신 Stream.toList()를 사용한 특별한 이유가 있다면 궁금합니다!
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.
scanner.next()의 경우 사실 별 다른 의미가 있어서 선택한 것은 아닙니다..! 이와 관련해서 찾아보니 next()의 경우 공백 문자 이전까지 입력을 받고, nextLine()의 경우 개행 문자 이전까지 입력을 받는것으로 이해했습니다. 현재 미션의 경우 한 라인 별로 구분하여 입력 받는 것이 더 적절하다고 판단하여 nextLine()으로 수정하도록 하겠습니다!
Stream.toList()는 최초 구현 당시에 InputView에서 Participant에 대한 맵핑도 함께 수행하다보니 스트림 연산의 용이성을 위해 해당 방식으로 구현을 진행했었습니다. 하지만 맵핑 로직을 컨트롤러로 분리함에 따라 더 이상 스트림 연산을 수행할 필요가 없어 말씀하신 Arrays.asList()를 사용하는 방식도 문제가 없을 것 같아 해당 방식으로 진행해보겠습니다!
| public GameConfigurationBuilder participants(List<Participant> participants) { | ||
| if (participants == null || participants.isEmpty()) { | ||
| throw new IllegalArgumentException("참가자 목록은 비어 있을 수 없습니다."); | ||
| } | ||
|
|
||
| this.participants = participants; | ||
|
|
||
| return this; | ||
| } |
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.
엔터를 입력 받은 후의 participants 배열을 찍어보니 아래와 같이 name이 공백인 객체가 들어가 있는 것을 확인했습니다.

따라서 Participant의 생성자에 공백 유무를 검사하는 로직을 추가하여 수정하였습니다~!
반영 커밋: d3d50f7
| public class LadderController { | ||
| public void run() { | ||
| try { | ||
| GameConfiguration configuration = readConfiguration(); |
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.
개별 값(participants, outcomes, width, height)을 바로 넘기지 않고, GameConfiguration 객체를 통해 입력값을 한 번에 전달하도록 설계하신 이유가 궁금합니다!
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.
제가 생각해보았을때 GameConfiguration객체를 도입한 이유는 다음과 같습니다:
-
입력 검증 로직과 게임 로직 분리
현재GameConfiguration객체의 경우GameConfigurationBuilder를 통해 생성되도록 의도하였는데, 검증 로직의 경우 빌더가 담당함으로써 컨트롤러에서는 게임 실행 자체만을 담당할 수 있도록 구성하였습니다. -
입력 값에 대한 순차적 검증
사실 빌더 패턴을 도입하게된 이유라고 볼 수도 있을 것 같은데, 기존validate()함수와 같은 방식으로 사용하게 되면, 아래와 같이 검증이 필요한 값들에 대하여 대체로 한 곳에서 검증을 수행합니다.
private static void validateParams(List<Participant> participants, List<String> outcomes, int width, int height) {
if (participants.size() != outcomes.size()) {
throw new IllegalArgumentException("참가자의 수와 실행 결과의 수는 같아야 합니다.");
}
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("크기는 양수만 입력할 수 있습니다.");
}
}해당 방식의 경우 검증할 값이 많아질 수록 가독성이 떨어진다고 판단했고, 값이 많아짐에 따라 메소드 별로 분리하는 방법도 생각해볼 수 있으나 이 방식 또한 결국 각각의 검증 메소드를 호출해야 한다는 부분이 효율적이지 못하다고 판단했습니다.
따라서 빌더를 통해 값 할당과 동시에 검증 로직을 수행하는 방식이 위 두가지 단점을 해결할 수 있다고 판단하여 빌더 패턴 방식을 도입해보았습니다.
Game생성의 단순화
만약 인자 수가 많아진다면 인자의 순서가 헷갈릴 수도 있고, 새로운 필드가 추가된다면 이에 따라 계속 수정해야 한다는 것이 번거롭다고 판단했습니다. 따라서 객체 하나만을 넘겨주는 방식을 통해 이러한 문제를 해결할 수 있었습니다.
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번에 관해서는
해당 방식의 경우 검증할 값이 많아질 수록 가독성이 떨어진다고 판단했고, 값이 많아짐에 따라 메소드 별로 분리하는 방법도 생각해볼 수 있으나 이 방식 또한 결국 각각의 검증 메소드를 호출해야 한다는 부분이 효율적이지 못하다고 판단했습니다.
라는 의견에 동의합니다! builder로 검증 로직을 분리하여 현재는 도메인이 더욱 비즈니스 로직에 집중할 수 있도록 설계된 것 같아요.
다만 매우매우 개인적으로는 도메인이 유효성까지 스스로 보장하는 것도 DDD 관점에서 괜찮은 접근이라고 생각합니다! 그래도 지금은 설계 의도도 명확하고, 충분히 적절해 보여요👍
| Stream.generate(InputView::getParticipantForResult) | ||
| .takeWhile((input) -> !input.equals("all")) | ||
| .forEach((input) -> result.getResults().keySet().stream() | ||
| .filter((participant) -> participant.getName().equals(input)) | ||
| .findFirst() | ||
| .ifPresentOrElse( | ||
| (target) -> OutputView.printGameResultOf(target, result), | ||
| () -> System.out.println("존재하지 않는 참가자입니다.") | ||
| ) | ||
| ); |
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.
하나의 stream에서 너무 많은 역할을 하고 있는 것 같아요! 로직을 분리해보면 좋을 것 같습니다~
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.
확인했습니다! 질문드린 내용이랑 같은 부분인지라 전체 코멘트에 관련 내용 달아놓도록 하겠습니다!!
|
|
||
| public class InputView { | ||
| private static final Scanner scanner = new Scanner(System.in); | ||
| private static final String INPUT_DELIMITER = ","; |
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.
구분자를 view계층에도 상수화시켜 관리 포인트를 줄인점이 매우 좋네요~
|
해윤님 작성해주신 리뷰에 대한 반영 및 질문에 대한 코멘트 달아놓았습니다! depth의 경우에도 해윤님께서 주신 의견과 더불어 한번 생각을 해보았는데, 2번 아이디어의 경우 현재 로직 상 반복문 내 인덱스( 또한 입력값에 대한 검증 로직 보강 관련해서도 말씀해주셨는데요, 현재 결과 값에 대한 검증은 별도로 이루어지고 있지 않아(단순 String으로 취급), 이에 대한 VO를 만들어서 검증 로직을 적용시켜 보았는데 (9648d42), 이외에 더 필요하다고 생각되는 검증 로직이 있을까요? 마지막으로 |
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.
리뷰 꼼꼼히 잘 반영해주셨네요👍 커밋을 기능 단위로 잘 찍어주셔서, 수정 범위를 확인하기 수월했어요~
또한 입력값에 대한 검증 로직 보강 관련해서도 말씀해주셨는데요, 현재 결과 값에 대한 검증은 별도로 이루어지고 있지 않아(단순 String으로 취급), 이에 대한 VO를 만들어서 검증 로직을 적용시켜 보았는데 (9648d42), 이외에 더 필요하다고 생각되는 검증 로직이 있을까요?
출력값 객체(Outcome)에 대한 코멘트는 아래 추가해두었습니다!
추가 검증으로는

높이 입력 시, 정수가 아닌 자료형이 입력됐을 경우 예외를 던지는 로직도 추가되면 좋을 것 같아요!
그 외에는 검증로직을 세세하게 잘 작성해주신 것 같습니다 👏
| if (name.length() > PARTICIPANT_NAME_MAX_LENGTH) { | ||
| throw new IllegalArgumentException("참가자의 이름은 최대 " + PARTICIPANT_NAME_MAX_LENGTH + "자만 가능합니다."); | ||
| } | ||
|
|
||
| if (name.isBlank()) { | ||
| throw new IllegalArgumentException("참가자의 이름은 공백일 수 없습니다."); | ||
| } |
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.
수한님께서 어떤 검증 로직은 도메인 내부에 두고, 어떤 검증 로직은 GameConfigurationBuilder 내부에 두었는지 기준이 궁금합니다!
도메인 값 자체의 유효성 검증은 도메인 내부에서 이루어졌다고 한다면(Participant나 Outcome)
GameConfigurationBuilder의 participants와 height도 도메인 값 자체의 유효성 검증이라는 생각이 들어서요. 의견이 궁금합니다 💭
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.
구현 당시에는 객체 자체에 대한 검증(e.g., Participant 이름 최대 글자 수, Outcome 값 공백 여부)은 도메인 내부에서 수행하도록 의도하였고, 객체가 다른 객체로 wrapping된 경우(e.g., List<Participant>)에 대해서 검증이 필요한 경우에는 GameConfigurationBuilder에서 담당하도록 의도하였습니다.
하지만 말씀하신대로 height 검증 로직의 경우에는 도메인 값 자체의 유효성 검증을 수행하는것임에도 현재 GameConfigurationBuilder 내부에 존재하는데, 이 부분에 있어서는 height와 Outcome간 기준이 모순되는 부분이 있는 것이 맞습니다. 😅
따라서 해당 부분에 대해서 생각을 해보았는데, 만약 제 구현 의도대로라면 height 역시 객체로 wrapping되어 그 내부에서 검증 로직을 수행하는 것이 타당하다고 생각합니다.
하지만 #83 (comment) 코멘트에서 말씀해주신 것 처럼 단순히 검증 로직 하나만을 위해 객체로 관리하는 것이 타당하지 않다고 한다면, Outcome 값 공백 유무에 대한 검증도 GameConfigurationBuilder에서 담당하는 것이 맞는 것 같습니다.
Outcome을 VO로 남길지 여부에 대한 의견은 #83 (comment) 에 이어서 남겨놓겠습니다!
| private static final String INPUT_DELIMITER = ","; | ||
|
|
||
| public static List<String> getParticipants() { | ||
| System.out.println("\n참여할 사람 이름을 입력하세요. (이름은 " + INPUT_DELIMITER + "로 구분하세요)"); |
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.
개발환경마다 줄바꿈 문자가 다르기에 \n을 직접 문자열 안에 넣어서 사용하는 것 보다는
System.out.println();
System.out.println("참여할 사람 이름을 입력하세요. (이름은 " + INPUT_DELIMITER + "로 구분하세요)");
다음과 같이 작성하는 것이 더욱 직관적이고 정확할 것 같아요!
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.
확인했습니다! 7650568 커밋에 반영했습니다.
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.
해당 클래스는 객체로 관리하기에는 이유가 부족해 보입니다.
검증 로직만을 담당하고 있다면, GameConfigurationBuilder::outcomes로 옮기는 것도 괜찮을 것 같아요!
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.
현재 Outcome 객체가 단순 검증 로직만을 담당하고 있는 것은 맞습니다! 하지만 만약 Outcome객체가 없었다면, 다음과 같은 방식으로 GameConfigurationBuilder에서 검증을 추가로 수행해야할 것으로 예상됩니다:
public GameConfigurationBuilder outcomes(List<String> outcomes) {
if (outcomes == null || outcomes.size() != participants.size()) {
throw new IllegalArgumentException("참가자의 수와 실행 결과의 수는 같아야 합니다.");
}
boolean isBlank = outcomes.stream()
.map(String::trim)
.anyMatch(String::isEmpty);
if (isBlank) {
throw new IllegalArgumentException("결과는 공백일 수 없습니다.");
}
this.outcomes = outcomes;
return this;
}만약 Outcome 객체가 존재한다면 공백에 대한 검증은 이미 Outcome 객체 자체에서 수행되기에 추가 공백 검사 로직이 필요 없어져 코드를 더 간소화할 수 있는 이점이 존재한다고 생각합니다.
이러한 이점이 있기에 객체로 관리해보는 것도 고려해볼 수 있을 것 같은데, 이와 관련해서 해윤님 의견은 어떠신지 궁금합니다!
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.
저도 수한님의 의견에 매우 동의합니다!
처음에는 단순 검증 로직만을 담당하는 VO가 과할 수도 있지 않을까 고민했는데,
- 확장성
- GameConfigurationBuilder에 관련된 코멘트에서 언급하신 역할 분리
- 일관성
측면에서 현재처럼 객체를 분리하는 구조를 유지하는 것이 좋을 것 같아요! 좋은 의견 감사합니다👍
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.
메서드 호출 체인이 다소 깊은 것 같은데
indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.
위 요구사항 때문인가요? 저도 비슷한 로직 (createLine와 createConnections)은 합치려고 고민해보았지만 depth를 1까지만 허용한다는 조건때문에 쉽지 않은 것 같긴하네요🤔
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.
네 맞습니다! 사실 두 로직이 원래 하나의 메소드로 구성되어 있었는데, 해당 조건을 만족하기 위해서 지금과 같은 형태로 변경하게 되었습니다.
안그래도 로직을 나누면서 각 메소드 자체의 길이는 줄었지만, 메소드 내 호출되는 다른 메소드를 따라가면서 이해해야 하는 과정이 추가되었다는 점에서 좀 아쉽다는 생각이 들긴 했습니다 ㅠ
| List<Outcome> outcomes = configuration.outcomes(); | ||
| Map<Participant, Outcome> result = new LinkedHashMap<>(); | ||
|
|
||
| OutputView.printLadderResult(this); |
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.
view가 domain 내부에서 호출되고있네요! 어떻게 수정할 수 있을까요?
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.
다시 살펴보니 굳이 Game객체 내에서 호출될 필요가 없음에도 호출되고 있었네요! 해당 기능의 경우 LadderController가 담당하도록 이관했습니다!
1814a51 커밋에 반영했습니다.
| public GameResult execute() { | ||
| List<Participant> participants = configuration.participants(); | ||
| List<Outcome> outcomes = configuration.outcomes(); | ||
| Map<Participant, Outcome> result = new LinkedHashMap<>(); | ||
|
|
||
| OutputView.printLadderResult(this); | ||
|
|
||
| for (int start = 0; start < configuration.width(); start++) { | ||
| int end = traverse(start); | ||
| Participant participant = participants.get(start); | ||
| Outcome outcome = outcomes.get(end); | ||
|
|
||
| result.put(participant, outcome); | ||
| } | ||
|
|
||
| return new GameResult(result); | ||
| } |
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.
현재 execute()가 사다리 출력, 경로 탐색, 결과 매핑까지 모두 담당하고 있어, 책임의 범위가 넓어보입니다!
Map<Participant, Outcome> result = new LinkedHashMap<>();
result에 관련된 로직은 Map<Participant, Outcome> results를 필드로 가지고있는 GameResult 내부로 옮겨도 될 것 같은데 어떻게 생각하시나요?
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.
사다리 출력 로직의 경우 #83 (comment) 에서 언급하신 대로 이관했습니다! 그렇다면 남은 로직은 경로 탐색, 결과 매핑인데, 경로 탐색 로직과 결과 맵핑 로직이 밀접한 관련이 있어서 만약 옮긴다면 다음과 같은 형식으로 옮겨야할 것 같은데, 해윤님이 의도하신 내용이 맞는지 궁금합니다!
GameResult.java
public class GameResult {
private final Map<Participant, Outcome> results;
GameResult(Map<Participant, Outcome> results) {
this.results = results;
}
public static GameResult from(GameConfiguration configuration, Function<Integer, Integer> func) {
Map<Participant, Outcome> results = new LinkedHashMap<>();
for (int start = 0; start < configuration.width(); start++) {
int end = func.apply(start);
Participant participant = configuration.participants().get(start);
Outcome outcome = configuration.outcomes().get(end);
results.put(participant, outcome);
}
return new GameResult(results);
}
public Map<Participant, Outcome> getResults() {
return Collections.unmodifiableMap(results);
}
}Game.java
public class Game {
private final Ladder ladder;
private final GameConfiguration configuration;
private Game(GameConfiguration configuration, Ladder ladder) {
this.configuration = configuration;
this.ladder = ladder;
}
public static Game of(GameConfiguration configuration) {
return new Game(configuration, LadderFactory.createLadder(configuration.width(), configuration.height()));
}
public static Game of(GameConfiguration configuration, Ladder ladder) {
return new Game(configuration, ladder);
}
public GameResult execute() {
return GameResult.from(configuration, this::traverse);
}
// ...
}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.
public GameResult execute() {
List<Participant> participants = configuration.participants();
List<Outcome> outcomes = configuration.outcomes();
Map<Participant, Outcome> result = new LinkedHashMap<>();
for (int start = 0; start < configuration.width(); start++) {
int end = traverse(start);
Participant participant = participants.get(start);
Outcome outcome = outcomes.get(end);
result.put(participant, outcome);
}
return new GameResult(result);
}
더 고민을 해보니 지금 로직도 역할 분리가 잘 되어있는 것 같습니다! 제가 로직을 착각했어요 😂
사다리 탐색 후, 이름과 결과를 매핑하여 게임 실행의 전체 플로우를 책임지는 메서드를 추가로 나누는 것이 오히려 과한 책임분리가 될 것 같네요.
출력 로직을 분리하는 것 만으로도, 충분히 깔끔하게 코드를 작성해주셨어요👍
| return line.connections().stream() | ||
| .filter((connection) -> connection.left() == col || connection.right() == col) | ||
| .findFirst() | ||
| .map((connection) -> getConnectedColumn(connection, col)) | ||
| .orElse(col); |
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.
stream이 길어지면 중간에
private int findNextColumn(Line line, int col) {
Optional<Connection> connected = line.connections().stream()
.filter(conn -> conn.left() == col || conn.right() == col)
.findFirst();
return connected
.map(conn -> getConnectedColumn(conn, col))
.orElse(col);
}
이렇게 변수로 분리해서 한번 끊어주는 것 도 좋을 것 같아요
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.
확실히 메소드 체인이 길어지면 읽는데 피로감이 있을 수도 있을 것 같네요!
bf3978b 커밋에 반영했습니다.
| private static String getConnectionSegment(Line line, int index) { | ||
| if (isConnected(line, index)) { | ||
| return "-----"; | ||
| } | ||
|
|
||
| return " "; | ||
| } |
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.
확인했습니다!
a16fcf5 커밋에 반영했습니다~
| private static void printParticipants(List<Participant> participants) { | ||
| String line = participants.stream() | ||
| .map((participant -> OutputView.centerAlign(participant.name()))) | ||
| .collect(Collectors.joining(" ")); | ||
|
|
||
| System.out.println(line); | ||
| } | ||
|
|
||
| private static void printOutcomes(List<Outcome> outcomes) { | ||
| String line = outcomes.stream() | ||
| .map((outcome -> OutputView.centerAlign(outcome.value()))) | ||
| .collect(Collectors.joining(" ")); | ||
|
|
||
| System.out.println(line); | ||
| } |
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.
두 로직 모두 centerAlign와 같은 클래스에 있기 때문에, .map 부분에서 OutputView.centerAlign -> centerAlign 바꿔도 정상작동 합니다!
또한 centerAlign메서드 내부에서 매번 width를 가져오는 것 보다는, 해당 두 메서드에서 한번 가져오고 매개변수로 넘겨주는 것이 더욱 효율적일 것 같습니다~
private static void printOutcomes(List<Outcome> outcomes) {
int width = Participant.PARTICIPANT_NAME_MAX_LENGTH;
String line = outcomes.stream()
.map((outcome -> centerAlign(outcome.value(), width)))
.collect(Collectors.joining(" "));
System.out.println(line);
}
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.
확인했습니다! 99a4d60 커밋에 반영했습니다.
|
이전 코멘트에서 말씀해주신 말씀해주신 높이 입력값 자료형 검증 로직의 경우 #83 (comment), #83 (comment) 코멘트에서의 티키타카가 완료된 이후에 구현 진행해보겠습니다! |
haeyoon1
left a comment
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.
모든 리뷰를 잘 반영해주시고, 이전 코멘트에도 꼼꼼하게 답변해주셔서 이번엔 몇 가지 코멘트만 추가로 남겼어요!
랜덤을 직접 생성하지 않고, 외부에서 주입받을 수 있게 하여 비즈니스 로직에 대한 테스트코드를 잘 작성해주신 것 같아서 매우 좋았습니다👍
다만 검증로직에대한 테스트코드가 조금 더 추가됐으면 좋겠어요!
- 참가자의 이름 입력이 비어있거나, 구분자만 입력 되었을 때 에러를 반환하는 테스트라든지
- 참가자의 수와 실행 결과의 수가 같지 않은 경우 에러를 반환하는 테스트라든지
조금만 더 보강되면 좋을 것 같아요~
미션 수고 많으셨습니다👍🙇♀️
| if (outcomes == null || outcomes.size() != participants.size()) { | ||
| throw new IllegalArgumentException("참가자의 수와 실행 결과의 수는 같아야 합니다."); | ||
| } |
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.
outcomes == null인 검증과 outcomes.size() != participants.size()인 검증은 검증하고자하는 로직이 다르기 때문에, 에러메세지를 다르게 하여 다른 if문으로 나누어도 좋을 것 같은데 어떻게 생각하시나요?
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.
맞습니다! outcomes == null인 경우는 빌더 패턴 사용 과정에서 값 자체가 주입이 안된 경우이므로 다르게 처리하는게 더 적합할 것 같습니다..!
마찬가지로 participants에 대한 검증 로직도 구분하여 7933714 커밋에 반영했습니다!
| .build(); | ||
|
|
||
| // then | ||
| SoftAssertions.assertSoftly((softly) -> { |
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.
중간에 실패 케이스가 있어도, 나머지 검증을 실행해주기 위해 SoftAssertions를 쓰신점 매우 좋네요!
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.
사실 저 부분에 있어서도 고민이 많았습니다! GameResult의 경우 VO이기 때문에 사실상 비즈니스 로직이 있다고는 볼 수 없어서 어떤 항목을 테스트해야하나 고민하던 중, results()의 경우 제가 임의로 불변성을 보장하도록 수정하였기에 제가 작성한 로직에 대해서만 테스트를 진행해보려고 했습니다. 그런데 다시 생각해보면 코드 구현부 자체에서 해당 메소드의 기능을 유추할 수 있는지라(Collections.unmodifiableMap()) 해당 테스트의 실효성이 있는지는 잘 모르겠습니다 🤔
해윤님은 이러한 VO에 대한 테스트에서는 어떠한 기준을 가지고 계신지 궁금합니다!
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.
저는 VO의 불변성 자체보다는, 검증 로직이 있다면 그 도메인 규칙을 중점적으로 테스트하는 편입니다.
이미 작성해두신 테스트를 삭제할 필요는 없지만, 단순히 unmodifiableMap이 잘 동작하는지만 확인하는 테스트는 굳이 필요 없을 것 같아요!
|
리뷰 과정에서 추가된 코드 및 언급하신 검증 로직에 대한 테스트도 추가로 작성했습니다! 해윤님께서 꼼꼼히 리뷰를 봐주셔서 덕분에 좋은 인사이트들을 많이 얻어갈 수 있었던 것 같습니다. 다시한번 리뷰해주시느라 정말 고생 많으셨습니다..! |
해윤님 안녕하세요! 리뷰어로써는 처음 뵙는 것 같은데, 이번 사다리 미션도 잘 부탁드리겠습니다 🙃
아래는 현재 프로젝트 구조입니다:
애플리케이션 실행 흐름은 다음과 같습니다:
InputView)GameConfigurationBuilder로 넘겨주어GameConfiguration생성과 동시에 값 검증을 수행합니다. (LadderController::readConfiguration())Game객체를 생성하여 게임을 실행합니다.다음은 사다리 생성 및 이동 로직에 대한 부가 설명입니다:
사다리 생성
사다리 이동
아래는 구현 과정에서 발생한 질문 사항입니다:
이번 미션에서는 함수형 프로그래밍 문법을 적극적으로 사용해보았는데, 사용 과정에서 로직이 복잡해질수록 depth 자체는 감소할 수 있어도 가독성 측면에서는 오히려 알아보기가 힘들어진다는 느낌을 받았습니다. (e.g.,
LadderController::handleOutcomeQuery)따라서 이러한 부분에 대해 어느 정도 trade-off가 존재한다고 생각하는데, 해윤님은 어떠한 생각을 가지고 계신지 궁금합니다..!
이번 미션의 경우에도 depth가 2를 넘지 않게 구현하도록 하는 조건이 명시되어 있었습니다.
LadderFactory::createConnections()메소드의 경우 고민을 해보았으나 도무지 depth를 여기서 더 줄일 방법이 떠오르지 않았습니다 ㅠ 혹시 해윤님은 이와 관련해서 다른 아이디어가 있으신지 궁금합니다!작성하다 보니 내용이 길어졌는데, 혹시 코드 관련해서 부가적인 설명이 필요한 부분이 있으시면 언제든지 말씀해주세요!
긴 글 읽어주셔서 감사합니다~!