Skip to content

Conversation

@chemistryx
Copy link

해윤님 안녕하세요! 리뷰어로써는 처음 뵙는 것 같은데, 이번 사다리 미션도 잘 부탁드리겠습니다 🙃

아래는 현재 프로젝트 구조입니다:

controller/
- LadderController.java : 전반적인 게임 Flow(입력, 실행, 출력)를 담당하는 클래스
model/
- ladder/
  - Connection.java : 사다리 가로 줄의 지점 간 연결을 정의하는 클래스
  - Ladder.java : 전체 사다리를 정의하는 클래스
  - LadderFactory.java : 사다리 생성을 담당하는 클래스
  - Line.java : 사다리의 가로 줄을 정의하는 클래스
- Game.java : 게임 실행을 담당하는 클래스
- GameConfiguration.java : 게임 설정을 정의하는 클래스
- GameConfigurationBuilder.java : 게임 설정 Builder 클래스
- GameResult.java : 게임 결과를 정의하는 클래스
- Participant.java : 각 참가자를 정의하는 클래스
view/
- InputView.java : 사용자 입력 처리 기능 수행
- OutputView.java : 사용자에게 정보 출력 기능 수행
Main.java : Main entrypoint

애플리케이션 실행 흐름은 다음과 같습니다:

  1. 사용자로부터 사다리 생성에 필요한 값들을 입력 받습니다. (InputView)
  2. 입력 값들을 GameConfigurationBuilder로 넘겨주어 GameConfiguration 생성과 동시에 값 검증을 수행합니다. (LadderController::readConfiguration())
  3. 생성한 configuration을 기반으로 Game 객체를 생성하여 게임을 실행합니다.
  4. 사다리 결과를 출력한 뒤, 사용자로부터 결과를 보고 싶은 사람을 입력 받습니다.

다음은 사다리 생성 및 이동 로직에 대한 부가 설명입니다:

사다리 생성

  1. 사다리 생성 자체는 LadderFactory에서 이루어집니다. 사다리는 height에 해당하는 만큼의 Line을 생성하며, 각 Linewidth(참가자 수)에 따라 좌우로 연결(Connection)들을 가집니다.
  2. createConnections() 메소드에서는 각 Line에서 인접한 세로줄 간 연결 여부를 무작위로 결정합니다. 각 위치마다 shouldConnect() 메소드를 호출하여 만약 true인 경우 현재 인덱스와 다음 인덱스 사이를 Connection으로 연결합니다.
  3. 한번 연결이 생성되면, 바로 다음 인덱스는 건너뛰도록(i += 1) 처리하여 인접한 가로줄이 겹치는 현상을 방지합니다.
  4. 이렇게 생성된 List<Connection>Line 객체로, 그리고 모든 Line객체들을 모아 하나의 Ladder를 구성합니다.

+) 또한 createLadder() 메소드에 랜덤 인스턴스 자체를 주입할 수 있도록 구현하여 테스트와 같은 상황에서 고정된 결과를 출력할 수 있도록 구현하였습니다.

사다리 이동

  1. 참가자 목록을 순회하면서 각 참가자의 시작 위치(start)를 기준으로 traverse() 메소드를 실행합니다.
  2. 위 과정에서 사다리의 각 Line을 순차적으로 내려가며, Connection이 존재하는 경우 해당하는 다음 지점으로 현재 위치(col)를 이동합니다.
  3. 다음 지점 계산의 경우 findNextColumn() 메소드에서 처리하는데, 주어진 Line내에서 현재 위치(col)가 포함된 Connection을 찾아, 연결된 반대편 인덱스(left or right)로 이동시키거나, 연결이 없을 경우 현재 위치를 그대로 유지합니다.
  4. 모든 Line을 순회한 뒤 반환되는 최종 위치(end)는 참가자가 도착한 세로줄을 의미하며, 이는 곧 참가자의 실행 결과를 의미합니다.

