Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
128 commits
Select commit Hold shift + click to select a range
2d931ea
docs(readme): 보드 초기화 관련 구현 기능 목록 작성
mvg01 Mar 24, 2026
b841d4a
test(setup-policy): 장기판 차림 정책 테스트 추가
EveryPine Mar 24, 2026
d2caeba
feat(setup-policy): 장기판 차림 정책 추가
mvg01 Mar 24, 2026
d1e853c
test(setup-command): 차림판 명령 테스트 추가
mvg01 Mar 25, 2026
04f9dc6
feat(setup-command): 차림 명령 기능 추가
EveryPine Mar 25, 2026
96236d6
test(board-generator): 보드판 기물 생성 테스트
mvg01 Mar 25, 2026
703c6cc
feat(board-generator): 보드판 기물 생성 기능 추가
EveryPine Mar 25, 2026
283a708
feat(view): 팀별 차림법 입력 기능 추가
mvg01 Mar 25, 2026
bccd555
feat(output): 보드판 출력 기능 추가
EveryPine Mar 25, 2026
1faa91f
docs(readme): 기물 이동 구현 기능 목록 작성
mvg01 Mar 25, 2026
455379e
test(parser): 정수 파싱 테스트 추가
mvg01 Mar 25, 2026
7f4caa6
test(board): 특정 팀 기물 여부 테스트 추가
EveryPine Mar 25, 2026
45894d7
test(board): 특정 팀 기물 여부 테스트에 빈 칸인 경우 추가
EveryPine Mar 25, 2026
ea4a672
test(board): 특정 팀 기물 여부 테스트 삭제
EveryPine Mar 25, 2026
4062415
test(board): 빈칸 여부 판정 테스트 추가
EveryPine Mar 25, 2026
fc807da
feat(board): 빈칸 여부 판정 기능 추가
EveryPine Mar 25, 2026
5168752
test(board): 기물 획득 테스트 추가
EveryPine Mar 25, 2026
95df400
feat(board): 기물 획득 기능 추가
EveryPine Mar 25, 2026
09263e1
test(piece): 특정 팀 기물 여부 판정 테스트 추가
EveryPine Mar 25, 2026
43276c9
feat(piece): 특정 팀 기물 여부 판정 기능 추가
EveryPine Mar 25, 2026
313d5e5
feat(board): 선택한 기물 위치맵 제공 기능 추가
EveryPine Mar 26, 2026
2daa947
test(chariot): 차 기물의 이동 가능한 위치 테스트 추가
mvg01 Mar 26, 2026
c05930b
test(position): 위치 객체 생성 테스트 추가
EveryPine Mar 26, 2026
e9b78ba
fix(position): 잘못된 값 입력 시 NPE가 발생하는 문제 수정
EveryPine Mar 26, 2026
60feacf
test(direction): 방향 객체 생성 테스트 추가
EveryPine Mar 26, 2026
159f9a3
feat(direction): 방향 객체 생성 기능 추가
EveryPine Mar 26, 2026
2bdd2aa
test(straight-movement): 도달 여부 판정 테스트 추가
EveryPine Mar 26, 2026
50e0b1f
feat(straight-movement): 도달 여부 판정 기능 추가
EveryPine Mar 26, 2026
ccf0c0a
test(straight-movement): 목적지 계산 테스트 추가
EveryPine Mar 26, 2026
280bbc3
feat(straight-movement): 목적지 계산 기능 추가
EveryPine Mar 26, 2026
7e3d518
test(straight-movement): 경로 자취 위치 리스트 계산 테스트 추가
EveryPine Mar 26, 2026
16f5a0c
feat(straight-movement): 경로 자취 위치 리스트 계산 기능 추가
EveryPine Mar 26, 2026
fd85740
refactor(movement): 인터페이스에서 클래스로 변경
mvg01 Mar 26, 2026
3644d3c
test(rule): 이동 가능한 자취 경로 계산 테스트 추가
mvg01 Mar 26, 2026
25f5c80
test(rule): 이동 가능한 자취 경로 계산 테스트 수정
EveryPine Mar 26, 2026
34d3805
feat(rule): 이동 가능한 자취 경로 계산 기능 추가
EveryPine Mar 26, 2026
9542298
refactor(movement): 이동 대상 기물을 파라미터로 전달하도록 수정
EveryPine Mar 26, 2026
eb7ca57
test(rule): 이동 가능한 목적지 계산 테스트 추가
EveryPine Mar 26, 2026
ad3f207
test(rule): 이동 가능한 목적지 계산 기능 추가
EveryPine Mar 26, 2026
095976c
test: board mediator를 파라미터로 전달하도록 변경
mvg01 Mar 26, 2026
26f7d5b
feat(chariot): 차 기물 이동 규칙 추가
mvg01 Mar 26, 2026
9e9ad83
test(cannon): 포 기물 이동 규칙 테스트 추가
EveryPine Mar 26, 2026
3d8d512
feat(cannon): 포 기물 이동 규칙 추가
EveryPine Mar 26, 2026
c4ae3c9
test(elephant): 상 기물 이동 규칙 테스트 추가
mvg01 Mar 27, 2026
21b4dfc
feat(movement): 도달 가능 여부 판정 기능 분리
EveryPine Mar 27, 2026
47918a8
feat(elephant): 상 기물 이동 규칙 추가
EveryPine Mar 27, 2026
8c8ec85
test(horse): 마 기물 이동 규칙 테스트 추가
EveryPine Mar 27, 2026
7ffe52e
feat(horse): 마 기물 이동 규칙 추가
EveryPine Mar 27, 2026
e86d143
test(general): 장 기물 이동 규칙 테스트 추가
mvg01 Mar 27, 2026
6296e87
feat(general): 장 기물 이동 규칙 추가
mvg01 Mar 27, 2026
d8788ea
test(guard): 사 기물 이동 규칙 테스트 추가
EveryPine Mar 27, 2026
cb7363e
feat(guard): 사 기물 이동 규칙 추가
EveryPine Mar 27, 2026
e785c2f
test(soldier): 병(졸) 기물 이동 규칙 테스트 추가
mvg01 Mar 27, 2026
83b42af
feat(soldier): 병(졸) 기물 이동 규칙 추가
EveryPine Mar 27, 2026
0fc7c04
refactor: 불필요한 코드 제거
mvg01 Mar 27, 2026
7dc30dc
refactor: Movement클래스 가독성 개선
mvg01 Mar 27, 2026
bfc634a
refactor: 생성자에서 일급 컬렉션 방어적 복사 수정
mvg01 Mar 27, 2026
30cca54
refactor: RuleWithNoTraces 로직 분리
mvg01 Mar 27, 2026
bab1236
refactor: RuleWithTraces 로직 분리
mvg01 Mar 27, 2026
672895a
refactor: RuleOfCannon 변수 네이밍 변경
mvg01 Mar 27, 2026
2e4c672
refactor: 패키지 구조 정리
mvg01 Mar 27, 2026
efb3680
refactor: 오타 수정
mvg01 Mar 28, 2026
ee3f7f9
refactor: SetupCommand 책임 분리
mvg01 Mar 28, 2026
d86f271
refactor: 클래스 네이밍 변경
mvg01 Mar 28, 2026
82f1e1e
refactor: Direction 방향 상수로 교체
mvg01 Mar 28, 2026
2da8100
refactor: Rule 인터페이스, 구현체 네이밍 수정
mvg01 Mar 29, 2026
6358f66
refactor: SlidingMoveRule 단순화
mvg01 Mar 29, 2026
f2e774e
refactor: CannonMoveRule 단순화
mvg01 Mar 29, 2026
c98114e
refactor: BoardMediator 내부 메서드 네이밍 변경
mvg01 Mar 29, 2026
1e3d2ef
refactor: 기물 클래스의 UNCATCHABLE_PIECE_TYPES 리스트 삭제
mvg01 Mar 29, 2026
056b770
refactor: BoardMediatorImpl 제거, Board가 BoardMediator 직접 구현
mvg01 Mar 29, 2026
f4a5813
feat: 보드판 출력에 인덱스 번호 추가
mvg01 Mar 31, 2026
b7f334f
docs: readme 게임 루프 로직 기능 목록 작성
mvg01 Mar 31, 2026
6ad1979
feat: TurnManager로 장기의 현재 턴을 관리하는 기능 추가
mvg01 Mar 31, 2026
69f3958
feat: 이동 가능한 위치를 초록 배경으로 시작화
mvg01 Mar 31, 2026
0084104
feat: 턴제 루프 구현
mvg01 Mar 31, 2026
2d2ade8
test: 보드에 왕이 2개인지 확인하는 단위 테스트 추가
mvg01 Mar 31, 2026
b4c4c9e
feat: 왕이 2개 미만이면 게임 루프 종료하도록 종료 조건 추가
mvg01 Mar 31, 2026
e347148
fix: 빈칸 선택 IllIllegalArgumentException으로 변경
mvg01 Mar 31, 2026
b1adc9f
feat: 이동가능한 위치가 없는 기물을 선택한 경우 다시 입력받도록 수정
mvg01 Mar 31, 2026
93cf544
fix: 빈칸 선택 시 예외를 IllegalArgumentException으로 변경
mvg01 Mar 31, 2026
ca133d4
feat: 게임 종료 메시지 추가
mvg01 Mar 31, 2026
78603c9
refactor: SetupCommand
mvg01 Apr 2, 2026
54d5a23
refactor: SetupStrategy에서 사용자 입력 검증으로 수정
mvg01 Apr 2, 2026
ae02236
refactor: controller 메서드 분리
mvg01 Apr 2, 2026
e2df58f
refactor: CannonMoveRule 변수 인라인화
mvg01 Apr 2, 2026
f1e051b
refactor: 메서드 네이밍 변경
mvg01 Apr 2, 2026
80b0ec3
refactor: TurnManager 내부 메서드 매개변수 타입 변경
mvg01 Apr 2, 2026
f7c46ae
refactor: SlidingMoveRule 필드 타입 수정
mvg01 Apr 2, 2026
9b02da6
refactor: setup에서 생성하는 맵을 모두 불변으로 수정
mvg01 Apr 2, 2026
2109d0e
refactor: board 생성자에 방어적 복사 적용
mvg01 Apr 2, 2026
dc7e3aa
test: SlidingMoveRuleTest 보강
mvg01 Apr 2, 2026
99a4901
refactor: SetupStrategy setup 패키지로 이동
mvg01 Apr 2, 2026
b15d18e
refactor: 테스트를 패키지 구조로 분리
mvg01 Apr 2, 2026
0210333
test: StepMoveRuleTest 테스트 추가
mvg01 Apr 2, 2026
d6f20fd
test: StepMoveRuleTest 테스트 수정
mvg01 Apr 2, 2026
81a84a8
test: StepMoveRuleTest 테스트 수정
mvg01 Apr 2, 2026
33b2605
test: CannonMoveRuleTest 테스트 케이스 추가
mvg01 Apr 2, 2026
ae38ff3
test: StepMoveRuleTest 테스트 케이스 추가
mvg01 Apr 2, 2026
4f108d1
test: StepMoveRuleTest 테스트 수정
mvg01 Apr 2, 2026
bab88e7
test: StepMoveRuleTest 테스트 수정
mvg01 Apr 2, 2026
e1fe200
test: CannonMoveRuleTest 테스트 케이스 추가
mvg01 Apr 2, 2026
78c7c33
Merge remote-tracking branch 'origin/step1' into step1
mvg01 Apr 2, 2026
a96700a
test: TurnManagerTest 추가
mvg01 Apr 2, 2026
022322f
test: ElephantTest 테스트 케이스 추가
mvg01 Apr 3, 2026
f379b99
refactor: StepMoveRule에서 정적 팩토리 메서드로 상,마의 이동 규칙 가독성 개선
mvg01 Apr 3, 2026
6dc67b5
refactor: Board의 게임종료조건 책임 분리
mvg01 Apr 3, 2026
0eae99a
test: 종료책임분리로 인한 테스트 수정
mvg01 Apr 3, 2026
b93a0f1
refactor: CannonMoveRule에서 캡슐화 위반 수정
mvg01 Apr 3, 2026
ed1a43f
refactor: 쓰지 않는 매개변수 제거
mvg01 Apr 3, 2026
299a565
refactor: MoveRule에 TeamType 파라미터 추가로 이동 규칙의 보드 의존성 감소
mvg01 Apr 3, 2026
641314f
fix: 승리한 나라로 출력되지 않는 오류 수정
mvg01 Apr 3, 2026
5ac4a06
refactor: calculateTraces에 piece 파라미터 제거
mvg01 Apr 3, 2026
e3f163e
refactor: Movement에서 Piece 의존성 제거
mvg01 Apr 3, 2026
2cc764b
refactor: Cannon 이동 규칙을 CalculateTracesForCannon으로 분리
mvg01 Apr 3, 2026
b7c49c8
fix: hasReachablePosition에서 from 변수 재할당으로 인한 위치 계산 오류 수정
mvg01 Apr 3, 2026
0bb172e
refactor: 사용하지 않는 메서드 제거
mvg01 Apr 3, 2026
0fbb82c
refactor: setup패키지를 board패키지 내부로 이동
mvg01 Apr 4, 2026
c452a58
refactor: Board에 TDA를 적용해 개선
mvg01 Apr 4, 2026
4edb6e2
refactor: piece에서 DTO에 필요한 getter로 네이밍 수정
mvg01 Apr 4, 2026
656cd3d
refactor: board test에서 테스트하는 메서드 변경
mvg01 Apr 4, 2026
c996d7c
refactor: board에서 piece를 반환하는 메서드 제거
mvg01 Apr 4, 2026
49413ca
refactor: GameManager 삭제
mvg01 Apr 4, 2026
af33f4e
refactor: BoardDTO 로직 OutputView로 분리
mvg01 Apr 4, 2026
6793253
refactor: 매직넘버 제거
mvg01 Apr 4, 2026
57771fb
refactor: 기물 공통 로직을 AbstractPiece 추상 클래스로 추출
mvg01 Apr 4, 2026
8ce46f6
refactor: AbstractPiece 공통 메서드에 final 키워드 추가
mvg01 Apr 4, 2026
601c500
refactor: 포 포획불가 로직을 CannonMoveRule에서 처리하도록 수정
mvg01 Apr 4, 2026
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
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,47 @@
# java-janggi

