Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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"`을 입력하면 전체 참가자의 결과를 한 번에 출력한다.

---
22 changes: 22 additions & 0 deletions src/main/java/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import controller.LadderController;
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);
int height = controller.inputHeight();

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

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

import domain.Ladder;
import domain.LadderGame;
import domain.PlayerName;
import domain.Players;
import domain.ResultName;
import domain.Results;
import view.InputView;
import view.OutputView;

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

public Players inputPlayers() {
outputView.printAskPlayers();
while (true) {
try {
String input = inputView.readString();
List<PlayerName> players = Arrays.stream(input.split(","))
.map(String::trim)
.map(PlayerName::new)
.toList();
return new Players(players);
} catch (IllegalArgumentException e) {
outputView.printException(e);
outputView.printRetryInputMessage();
}
}
}

public Results inputResults(Players players) {
outputView.printAskResults();
while (true) {
try {
String input = inputView.readString();
List<ResultName> result = Arrays.stream(input.split(","))
.map(String::trim)
.map(ResultName::new)
.toList();
Results results = new Results(result);
LadderGame.validatePlayerAndResultCount(players, results);
return results;
} catch (IllegalArgumentException e) {
outputView.printException(e);
outputView.printRetryInputMessage();
}
}
}

public int inputHeight() {
outputView.printAskHeight();
while (true) {
try {
int height = inputView.readInt();
if (height < 0) {
throw new IllegalArgumentException("높이는 양수여야 합니다.");
}
Comment on lines +66 to +69

Choose a reason for hiding this comment

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

👀 Comment

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

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

위에서 이런 코멘트를 남겨주셨습니다.
저는 태우가 controller에서 입력값을 검증하고,
재요청을 받는것 같은 처리를 한 것이 적절하다고 판단이 됩니다. 굿👍

"validation을 어디서 할까?" 이건 다들 고민하는 영역인 것 같아요.

  • inputView 에서 입력받을 때
    • 웹 어플리케이션으로 예를 들면, 전화번호 input 태그에 숫자가 아닌 문자를 입력했을 때, 즉시 경고를 띄울 수 있겠죠.
  • Controller
    • View에서 입력 받은 정보를 domain에 넘겨줘도 괜찮은지 판단하거나
    • Domain에서 응답 받는 정보를 view에 출력하도록 넘겨줘도 괜찮은지 판단할 수 있겠네요.
  • Domain
    • 이름은 다섯글자를 넘으면 안된다와 같은 중요한 비즈니스 정책을 담고 있을 수 있겠습니다.

각 영역에서 검증은 모두 있을 수 있고 목적이 약간씩은 다른 것 같아요.
이런 console 어플리케이션을 구현하다보면 그 경계가 모호하고 규모가 너무 작아 헷갈릴 수 있습니다.
하지만, 꼭 필요한 고민이라고 생각하고 추후에 다른 환경에서 개발할 때도
이렇게 각 영역에서 검증해야되는 것이 어떤것일까 고민하시면 좋은 설계를 할 수 있으실거에요.

Copy link
Author

Choose a reason for hiding this comment

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

validation 책임 분리에 대한 조언 감사합니다!
말씀해주신 것처럼 View(InputView)에서의 즉각적인 입력 포맷 검증, Controller에서의 전달 전 검증, Domain 내부의 비즈니스 룰 검증 각각의 영역을 잘 이해하게 됐습니다.
이 관점을 기반으로 나중에 웹 환경으로 확장할 때 validation 위치를 더 의식적으로 선택해보겠습니다!

return height;
} catch (InputMismatchException | IllegalArgumentException e) {
outputView.printException(e);
outputView.printRetryInputMessage();
}
}
}

public LadderGame startLadderGame(int 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(), results.getResults());
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;
}
}
44 changes: 44 additions & 0 deletions src/main/java/domain/Connect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package domain;

public enum Connect {
CONNECTED(true) {
@Override
public int moveRight(int index) {
return index + 1;
}

@Override
public int moveLeft(int index) {
return index - 1;
}
},
DISCONNECTED(false) {
@Override
public int moveRight(int index) {
return index;
}

@Override
public int moveLeft(int index) {
return index;
}
};

private final boolean value;

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

public abstract int moveRight(int index);

public abstract int moveLeft(int index);

public boolean isConnected() {
return value;
}

public static Connect from(boolean value) {
return value ? CONNECTED : DISCONNECTED;
}
}
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(int height, int playerCount, Random random) {
List<Line> temp = new ArrayList<>();

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

public List<Line> getLines() {
return lines;
}
}
Comment on lines +7 to +25

Choose a reason for hiding this comment

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

🔥 Request Change

생성자에서 List 필드를 주입할 때, copyOf를 사용하는 것에 대한 리뷰에 다음과 같이 답해주셨습니다.

Names 내부에서 copyOf가 적용되어 있기 때문에 Players도 방어된 상태라고 생각했습니다.
하지만 공유해주신 자료(방어적 복사, 얕은 복사, 깊은 복사)를 참고하면서 copyOf는 얕은 복사에 해당하고, 이는 Names 내부의 리스트만 불변으로 만들 뿐 Players의 최종 상태까지 보장하지 않는다는 점을 이해했습니다.

리뷰에서 말씀주신 것처럼 Names는 도메인에서 의미가 다소 추상적인 개념이라고 판단하여 제거하였고,
Players / Results에서도 생성자에서 직접 List.copyOf(...)를 적용하여
도메인 객체 생성 시점에서 불변을 보장하도록 리팩터링하였습니다.

[질문]

태우가 방어적 복사를 사용하는 이유는 무엇인가요?
"여기서 copyOf를 쓰면 안되지!!" 라는 뉘앙스보다는 어떤 의도로 사용하였고, 어떤 트레이드 오프를 고려하였는가에 대한 질문입니다.

[추가 설명]

Players나 Results에도 비슷한 방식이 사용되었지만, 일단 Ladder를 대상으로 설명하겠습니다.

    private final List<Line> lines;

이 필드가 실제로 불변일까요?

저는 lines가 현재 실제로 불변하지도 않고, 불변해야할 필요가 있는지에 대한 확신도 없습니다.

        Ladder ladder = new Ladder(height, players.size(), new Random(1));
        ladder.getLines().add(Line.create(players.size(), new Random(1)));

위와 같은 방식으로 ladder가 생성된 이후에 lines를 불러,
java collection에서 제공하는 메서드로 값을 추가할 수 있겠죠.
ladder의 lines의 참조변수는 변하지 않아도 그 참조값이 가르키는 리스트의 모양은 바뀔 수 있습니다.
이는 불변이라고 할 수 없겠죠.

이를 만약 막고 싶다면, 제가 참고 자료로 남겼던 블로그에도 소개되는 unmodifiableList를 활용해도 충분히 막을 수 있습니다.

Copy link
Author

Choose a reason for hiding this comment

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

copyOf를 사용한 이유는 해당 도메인(Players / Results / Ladder)이 사다리 게임에서 핵심 역할을 가지고 있고,
생성 이후 외부 컬렉션이 변경되면 전체 게임 결과 흐름이 깨질 수 있다고 판단했기 때문입니다.

그리고 저는 이 컬렉션을 생성 이후 외부에서 add/remove 등으로 조작하는 형태로 사용할 계획이 없었기 때문에,
생성 시점에서 copyOf로 입력값만 분리하면 충분하다고 판단했습니다.

다만 copyOf는 얕은 복사이기 때문에
ladder.getLines().add(Line.create(players.size(), new Random(1))); 와 같은 호출이 가능하다는 점을 다시 인지했습니다.

저와 같이 “외부에서 컬렉션 조작을 전제로 하지 않은 경우”에도
getter 노출 단계까지 완전한 불변성을 구현하는 것을 권장하시는지 궁금합니다.

52 changes: 52 additions & 0 deletions src/main/java/domain/LadderGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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 = line.moveOf(position);
}
return position;
}


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

int index = player.indexOf(new PlayerName(name));
if (index < 0) {
throw new IllegalArgumentException("존재하지 않는 플레이어입니다.");
}
index = move(index);
return result.get(index).value();
}

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

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

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

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

static Line of(List<Connect> points) {
return new Line(points);
}

public static Line create(int playerCount, Random random) {
List<Connect> 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(next);
prev = next;
}
return new Line(points);
}

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

private Connect rightOf(int index) {
if (index >= points.size()) return Connect.DISCONNECTED;
return points.get(index);
}

private Connect leftOf(int index) {
if (index <= 0) return Connect.DISCONNECTED;
return points.get(index - 1);
}

public int moveOf(int index) {
Connect right = rightOf(index);
if (right.isConnected()) return right.moveRight(index);

Connect left = leftOf(index);
if (left.isConnected()) return left.moveLeft(index);

return index;
}

public List<Connect> getPoints() {
return points;
}
}
Loading