아래는 구현 과정에서 발생한 질문 사항입니다:

  1. 이번 미션에서는 함수형 프로그래밍 문법을 적극적으로 사용해보았는데, 사용 과정에서 로직이 복잡해질수록 depth 자체는 감소할 수 있어도 가독성 측면에서는 오히려 알아보기가 힘들어진다는 느낌을 받았습니다. (e.g., LadderController::handleOutcomeQuery)
    따라서 이러한 부분에 대해 어느 정도 trade-off가 존재한다고 생각하는데, 해윤님은 어떠한 생각을 가지고 계신지 궁금합니다..!

  2. 이번 미션의 경우에도 depth가 2를 넘지 않게 구현하도록 하는 조건이 명시되어 있었습니다. LadderFactory::createConnections() 메소드의 경우 고민을 해보았으나 도무지 depth를 여기서 더 줄일 방법이 떠오르지 않았습니다 ㅠ 혹시 해윤님은 이와 관련해서 다른 아이디어가 있으신지 궁금합니다!

작성하다 보니 내용이 길어졌는데, 혹시 코드 관련해서 부가적인 설명이 필요한 부분이 있으시면 언제든지 말씀해주세요!
긴 글 읽어주셔서 감사합니다~!

Copy link

@haeyoon1 haeyoon1 left a 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의 흐름을 따라가며, 비즈니스 로직을 제외한 부분을 중심으로 피드백을 남겨보았습니다. 다음 리뷰에서는 비즈니스 로직을 위주로 읽어보겠습니다!

🤔 질문사항 답변

아래는 질문사항에 대한 답변입니다

  1. 이번 미션에서는 함수형 프로그래밍 문법을 적극적으로 사용해보았는데, 사용 과정에서 로직이 복잡해질수록 depth 자체는 감소할 수 있어도 가독성 측면에서는 오히려 알아보기가 힘들어진다는 느낌을 받았습니다. (e.g., LadderController::handleOutcomeQuery)
따라서 이러한 부분에 대해 어느 정도 trade-off가 존재한다고 생각하는데, 해윤님은 어떠한 생각을 가지고 계신지 궁금합니다..!

새로운 방식을 시도해보신 점이 정말 좋네요👍
저는 함수형 프로그래밍 방식을 도입했더니 코드의 가독성이 좋아지지 않았다면, 다른 방법도 고안해볼 것 같아요! 예를 들어 depth를 줄이기 위해, stream등의 문법을 사용했는데 가독성이 안좋아졌다면, 다른 방법으로 depth를 줄일 수 있는 방법을 생각해보는거죠.

또한 어떤 로직이 너무 길고 복잡해보인다 하면, 한 메서드 또는 절이 하나의 역할만 하는가? 를 먼저 고민해보면 좋을 것 같아요. 하나의 절이 많은 역할을 수행하게되면 읽기 힘든 복잡한 코드가 되기 쉬우니까요!
말씀해주신 LadderControllerhandleOutcomeQuery 메서드를 보도록하겠습니다. 해당 메서드는 입력 + 반복 제어 + 결과 조회 + 출력의 역할을 하고있어요. 근데 네가지 기능을 하나의 stream절에서 실행하고 있으니 딱 봤을 때 흐름이 눈에 쉽게 들어오지 않는 것 같아요. 이러한 점들을 고려하여 리팩토링 진행해보시면 좋을 것 같습니다.

결론은 읽기 쉬운 코드를 작성하는 것은 매우 중요하기 때문에, 가독성에서의 문제를 느꼈다면 사용하지 말자(=대안을 찾자)! 였습니다

  1. 이번 미션의 경우에도 depth가 2를 넘지 않게 구현하도록 하는 조건이 명시되어 있었습니다. LadderFactory::createConnections() 메소드의 경우 고민을 해보았으나 도무지 depth를 여기서 더 줄일 방법이 떠오르지 않았습니다 ㅠ 혹시 해윤님은 이와 관련해서 다른 아이디어가 있으신지 궁금합니다!

