Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions README.md

Choose a reason for hiding this comment

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

README를 잘 작성해 주셨군요! 사용자가 보고 이해하기 쉬운 좋은 README라고 생각 합니다! 그리고 프로젝트 구조가 변경되어도 크게 고칠 부분이 없다는 점도 좋아요! 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 🪜 사다리 타기 게임
참가자의 이름과 원하는 결과를 입력하면 랜덤한 사다리를 생성하고,
각 사람이 어떤 결과에 도착하는지 확인할 수 있는 **사다리 게임**입니다.

---

## 🕹️ 사용 방법

### 1. 참여자 이름 입력
- 최대 5글자까지 입력 가능합니다.
- 이름은 쉼표 `,`로 구분해서 입력해야 합니다.
- 입력한 순서대로 사다리 시작 위치가 지정됩니다.

### 2. 실행 결과 입력
- 참여자 수와 같은 개수의 결과 입력해주세요.
- 순서는 이름과 일치해야 합니다.
- 입력한 순서대로 실행 결과 위치가 지정됩니다.

### 3. 사다리 높이 입력
- 사다리의 세로 줄 수를 숫자로 입력해주세요.

### 4. 생성된 사다리 확인
- 각 참여자의 시작 지점, 실행 결과 위치가 출력됩니다.

### 5. 결과 확인

- 특정 이름을 입력하면 해당 사람의 결과가 출력됩니다.
- `"all"`을 입력하면 전체 결과를 한 번에 확인할 수 있습니다.
- `"all"`을 입력하면 모든 참여자의 최종 결과를 출력하고 프로그램이 종료됩니다.

---
## 🖥️ 실행 예시
```
참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)
neo,brown,brie,tommy

실행 결과를 입력하세요. (결과는 쉼표(,)로 구분하세요)
꽝,5000,꽝,3000

최대 사다리 높이는 몇 개인가요?
5

사다리 결과

neo brown brie tommy
|-----| |-----|
| |-----| |
|-----| | |
| |-----| |
|-----| |-----|
꽝 5000 꽝 3000

결과를 보고 싶은 사람은?
neo

실행 결과

결과를 보고 싶은 사람은?
all

실행 결과
neo : 꽝
brown : 3000
brie : 꽝
tommy : 5000
```
14 changes: 14 additions & 0 deletions src/main/java/LadderApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import controller.LadderController;
import view.InputView;
import view.OutputView;

public class LadderApplication {

public static void main(String[] args) {
InputView inputview = new InputView();
OutputView outputview = new OutputView();

LadderController ladderController = new LadderController(inputview, outputview);
ladderController.run();
}
}
77 changes: 77 additions & 0 deletions src/main/java/controller/LadderController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package controller;

import domain.Height;
import domain.Ladder;
import domain.Width;
import view.InputView;
import view.OutputView;

import java.util.List;
import java.util.Random;

