Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ec68a0e
로또 미션 완성
Oct 30, 2025
ab34af1
이름을 원시값으로 포장한 Name 클래스 추가
Oct 31, 2025
b5d6346
Names를 Name 기반 일급 컬렉션으로 리팩터링
Oct 31, 2025
6425261
Players 이름 중복 제거 로직 추가
Oct 31, 2025
b3e4558
배열 직접 사용 X
Oct 31, 2025
e0819f4
높이를 int 대신 Height 객체로 변경하고, Results 입력 시 Players 전달하도록 수정
Oct 31, 2025
8421f74
잘못된 입력 시 재입력 루프 추가 및 원시값 포장·일급 컬렉션 적용에 따른 수정
Oct 31, 2025
7bd2af2
사다리 연결 상태 표현을 위한 Connect enum 추가
Oct 31, 2025
94e6bde
사다리 높이 검증을 위한 Height 원시값 포장 클래스 추가
Oct 31, 2025
af22352
높이가 0인 경우에도 사다리 한 줄이 생성되도록 보정
Oct 31, 2025
ecac217
원시값 포장 및 일급 컬렉션 적용으로 인한 LadderGame 로직 수정
Oct 31, 2025
1514d48
Line에 일급 컬렉션(Point) 적용 및 Connect enum으로 의미 전달 명확화
Oct 31, 2025
54ddd4d
입력값을 원시값으로 포장하는 Name 클래스 추가
Oct 31, 2025
99aa533
Names를 일급 컬렉션으로 변경하고 배열 사용 제거
Oct 31, 2025
d8ce27f
Players에 중복 제거 로직 추가 및 일급 컬렉션 구조 개선
Oct 31, 2025
cbd131e
원시값 포장 및 일급 컬렉션 적용으로 인한 수정, 입력 예외 메시지 출력 기능 추가
Oct 31, 2025
5d6dfca
Connect.from() 메서드 동작 검증 테스트 추가
Oct 31, 2025
fb63a02
LadderGame 도메인 검증 및 결과 매핑 테스트 추가
Oct 31, 2025
a7b8c81
Ladder 높이에 따른 사다리 생성 로직 테스트 추가
Oct 31, 2025
ffe782d
Line 생성 규칙 및 이동 경계 로직 테스트 추가
Oct 31, 2025
fb624ea
Name 객체 생성 테스트 추가
Oct 31, 2025
851055c
Name 값 객체의 입력 검증 및 동등성 테스트 추가
Oct 31, 2025
6a287b1
Players 이름 파싱, 길이 검증 및 중복 제거 테스트 추가
Oct 31, 2025
5d7fbc9
Point 생성 시 Connect 값 정상 저장 여부 테스트 추가
Oct 31, 2025
2ff5f21
Results 문자열 파싱 테스트 추가
Oct 31, 2025
6fa0b0d
사다리 게임 명세서 추가
Oct 31, 2025
bf10258
수정
Oct 31, 2025
5326b93
도메인 검증 규칙 변경에 맞게 Controller 및 관련 클래스 로직 수정
Nov 3, 2025
9317192
Connect enum에 이동 책임을 부여하여 도메인 응집도 개선
Nov 3, 2025
339963b
Height 객체 제거에 따른 Ladder 생성부 및 연관 로직 수정
Nov 3, 2025
5570d3e
이동 판단을 Line으로, 실제 이동 계산을 Connect로 위임하여 LadderGame 책임 분리
Nov 3, 2025
9507b6b
Line은 연결 여부 판단만 담당하고 이동 계산은 Connect로 위임하도록 책임 분리
Nov 3, 2025
f5e4d9d
Players가 PlayerName 리스트를 직접 가지도록 구조 정리
Nov 3, 2025
370c1c8
추상 Name/Names 제거 후 PlayerName VO로 도메인 의미 명확화
Nov 3, 2025
b4a8608
추상 Name/Names 제거 후 ResultName VO로 결과 도메인 의미 명확화
Nov 3, 2025
ab293fe
Results가 ResultName 리스트를 직접 가지도록 구조 정리
Nov 3, 2025
fdb8c20
삭제파일
Nov 3, 2025
4f5bc72
Name/Names 삭제에 따른 수정
Nov 3, 2025
4db9002
이동 로직 테스트 추가
Nov 3, 2025
e23bafd
PlayerName/ResultName 기반으로 LadderGame 테스트 수정 및 예외 케이스 추가
Nov 3, 2025
7e4d366
Height 제거에 따라 LadderTest를 int height 기반으로 수정
Nov 3, 2025
ff4df5e
Line 이동 규칙을 moveOf 기준으로 재구성 및 테스트 케이스 보강
Nov 3, 2025
33e9c62
PlayerName VO 검증 테스트 추가
Nov 3, 2025
e023d6d
PlayerName 기반으로 PlayersTest 수정
Nov 3, 2025
ff8a8db
ResultName VO 검증 테스트 추가
Nov 3, 2025
bf23911
ResultName 리스트 기반으로 ResultsTest 수정
Nov 3, 2025
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
54 changes: 54 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 사다리 게임