생각나는 방법은

  1. 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로 만들 수 있을 것 같아요!

  1. 메서드 분리
if (shouldConnect(random)) {
                connections.add(new Connection(i, i + 1));
                i += 1; // skip the right next line
            }

가장 많이 사용해 보셨을 방법인데요, 위 로직을 메서드로 분리하면 depth가 줄어들게 됩니다. 이렇게 되면 반복과, 연결 생성을 별도의 메서드로 분리할 수 있다는 장점도 있겠네요!

Comment on lines 3 to 18
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;
}
}

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()등의 코드가 자동 생성된다는 장점이 있습니다.
이러한 개념이 있다고 알고 넘어가면 좋을 것 같아요~

Copy link
Author

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();

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에서 진행하지 않고, 단계 별로 계층을 다르게 한 이유가 궁금합니다!

Choose a reason for hiding this comment

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

추가로 participant 입력에 대한 검증 로직이 없어

  1. , 또는 ,,(,만으로 이루어진 문자열) 입력한 경우
  2. 공백 혹은 아무것도 입력하지 않은 경우

에 예외를 던지지 않고 다음 단계 입력 로직이 실행되네요! 적절한 위치에 검증 로직을 추가해 위와 같은 경우에도 예외문을 던지면 좋을 것 같은데, 어디에 위치시키면 좋을 것 같나요?

Copy link
Author

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로 변환)만 담당하고, 이후 해당 값들을 의미있는 객체로 변환하는 과정은 컨트롤러와 같이 관련 있는 로직에서 담당하는 것이 더 좋다고 판단하여 두 단계로 나누어서 이루어질 수 있도록 작성하였습니다!

Copy link
Author

@chemistryx chemistryx Nov 1, 2025

Choose a reason for hiding this comment

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

추가로 participant 입력에 대한 검증 로직이 없어

  1. , 또는 ,,(,만으로 이루어진 문자열) 입력한 경우
  2. 공백 혹은 아무것도 입력하지 않은 경우

에 예외를 던지지 않고 다음 단계 입력 로직이 실행되네요! 적절한 위치에 검증 로직을 추가해 위와 같은 경우에도 예외문을 던지면 좋을 것 같은데, 어디에 위치시키면 좋을 것 같나요?

제가 테스트 해보았을때는 두 케이스(,, ,,) 모두 정상적으로 검증 로직(GameConfigurationBuilder::participants())이 실행되는데, 혹시 제가 놓친 부분이 있을까요?

image

공백 입력의 경우 #83 (comment) 에서 이어서 다루도록 하겠습니다!!

Copy link

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();

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()를 사용한 특별한 이유가 있다면 궁금합니다!

Copy link
Author

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()를 사용하는 방식도 문제가 없을 것 같아 해당 방식으로 진행해보겠습니다!

5ef1716 ,2573545 커밋에 반영했습니다!

Comment on lines +10 to +18
public GameConfigurationBuilder participants(List<Participant> participants) {
if (participants == null || participants.isEmpty()) {
throw new IllegalArgumentException("참가자 목록은 비어 있을 수 없습니다.");
}

this.participants = participants;

return this;
}

Choose a reason for hiding this comment

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

현재 입력 터미널에서 엔터만 입력하여도 해당 로직이 오류를 감지하지 못하고있는데, 어디를 수정해야 할까요🤔

Copy link
Author

Choose a reason for hiding this comment

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

엔터를 입력 받은 후의 participants 배열을 찍어보니 아래와 같이 name이 공백인 객체가 들어가 있는 것을 확인했습니다.
image

따라서 Participant의 생성자에 공백 유무를 검사하는 로직을 추가하여 수정하였습니다~!

반영 커밋: d3d50f7