public class LadderController {

private final InputView inputView;
private final OutputView outputView;

public LadderController(InputView inputView, OutputView outputView) {
this.inputView = inputView;
this.outputView = outputView;
}
Comment on lines +13 to +19

Choose a reason for hiding this comment

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

View를 생성자 매개변수로 받아 의존성을 확실하게 표현해주고 있는 점이 좋은 것 같아요
저는 view의 모든 메서드를 static으로 만들어 사용하는데 매개변수로 받는 점이 Controller만 View에 의존할 수 있다는 규칙을 잘 나타내는 것 같습니다.


public void run() {
List<String> playerNames = readValidatedPlayerNames();
List<String> gameResults = readValidatedGameResults(playerNames.size());

Width width = new Width(playerNames.size());
Height height = inputView.readHeight();
Ladder ladder = Ladder.generate(width, height, new Random());
outputView.printLadder(playerNames, ladder, gameResults);

String request = inputView.readResultRequest();
handleResultRequest(request, playerNames, ladder, gameResults);
}

private List<String> readValidatedPlayerNames() {
List<String> names = inputView.readPlayerNames();
for (String name : names) {
if (name.isEmpty() || name.length() > 5) {
throw new IllegalArgumentException("참여자 이름은 1자 이상 5자 이하여야 합니다: " + name);
}
}
return names;
}

private List<String> readValidatedGameResults(int expectedSize) {
List<String> results = inputView.readGameResults();
if (results.size() != expectedSize) {
throw new IllegalArgumentException("이름과 결과의 개수가 일치하지 않습니다.");
}
return results;
}

Choose a reason for hiding this comment

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

입력값 검증을 Controller에서 구현 하셨군요! 그리고 메서드 명을 readValidate+{입력값} 형식으로 결정하셨네요.

저도 평소에 입력값을 검증하는 역학을 어떤 계층에게 넘겨야 할 지 고민을 많이 하는 편이에요! 주로 입력값을 받고 원시값 포장이나 일급 컬렉션을 구현하여 값에 대한 검증 메서드를 추가하여 검증을 구현하고 있어요.

지우님도 Height값이나 Width값 같은 값을 검증하실 경우 원시값 포장을 통해 객체에게 검증 책임을 넘기신 걸로 아시는데 두 방법을 같이 사용하시게 된 이유가 궁금합니다!

Copy link
Author

Choose a reason for hiding this comment

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

입력값 검증 로직을 보통 마무리, 리팩토링 단계에서 살펴보게 되는데요,
이때 시스템 흐름을 따라서 코드를 살펴보다 보니 입력값 검증이 어느 객체의 책임인지에 대한 판단을 제대로 못 하게 되는 것 같아요. 그래서 결론적으로 책임분리도 명확히 안 되어 있게 되는 것 같습니다.

주로 입력값을 받고 원시값 포장이나 일급 컬렉션을 구현하여 값에 대한 검증 메서드를 추가하여 검증을 구현

이 말씀에 대해 머리로는 알고 있고 매우 동의하지만, 혼자 미션을 진행할 때 적용이 안 되는 점이 참 아쉽네요 ㅜㅜ
이번 리뷰를 통해 좀 더 인지하고 다음부터 신경써서 적용해보도록 하겠습니다!


private void handleResultRequest(String request, List<String> names, Ladder ladder, List<String> results) {
while (!request.equals("all")) {// 입력이 'all'이 아닐 때
try {
int playerIndex = getValidPlayerIndex(request, names);
outputView.printSingleResult(
names.get(playerIndex),
results.get(ladder.getFinalPosition(playerIndex))
);
} catch (IllegalArgumentException e) {
System.out.println("잘못된 이름입니다. 다시 입력해주세요.");
}
request = inputView.readResultRequest();
}
// 'all' 입력될 때
outputView.printAllResults(names, results, ladder);
}

Choose a reason for hiding this comment

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

try catch 문을 사용하여 예외를 잘 처리해 주셨네요! 프로그램이 바로 종료되지 않고 다시 입력을 받을 수 있도록 하신 점에서 생각을 많이 하신 게 느껴져요! 😯

Copy link
Author

Choose a reason for hiding this comment

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

사실 프로그래밍 요구사항(depth관련 요구사항)에는 어긋나는 지점이라 어떻게 처리해야 할지 고민을 많이 했었는데 달리 방법이 떠오르질 않아 이대로 구현했습니다 😅

실행화면 예시에 맞게 all이 입력될 때까진 계속해서 참여자 이름을 입력받아야 한다는 것에 초점을 많이 맞춘 결과에요..ㅎㅎ

Choose a reason for hiding this comment

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

사실 저는 try-catch 문 같은 경우는 괜찮다고... 생각을 해요! 가독성을 해치는 코드도 아니라고 생각해서 어쩔 수 없는 부분이라 생각합니다


private int getValidPlayerIndex(String name, List<String> names) {
int index = names.indexOf(name);
if (index == -1) {
throw new IllegalArgumentException("잘못된 이름입니다: " + name);
}
return index;
}
}
12 changes: 12 additions & 0 deletions src/main/java/domain/Height.java

Choose a reason for hiding this comment

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

Height, Width가 잘 포장되어있네요! 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package domain;

