Skip to content

Conversation

@LeeHwansub
Copy link

자동차 경주 게임

프로젝트 개요

SOLID 원칙과 MVC 패턴을 적용한 객체지향 자동차 경주 게임을 구현했습니다. 단일 책임 원칙을 준수하여 코드의 유지보수성과 확장성을 높이고, 깔끔한 아키텍처를 구축했습니다.

주요 성과

완료된 기능

  • 완전한 MVC 패턴 구현: Model(Car) - View(GameView) - Controller(GameController) 완전 분리
  • SOLID 원칙 준수: 각 클래스가 단일 책임만 수행
  • 포괄적인 예외 처리: 12가지 검증 케이스로 견고한 입력 검증
  • 100% 테스트 통과: 16개 테스트 케이스 모두 통과 (기능 4개 + 예외처리 12개)
  • Indent Depth 최대 2: 요구사항 완벽 준수

코드 메트릭

  • 총 라인 수: 297줄 (8개 파일로 분리)
  • App.js: 178줄 → 14줄 (92% 감소)
  • 평균 파일 크기: ~37줄
  • 테스트 커버리지: 16/16 (100%)

시스템 아키텍처

파일 구조
src/
├── constants/ # 상수 및 에러 메시지 중앙 관리
├── models/ # 도메인 모델 (Car)
├── validators/ # 검증 로직 분리
├── views/ # UI 출력 담당
├── controllers/ # 게임 흐름 제어
├── App.js # 메인 앱 (Facade 패턴)
└── index.js # 진입점

아키텍처 다이어그램

architecture-diagram

주요 리팩토링 이력

  1. 코드 분리 및 모듈화
    문제점: 모든 로직이 App.js에 집중되어 단일 책임 원칙 위배
    해결: 8개 파일로 분리하여 각 모듈의 책임 명확화

  2. 에러 메시지 세분화 및 중앙 관리
    문제점: 에러 메시지 하드코딩, 일반적인 메시지로 구체적 문제 파악 어려움
    해결: ErrorMessage.js로 중앙 관리, 구체적인 에러 메시지 제공

  3. MissionUtils API 오류 수정
    문제: readLineAsync() 인자 중복 전달로 인한 에러
    해결: 입력 처리 로직 개선

  4. 랜덤값 범위 오류 수정
    문제: 요구사항(09 중 4 이상)과 다른 구현(49만 선택)
    해결: 올바른 랜덤값 범위 적용

  5. 검증 로직 통합 및 변수명 개선
    문제: 검증 로직 산재, 애매한 변수명
    해결: Validator 클래스로 통합, 명확한 변수명 적용

테스트 결과

기능 테스트 (4개)

  • 단일 라운드에서 단독 우승자 결정
  • 여러 라운드 경주 진행
  • 공동 우승자 결정
  • 3대의 자동차가 다른 거리 이동

예외 처리 테스트 (12개)

  • 자동차 이름 검증 (5개): 5자 초과, 빈 문자열, 공백 포함, 앞뒤 공백, 빈 이름 포함
  • 이동 횟수 검증 (7개): 빈 문자열, 숫자 아님, 음수, 0, 소수, 공백 포함, 특수문자

적용된 디자인 패턴

  • Repository Pattern: ErrorMessage.js, constants.js - 데이터 중앙 관리
  • Strategy Pattern: Validator.js - 검증 전략 캡슐화
  • MVC Pattern: Model(Car) - View(GameView) - Controller(GameController)
  • Facade Pattern: App.js - 복잡한 시스템 단순화

도전과 성과

성공한 부분

  • 관심사 분리: MVC 패턴으로 각 계층의 역할 명확화
  • 예외 처리: 12가지 검증 케이스로 모든 엣지 케이스 커버
  • 코드 구조 명시대로 적용: Indent Depth 최대 2로 가독성 극대화
  • 포괄적인 테스트: BDD 패턴 적용으로 테스트 구조 명확화

학습한 점

  • SOLID 원칙의 중요성: 단일 책임 원칙이 코드 품질에 미치는 영향
  • MVC 패턴의 효과: 관심사 분리로 인한 유지보수성 향상
  • 예외 처리의 세분화: 구체적인 에러 메시지가 사용자 경험에 미치는 영향
  • 테스트 주도 개발: Given-When-Then 패턴으로 테스트 가독성 향상

코드 품질 개선

  • 가독성: 명확한 변수명과 JSDoc 주석으로 코드 이해도 향상
  • 유지보수성: 모듈화된 구조로 변경 영향도 최소화
  • 확장성: 새로운 기능 추가 시 기존 코드 수정 최소화
  • 안정성: 포괄적인 예외 처리로 런타임 에러 방지