## 💡 기능 요구사항

1. 사다리 게임에 참여하는 사람에 이름을 최대 5글자까지 부여할 수 있다.
2. 사다리를 출력할 때 사람 이름도 같이 출력한다.
3. 사람 이름은 쉼표(,)를 기준으로 구분한다.
4. 개인별 이름을 입력하면 개인별 결과를 출력한다.
5. `"all"`을 입력하면 전체 참여자의 실행 결과를 출력한다.

---

## 클래스 소개

| 클래스 | 역할|
|----------------------------|---|
| `Name` | 이름 단위를 표현 |
| `Names` | 이름들의 일급 컬렉션 |
| `Players` | 참가자 이름 목록 관리 및 중복 제거 |
| `Results` | 실행 결과 목록 관리 |
| `Height` | 사다리 높이 검증 및 저장 |
| `Connect` | `CONNECTED` / `DISCONNECTED` 상태 Enum |
| `Point` | 연결 여부(`Connect`) 표현 |
| `Line` | 한 줄의 연결 상태(`Point`) 관리 |
| `Ladder` | 사다리 전체를 구성 (여러 Line의 집합) |
| `LadderGame` | 이동 로직 및 결과 매핑 수행 |
| `LadderController` | 전체 게임 실행 흐름 제어 |
| `InputView` / `OutputView` | 입출력 담당 |
| `Main` | 프로그램 실행의 진입점 담당 |

---

## 프로그램 실행 흐름

1. **입력 단계**
- 참여자 이름, 실행 결과, 사다리 높이를 순서대로 입력받는다.
- 이름과 결과의 개수가 다르면 예외를 발생시킨다.
- 이름이 5글자를 초과하면 예외를 발생한다.
- 사다리 높이가 양수가 아니거나 숫자가 아닌 입력이 들어오면 예외를 발생시킨다.

---

2. **사다리 생성 단계**
- 입력받은 높이와 인원 수를 기반으로 `Ladder` 객체를 생성한다.
- 내부적으로 `Line.create()`를 호출하여 각 Line의 연결 상태를 무작위로 생성한다.
- 높이가 0인 경우에도 1줄의 사다리가 생성된다.

---

3. **결과 조회 단계**
- 사용자가 이름을 입력하면 해당 참가자의 결과를 출력한다.
- `"all"`을 입력하면 전체 참가자의 결과를 한 번에 출력한다.

---
23 changes: 23 additions & 0 deletions src/main/java/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import controller.LadderController;
import domain.Height;
import domain.LadderGame;
import domain.Players;
import domain.Results;
import view.InputView;
import view.OutputView;

public class Main {
public static void main(String[] args) {
InputView inputView = new InputView();
OutputView outputView = new OutputView();
LadderController controller = new LadderController(inputView, outputView);

Players players = controller.inputPlayers();
Results results = controller.inputResults(players);
Height height = controller.inputHeight();

LadderGame game = controller.startLadderGame(height, players, results);
controller.showResult(game, players);

}
}
89 changes: 89 additions & 0 deletions src/main/java/controller/LadderController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package controller;

import domain.Height;
import domain.Ladder;
import domain.LadderGame;
import domain.Players;
import domain.Results;
import view.InputView;
import view.OutputView;

import java.util.InputMismatchException;
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;
}

public Players inputPlayers() {
outputView.printAskPlayers();
while (true) {
try {
return new Players(inputView.readString());
} catch (IllegalArgumentException e) {
outputView.printException(e);
outputView.printRetryInputMessage();
}
}
}

public Results inputResults(Players players) {
outputView.printAskResults();
while (true) {
try {
Results results = new Results(inputView.readString());
LadderGame.validatePlayerAndResultCount(players, results);
return results;
} catch (IllegalArgumentException e) {
outputView.printException(e);
outputView.printRetryInputMessage();
}
}
}