public record Height(int value) {

private static final int MIN_HEIGHT_VALUE = 1;

public Height {
if (value < MIN_HEIGHT_VALUE) {
throw new IllegalArgumentException("사다리의 높이는 1 이상이어야 합니다.");
}
}
}
62 changes: 62 additions & 0 deletions src/main/java/domain/Ladder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package domain;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Ladder {

private final List<Line> lines;
private final Width width;

public Ladder(List<Line> lines, Width width) {
this.lines = lines;
this.width = width;
}

public static Ladder generate(Width width, Height height, Random random) {
List<Line> lines = new ArrayList<>();
for (int i = 0; i < height.value(); i++) {
Line line = new Line(width);
line.connect(random);
lines.add(line);
}
return new Ladder(lines, width);
}

Choose a reason for hiding this comment

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

generatre 함수가 Random 객체를 매개변수로 받아서 connect에 전해주는 것으로 Random은 Line에서 사용되기 위해 매개변수로 2번 들어가고 있어요! Line에 Random 객체를 생성해 줄 수도 있었을 텐데 이런 방식으로 만든 건 역시 테스트에 용이하기 때문일까요?🤔

Copy link
Author

@Ji-Woo-Kim Ji-Woo-Kim May 9, 2025

Choose a reason for hiding this comment

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

말씀하신 것처럼 테스트 용이성을 의도한 것은 아니었고,
순전히 "랜덤하게 생성해야 한다"는 요구사항때문에 generate 메서드에 Random을 주입하는 방식으로 구현하게 됐어요!
근데 이 덕분에 테스트 코드 작성할 때 seed값을 고정해서 주입할 수 있게 된 것 같습니다.

Line에 Random 객체를 생성해 줄 수도 있었을 텐데

이렇게 했다면 캡슐화 측면에서는 훨씬 좋았을 것 같은데 저는 가독성, 흐름이 잘 읽히도록 하는 것에 좀 더 치우쳐서 구현하는 스타일이라 자연스럽게 이런 코드가 나온 것 같습니다 ..ㅎㅎ

Choose a reason for hiding this comment

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

가독성도 중요한 측면이라고 생각해요! 확실히 가독성 측면에서는 코드가 깔끔해서 이해하는데 큰 어려움이 없었습니다 테스트 용이성은 일석이조였군요!


public int getFinalPosition(int startPosition) {
int currentPosition = startPosition;
for (Line line : lines) {
currentPosition = moveToNextPosition(currentPosition, line);
}
return currentPosition;
}

private int moveToNextPosition(int currentPosition, Line line) {
List<Point> points = line.getPoints();

if (canMoveLeft(currentPosition, points)) {
return currentPosition - 1;
}
if (canMoveRight(currentPosition, points)) {
return currentPosition + 1;
}
return currentPosition;
}

private boolean canMoveLeft(int currentPosition, List<Point> points) {
return currentPosition > 0 && points.get(currentPosition - 1).isConnectedToRight();
}

private boolean canMoveRight(int currentPosition, List<Point> points) {
return currentPosition < points.size() - 1 && points.get(currentPosition).isConnectedToRight();
}

Choose a reason for hiding this comment

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

현재 위치에서 이동 가능한 방향으로 이동한 위치, 이동 가능한 방향이 없다면 현 위치를 반환하는 메서드네요! 제가 Line의 connect() 메서드를 보았을 땐 양 끝에서의 이동 불가 처리가 생성 과정에서 이미 잘 되어있다고 생각했어요!

그래서 왜 다시 포지션을 확인할까 생각했는데 canMoveLeft함수에선 반드시 현재 포지션보다 1 낮은 부분의 오른쪽 이동 가능 여부를 확인하기에 오류가 나는 것 같아요!

그렇다면 제 생각엔 isConnectedToLeft 메서드를 만들면 메서드의 의도가 조금 더 직관적으로 다가오고 현재 위치가 양 끝이어도 오류 없이 잘 동작할 것 같은데 isConnectedToRight 메서드만 있는 이유가 궁금합니다!

Copy link
Author

Choose a reason for hiding this comment

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

아.. 이 부분은 제가 너무 로직을 복잡하게 구현해서 생긴 의문이신 것 같아요. 🤦‍♀️

isConnectedToLeft()가 필요 없는 이유를 설명드리자면,
왼쪽으로 갈 수 있는지를 확인할 때, 내 현재 위치에서 왼쪽에 있는 point(즉, 왼쪽 기둥)가 오른쪽과 연결이 되어있는지만 판단하면 되도록 로직이 구성되어 있어요 (복잡하죠..)😅

더불어 메서드 네이밍까지 로직 이해를 방해한 것 같은데요.
canMoveLeft() 안에서 points.get(currentPosition - 1).isConnectedToRight() 라고 되어 있는데, 메서드명과 내부에서 사용되는 메서드명의 방향이 달라 더 헷갈리셨을 거라 보입니다.

미션 진행을 시간을 넉넉히 두고 한게 아니어서 가독성이 떨어지는 복잡한 로직이 생성되어 버렸네요. Enum을 적용하면서 많이 바뀌게 될 부분으로 보이는데 다시 고민해보고 수정해보도록 하겠습니다!!💻🔥


public List<Line> getLines() {
return lines;
}

public Width getWidth() {
return width;
}
}
33 changes: 33 additions & 0 deletions src/main/java/domain/Line.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package domain;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Line {

private final List<Point> points;

public Line(Width width) {
points = new ArrayList<>();
for (int i = 0; i < width.value(); i++) {
points.add(new Point());
}
}

public void connect(Random random) {
for (int i = 0; i < points.size() - 1; i++) { // 가로줄 겹침 방지
if (i > 0 && points.get(i - 1).isConnectedToRight()) {
continue;
}
if (random.nextBoolean()) {
points.get(i).connectToRight();
points.get(i + 1).connectToLeft();
}
}
}

Choose a reason for hiding this comment

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

사다리가 연속되지 않도록 코드를 잘 작성하셨네요! 확실히 어떤 원리로 작성되었는지 한눈에 알아볼 수 있는 좋은 코드 같아요!

그런데 이 부분은 미션 조건이었던 depth가 유지되지 않고 있어요.

num을 적용하라는 요구사항을 도저히... 어떻게 적용해야 할지 모르겠어서 포기를 했는데요. 혹시 어떻게 접근하셨을지 궁금합니다!

지우님이 아까 이러한 고민을 하고 있다고 말씀해주셨는데 저는 이 depth를 해결하기 위해서 enum을 사용했던 것 같아요! point의 이동 가능 여부를 enum 객체로 두면 enum에서 메서드를 구현하여 이러한 문제를 해결할 수 있었어요!

물론 저도 2개짜리 상태에 굳이...? 그냥 Boolean을 원시값 포장하면 되는거 아닌가...🤔 라는 생각이 들긴 했지만 확실히 depth 문제를 해결하기에 좋았고 가독성도 훨씬 괜찮았던 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

저는 이 depth를 해결하기 위해서 enum을 사용했던 것 같아요! point의 이동 가능 여부를 enum 객체로 두면 enum에서 메서드를 구현하여 이러한 문제를 해결할 수 있었어요!

다른 분들도 이동 가능 여부에 enum을 적용하셨던데,
지환님이 이렇게 판단하실 수 있게 된 과정이 궁금합니다.!
(이 부분에 enum 적용하면 되겠다라고 자연스레 생각이 드신 건지..)

Choose a reason for hiding this comment

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

저는 한 객체가 특정 상태를 가지고 있고, 그 상태에 따라 특정 상황을 발생시키거나 특정 값을 반환해야 할 경우 enum을 떠올리게 되는 것 같아요! 예전에 게임 개발 과제를 한 적이 있었는데 적 몬스터의 상태를 enum으로 적용해서 편했던 기억이 있습니다
이번 미션에서는 사실 상태가 두 개 밖에 없어서 저도 적용해야하나 말아야 하나 싶었는데 아무래도 enum객체의 메서드를 활용하는게 가독성에 좋다보니 요구사항도 만족할 겸 사용했던 것 같네요!


public List<Point> getPoints() {
return points;
}
}
24 changes: 24 additions & 0 deletions src/main/java/domain/Point.java