- constants.js: 게임 설정 상수 정의
- ErrorMessage.js: 에러 메시지 중앙 관리
- Car 클래스: 이름과 위치 정보 관리
- move(): 0-9 랜덤값으로 전진 판단
- shouldAdvance(): 전진 여부 결정 로직
- Validator 클래스: 입력 검증 담당
- validateCarNames(): 자동차 이름 검증 (길이, 공백 체크)
- validateMovementCount(): 시도 횟수 검증 (숫자, 범위 체크)
- GameView: 모든 메서드를 static으로 구현하여 상태 없는 디자인
- 사용자 입력 처리 및 게임 결과 출력
- printCarStatus(), printRoundResult(), printWinners() 구현
- GameController: 전체 게임 흐름 관리 및 조율
- run(): 게임 실행 전체 프로세스 통합
- 입력 처리, 게임 진행, 우승자 판정 기능
- Controller에 전체 로직 위임하여 App은 진입점 역할만 수행
- Clean Code 원칙에 따라 단순하고 명확한 구조로 개선
- 프로젝트 구조 및 아키텍처 설명
- MVC 패턴 기반 설계 원칙 문서화
- 기능 요구사항 및 구현 내용 정리
- POSIX 규칙 준수 위해 EOF 개행 추가
- 게임 실행 흐름을 보여주는 시퀀스 다이어그램 추가
- 계층 구조를 보여주는 아키텍처 다이어그램 추가
- GitHub 표시를 위한 PNG 이미지 생성
- 자동차 이름 관련 에러 메시지 4가지로 세분화
- 이동 횟수 관련 에러 메시지 6가지로 세분화
- 구체적인 에러 원인 파악 가능하도록 개선
- 사용자에게 명확한 피드백 제공
- 세분화된 에러 메시지 적용
- 변수명을 더 직관적이고 이해하기 쉽게 변경
- JSDoc 주석 추가로 코드 가독성 향상
- 사용하지 않는 메서드 제거
- 기능 테스트 케이스 상세화 (단일/다중 라운드, 다중 우승자)
- 예외 테스트 케이스 확장 (자동차 이름, 이동 횟수)
- Given-When-Then 패턴으로 주석 구조화
- 한글 주석으로 테스트 의도 명확화
- 시스템 아키텍처 다이어그램 이미지로 교체
- 코드 메트릭 업데이트 (297줄, 16개 테스트 통과)
- 에러 메시지 세분화 내용 추가
- 검증 로직 개선 및 변수명 직관화 내용 추가
- 테스트 결과 상세화 (기능/예외 테스트, BDD 패턴 적용)
- 사용하지 않는 상수 제거
- 코드 정리 및 최적화
- Car 모델에 고유 ID 시스템 추가 (idCounter)
- 중복 이름이 있을 때 ID로 구분하여 표시 (예: pobi#1, pobi#2)
- GameView에서 중복 이름 구분 출력 구현
- 중복 이름 처리 테스트 추가
- 모든 기존 테스트 통과 확인
Copy link

@manNomi manNomi left a comment

Choose a reason for hiding this comment

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

MVC 패턴을 깔끔하게 쓰셔서 코드를 읽는데 시간이 많이 단축되었습니다
제코드는 항상 잘못 작성된 패턴이라 제가 읽기에도 벅찼는데 작성자분 코드는
패턴 덕분에 읽기 좋았습니다 좋은코드 감사합니다!

async run() {
const controller = new GameController();
await controller.run();
}
Copy link

Choose a reason for hiding this comment

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

저 또한 고민했던 것인데
저는 App.js 자체가 작은 컨트롤러라고 생각했었습니다

MVC를 적용할때 콘트롤러가 대부분의 요구사항에는 하나 뿐이고요
App.js 에 코드가 두줄밖에 안올라가다보니 굳이 콘트롤러를 분리해야하나 고민이 되더라고요

실제로 저도 작성자분과 동일하게 분리하기도 했었고요 그래서 작성자분은 이에대해서 어떻게 생각하시는지 궁금합니다

Copy link
Author

Choose a reason for hiding this comment

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

저도 그 부분이 헷갈려서 처음에는 App.js에 모든 컨트롤러의 내용의 코드를 넣었다가,
지난 피드백에 App.js가 무거운 느낌이라는 피드백을 받아, 따로 컨트롤러를 생성해 전체 게임 흐름을 시작하는 오케스트레이션 역할을 진행 했습니다!

지금처럼의 작은 프로그램 같은 경우는 상관없지만, 나중에 확장성이랑 유지보수 측면을 생각하면 App.js를 조금 가볍게 진행했습니다!

// 자동차 이름 입력 및 검증
const carNamesInput = await this.getCarNamesInput();
const carNames = this.parseCarNames(carNamesInput);
Validator.validateCarNames(carNames);
Copy link

Choose a reason for hiding this comment

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

Static 선언 validator 좋은데요!!
혹시 validatror를 static 선언하는것과 객체로 선언하는것의 차이가 있다고 생각하시나요?
또는 함수로 선언할때도요
모두 필드를 사용하거나 하는 함수가 아니라서 클래스로 분리하는 이유가 있을까 고민이 되더라고요

그리고 class로 분리했을때 Validator.validate 가 겹치는 느낌이라
Validator.carNames와같이 접근가능하도록 하는것은 어떨까요 ? 의미상 빠르게 읽히더라고요

Copy link

Choose a reason for hiding this comment

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

그리고 저도 고민했던 내용인데
Validator에 모든 validate를 묶는것이 옳을까였습니다.
자동차 이름의 제한은 Car 객체의 책임이지 않나 싶기도 하더라고요
작성자분 처럼 저도 하나에 모아두었었는데 고민이 되었던것 같습니다
작성자분은 어떤 이유로 하나로 묶으신건가요 ??

Copy link
Author

Choose a reason for hiding this comment

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

Static 선언의 차이
->static으로 선언한것은 객체의 입력에만 의존하고 내부 상태를 보관하지 않고, 인스턴스 필드가 없으므로 객체 생성이 불필요하다는 판단하에 진행하였고, 사용 단순화를 통해서 진행했습니다!

class로 분리했을때 Validator.validate 가 겹치는 느낌이라
Validator.carNames와같이 접근가능하도록 하는것은 어떨까요 ?
->이부분은 생각해본적 없는데 이런 방법도 있었군요! 감사합니다! :)