public Height inputHeight() {
outputView.printAskHeight();
while (true) {
try {
return new Height(inputView.readInt());
} catch (InputMismatchException | IllegalArgumentException e) {
outputView.printException(e);
outputView.printRetryInputMessage();
}
}
}

public LadderGame startLadderGame(Height height, Players players, Results results) {
Ladder ladder = new Ladder(height, players.size(), new Random());
LadderGame game = new LadderGame(ladder, players, results);
outputView.printLadderResultTitle();
outputView.printLadder(ladder
, players.getPlayers().getValues()
, results.getResults().getValues());
return game;
}

public void showResult(LadderGame game, Players players) {
boolean run = true;
while (run) {
outputView.printAskResultByPlayer();
String name = inputView.readString();
run = validateRun(game, players, name);
}
}

private boolean validateRun(LadderGame game, Players players, String name) {
if (name.equals("all")) {
outputView.printAllResults(game.findAll(), players);
return false;
}
String result = game.findResultByPlayer(name);
outputView.printSingleResult(result);
return true;
}
}
20 changes: 20 additions & 0 deletions src/main/java/domain/Connect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package domain;

public enum Connect {
CONNECTED(true),
DISCONNECTED(false);

private final boolean value;

Connect(boolean value){
this.value = value;
}

public boolean isConnected(){
return value;
}

public static Connect from(boolean value){
return value ? CONNECTED : DISCONNECTED;
}
}
Comment on lines 3 to 44

Choose a reason for hiding this comment

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

👀 Comment

사실 이번 PR에서 가장 신경이 쓰이는 부분은 이쪽인데요.
enum은 여러 프로그래밍 언어에서 제공하는 객체 정의 방식인데요.
이를 이용해서 java에서는 정말 기발한 방법으로 좋은 코드를 짤 수 있습니다.
단지 true false 밸류를 저장하기엔 너무 아까워요!
사실 어떤 리뷰를 드려야될지 모르겠어서 이렇게 아쉬움만 표현했는데요.

NEXT STEP 학습 테스트를 진행해보셨다면, 아마 다른 코멘트를 반영하시다보면 아이디어가 떠오를 것이라 생각합니다.
꼭 생각나지 않으셔도 좋습니다. 다음 코멘트 때 제가 제안을 드려볼게요.

Copy link
Author

Choose a reason for hiding this comment

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

말씀 주신 힌트 기반으로 방향성을 잡아봤습니다!

"나 지금 N번째야" 라고 Line에게 물어보면 → Line이 이동해야 할 방향을 판단하고,
실제 이동은 각 상태(enum)가 담당하도록 책임을 분리했습니다.

이렇게 나누니 Line은 판단만 / Connect는 실제 이동만 담당하게 되어
역할이 더 명확해지고 유지보수도 쉬워진 것 같습니다.

제가 선택한 이 방향도 충분히 올바른 해결 방식 중 하나일까요?
다음 리뷰 때 추가로 조언 주시면 감사하겠습니다

36 changes: 36 additions & 0 deletions src/main/java/domain/Height.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package domain;

public class Height {
private static final int HEIGHT_MIN = 0;
private final int height;

public Height(int height) {
if (height < HEIGHT_MIN) {
throw new IllegalArgumentException("사다리 높이는 양수여야 합니다.");
}

this.height = height;
}

Choose a reason for hiding this comment

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

👀 Comment

위에 남긴 리뷰들과 조금은 비슷한 맥라의 리뷰일 수 있겠는데요.
높이라는 입력값을 표현할 객체가 필요할까?라는 생각이 좀 들었어요.

객체나 메서드의 책임범위를 점점 좁혀가는 것은 클린한 객체지향 코드를 작성하는데 매우 중요한 태도중 하나입니다.
하지만 객체가 많아진다는 것은 그만큼 관리포인트가 늘어나는 것이기도합니다.

이렇게 미션에서 거의 원칙과 같이 제시하는 방향성에도 장단점이 존재하는 것이죠.

생성 로직에서 텍스트로 보이는 것과 같이 사다리이기 때문에 0보다는 커야합니다.
이 책임은 Ladder가 갖기에도 충분한 가치 아닐까합니다.

아예 입력값을 검증하고 싶은 것이라면, inputView가 가져야할 책임일 수도 있겠구요.

Copy link
Author

Choose a reason for hiding this comment

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

처음에는 문제 조건에서 원시값 포장 / 일급컬렉션을 적극적으로 활용하라는 가이드가 있어 Height도 VO로 분리했었습니다.
하지만 리뷰 내용처럼 ‘높이’라는 값은 별도 도메인 규칙이나 확장 여지가 거의 없어서, 이 경우에는 오히려 관리 포인트만 늘리고 있다고 판단했습니다.

따라서 Height 객체는 제거하고 입력값 검증은 컨트롤러에서 처리하는 방향으로 진행했습니다.
컨트롤러는 사용자 입력 → 도메인으로 넘겨주기 전의 최종 validation/파싱 책임을 가져가는 레이어라고 생각하고 있으며, 현재는 컨트롤러에서 height 검증과 예외 처리 흐름(while/retry)까지 담당하도록 구성했습니다.

혹시 이 부분을 InputView로 위임하는 방식이 더 적절한 케이스라면 조언 부탁드립니다. 저는 지금 단계에서는 Controller가 해당 책임을 가져가는 방향이 더 자연스럽다고 판단했습니다.


public int getHeight() {
return height;
}

@Override
public String toString() {
return String.valueOf(height);
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Height)) return false;
Height other = (Height) obj;
return height == other.getHeight();
}

@Override
public int hashCode() {
return Integer.hashCode(height);
}
}
25 changes: 25 additions & 0 deletions src/main/java/domain/Ladder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package domain;

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

public class Ladder {
private final List<Line> lines;

public Ladder(Height height, int playerCount, Random random) {
List<Line> temp = new ArrayList<>();

for (int i = 0; i < height.getHeight(); i++) {
temp.add(Line.create(playerCount, random));
}
if (height.getHeight() == 0) {
temp.add(Line.create(playerCount, random));
}
this.lines = List.copyOf(temp);
}

public List<Line> getLines() {
return lines;
}
}
54 changes: 54 additions & 0 deletions src/main/java/domain/LadderGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package domain;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class LadderGame {
private final Ladder ladder;
private final Players players;
private final Results results;

public LadderGame(Ladder ladder, Players players, Results results) {
this.ladder = ladder;
this.players = players;
this.results = results;
}

public static void validatePlayerAndResultCount(Players players, Results results) {
if (players.size() != results.size()) {
throw new IllegalArgumentException("참가자와 결과 수는 같아야 합니다.");
}
}

private int move(int position) {
for (Line line : ladder.getLines()) {
position += calculateNextPosition(line, position);
}
return position;
}

private int calculateNextPosition(Line line, int position) {
if (line.validateMoveRight(position).isConnected()) return 1;
if (line.validateMoveLeft(position).isConnected()) return -1;
return 0;
}

public String findResultByPlayer(String name) {
List<Name> player = players.getPlayers().getValues();
List<Name> result = results.getResults().getValues();

int index = player.indexOf(new Name(name));
index = move(index);
return result.get(index).value();
}

public Map<String, String> findAll() {
Map<String, String> map = new HashMap<>();
List<Name> player = players.getPlayers().getValues();
for (Name name : player) {
map.put(name.value(), findResultByPlayer(name.value()));
}
return map;
}
}
47 changes: 47 additions & 0 deletions src/main/java/domain/Line.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package domain;

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

public class Line {
private final List<Point> points;

private Line(List<Point> points) {
this.points = List.copyOf(points);
}

public static Line create(int playerCount, Random random) {
List<Point> points = new ArrayList<>();
Connect prev = Connect.DISCONNECTED;

for (int i = 0; i < playerCount - 1; i++) {
Connect next = Connect.from(random.nextBoolean());
next = checkPrev(prev, next);
points.add(new Point(next));
prev = next;
}
return new Line(points);
}

private static Connect checkPrev(Connect prev, Connect next) {
if (prev.isConnected()) {
return Connect.DISCONNECTED;
}
return next;
}

public Connect validateMoveRight(int index) {
if (index >= points.size()) return Connect.DISCONNECTED;
return points.get(index).point();
}

public Connect validateMoveLeft(int index) {
if (index == 0) return Connect.DISCONNECTED;
return points.get(index - 1).point();
}

Choose a reason for hiding this comment

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

🔥 Request Change

태우도 위에서 사용했듯이, validate는 관습적으로 상태 검증해서 예외를 발생시킬 때 많이 사용하는 메서드명입니다.

조금 더, n번째 point에서 왼쪽으로 갈 수 있는지 오른쪽으로 갈 수 있는지 물어보는 듯한 네이밍으로 갈 수 있으면 좋겠어요.


여기서부턴 루카의 참견입니다.

여기서 조금 힌트를 얻으셨으면 하는 것은
Line에게 물어보는 방식도 있을 수 있겠죠?
예를 들면,

"나 지금 N번째야" 라고 Line에게 물어봄 -> 라인 "너는 N-1로 가"

이렇게요.😁

Copy link
Author

Choose a reason for hiding this comment

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

말씀해주신 네이밍 방향 참고하여
validateMoveRight / validateMoveLeft 는 각각 rightOf, leftOf 로 변경해보았습니다.
"어느 방향으로 갈 수 있는지" 판단 역할만 가지는 의미가 훨씬 더 자연스럽게 맞는 것 같습니다.


public List<Point> getPoints() {
return points;
}
}
34 changes: 34 additions & 0 deletions src/main/java/domain/Name.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package domain;

public class Name {
private final String value;

public Name(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("비어 있을 수 없습니다.");
}
this.value = value.trim();
}

public String value() {
return value;
}

@Override
public String toString() {
return value;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Name)) return false;
Name name = (Name) obj;
return value.equals(name.value);
}

@Override
public int hashCode() {
return value.hashCode();
}
}

Choose a reason for hiding this comment

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

🔥 Request Change

'Name'은 구체적인 개념일까요? 추상적인 개념일까요?
관점에 차이가 있을 수 있을 것 같아요.

이 사다리 게임에서 2가지 의미로 사용이 되는데요.
'참여자의 이름' 과 '결과의 이름'을 표현하는데 사용되고 있습니다.
두가지 사이에는 비슷한 개념도 있고 다른 개념도 존재합니다.

이름이기 때문에 한글자 이상 문자열 값을 갖는다는 것이 대표적인 비슷한 점이겠네요.
반면,
'참여자의 이름'은 5글자 이상일 수 없고, Unique한 값이기 때문에 같은 이름은 같은 존재로 판단할 수 있다.
'결과의 이름'은 글자 제한이 없고, Unique한 값이 아니기 때문에 같은 이름이라도 같은 존재라고 판단할 수 없다.
이런 분명한 개념적 차이가 존재합니다.
따라서 저는 이 Name이라는 객체가 꽤나 추상적인 개념으로 사용되고 있다고 생각됩니다.

객체지향적인 설계를 하다보면, 중복을 줄이고 생각이 들 수 밖에 없는데요.
그래야 책임 분리도 확실해지고 유지보수도 용이한 코드가될테니까요.

이를 진짜 중복으로 볼것인지 아닌지는 조금 더 고민해볼 필요가 있을 것 같아요.

저는 Name 래핑 클래스가 java에서 제공하는 String을 대체할만큼 가치가 있게 느껴지진 않습니다.
그리고 이정도 추상화된 개념이라면 앞으로 역할 및 책임이 부여되기도 힘들어보이구요.

그래서 저는 개인적으로 두 개념을 따로 일급컬렉션으로 만들어서 조금 더 응집도를 높여보셨으면 좋겠습니다.
혹시나 다른 의견이 있으시면 편하게 말씀해주세요.

[참고자료: 우발적 중복(greengööse의 블로그)]

Copy link
Author

Choose a reason for hiding this comment

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

단순히 String 을 감싸는 Name / 그리고 그걸 리스트로 감싸는 Names 구조로 사용했습니다.
그리고 Players, Results 에서 이를 재사용하면 중복을 줄일 수 있을 거라고 생각했습니다.

하지만 링크에서 말하는 우발적 중복 개념을 보고 다시 생각해보니,
저는 서로 다른 개념을 같은 것으로 간주해버린 상태였던 것 같습니다.
PlayerName 과 ResultName 은 “둘 다 이름이다” 라는 표면적 공통점만 있을 뿐
제약 조건과 책임은 전혀 다르다는 것을 뒤늦게 인지했습니다.

그래서 Name/Names 로 묶어둔 것은 같은 개념이 아니라
“다른 개념을 하나처럼 써버린 중복”이었고, 우발적 중복으로 봐야 했다는 것을 깨달았습니다.

그래서 PlayerName / ResultName 으로 각각 구체적인 VO로 분리했고,
각 도메인이 자신의 제약을 스스로 갖도록 하는 방향으로 수정했습니다.

좋은 관점 제시 감사합니다 🙇‍♂️

Loading