Choose a reason for hiding this comment

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

Point 클래스는 사다리 클래스를 만들기 위해 아마 가장 깔끔한 구성인 것 같아요! 다른 리뷰에서 언급드렸듯이 enum을 적용하면 미션 요구사항도 맞추고 조금 더 직관적인 코드를 만들 수 있을 것 같습니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package domain;

public class Point {

private boolean connectedToRight;
private boolean connectedToLeft;

public Point() {
this.connectedToRight = false;
this.connectedToLeft = false;
}

public boolean isConnectedToRight() {
return connectedToRight;
}

public void connectToRight() {
this.connectedToRight = true;
}

public void connectToLeft() {
this.connectedToLeft = true;
}
}
12 changes: 12 additions & 0 deletions src/main/java/domain/Width.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package domain;

public record Width(int value) {

private static final int MIN_WIDTH_VALUE = 1;

public Width {
if (value < MIN_WIDTH_VALUE) {
throw new IllegalArgumentException("사다리의 넓이는 1 이상이어야 합니다.");
}
}
}
47 changes: 47 additions & 0 deletions src/main/java/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package view;

import domain.Height;
import domain.Width;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;

public class InputView {

private static final Scanner scanner = new Scanner(System.in);

public List<String> readPlayerNames() {
System.out.println("참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요): ");

String userInput = scanner.nextLine();
return Arrays.stream(userInput.split(","))
.map(String::trim)
.filter(name -> !name.isEmpty())
.collect(Collectors.toList());
Comment on lines +18 to +22

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.

이 부분이 예외로 처리할지 시스템 자동적으로 처리할지에 대한 고민의 결과로 나온 것이 아니라
단순히 '들어온 입력 값을 시스템에서 사용하도록 가공해야지'라는 생각만 하다 구현된 코드인데요,,!

제가 코드 하나하나에 생각보다 많은 판단을 하지 않고 넘어가는 것 같아 반성하게 되네요 ㅜㅜ

이런 입력에 대해 예외를 발생시키는 것과 자동으로 처리하고 넘어가는 것 중 어떤 방식이 더 좋다고 생각

이 질문에 대해 고민해 본 결과 다음과 같은 생각을 하게 됐습니다:
이름에 공백은 넣을 수 없다는 조건이 명시된 경우라면 예외를 발생시키는 것이 좋고
달리 공백에 대한 사전 조건이 명시되지 않았다면 공백정도는 시스템 내부에서 자동으로 처리하고 넘어가는 것도 나쁘지 않은 것 같습니다..(이런 생각이 위험요인을 만드는 걸까요,,? 🤔)

지환님의 의견도 궁금해지는 부분이네요..!

Choose a reason for hiding this comment

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

네 저도 공백정도는 처리하고 넘어가는 것이 좋다고 생각해요! 하지만 특정 상황에 있어서는 사용자의 인지 없이 입력값을 변경하는 것이 위험을 초래할 수 있다고는 생각하는 편이었어요! 사실 그냥 없애도 되고... 가장 좋은 방법은 없애고 난 뒤 사용자에게 따로 고지하는 방법도 좋다고 생각해요 여러가지 의견이 나와서 좋네요!

}

public List<String> readGameResults() {
System.out.println();
System.out.println("실행 결과를 입력하세요. (결과는 쉼표(,)로 구분하세요): ");

String userInput = scanner.nextLine();
return new ArrayList<>(Arrays.asList(userInput.split(",")));
}

public Height readHeight() {
System.out.println();
System.out.println("최대 사다리 높이는 몇 개인가요?");
int height = scanner.nextInt();
scanner.nextLine();
return new Height(height);
}

public String readResultRequest() {
System.out.println();
System.out.println("결과를 보고 싶은 사람은?");
return scanner.nextLine().trim();
}
}
38 changes: 38 additions & 0 deletions src/main/java/view/LadderPrinter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package view;

import domain.Ladder;
import domain.Line;
import domain.Point;

import java.util.List;

public class LadderPrinter {

private static final int CONNECTION_WIDTH = 5;

public void printLadder(Ladder ladder) {
int width = ladder.getWidth().value();
for (Line line : ladder.getLines()) {
printLine(line, width);
}
}

private void printLine(Line line, int width) {
StringBuilder lineBuilder = new StringBuilder();
List<Point> points = line.getPoints();

for (int i = 0; i < width; i++) {
lineBuilder.append("|");
lineBuilder.append(printConnection(points, i));
}

System.out.println(lineBuilder);
}

private String printConnection(List<Point> points, int i) {
if (i < points.size() && points.get(i).isConnectedToRight()) {
return "-".repeat(CONNECTION_WIDTH);
}
return " ".repeat(CONNECTION_WIDTH);
}
}
Loading