장기 미션 저장소
# 구현 기능

- 보드 초기화
- 각 플레이어에게 장기판 차림 방법을 입력 받는다.
- 먼저 플레이하는 초나라부터 명령 번호를 입력하여 판차림 방법을 결정한다.
- 1, 2, 3, 4 이외의 숫자가 들어오는 경우 예외 처리한다.
- 선택할 수 있는 판차림은 다음과 같다.
1. 왼상 차림
2. 오른상 차림
3. 안상 차림
4. 바깥상 차림
- 입력한 장기판 차림을 기반으로 보드를 세팅한다.
- 보드 출력
- 빈 공간은 온점 전각 문자를 활용한다.
- 팀 구분은 색상(빨강, 파랑)으로 구분한다.
- 기물은 각 기물에 대응하는 한나라의 한자로 표기한다.

- 기물 이동 구현
- 움직일 기물의 좌표를 플레이어에게 입력받는다.
- 다음의 경우 다시 입력받는다.
- `row`, `column` 형식이 아닌 경우
- 플레이어의 팀이 아닌 기물의 좌표를 입력한 경우
- 입력한 기물이 이동 가능한 위치가 없는 경우
- 입력받은 위치의 기물이 이동 가능한 위치를 출력한다.
- 이동 가능한 위치는 초록색으로 표현된다.
- 플레이어에게 이동 가능한 위치 중 하나를 입력받는다.
- 다음의 경우 다시 입력받는다.
- `row`, `column` 형식이 아닌 경우
- 이동 가능한 위치가 아닌 경우
- 입력받은 위치로 기물을 이동시킨다.
- 각 기물의 이동 규칙은 다음과 같다.
- 차 (車): 가로/세로 직선 이동가능, 거리 제한 없음, 중간에 기물이 있으면 못 지나감
- 마 (馬): 1칸 직선 + 1칸 대각선, 가는 길이 막히면 이동 불가
- 상 (象): 1칸 직선 + 2칸 대각선, 가는 길이 막히면 이동 불가
- 사 (士): 모든 방향으로 1칸 이동 가능
- 장 (將): 모든 방향으로 1칸 이동 가능
- 포 (砲): 반드시 하나를 넘어 전후좌우로 이동이 가능하고 포는 서로 넘거나 잡을 수 없다.
- 졸 (卒): 앞, 왼쪽, 오른쪽 1칸씩 이동 가능

- 게임 루프 로직
- 처음엔 한나라(Red Team)로 시작한다.
- 각 플레이어가 움직일 기물의 위치를 입력한다.
- 기물 이동 구현의 규칙과 같다.
- 한나라 -> 초나라 -> 한나라... 반복된다.
- 종료 조건은 어느 한 팀의 장 기물이 잡히면 게임은 종료된다.
11 changes: 11 additions & 0 deletions src/main/java/janggi/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package janggi;

import janggi.controller.JanggiController;

public class Application {

public static void main(String[] args) {
final JanggiController janggiController = new JanggiController();
janggiController.run();
}
}
103 changes: 103 additions & 0 deletions src/main/java/janggi/controller/JanggiController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package janggi.controller;

import janggi.domain.Position;
import janggi.domain.board.Board;
import janggi.domain.board.BoardGenerator;
import janggi.domain.board.setup.ElephantFormation;
import janggi.domain.board.setup.SetupStrategy;
import janggi.domain.team.BlueTeam;
import janggi.domain.team.RedTeam;
import janggi.domain.team.Team;
import janggi.domain.team.TeamType;
import janggi.domain.team.TurnManager;
import janggi.dto.BoardDto;
import janggi.utils.RetryExecutor;
import janggi.view.InputView;
import janggi.view.OutputView;
import java.util.List;

public class JanggiController {

public JanggiController() {
}

public void run() {
Board board = BoardGenerator.generate(setupRedTeam(), setupBlueTeam());
TurnManager turnManager = new TurnManager();
while (board.hasGeneral(turnManager.currentTeamType())) {
playTurn(board, turnManager);
}
turnManager.changeTurn();
OutputView.printGameOverMessage(turnManager.currentTeamTypeToString());
}

private Team setupRedTeam() {
OutputView.printSetupGuide(TeamType.RED);
final SetupStrategy setupStrategyCommand = RetryExecutor.retry(this::readSetupCommand);
final ElephantFormation elephantFormation = setupStrategyCommand.toPolicy();
return new RedTeam(elephantFormation);
}

private Team setupBlueTeam() {
OutputView.printSetupGuide(TeamType.BLUE);
final SetupStrategy setupStrategyCommand = RetryExecutor.retry(this::readSetupCommand);
final ElephantFormation elephantFormation = setupStrategyCommand.toPolicy();
return new BlueTeam(elephantFormation);
}

private SetupStrategy readSetupCommand() {
int inputCommand = InputView.readSetupCommand();
return SetupStrategy.from(inputCommand);
}

private void playTurn(Board board, TurnManager turnManager) {
OutputView.printBoard(BoardDto.from(board), turnManager.currentTeamTypeToString());
Position from = findFromPosition(board, turnManager);
List<Position> movable = board.calculateMovablePositions(from);
OutputView.printBoardWithMovable(BoardDto.from(board), movable);
Position to = RetryExecutor.retry(() -> inputToPosition(movable));
board.movePiece(from, to);
turnManager.changeTurn();
}

private Position findFromPosition(Board board, TurnManager turnManager) {
while (true) {
Position from = RetryExecutor.retry(() -> inputFromPosition(board, turnManager));
List<Position> movable = board.calculateMovablePositions(from);
if (!movable.isEmpty()) {
return from;
}
OutputView.printErrorMessage("이동 가능한 위치가 없습니다. 다른 기물을 선택하세요.");
}
}

private Position inputFromPosition(Board board, TurnManager turnManager) {
while (true) {
try {
OutputView.printInputFromPosition();
Position from = InputView.readPosition();
if (!board.isSameTeamType(from, turnManager.currentTeamType())) {
throw new IllegalArgumentException("자신의 기물을 선택하세요.");
}
return from;
} catch (IllegalArgumentException e) {
OutputView.printErrorMessage(e.getMessage());
}
}
}

private Position inputToPosition(List<Position> movable) {
while (true) {
try {
OutputView.printInputToPosition();
Position to = InputView.readPosition();
if (!movable.contains(to)) {
throw new IllegalArgumentException("이동 불가능한 위치입니다.");
}
return to;
} catch (IllegalArgumentException e) {
OutputView.printErrorMessage(e.getMessage());
}
}
}
}
87 changes: 87 additions & 0 deletions src/main/java/janggi/domain/Position.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package janggi.domain;

import janggi.domain.movement.Direction;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

public final class Position {

public static final int MAXIMUM_ROW = 10;
public static final int MAXIMUM_COLUMN = 9;
public static final int MINIMUM_ROW = 1;
public static final int MINIMUM_COLUMN = 1;
private static final int ROW_FLIP_VALUE = 11;
private static final Map<Integer, Map<Integer, Position>> CACHE;

static {
CACHE = new LinkedHashMap<>();
for (int row = MINIMUM_ROW; row <= MAXIMUM_ROW; row++) {
CACHE.put(row, new LinkedHashMap<>());
}
}

private final int row;
private final int column;

private Position(final int row, final int column) {
this.row = row;
this.column = column;
}

public static Position valueOf(final int row, final int column) {
validateRowRange(row);
validateColumnRange(column);
final Map<Integer, Position> secondaryMap = CACHE.get(row);
if (!secondaryMap.containsKey(column)) {
secondaryMap.put(column, new Position(row, column));
}
return secondaryMap.get(column);
}

private static void validateRowRange(final int row) {
if (row < MINIMUM_ROW || row > MAXIMUM_ROW) {
throw new IllegalArgumentException(String.format("행 입력은 %d~%d을 입력해야 합니다.", MINIMUM_ROW, MAXIMUM_ROW));
}
}

private static void validateColumnRange(final int column) {
if (column < MINIMUM_COLUMN || column > MAXIMUM_COLUMN) {
throw new IllegalArgumentException(String.format("열 입력은 %d~%d을 입력해야 합니다.", MINIMUM_COLUMN, MAXIMUM_COLUMN));
}
}

public Position flipAroundMiddleRow() {
return Position.valueOf(ROW_FLIP_VALUE - row, column);
}

public boolean checkNextBound(final int distance, final Direction direction) {
final int nextRow = row + direction.getRowDirection() * distance;
final int nextColumn = column + direction.getColumnDirection() * distance;

return nextRow >= MINIMUM_ROW && nextRow <= MAXIMUM_ROW && nextColumn >= MINIMUM_COLUMN
&& nextColumn <= MAXIMUM_COLUMN;
}

public Position calculateNext(final int distance, final Direction direction) {
final int nextRow = row + direction.getRowDirection() * distance;
final int nextColumn = column + direction.getColumnDirection() * distance;

return Position.valueOf(Math.clamp(nextRow, MINIMUM_ROW, MAXIMUM_ROW),
Math.clamp(nextColumn, MINIMUM_COLUMN, MAXIMUM_COLUMN));
}

@Override
public boolean equals(final Object object) {
if (object == null || getClass() != object.getClass()) {
return false;
}
final Position position = (Position) object;
return row == position.row && column == position.column;
}

@Override
public int hashCode() {
return Objects.hash(row, column);
}
}
61 changes: 61 additions & 0 deletions src/main/java/janggi/domain/board/Board.java
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  1. 보드 상태를 어디에서 관리해야 하는지에 대한 설계
    가장 크게 고민한 부분입니다. 사전학습과 토론 과정에서도 기물이 보드의 상태를 직접 알 책임이 없다고 생각했고, 기물은 자신의 이동 규칙만 알고 있어야 한다고 생각했습니다. 하지만 실제로 이동 가능한 위치를 계산하는 과정에서는 보드 상태가 계속 필요했고, 이를 기물에 직접 전달하다 보면 기물이 보드를 알게 되는 구조가 되어 불편했습니다.

그래서 보드와 기물 사이에 BoardMediator를 두고, 기물은 보드의 구체적인 구현을 모르더라도 필요한 정보만 받아올 수 있도록 설계했습니다. 이 방식이 캡슐화를 유지하는 방향인지, 혹은 오히려 중재자인 BoardMediator에게 책임이 과하게 몰리는 구조인지 피드백을 받고 싶습니다. 찰리라면 어떻게 기물과 보드의 상태를 관리했을지도 듣고 싶습니다.

결론만 말씀드리면 캡슐화가 지켜지는 방향은 아니라고 생각해요 :)
결국 보드의 상태를 직접 해석하는 상태 아닐까요? 🤔

궁금한 점에 대해서 먼저 질문드릴게요!

  1. 기물이 보드를 알게되면 어떤 단점이 있나요?
  2. 보드와 기물 사이에 BoardMediator를 두고, 기물은 보드의 구체적인 구현을 모르더라도 필요한 정보만 받아올 수 있도록 설계했습니다 BoardMediator 만 본다면 여러 책임이 있을까요?
  3. BoardMediatorImpl 클래스를 작성한 이유는 무엇인가요? Board 가 BoardMediator 를 의존하는 구조는 사용할수는 없었을까요?

Copy link
Copy Markdown
Author

@mvg01 mvg01 Mar 29, 2026

Choose a reason for hiding this comment

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

  1. 보드의 캡슐화가 붕괴된다고 생각했습니다. 보드가 Map에서 기물 객체를 Position의 Key로 저장하는 방향으로 설계했는데, 만약 기물이 보드를 알게되면 서로가 서로를 아는 상황이 생기게 됩니다. 따라서 단점은 Board의 상태 변경 시, 각각의 기물이 가진 보드도 상태를 변경해줘야 하기에 복잡도가 증가하고, SRP가 위배되지 않을까라고 생각했습니다.
  2. 단순히 보드의 상태만 제공해주는 책임이 있어야 할 것이라고 생각했습니다. 기물들에게 직접적으로 Board의 상태를 제공해주는대신, 중재자 클래스를 거쳐서 필요로 하는 정보(map의 해당 위치에 기물이 있는가 등)만 제공해 주는 것이라고 판단했습니다.
  3. 중재자 패턴에 대해서 간단하게 학습 한 후, 설계한 것이었는데. 보드의 캡슐화가 붕괴된다고 생각했습니다. 보드가 Map에서 기물 객체를 Position의 Key로 저장하는 방향으로 설계했는데, 만약 기물이 보드를 알게되면 서로가 서로를 아는 상황이 생기게 됩니다. 따라서 단점은 Board의 상태 변경 시, 각각의 기물이 가진 보드도 상태를 변경해줘야 하기에 복잡도가 증가하고, SRP가 위배되지 않을까라고 생각했습니다.

3번 답변에 이어서 코멘트하겠습니다. 현재의 방식이 결국 캡슐화가 지켜지지 않는 방향이라고 답변을 주셔서 혼란이 왔습니다.. 캡슐화를 지키면서 기물에게 보드의 상태를 어떻게 제공할지 잘 모르겠습니다. 그렇다면 기물이 꼭 보드의 상태를 알아야만 움직일 수 있는가? 라는 질문을 던져보아도 계속 기물이 보드의 상태를 알아야만 움직임을 담당할 수 있을 것 같다고 생각이 듭니다. 우선 이번 리뷰에선 여기까지 정리하며 고민을 해보도록 하겠습니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  1. 보드의 캡슐화가 붕괴된다고 생각했습니다. 보드가 Map에서 기물 객체를 Position의 Key로 저장하는 방향으로 설계했는데, 만약 기물이 보드를 알게되면 서로가 서로를 아는 상황이 생기게 됩니다. 따라서 단점은 Board의 상태 변경 시, 각각의 기물이 가진 보드도 상태를 변경해줘야 하기에 복잡도가 증가하고, SRP가 위배되지 않을까라고 생각했습니다.

Piece 가 Board 를 상태로 가지는 설계를 생각하셨군요 🤔
메서드에 인자로 넘기는 방법도 있을것 같네요. 그럼 상태 동기화 문제는 해결이 되겠죠?
그래도 순환 참조라는 문제가 남는데
순환 참조의 관점에서 문제를 파악해보시면 좋겠어요~

  1. 단순히 보드의 상태만 제공해주는 책임이 있어야 할 것이라고 생각했습니다. 기물들에게 직접적으로 Board의 상태를 제공해주는대신, 중재자 클래스를 거쳐서 필요로 하는 정보(map의 해당 위치에 기물이 있는가 등)만 제공해 주는 것이라고 판단했습니다.

오히려 중재자인 BoardMediator에게 책임이 과하게 몰리는 구조인지 요런 질문을 하셔서
BoardMediator 만 본다면 책임은 적절해보였어요!