Validator에 모든 validate를 묶는것이 옳을까?
-> 이 부분은 확장성과 유지보수성, 책임성을 염두에 두었습니다! car.js는 비지니스 로직에 염두하여 책임성을 controller, model 사이에 진행하고, 후에 검증단계에서 추가할 부분이 생기면 그부분을 단순히 추가하는 부분에 대해서 car.js에 넣으면 유지보수가 어렵다고 생각합니다!

/**
* 게임 컨트롤러 (MVC - Controller)
* 전체 게임 흐름을 관리하고 조율하는 역할
*/
Copy link

Choose a reason for hiding this comment

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

콘트롤러가 로직이 다소 많은것 같다는 생각도 듭니다
무슨일을 해라 명령을 해주는것이 콘트롤러의 역할이라고 생각이 드는데
로직 또한 포함이 되어있어서 명령을 내리는 사람이 모든 일을 하는 느낌이라
저또한 고민이되네요.. model에게 일을 시키기 위해서는 일을 알아야하니까요

그래서 서비스 계층이 있는것 같기도 하고요 지금 코드가 의도도 이해되고 구조도 적합해서 수정할필요 없이 좋다고 생각이 들기도 합니다!

return this.name;
}
}

Copy link

Choose a reason for hiding this comment

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

jsDoc 깔끔하게 잘쓰셨네요..
저도 적용한다고 하고 못했는데 너무 좋은것 같스빈다

this.name = name;
this.position = 0;
this.id = ++Car.idCounter; // 고유 ID 할당
}
Copy link

Choose a reason for hiding this comment

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

오!!
저는 증복된 이름의 경우 애러처리했는데
이런 방법도 존재하는군요
index처럼 작동하게 너무 좋은 방법이네요

Copy link
Author

Choose a reason for hiding this comment

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

저도 이부분 나중에 하나씩 예외 사항 확인 테스트 하다가 같은 이름은 어떤식으로 표기해야되지??
index를 통해서 표기하자는 생각이 들어 진행했습니다!
name(a),name(b) 이런식으로 표기 하려다가 이것보다는 index를 통해서 간단한게 좋을꺼 같아서 진행했습니다!

Copy link

@inseong01 inseong01 left a comment

Choose a reason for hiding this comment

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

커밋 메시지가 헤더, 본문으로 잘 구분되어 있어서 어떤 작업을 했는지 파악하기 수월했습니다.

2주 차 미션하시느라 수고하셨습니다~

* 전체 게임 실행 흐름 관리
*/
async run() {
// 자동차 이름 입력 및 검증

Choose a reason for hiding this comment

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

run 메서드 코드가 명확해서 내부 주석은 제외해도 좋을 것 같습니다.

Comment on lines +55 to +59
for (const car of cars) {
if (car.position > maxPosition) {
maxPosition = car.position;
}
}

Choose a reason for hiding this comment

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

Suggested change
for (const car of cars) {
if (car.position > maxPosition) {
maxPosition = car.position;
}
}
const carPositions = cars.map((car) => car.position);
const maxPosition = Math.max(...carPositions);

이렇게 하면 들여쓰기 깊이를 줄일 수 있을 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

감사합니다 ! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants