이 커밋의 핵심은 Cell 클래스가 자신의 상태를 직접 관리하도록 리팩토링한 것입니다.
Cell은 단순히 표시할 문자열(sign)만 저장하는 값 객체였습니다:
// Cell은 그냥 "표시 문자"를 감싸는 껍데기
private final String sign;
// 게임 상태는 별도의 2차원 배열들이 관리
private static final Integer[][] NEARBY_LAND_MINE_COUNTS = ...;
private static final boolean[][] LAND_MINES = ...;Cell.ofFlag(),Cell.ofLandMine()등은 문자열만 다른 Cell을 생성- 지뢰 여부, 주변 지뢰 수는 별도 배열(
LAND_MINES,NEARBY_LAND_MINE_COUNTS)이 관리 - 게임 로직이
MinesweeperGame에 집중됨
Cell이 자신의 모든 상태를 직접 보유:
private int nearByLandMineCount; // 주변 지뢰 수
private boolean isLandMine; // 지뢰 여부
private boolean isFlagged; // 깃발 표시 여부
private boolean isOpened; // 열렸는지 여부| 항목 | Before | After |
|---|---|---|
| Cell 생성 | Cell.ofClosed() (문자열 "□") |
Cell.create() (상태 초기화) |
| 지뢰 표시 | LAND_MINES[row][col] = true |
cell.turnOnLandMine() |
| 주변 지뢰 수 | NEARBY_LAND_MINE_COUNTS[row][col] = count |
cell.updateNearByLandMineCount(count) |
| 지뢰 확인 | LAND_MINES[row][col] 조회 |
cell.isLandMine() |
| 셀 열기 | BOARD[row][col] = Cell.ofOpened() |
cell.opened() |
| 깃발 꽂기 | BOARD[row][col] = Cell.ofFlag() |
cell.flag() |
| 표시 문자 | 생성 시 결정됨 | getSign()이 상태 기반으로 동적 결정 |
Before: 생성 시점에 문자열이 결정됨
After: 현재 상태를 보고 동적으로 결정
public String getSign() {
if (isOpened) {
if (isLandMine) return "☼";
if (hasLandMineCount()) return String.valueOf(nearByLandMineCount);
return "■"; // 빈 칸
}
if (isFlagged) return "⚑";
return "□"; // 미확인 셀
}- 정보 은닉(Encapsulation): 지뢰/주변 지뢰 수 등의 정보가 Cell 내부로 캡슐화
- 별도 배열 제거:
LAND_MINES,NEARBY_LAND_MINE_COUNTS배열 삭제 →BOARD하나로 통합 - 객체에게 메시지 보내기:
cell.flag(),cell.opened()처럼 객체에게 행동을 요청 - Tell, Don't Ask: 상태를 꺼내서 판단하지 않고, 객체가 스스로 판단 (
cell.isChecked())
이 커밋의 핵심은 Cell이 "어떻게 그릴지"를 결정하지 않고, 출력 담당 객체(ConsoleOutputHandler)가 결정하도록 책임을 분리한 것입니다.
// Cell 인터페이스에 출력용 상수가 존재
public interface Cell {
String FLAG_SIGN = "⚑";
String UNCHECKED_SIGN = "□";
String getSign(); // Cell이 직접 문자열 반환
}
// 각 Cell 구현체가 어떤 기호로 보여줄지 결정
public class LandMineCell implements Cell {
private static final String LAND_MINE_SIGN = "☼";
@Override
public String getSign() {
if (cellState.isOpened()) return LAND_MINE_SIGN;
if (cellState.isFlagged()) return FLAG_SIGN;
return UNCHECKED_SIGN;
}
}문제점: 도메인 객체(Cell)가 UI 표현 방식(콘솔 기호)을 알고 있음
public enum CellSnapshotStatus {
EMPTY("빈 셀"),
FLAG("깃발"),
LAND_MINE("지뢰"),
NUMBER("숫자"),
UNCHECKED("확인 전");
}public class CellSnapshot {
private final CellSnapshotStatus status;
private final int nearByLandMineCount;
public static CellSnapshot ofEmpty() { ... }
public static CellSnapshot ofFlag() { ... }
public static CellSnapshot ofLandMine() { ... }
public static CellSnapshot ofNumber(int count) { ... }
public static CellSnapshot ofUnchecked() { ... }
}public interface Cell {
CellSnapshot getSnapshot(); // 문자열 대신 상태 객체 반환
}
public class LandMineCell implements Cell {
@Override
public CellSnapshot getSnapshot() {
if (cellState.isOpened()) return CellSnapshot.ofLandMine();
if (cellState.isFlagged()) return CellSnapshot.ofFlag();
return CellSnapshot.ofUnchecked();
}
}public class ConsoleOutputHandler implements OutputHandler {
private static final String LAND_MINE_SIGN = "☼";
private static final String EMPTY_SIGN = "■";
private static final String FLAG_SIGN = "⚑";
private static final String UNCHECKED_SIGN = "□";
private String decideCellSignFrom(CellSnapshot snapshot) {
CellSnapshotStatus status = snapshot.getStatus();
if (status == CellSnapshotStatus.EMPTY) return EMPTY_SIGN;
if (status == CellSnapshotStatus.FLAG) return FLAG_SIGN;
if (status == CellSnapshotStatus.NUMBER) return String.valueOf(snapshot.getNearByLandMineCount());
if (status == CellSnapshotStatus.UNCHECKED) return UNCHECKED_SIGN;
if (status == CellSnapshotStatus.LAND_MINE) return LAND_MINE_SIGN;
throw new IllegalStateException("Unknown status: " + status);
}
}| 항목 | Before | After |
|---|---|---|
| Cell 반환값 | String getSign() |
CellSnapshot getSnapshot() |
| 출력 기호 상수 위치 | Cell 인터페이스 |
ConsoleOutputHandler |
| 기호 결정 책임 | 각 Cell 구현체 | ConsoleOutputHandler |
-
관심사의 분리 (Separation of Concerns)
- Cell: 게임 로직과 상태 관리에만 집중
- ConsoleOutputHandler: 출력 형식 결정에만 집중
-
출력 방식 교체 용이
- 콘솔 → GUI로 변경 시
Cell수정 불필요 - 새로운
GuiOutputHandler만 구현하면 됨
- 콘솔 → GUI로 변경 시
-
Enum을 통한 타입 안전성
- 문자열 대신
CellSnapshotStatusEnum 사용 - 컴파일 타임에 오류 검출 가능
- 문자열 대신
-
Snapshot 패턴
- 현재 상태의 "스냅샷"을 전달하여 불변성 확보
- Cell 내부 상태를 직접 노출하지 않음
이 커밋은 if-else 분기를 다형성으로 대체하기 위한 구조를 준비한 것입니다.
이전 커밋에서 ConsoleOutputHandler에 다음과 같은 코드가 있었습니다:
private String decideCellSignFrom(CellSnapshot snapshot) {
CellSnapshotStatus status = snapshot.getStatus();
if (status == CellSnapshotStatus.EMPTY) return EMPTY_SIGN;
if (status == CellSnapshotStatus.FLAG) return FLAG_SIGN;
if (status == CellSnapshotStatus.NUMBER) return String.valueOf(snapshot.getNearByLandMineCount());
if (status == CellSnapshotStatus.UNCHECKED) return UNCHECKED_SIGN;
if (status == CellSnapshotStatus.LAND_MINE) return LAND_MINE_SIGN;
throw new IllegalStateException("Unknown status: " + status);
}문제점: 새로운 셀 상태가 추가되면 if-else를 수정해야 함 (OCP 위반)
public interface CellSignProvidable {
String provide(CellSnapshot cellSnapshot);
}public class EmptyCellSignProvider implements CellSignProvidable {
private static final String EMPTY_SIGN = "■";
@Override
public String provide(CellSnapshot cellSnapshot) {
return EMPTY_SIGN;
}
}
public class FlagCellSignProvider implements CellSignProvidable {
private static final String FLAG_SIGN = "⚑";
@Override
public String provide(CellSnapshot cellSnapshot) {
return FLAG_SIGN;
}
}
public class LandMineCellSignProvider implements CellSignProvidable {
private static final String LAND_MINE_SIGN = "☼";
@Override
public String provide(CellSnapshot cellSnapshot) {
return LAND_MINE_SIGN;
}
}
public class NumberCellSignProvider implements CellSignProvidable {
@Override
public String provide(CellSnapshot cellSnapshot) {
return String.valueOf(cellSnapshot.getNearByLandMineCount());
}
}
public class UncheckedCellSignProvider implements CellSignProvidable {
private static final String UNCHECKED_SIGN = "□";
@Override
public String provide(CellSnapshot cellSnapshot) {
return UNCHECKED_SIGN;
}
}CellSignProvidable (인터페이스)
│
├── EmptyCellSignProvider → "■"
├── FlagCellSignProvider → "⚑"
├── LandMineCellSignProvider → "☼"
├── NumberCellSignProvider → "1"~"8"
└── UncheckedCellSignProvider → "□"
-
OCP (Open-Closed Principle)
- 새로운 셀 상태 추가 시 기존 코드 수정 없이 새 Provider만 추가
-
SRP (Single Responsibility Principle)
- 각 Provider는 하나의 상태에 대한 기호만 책임짐
-
다형성을 통한 분기 제거
- if-else 체인 → 인터페이스 호출로 대체 예정 (다음 커밋에서)
-
전략 패턴 (Strategy Pattern) 준비
- 상황에 맞는 Provider를 선택하여 사용하는 구조
깃발을 하나만 꽂았는데 여러 셀에 깃발이 동시에 꽂히는 현상
선택할 좌표를 입력하세요. (예: a1)
b1
선택한 셀에 대한 행위를 선택하세요. (1: 오픈, 2: 깃발 꽂기)
2
a b c d e
1 ⚑ ⚑ ⚑ ⚑ ⚑ ← 전부 깃발!
2 □ □ □ ⚑ ⚑
3 □ ⚑ □ ⚑ ⚑
4 □ □ □ ⚑ ⚑
private void initializeEmptyCells(CellPositions cellPositions) {
List<CellPosition> allPositions = cellPositions.getPositions();
Cell cell = new EmptyCell(); // ← 하나의 인스턴스 생성
for (CellPosition position : allPositions) {
updateCellAt(position, cell); // ← 모든 위치에 같은 객체 할당!
}
}모든 셀이 동일한 EmptyCell 객체를 참조하고 있어서, 하나의 셀에 flag()를 호출하면 모든 셀에 영향을 줌
private void initializeEmptyCells(CellPositions cellPositions) {
List<CellPosition> allPositions = cellPositions.getPositions();
for (CellPosition position : allPositions) {
updateCellAt(position, new EmptyCell()); // ← 매번 새 인스턴스!
}
}
private void initializeLandMineCells(List<CellPosition> landMinePositions) {
for (CellPosition position : landMinePositions) {
updateCellAt(position, new LandMineCell()); // ← 매번 새 인스턴스!
}
}-
가변 객체(Mutable Object) 공유 주의
- 상태를 가진 객체를 여러 곳에서 공유하면 의도치 않은 부작용 발생
-
불변 객체가 아니라면 인스턴스를 공유하지 말 것
CellState가 가변이므로Cell도 가변 → 공유하면 안 됨
-
Flyweight 패턴과의 차이
- 불변 객체라면 인스턴스 공유 가능 (Flyweight 패턴)
- 가변 객체는 반드시 별도 인스턴스 필요
이전 커밋에서 만든 CellSignProvider들을 실제로 사용하기 시작한 커밋입니다.
public class ConsoleOutputHandler implements OutputHandler {
private static final String LAND_MINE_SIGN = "☼";
private static final String EMPTY_SIGN = "■";
private static final String FLAG_SIGN = "⚑";
private static final String UNCHECKED_SIGN = "□";
private String decideCellSignFrom(CellSnapshot snapshot) {
CellSnapshotStatus status = snapshot.getStatus();
if (status == CellSnapshotStatus.EMPTY) {
return EMPTY_SIGN;
}
if (status == CellSnapshotStatus.FLAG) {
return FLAG_SIGN;
}
// ...
}
}public class ConsoleOutputHandler implements OutputHandler {
// 상수 제거됨!
private String decideCellSignFrom(CellSnapshot snapshot) {
CellSnapshotStatus status = snapshot.getStatus();
if (status == CellSnapshotStatus.EMPTY) {
CellSignProvidable cellSignProvider = new EmptyCellSignProvider();
return cellSignProvider.provide(snapshot);
}
if (status == CellSnapshotStatus.FLAG) {
CellSignProvidable cellSignProvider = new FlagCellSignProvider();
return cellSignProvider.provide(snapshot);
}
if (status == CellSnapshotStatus.LAND_MINE) {
CellSignProvidable cellSignProvider = new LandMineCellSignProvider();
return cellSignProvider.provide(snapshot);
}
if (status == CellSnapshotStatus.NUMBER) {
CellSignProvidable cellSignProvider = new NumberCellSignProvider();
return cellSignProvider.provide(snapshot);
}
if (status == CellSnapshotStatus.UNCHECKED) {
CellSignProvidable cellSignProvider = new UncheckedCellSignProvider();
return cellSignProvider.provide(snapshot);
}
throw new IllegalStateException("Unknown status: " + status);
}
}| 항목 | 상태 |
|---|---|
| 상수 분리 | ✅ 완료 (각 Provider로 이동) |
| Provider 사용 | ✅ 완료 |
| if-else 제거 | ❌ 아직 남아있음 |
if-else 분기가 여전히 존재합니다. 새로운 CellSnapshotStatus가 추가되면:
- 새 Provider 클래스 생성
decideCellSignFrom메서드에 if문 추가 ← 여전히 수정 필요!
→ 다음 커밋에서 if-else를 완전히 제거할 예정
드디어 if-else 분기를 완전히 제거하고 다형성을 완성한 커밋입니다.
각 Provider가 자신이 처리할 수 있는 상태인지 스스로 판단하도록 변경
public interface CellSignProvidable {
boolean supports(CellSnapshot cellSnapshot); // 추가!
String provide(CellSnapshot cellSnapshot);
}public class EmptyCellSignProvider implements CellSignProvidable {
@Override
public boolean supports(CellSnapshot cellSnapshot) {
return cellSnapshot.isSameStatus(CellSnapshotStatus.EMPTY);
}
@Override
public String provide(CellSnapshot cellSnapshot) {
return EMPTY_SIGN;
}
}public class CellSnapshot {
public boolean isSameStatus(CellSnapshotStatus cellSnapshotStatus) {
return this.status == cellSnapshotStatus;
}
}Before (if-else 체인):
private String decideCellSignFrom(CellSnapshot snapshot) {
CellSnapshotStatus status = snapshot.getStatus();
if (status == CellSnapshotStatus.EMPTY) {
return new EmptyCellSignProvider().provide(snapshot);
}
if (status == CellSnapshotStatus.FLAG) {
return new FlagCellSignProvider().provide(snapshot);
}
// ... 계속되는 if-else
throw new IllegalStateException("Unknown status");
}After (다형성 + Stream):
private String decideCellSignFrom(CellSnapshot snapshot) {
List<CellSignProvidable> cellSignProviders = List.of(
new EmptyCellSignProvider(),
new FlagCellSignProvider(),
new LandMineCellSignProvider(),
new NumberCellSignProvider(),
new UncheckedCellSignProvider()
);
return cellSignProviders.stream()
.filter(provider -> provider.supports(snapshot))
.findFirst()
.map(provider -> provider.provide(snapshot))
.orElseThrow(() -> new IllegalStateException("Unknown status"));
}| 단계 | Before (if-else) | After (다형성) |
|---|---|---|
| 1 | Provider 클래스 생성 | Provider 클래스 생성 |
| 2 | if문 추가 (기존 코드 수정) | List에 추가만 |
| 3 | - | 기존 로직 수정 없음! |
-
OCP (Open-Closed Principle) 완성
- 새 상태 추가 시 기존 코드 수정 없이 확장 가능
-
책임의 분산
- "내가 처리할 수 있는가?" 판단을 각 Provider가 담당
ConsoleOutputHandler는 판단하지 않고 위임만 함
-
Chain of Responsibility 패턴
- Provider 목록을 순회하며 처리 가능한 객체를 찾음
-
선언적 코드
- "어떻게(How)"가 아닌 "무엇을(What)" 중심으로 표현