중재자 패턴에 대해서 간단하게 학습 한 후, 설계한 것이었는데. 보드의 캡슐화가 붕괴된다고 생각했습니다. 보드가 Map에서 기물 객체를 Position의 Key로 저장하는 방향으로 설계했는데, 만약 기물이 보드를 알게되면 서로가 서로를 아는 상황이 생기게 됩니다. 따라서 단점은 Board의 상태 변경 시, 각각의 기물이 가진 보드도 상태를 변경해줘야 하기에 복잡도가 증가하고, SRP가 위배되지 않을까라고 생각했습니다.

결론만 말씀드리면 캡슐화가 지켜지는 방향은 아니라고 생각해요 :) 결국 보드의 상태를 Movement가 직접 해석하는 상태 아닐까요? 🤔
요 의견에 대해 설명을 붙이자면
BoardMediator 의 인터페이스는 결국 Board 의 내부 데이터 관점 의 인터페이스를 가지고 있었어요,
이건 캡슐화가 깨졌다고 판단할 수 있는 측면이 있어요
Piece 가 BoardMediator 에게 이 위치에 뭐가 있냐? 라고 물어보고 있는데
Piece 의 관점을 반영한다면 이 위치가 막혀있나?, 이 위치에 적이 있냐? 라는 도메인에 가까운 질문으로 소통하는게 객체지향적이고 캡슐화를 지키는것이라 생각해요


++) 캡슐화가 붕괴된다 라고 하셨지만
무빙은 강하게 의존하고 있는 부분을 느슨하게 변경하고싶다 를 원하는것 같아요 🤔

  • 캡슐화란 무엇일까요?
  • Piece 가 Board 를 의존하면 무조건 캡슐화가 붕괴되는걸까요? 그럼 Board 가 Piece 를 의존하는것도 캡슐화가 깨졌다고 볼 수 있을까요? 😳

+++) SRP 가 위배된다고 생각하신 이유도 궁금해요

  • Board 를 의존하게되면 SRP 관점에서 위배되는 부분은 어떤 점인가요?
  • BoardMediator 를 의존하기만하면 SRP 는 해결되는건가요?
  • Movement 클래스가 변경되어야하는 이유가 몇가지 있는지 세어보는것도 좋겠어요 :)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Piece 가 BoardMediator 에게 이 위치에 뭐가 있냐? 라고 물어보고 있는데 Piece 의 관점을 반영한다면 이 위치가 막혀있나?, 이 위치에 적이 있냐? 라는 도메인에 가까운 질문으로 소통하는게 객체지향적이고 캡슐화를 지키는것이라 생각해요

이 코멘트를 보고 어떤 코드가 문제의 시작일까 다시 생각을 해봤습니다.
Board.java의 이 함수가 가장 캡슐화를 위반하게 하고 있는 원인인 것 같습니다.

@Override
public Piece getPieceInPosition(final Position position) {
    return findPieceByPosition(position);
}

이 코드에서 Piece 객체를 주면 이걸 받은 movement나 Piece같은 다른 객체들이 다시 Piece의 상태를 getTeamType()으로 다시 확인하는 경우도 있었습니다. 이런 부분은 TeamType을 리턴해주거나 하는 방식으로 수정할 수 있을 것 같습니다.
하지만 piece객체 그 자체가 필요한 부분이 이미 더 많습니다. Piece의 행동함수 (piece의 경로에서 갈 수 있는 위치 계산)을 실행하려면 piece 객체 그 자체가 필요합니다.

솔직하게 얘기한다면 지금 가장 헷갈리는건 어디까지가 캡슐화고, 괜찮은 의존 관계인지를 모르겠습니다. 캡슐화는 내부 구현을 숨기고 필요한 행동만 외부에 노출하는 것입니다.이 관점에서 위의 getPieceInPosition 가 문제라는 것은 확실히 알았습니다. 하지만 지금 사이클에서 getPieceInPosition 없이 할 수 있을지 생각해보면 막막하기만 합니다 😭

Copy link
Copy Markdown
Author

@mvg01 mvg01 Apr 3, 2026

Choose a reason for hiding this comment

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

캡슐화란 무엇일까요?
Piece 가 Board 를 의존하면 무조건 캡슐화가 붕괴되는걸까요? 그럼 Board 가 Piece 를 의존하는것도 캡슐화가 깨졌다고 볼 수 있을까요? 😳

  • Piece와 Board가 서로 의존한다고 해도 캡슐화가 깨지는 것은 아니다 라는 것 까지는 어느정도 이해가 됩니다.
  • 지금은 Board가 Piece를 의존하고 있고, Piece는 BoardMediator를 의존하고 있는 관계라 순환 참조는 어떻게든 피한 상황은 만들었다고 생각하는데, 어떻게 캡슐화를 깨지 않을 수 있을지 여전히 모르겠습니다.