public class LadderController {
public void run() {
try {
GameConfiguration configuration = readConfiguration();

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 객체를 통해 입력값을 한 번에 전달하도록 설계하신 이유가 궁금합니다!

Copy link
Author

@chemistryx chemistryx Nov 1, 2025

Choose a reason for hiding this comment

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

제가 생각해보았을때 GameConfiguration객체를 도입한 이유는 다음과 같습니다:

  1. 입력 검증 로직과 게임 로직 분리
    현재 GameConfiguration객체의 경우 GameConfigurationBuilder를 통해 생성되도록 의도하였는데, 검증 로직의 경우 빌더가 담당함으로써 컨트롤러에서는 게임 실행 자체만을 담당할 수 있도록 구성하였습니다.

  2. 입력 값에 대한 순차적 검증
    사실 빌더 패턴을 도입하게된 이유라고 볼 수도 있을 것 같은데, 기존 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("크기는 양수만 입력할 수 있습니다.");
    }
}

해당 방식의 경우 검증할 값이 많아질 수록 가독성이 떨어진다고 판단했고, 값이 많아짐에 따라 메소드 별로 분리하는 방법도 생각해볼 수 있으나 이 방식 또한 결국 각각의 검증 메소드를 호출해야 한다는 부분이 효율적이지 못하다고 판단했습니다.
따라서 빌더를 통해 값 할당과 동시에 검증 로직을 수행하는 방식이 위 두가지 단점을 해결할 수 있다고 판단하여 빌더 패턴 방식을 도입해보았습니다.

  1. Game 생성의 단순화
    만약 인자 수가 많아진다면 인자의 순서가 헷갈릴 수도 있고, 새로운 필드가 추가된다면 이에 따라 계속 수정해야 한다는 것이 번거롭다고 판단했습니다. 따라서 객체 하나만을 넘겨주는 방식을 통해 이러한 문제를 해결할 수 있었습니다.

Copy link

Choose a reason for hiding this comment

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

로직 생성의 이유를 명확히 고민하고 코드로 잘 구현하신 점이 정말 좋네용👍

2번에 관해서는

해당 방식의 경우 검증할 값이 많아질 수록 가독성이 떨어진다고 판단했고, 값이 많아짐에 따라 메소드 별로 분리하는 방법도 생각해볼 수 있으나 이 방식 또한 결국 각각의 검증 메소드를 호출해야 한다는 부분이 효율적이지 못하다고 판단했습니다.

라는 의견에 동의합니다! builder로 검증 로직을 분리하여 현재는 도메인이 더욱 비즈니스 로직에 집중할 수 있도록 설계된 것 같아요.

다만 매우매우 개인적으로는 도메인이 유효성까지 스스로 보장하는 것도 DDD 관점에서 괜찮은 접근이라고 생각합니다! 그래도 지금은 설계 의도도 명확하고, 충분히 적절해 보여요👍

Comment on lines 44 to 53
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("존재하지 않는 참가자입니다.")
)
);

Choose a reason for hiding this comment

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

하나의 stream에서 너무 많은 역할을 하고 있는 것 같아요! 로직을 분리해보면 좋을 것 같습니다~

Copy link
Author

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 = ",";

Choose a reason for hiding this comment

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

구분자를 view계층에도 상수화시켜 관리 포인트를 줄인점이 매우 좋네요~

@chemistryx
Copy link
Author

해윤님 작성해주신 리뷰에 대한 반영 및 질문에 대한 코멘트 달아놓았습니다!
말씀해주신대로 하나의 스트림에 대하여 너무 많은 로직이 존재하여 가독성이 떨어지는 부분에 대해서는 기능을 분리하여 적절한 타협점을 찾으려고 시도해보았습니다. (854f13a)

depth의 경우에도 해윤님께서 주신 의견과 더불어 한번 생각을 해보았는데, 2번 아이디어의 경우 현재 로직 상 반복문 내 인덱스(i)를 직접 조작하는 부분이 있어 따로 메소드로 분리하는 것이 쉽지 않다고 판단하여 1번 아이디어로 적용해보았습니다! (82d4c5c)

또한 입력값에 대한 검증 로직 보강 관련해서도 말씀해주셨는데요, 현재 결과 값에 대한 검증은 별도로 이루어지고 있지 않아(단순 String으로 취급), 이에 대한 VO를 만들어서 검증 로직을 적용시켜 보았는데 (9648d42), 이외에 더 필요하다고 생각되는 검증 로직이 있을까요?

마지막으로 README.md관련해서는 천천히 한번 반영해보도록 하겠습니다!

Copy link

@haeyoon1 haeyoon1 left a 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)에 대한 코멘트는 아래 추가해두었습니다!

추가 검증으로는
image
높이 입력 시, 정수가 아닌 자료형이 입력됐을 경우 예외를 던지는 로직도 추가되면 좋을 것 같아요!
그 외에는 검증로직을 세세하게 잘 작성해주신 것 같습니다 👏

Comment on lines +7 to +13
if (name.length() > PARTICIPANT_NAME_MAX_LENGTH) {
throw new IllegalArgumentException("참가자의 이름은 최대 " + PARTICIPANT_NAME_MAX_LENGTH + "자만 가능합니다.");
}

if (name.isBlank()) {
throw new IllegalArgumentException("참가자의 이름은 공백일 수 없습니다.");
}
Copy link

Choose a reason for hiding this comment

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

수한님께서 어떤 검증 로직은 도메인 내부에 두고, 어떤 검증 로직은 GameConfigurationBuilder 내부에 두었는지 기준이 궁금합니다!

도메인 값 자체의 유효성 검증은 도메인 내부에서 이루어졌다고 한다면(Participant나 Outcome)
GameConfigurationBuilderparticipantsheight도 도메인 값 자체의 유효성 검증이라는 생각이 들어서요. 의견이 궁금합니다 💭

private static final String INPUT_DELIMITER = ",";

public static List<String> getParticipants() {
System.out.println("\n참여할 사람 이름을 입력하세요. (이름은 " + INPUT_DELIMITER + "로 구분하세요)");
Copy link

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 + "로 구분하세요)");

다음과 같이 작성하는 것이 더욱 직관적이고 정확할 것 같아요!

Copy link

Choose a reason for hiding this comment

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

해당 클래스는 객체로 관리하기에는 이유가 부족해 보입니다.
검증 로직만을 담당하고 있다면, GameConfigurationBuilder::outcomes로 옮기는 것도 괜찮을 것 같아요!

Copy link

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까지만 허용한다는 조건때문에 쉽지 않은 것 같긴하네요🤔

List<Outcome> outcomes = configuration.outcomes();
Map<Participant, Outcome> result = new LinkedHashMap<>();

OutputView.printLadderResult(this);
Copy link

Choose a reason for hiding this comment

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

view가 domain 내부에서 호출되고있네요! 어떻게 수정할 수 있을까요?

Comment on lines +29 to +45
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);
}
Copy link

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 내부로 옮겨도 될 것 같은데 어떻게 생각하시나요?

Comment on lines +59 to +63
return line.connections().stream()
.filter((connection) -> connection.left() == col || connection.right() == col)
.findFirst()
.map((connection) -> getConnectedColumn(connection, col))
.orElse(col);
Copy link

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);
}

이렇게 변수로 분리해서 한번 끊어주는 것 도 좋을 것 같아요

Comment on lines +73 to +79
private static String getConnectionSegment(Line line, int index) {
if (isConnected(line, index)) {
return "-----";
}

return " ";
}
Copy link

Choose a reason for hiding this comment

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

"-----" " "는 상수화하면 좋을 것 같아요!

Comment on lines +43 to +57
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);
}
Copy link

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);
    }

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