[🚀 사이클1 - 미션 (보드 초기화 + 기물 이동)] 나무 미션 제출합니다.#264
[🚀 사이클1 - 미션 (보드 초기화 + 기물 이동)] 나무 미션 제출합니다.#264symflee wants to merge 33 commits intowoowacourse:symfleefrom
Conversation
Add core domain models and IO layer for board initialization phase. - Add domain models: Board, JanggiGame, Piece, PieceType, Pieces, Position, Turn - Add value objects: Arrangement, Col, Row, Team - Add state: GameState - Add IO layer: GameConsole, InputView, OutputView - Add Application entry point - Update README with implemented feature checklis
…t setup Validate empty input in InputView and invalid range in Arrangement. Introduce retryUntilSuccess in GameConsole to re-prompt on IllegalArgumentException.
Add PositionLayout for placing fixed/variable pieces by team, add Arrangements VO to hold both teams' setup choices, and wire setupBoard in JanggiGame via Board.of().
…Layout Implement Board.of(), Pieces.of(), and PositionLayout.build() to place fixed and variable pieces for both HAN and CHO teams.
…dency fix Move PieceType to domain.vo to resolve circular dependency between domain.game and domain.vo. Add display names to PieceType/Team, and implement printBoard in OutputView to render the board state.
…validation Implement Coordinate VO to parse and validate 'A8 a7' format input, add Col.toCol/Row.toRow converters, and wire move input loop in GameConsole.
- Add Movement interface with candidatePaths per piece type - Implement ElephantMovement, HorseMovement, SoldierMovement, GeneralMovement, GuardMovement - Add MovementFactory, MovementValidator for move legality checks - Add Board.move and JanggiGame.move to apply validated moves - Add Position.shift/canShift, Col.shift/canShift, Row.shift/canShift - Add Pieces.move to return new immutable board state - Refactor ElephantMovement: replace *2 trick with explicit step-by-step validation - Fix PositionLayout General position and Arrangement order - Update GameConsole to execute move within game loop
Add Team.getEnemy and Turn.changeTeam to alternate between HAN/CHO, call JanggiGame.nextTurn in GameConsole after each successful move.
Add Piece.isOwnedBy to check team ownership via enum ==, reject moves on opponent pieces with IllegalArgumentException.
- ColDelta, RowDelta, Delta record로 이동 방향 원시값 포장 - Position.canShift/shift가 Delta를 받도록 변경 - 모든 Movement 클래스의 int[][] → List<Delta> 변환 - HorseMovement, ElephantMovement는 HorseMove/ElephantMove inner record 도입 - List<Path> → Paths 일급 컬렉션 도입 - Movement 인터페이스 반환 타입 Paths로 변경 - PlayingState 예시 코드 추가
- Team: getTeamName→display, getPrefix 삭제 후 displayPiece/colorCode 추가, getEnemy→enemy - PieceType: getDisplayName→display - Row: getValue→display - Arrangement: getInnerPieces→innerPieces - Arrangements: getHan/getCho 삭제 후 arrangeFor(Team)으로 통합 - Piece: getTeam 삭제, isSameTeamAs/colorCode 추가 - Turn: getTeam 삭제, colorCode/belongsTo 추가 - Coordinate: getStart→from, getEnd→to - Command: getValue 삭제 후 toArrangement/toCoordinate 추가 - Board: hasFriend/hasEnemy(Team) → hasFriendOf/hasEnemyOf(Piece) 로 변경 - MovementFactory: getTeam 대신 isOwnedBy로 병사 이동 분기 - OutputView: 삼항 연산자 제거, colorCode 위임
- StepPieceMovement: for+while 중첩 제거, addLinePath/buildLine 분리 - HorseMovement, ElephantMovement: for+if 중첩 제거, addMovePaths 분리 - OutputView: 이중 for 제거, appendHeader/appendRows/appendRow/appendCell/cellDisplay 분리, else 제거 - PositionLayout: build 메서드 분리, 약어 i → index 변경
- Board: move()에서 coordinate.from/to 중간 변수로 분리 - PlayingState: display()에서 game.getBoard/getTurn 중간 변수로 분리 - MovementValidator: 체이닝 호출 중간 변수 분리, isNonCannonAt/isCannonAt 헬퍼 추출, pos/p → position/piece 축약 제거
- domain.board: Board, Pieces, Position, PositionLayout, Col, Row 통합 - domain.piece: Piece를 game에서 이동, PieceType/Team을 vo에서 이동 - domain.setup: Command를 command에서 이동, Coordinate를 vo에서 이동 - domain.movement: Delta/ColDelta/RowDelta를 vo에서 이동 - domain.game: JanggiGame, Turn만 유지 - domain.vo, domain.command 패키지 제거
pci2676
left a comment
There was a problem hiding this comment.
질문에 대한 답변을 남겨두었으니 확인해주세요.
그리고 몇가지 개선해보면 좋을 부분에 리뷰를 남겨두었으니 함께 확인해주세요.
| public class Pieces { | ||
| private final Map<Position, Piece> pieces; | ||
|
|
||
| public Pieces(Map<Position, Piece> pieces) { |
There was a problem hiding this comment.
넵, 방어적 복사가 반드시 필요한 곳이라고 생각이 되는데 누락이 되어있었네요.
Key와 Value는 불변이므로, new HashMap으로 얕은 복사를 수행해 방어적 복사를 적용하였습니다.
src/main/java/domain/board/Col.java
Outdated
| return String.valueOf(character); | ||
| } catch (IllegalArgumentException exception) { | ||
| throw new IllegalArgumentException("[ERROR] 좌표 형식이 틀렸습니다."); | ||
| } |
There was a problem hiding this comment.
String.valueOf(char)이 IllegalArgumentException을 발생시키나요?
There was a problem hiding this comment.
String.valueOf(char)은 IllegalArgumentException을 발생시키지 않는데, try-catch가 잘못 사용되고 있었습니다. 현재 구조에서는 Coordinate에서 이미 좌표 문자열 형식을 검증하고 있어, Column 쪽의 추가 예외 처리는 실질적으로 의미가 없다고 판단했습니다.
해당 메서드는 제거하고, Column은 전달받은 문자를 enum으로 변환하는 흐름만 남기도록 수정하였습니다.
| return isValidForType(piece, candidatePaths, to); | ||
| } | ||
|
|
||
| private boolean isValidForType(Piece piece, Paths candidatePaths, Position to) { |
There was a problem hiding this comment.
isValidForType 메서드가 Piece의 타입을 if 체인으로 분기하고 있는데요, 기물 종류가 추가될 때마다 이 메서드를 수정해야 하는 구조가 됩니다. 각 기물의 검증 로직이 기물 자체 혹은 Movement 쪽에 위임될 수 있는 방법은 없을까요?
| return pieceType; | ||
| } | ||
|
|
||
| public boolean isChariot() { |
There was a problem hiding this comment.
isChariot(), isCannon(), isStepPiece(), isSoldier() 같은 타입 확인 메서드가 여러 개 있는데요, 이 메서드들이 Piece 외부(특히 MovementValidator)에서 분기 조건으로 사용되고 있습니다. 객체에게 "너 뭐야?"라고 물어보고 외부에서 판단하는 것보다, 객체에게 "이거 해줘"라고 메시지를 보내는 방식은 어떨까요? 이 부분이 위의 MovementValidator 코멘트와도 연결되는 지점입니다.
| } | ||
|
|
||
| public static Movement create(Piece piece) { | ||
| if (piece.isSoldier()) { |
There was a problem hiding this comment.
SOLDIER만 별도 분기로 처리하고 있는데요, MOVEMENT_SUPPLIERS 맵에 SOLDIER도 등록할 수 있는 방법은 없을까요? 예를 들어 Piece가 팀 정보를 갖고 있으니, Supplier 대신 Function<Piece, Movement> 같은 형태로 통일하는건 어떻게 생각하시나요?
There was a problem hiding this comment.
말씀대로 Soldier만 별도로 처리되고 있는 부분이 좋지 못하다고 느꼈었는데, Function을 이용할 생각을 못했었네요. 반영하였습니다!
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| public class StepPieceMovement implements Movement { |
There was a problem hiding this comment.
클래스 이름이 StepPieceMovement인데, 실제로는 차(Chariot)와 포(Cannon)의 직선 이동 경로를 생성하는 역할을 하고 있어요. "StepPiece"라는 이름에서 이 역할이 잘 드러나는지 한번 생각해보시면 좋겠습니다. Piece.isStepPiece()는 마(Horse)와 상(Elephant)을 가리키고 있어서 무엇을 의미하는지 헷갈리네요.
There was a problem hiding this comment.
우선 모호한 네이밍을 변경하여 통일성을 갖추도록 수정하였습니다.
| import java.util.Map; | ||
|
|
||
| public class PositionLayout { | ||
| private static final Map<Position, PieceType> hanPiecesLayout = Map.ofEntries( |
There was a problem hiding this comment.
Java 네이밍 컨벤션에서 static final 필드는 UPPER_SNAKE_CASE를 사용하는데요, hanPiecesLayout이 소문자 카멜케이스로 되어 있네요. HAN_PIECES_LAYOUT 같은 형태가 더 자연스러울 것 같습니다.
| public Path(List<Position> positions) { | ||
| if (positions == null || positions.isEmpty()) { | ||
| throw new IllegalArgumentException("[ERROR] 경로는 적어도 하나 이상의 포지션을 가져야 합니다."); | ||
| } |
src/main/java/domain/board/Col.java
Outdated
| @@ -0,0 +1,35 @@ | |||
| package domain.board; | |||
|
|
|||
| public enum Col { | |||
| for (Path path : candidatePaths.asList()) { | ||
| if (isChariotPathValid(piece, path, to)) { | ||
| return true; |
symflee
left a comment
There was a problem hiding this comment.
비밥, 안녕하세요.
지난 번 피드백 주신 부분들을 반영하여 다시 리뷰 요청을 드립니다.
감사합니다!
| public class Pieces { | ||
| private final Map<Position, Piece> pieces; | ||
|
|
||
| public Pieces(Map<Position, Piece> pieces) { |
There was a problem hiding this comment.
넵, 방어적 복사가 반드시 필요한 곳이라고 생각이 되는데 누락이 되어있었네요.
Key와 Value는 불변이므로, new HashMap으로 얕은 복사를 수행해 방어적 복사를 적용하였습니다.
| import java.util.Map; | ||
|
|
||
| public class PositionLayout { | ||
| private static final Map<Position, PieceType> hanPiecesLayout = Map.ofEntries( |
| } | ||
|
|
||
| public void displayRequestCommand(OutputView outputView) { | ||
| gameState.display(this, outputView); |
There was a problem hiding this comment.
불변성이라는 말에 현혹되어 읽기 쉬운 코드를 작성하지 못하는건 아무래도 실용적이지 못해서요. 프로그래머는 예술가가 아닌 엔지니어니까요.
말씀하신 대로 실용성과 가독성 사이에서 고민이 많았고, 이러한 고민조차도 옳은 방향일까 의문이 많았었습니다. 덕분에 방향을 잡는 데 큰 도움이 되었습니다. 감사합니다
| } | ||
|
|
||
| public static Movement create(Piece piece) { | ||
| if (piece.isSoldier()) { |
There was a problem hiding this comment.
말씀대로 Soldier만 별도로 처리되고 있는 부분이 좋지 못하다고 느꼈었는데, Function을 이용할 생각을 못했었네요. 반영하였습니다!
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| public class StepPieceMovement implements Movement { |
There was a problem hiding this comment.
우선 모호한 네이밍을 변경하여 통일성을 갖추도록 수정하였습니다.
| } | ||
|
|
||
| public Arrangements assignArrangement(Team team, Arrangement arrangement) { | ||
| arrangements.put(team, arrangement); |
src/main/java/io/GameConsole.java
Outdated
| private <T> T retryUntilSuccess(Supplier<T> action) { | ||
| try { | ||
| return action.get(); | ||
| } catch (IllegalArgumentException e) { |
There was a problem hiding this comment.
말씀하신 점이 맞습니다. 솔직하게는 Indent 1 제약을 맞추려다가 지금 수정하여 적용한 구조가 떠오르지 않았고, stackOverFlow 발생 가능성이 낮을 것이라 생각하고 재귀로 안일하게 타협하였습니다.
기존의 Supplier를 사용하던 것을 Runnable을 사용하도록 변경해 재귀를 제거하고, Indent 1을 유지하도록 수정했습니다
src/main/java/domain/board/Col.java
Outdated
| @@ -0,0 +1,35 @@ | |||
| package domain.board; | |||
|
|
|||
| public enum Col { | |||
src/main/java/domain/board/Col.java
Outdated
| return String.valueOf(character); | ||
| } catch (IllegalArgumentException exception) { | ||
| throw new IllegalArgumentException("[ERROR] 좌표 형식이 틀렸습니다."); | ||
| } |
There was a problem hiding this comment.
String.valueOf(char)은 IllegalArgumentException을 발생시키지 않는데, try-catch가 잘못 사용되고 있었습니다. 현재 구조에서는 Coordinate에서 이미 좌표 문자열 형식을 검증하고 있어, Column 쪽의 추가 예외 처리는 실질적으로 의미가 없다고 판단했습니다.
해당 메서드는 제거하고, Column은 전달받은 문자를 enum으로 변환하는 흐름만 남기도록 수정하였습니다.
| } | ||
|
|
||
| public void displayRequestCommand(OutputView outputView) { | ||
| gameState.display(this, outputView); |
There was a problem hiding this comment.
다만
JanggiGame이displayRequestCommand(OutputView)처럼 출력 로직을 직접 호출하는 부분은 도메인 객체가 View에 의존하게 만드는데요, 불변성과는 별개로 도메인과 UI의 경계에 대해서도 한번 생각해보시면 좋겠습니다.JanggiGame이 상태를 반환하고, 출력은 외부에서 처리하는 구조는 어떨까요?
State에 따라 달라지는 OutputView 호출을 Console에서 결정하도록 수정해보았습니다.
이에 따라 JanggiGame이 OutputView를 직접 호출하던 의존은 제거했습니다. 다만 현재는 Console에서 상태에 따라 if 분기를 두고 있는데, 이 부분은 아직 아쉬움이 남는다고 느껴집니다.
pci2676
left a comment
There was a problem hiding this comment.
조금 더 개선하거나 생각해보면 좋을 부분에 리뷰를 남겨두었으니 확인해주세요.
| public String display() { | ||
| return team.displayPiece(pieceType); | ||
| } |
There was a problem hiding this comment.
도메인이 view를 위한 로직을 구현하고 있네요. 의존성 방향이 역방향이어야 할것 같지 않나요?
다른 부분도 동일한 부분이 많이 보이니 함께 개선해주세요
| public boolean isCannon() { | ||
| return pieceType == PieceType.CANNON; | ||
| } | ||
|
|
||
| public boolean isHorseOrElephant() { | ||
| return pieceType == PieceType.HORSE || pieceType == PieceType.ELEPHANT; | ||
| } | ||
|
|
||
| public boolean isSoldier() { | ||
| return pieceType == PieceType.SOLDIER; | ||
| } |
There was a problem hiding this comment.
하위타입을 구분하는 메서드가 상위 타입에 위치해서 추상화 관점에서 아쉬움이 남는데요.
- 정말 이 메서드가 필요한지 확인하고 구조를 변경한다.
- 타입을 묻는 메서드가 아닌 책임을 묻는 이름으로 변경한다.
두가지 방법이 있어보입니다. 가능하면 구조, 설계를 변경해보시면 좋을것 같네요.
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| public final class MovementValidator { |
There was a problem hiding this comment.
이 클래스 너무 큽니다. SRP와 OCP관점에서 위배되는 사항도 많이 보이기도 하네요.
각 기물의 움직임을 판단하는것도 기물에게 스스로 하도록 할 순 없을까요?
isXXX와 같은 타입을 묻는 부분이 많이 제거될 수 도 있을것 같아요.
| public class Coordinate { | ||
| private static final Pattern POSITION_PATTERN = Pattern.compile("^[a-iA-I][0-9] [a-iA-I][0-9]$"); |
There was a problem hiding this comment.
한번 고민해보시고 다른 부분도 함께 확인해보시면 좋을것 같은데요.
도메인 객체들이 사용하는 값, 메서드가 특정 view에 의존적으로 설계되지 않았는지 고민해보시면 좋을것 같습니다.
view의 형태가 달라져서 console이 아닌 다른 방식으로 입력을 받아도 변경하지않고 자연스럽게 사용가능한지 검토해보시면 좋을것 같아요.
지금 상태로도 충분히 괜찮을것 같다. 의존적이지 않다 라고 생각하신다면 꼭 수정하실 필요는 없습니다.
| .orElse(false); | ||
| } | ||
|
|
||
| private void validateMove(Coordinate coordinate, Turn turn) { |
There was a problem hiding this comment.
private 메서드가 꽤 크네요.
Board가 갖고 있기에 조금 무거운 책임이지 않나 싶어보여요
질문입니다. Board의 역할과 책임은 어떻게 / 어느 범위로 설계하셨나요?
| return paths; | ||
| } | ||
|
|
||
| private Paths addPathIfReachable(Paths paths, Position from, Delta delta) { |
|
|
||
| public boolean canShift(int delta) { | ||
| int next = this.ordinal() + delta; | ||
| return next >= 0 && next < Column.values().length; |
| private static Movement soldierMovementFor(Piece piece) { | ||
| if (piece.isOwnedBy(Team.HAN)) { | ||
| return new SoldierMovement(Team.HAN); | ||
| } | ||
| return new SoldierMovement(Team.CHO); | ||
| } |
| } | ||
|
|
||
| @Override | ||
| public Paths candidatePaths(Position from) { |
There was a problem hiding this comment.
메서드 네이밍에 동사가 없는것 같은데 혹시 네이밍은 어떤 원칙으로 하셨나요?
| Movement movement = MovementFactory.create(piece); | ||
| Paths paths = movement.candidatePaths(from); | ||
|
|
||
| MovementValidator validator = new MovementValidator(this); |
비밥, 안녕하세요.
리뷰를 잘 부탁드리겠습니다.
체크 리스트
test를 실행했을 때, 모든 테스트가 정상적으로 통과했나요?어떤 부분에 집중하여 리뷰해야 할까요?
가능한 한 장기 게임의 도메인 객체를 불변에 가깝게 유지하고 싶었습니다.
예를 들어
Board나Turn은 변경 대신 새 객체를 반환하도록 구성하는 방향을 고민했습니다.그런데 결국 이 객체들을 들고 있는
JanggiGame은this.board = ...,this.turn = ...처럼 참조를 변경하게 되므로, 여전히 가변입니다.그래서 “어디까지를 불변으로 보는 것이 합당한가”가 가장 고민되었습니다.
제 생각에는 내부 도메인을 불변으로 두면 이전 상태를 보존하기 쉽고, 변경 영향을 좁힐 수 있다는 장점이 있습니다.
다만 상위 객체가 계속 참조를 교체한다면, 불변 설계를 어디까지 유지하는 것이 의미 있는지 확신이 서지 않았습니다.
이 부분에서 궁금한 점은 다음과 같습니다.