Board 를 의존하게되면 SRP 관점에서 위배되는 부분은 어떤 점인가요?
BoardMediator 를 의존하기만하면 SRP 는 해결되는건가요?
Movement 클래스가 변경되어야하는 이유가 몇가지 있는지 세어보는것도 좋겠어요 :)

  • Board는 의존할 수 밖에 없다고 생각됩니다. 의존과 SRP 두가지를 지킬 수 있는 건지 감이 안옵니다.
  • SRP가 해결되었다고 생각들지 않습니다.
  • movement와 board는 변경되어야할 이유가 상당히 많다는 것은 인지했습니다.

이제 여기서 뭘 더 건드려야될지 모르겠습니다.. 😢

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Movement쪽에선 Piece의 의존은 제거하였습니다. 하지만 아직 Controller에는 TDA가 지켜지지 않고 있습니다. 여전히 여기는 어떻게 해야할지 감이 잘 안잡힙니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package janggi.domain.board;

import janggi.domain.Position;
import janggi.domain.piece.Piece;
import janggi.domain.team.TeamType;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class Board implements BoardMediator {

private final Map<Position, Piece> positionPieceMap;

public Board(final Map<Position, Piece> positionPieceMap) {
this.positionPieceMap = new LinkedHashMap<>(positionPieceMap);
}

public void movePiece(final Position from, final Position to) {
positionPieceMap.put(to, positionPieceMap.remove(from));
}

public List<Position> calculateMovablePositions(Position from) {
return findPieceByPosition(from).calculateMovablePositions(from, this);
}

@Override
public boolean hasPieceAt(final Position position) {
return hasPieceIn(position);
}

@Override
public boolean hasGeneral(TeamType teamType) {
return positionPieceMap.values().stream()
.anyMatch(piece -> piece.isGeneral() && piece.isSameTeamType(teamType));
}

@Override
public boolean isCannon(Position position) {
return findPieceByPosition(position).isCannon();
}

@Override
public boolean isSameTeamType(Position position, TeamType teamType) {
return findPieceByPosition(position).isSameTeamType(teamType);
}

public Map<Position, Piece> getPositionPieceMapForDTO() {
return Map.copyOf(positionPieceMap);
}

private boolean hasPieceIn(final Position position) {
return positionPieceMap.containsKey(position);
}

private Piece findPieceByPosition(final Position position) {
if (hasPieceIn(position)) {
return positionPieceMap.get(position);
}
throw new IllegalArgumentException("요청된 위치에는 기물이 존재하지 않습니다.");
}
}
20 changes: 20 additions & 0 deletions src/main/java/janggi/domain/board/BoardGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package janggi.domain.board;

import janggi.domain.Position;
import janggi.domain.piece.Piece;
import janggi.domain.team.Team;
import java.util.LinkedHashMap;
import java.util.Map;

public final class BoardGenerator {

private BoardGenerator() {
}

public static Board generate(final Team redTeam, final Team blueTeam) {
final Map<Position, Piece> positionPieceMap = new LinkedHashMap<>();
positionPieceMap.putAll(redTeam.generatePieces());
positionPieceMap.putAll(blueTeam.generatePieces());
return new Board(positionPieceMap);
}
}
14 changes: 14 additions & 0 deletions src/main/java/janggi/domain/board/BoardMediator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package janggi.domain.board;

import janggi.domain.Position;
import janggi.domain.team.TeamType;

public interface BoardMediator {
boolean hasPieceAt(Position position);

boolean hasGeneral(TeamType teamType);

boolean isCannon(Position position);

boolean isSameTeamType(Position position, TeamType teamType);
}
30 changes: 30 additions & 0 deletions src/main/java/janggi/domain/board/setup/ElephantFormation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package janggi.domain.board.setup;

import janggi.domain.Position;
import janggi.domain.piece.PieceType;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

public abstract class ElephantFormation {
static final Map<Position, PieceType> COMMON_BOARD_MAP;

static {
Map<Position, PieceType> boardMap = new LinkedHashMap<>();
boardMap.put(Position.valueOf(1, 1), PieceType.CHARIOT);
boardMap.put(Position.valueOf(1, 4), PieceType.GUARD);
boardMap.put(Position.valueOf(1, 6), PieceType.GUARD);
boardMap.put(Position.valueOf(1, 9), PieceType.CHARIOT);
boardMap.put(Position.valueOf(2, 5), PieceType.GENERAL);
boardMap.put(Position.valueOf(3, 2), PieceType.CANNON);
boardMap.put(Position.valueOf(3, 8), PieceType.CANNON);
boardMap.put(Position.valueOf(4, 1), PieceType.SOLDIER);
boardMap.put(Position.valueOf(4, 3), PieceType.SOLDIER);
boardMap.put(Position.valueOf(4, 5), PieceType.SOLDIER);
boardMap.put(Position.valueOf(4, 7), PieceType.SOLDIER);
boardMap.put(Position.valueOf(4, 9), PieceType.SOLDIER);
COMMON_BOARD_MAP = Collections.unmodifiableMap(boardMap);
}

public abstract Map<Position, PieceType> offerBoardMap();
}
Loading