프로그래밍 공부
작성일
2024. 1. 25. 16:34
작성자
WDmil
728x90

4.7 체스 프로그램 디자인하기

간단한 체스 프로그램을 대상으로 하여 C++프로그램을 체계적으로 디자인해보자.

전체 디자인 절차를 개괄적으로 이해하기 위해 우선 먼저 그대로 진행해보고 모르는 부분은 따로 찾아보는 것도 좋은 방법이다.


4.7.1 요구사항

디자인에 들어가기 전에 프로그램의 기능과 성능에 대해 요구되는 조건을 명확하게 정리하는 것이 중요하다. 이상적으로는 이런 요구 조건은 요구 사항 명세서라는 이름으로 문서화되어 있어야 한다. 체스 프로그램에 대한 요구 사항은 다음과 같다. ( 실제라면 더 자세하고 개수도 많을 것 이다.)

 

  • 프로그램은 체스의 표준 룰을 지원한다.
  • 프로그램은 두 명의 플레이어를 지원한다. 인공지능 컴퓨터 플레이어는 지원하지 않는다.
  • 프로그램은 텍스트 기반 인터페이스를 지원한다.
    • 프로그램은 체스판과 말을 일반 문자로 표현한다.
    • 플레이어는 체스보드의 위치를 숫자로 입력하는 방법으로 말을 이동시킨다.

이러한 요구사항을 자세히 작성하면, 프로그램이 사용자가 기대하는 대로 디자인될 수 있게 해준다.


4.7.2 디자인 단계

프로그램을 디자인할 때는 큰 부분에서 세부적인 부분까지 체계적으로 수행해야 한다. 

이번 단계는 모든 프로그렘에 적용되지는 않지만 일반적인 가이드라인을 제공할 것이다. 디자인은 적절한 다이어그램과 테이블을 포함하여야 한다. 소프트웨어 산업 표준에서는 소프트웨어 설계 도면을 UML 이라는 형식을 따라서 작성하도록 하고 있다.

 

UML은 소프트웨어 설계를 문서화하기 위한 매우 다양한 다이어그램 형식을 정의하고 있다. 예를 들면 클래스 다이어그램, 시퀸스 다이어그램 등이 있다. 따라서 가능하다면 UML을 사용하고 UML을 사용하지 않더라도 그와 비슷한 형태로 문서를 작성할 것을 권장한다. 하지만 UML문법에 집착할 필요는 없다고 본다. 어떤 포멧을 따르느냐 여부보다는 명확하고 이해하기 쉽게 다이어그램이 그려졌느냐가 더 중요하다.


4.7.2.1 서브시스템으로의 분할

처음으로 할 일은 프로그램을 일반적인 기능을 기준으로 서브시스템 별로 분할하고 각 서브시스템 간 인터페이스와 상호연동을 정의하는 것이다. 이단계에서는 상세한 데이터 구조나 알고리즘은 물론 클래스에 대해서도 걱정하지 않아도 된다.

 

단지 프로그램의 구성 요소와 그 연계 방식에 대한 감을 잡는 것이 목적이다. 서브 시스템 목록을 테이블로 만들어서 서브시스템별로 상위 수준 동작과 기능, 다른 서브시스템이 이용할 수 있도록 익스포트된(export)된 인터페이스, 그리고 해당 서브시스템이 이용할 외부 서브시스템 인터페이스를 기술한다.

 

체스 게임 프로그렘에 있어서 추천되는 디자인은 데이터의 저장부와 표현부를 분리하는 모델-뷰-컨트롤러(MVC)패러다임 이다.

 

이 디자인 패러다임은 많은 애플리케이션에서 흔하게 부딛히는 데이터의 조작과 그 데이터에 대한 하나 또는 복수 개의 표현을 다루기 위한 모델을 제공한다. MVC에서는 다루어야할 데이터의 집합을 모델 이라고 하고, 그 데이터에 대한 가시적인 표현을 뷰 라고 한다. 그리고 어떤 이벤트에 맞추어 데이터를 변경하는 코드를 컨트롤러 라고 한다.

 

이 디자인 패러다임은 텍스트 기반 인터페이스와 그래픽 기반 인터페이스를 쉽게 바꿔서 탑제할 수 있게 한다.

서브시스템 이름 인스턴스 개수 기능 익스포트된 인터페이스 사용할 인터페이스
GamePlay 1 게임 시작
게임 진행 제어
그리기 제어
승자 선언
게임 종료
게임 종료 차례 바꾸기
(Player에서 제공)
그리기
(ChessBoardView에서 제공)
Chess Board 1 체스 말 저장
무승부/체크메이트 검사
특정 위치의 말 얻기
특정 위치에 말 설정 하기
게임 종료
(GamePlaye에서 제공)
ChessBoardView 1 체스보드 상태 그리기 그리기 그리기
(ChessPieceVierw에서 제공)
ChessPiece 32 이동
룰에 맞는 이동인지 체크
이동
이동 체크
특정 위치의 체스 말 얻기
(ChessBoard에서 제공)
특정 위치에 체스 말 성정하기
(ChessBoard에서 제공)
ChessPieceView 32 해당 체스 말 그리기 그리기 없음
Player 2 사용자 연동:
사용자에게 이동명령 수신 대기 표시, 사용자의 이동 명령 입력받기
체스 말 이동
차례 바꾸기 특정 위치의 체스말 얻기
(ChessBoard에서 제공)
체스 말 이동
(ChessPiece에서 제공)
이동이 룰에 맞는지 검사
(CHessPiece에서 제공)
ErrorLogger 1 로그 파일에 에러 메세지 저장 에러 로그 없음

 

표를 보면 알 수 있듯 체스 게임은 GamePlay 서브시스템, ChessBoard와 ChessBoardView, 32개의 ChessPiece와 CHessPieceViews, 2개의 Player그리고 Errogger로 구성된된다, 이러한 구성이 꼭 정답이라는 보장이 없다. 소프트웨어 디자인은 프로그래밍이 그렇듯이 같은 목적을 달성하는 여러 가지 방법이 있을 수 있다.

 

물론 모든 방법이 동등하지는 않다. 분명 더 나은 방법도 있고 더 못한 방법도 있다. 하지만 비슷한 좋은 방법이 여러개 있을 떄도 많다.

 

좋은 디자인은 기본적인 기능을 기준으로 프로그램을 서브시스템으로 나눈다, 예를 들어 Player는 ChessBOard, CHessPiece, 또는 GamePlay와 분명히 구분된다. Player를 GamePlay에 한덩어리로 집어넣는다면 이상하다.

 

왜냐하면 Player는 논리적으로 GamePlay와 구분되기 떄문이다. 다른방법은 이렇게 자명하지 않을 것이다.

 

이 MVC디자인에서 ChessBoard와 ChessPiece서브시스템은 model의 역할을 한다.

ChessBOardView와 ChessPieceView는 View의 부분이고

Palyer는 Controller의 부분이다.

 

테이블로는 서브시스템의 관계를 설명하기 어려울 떄가 많다. 서브시스템 간의 관계는 다이어그램을 그려서 서브시스템 간 이용 관계를 화살표로 표현하면 설명이 쉽다.


4.7.2.2 스레드 사용 모델 선택

디자인 단계에서 알고리즘의 어떤 루프에 스레드를 사용할지 검토하는 것은 너무 이르다. 대신 상위 수준에서 몇 개의 스레드를 사용할 지, 스레드 간에 어떤 상호 연동을 할지 생각한다. 상위 수준 스레드의 예로 UI스레드나 오디오 재생 스레드, 네트워크 통신 스레드 같은것들이 있을 수 있다. 멀티 스레드 디자인에서는 스레드 간 데이터 공유를 가능한 피해서 디자인을 단순하고 안전하게 하는 것이 바람직하다. 만약 데이터 공유를 피할 수 없다면 스레드 락 메커니즘을 사용해야 한다.

 

만약 멀티스레드 프로그램에 익숙하지 않거나, 플랫폼 자체가 멀티 스레드를 지원하지 않는다면 싱글 스레드로 프로그램을 만들어야 한다. 하지만 프로그램이 몇개의 확연히 구분되는 작업으로 나뉘어지고 각 작업이 병렬적으로 수행되어야 한다면 멀티스레드를 이용하는 것이 좋은 선택이다. 예를 들어 그래픽 유저 인터페이스 애플리케이션은 한 스레드에 작업을 맡기고 다른 스래드는 사용자의 버튼 클릭이나 메뉴 선택과 같은 사용자 입력을 대기하도록 만들 때가 많다.

 

체스 프로그램은 싱글 스레드만으로도 게임 진행을 할 수 있다.


4.7.2.3 서브시스템 간의 클래스 계층 설계

이 단계에서는 프로그램에 적용할 클래스 계층을 정한다. 체스 프로그램은 체스 말을 표현하기 위해 클래스 계층이 필요하다.

그림 4-1

이 계층도에서 범용 ChessPiece 클래스는 추상화 베이스 역할을 한다. ChessPieceView도 비슷한 클래스 계층을 요구한다.

 

ChessBoardView 클래스는 텍스트 기반 체스판 표현과 그래픽 기반 체스판 표현을 모두 지원할 수 있도록 계층을 가지게 디자인 한다.

그림 4-2

Controller인 Player도 ChessBoardView와 비슷한 클래스 계층을 가진다.


4.7.2.4 클래스 대상 식별, 데이터구조, 알고리즘, 각 서브시스템 패턴

이 단계에서는 각 서브시스템의 세부적이고 고유한 부분을 깊이 있게 검토한다. 각 서브시스템의 어떤 부분을 클래스화 할 것인가에 대한 것도 검토하는데, 보통 서브시스템 항목 각각이 클래스가 될 때가 많다.

서브시스템 클래스 데이터 구조 알고리즘 패턴
GamePlay GamePlay 클래스 GamePlay 객체는 ChessBoard 객체와 2개의 Player객체를 가짐 각 플레이어 한번씩 플레이 차례 부여 없음
ChessBoard ChessBoard 클래스 ChessBoard 객체는 ChessPiece객체 크기 32의 2차원 배열을 가짐 각 말의 이동마다 승리 또는 무승부가 일어났는지 검사 없음
ChessBoardView 상위 추상 클래스
ChessBoardVIew
하위 구현 클래스
ChessBoardVIewConsole,
CHessBoardVIewGUI2D...
체스보드를 어떻게 그릴지에 대한 정보 저장 체스보드 그리기 관찰자(Observer)
패턴
ChessPiece 상위 추상 클래스
CHesspiece
하위 클래스
Rook, Bishop, Knihgt, King, Pawn, Queen
각 말(Piece)은 체스보드 위에서의 위치를 저장 각 말은 체스보드 상에서의 움직임이 체스 룰에 맞는지 검사 없음
ChessPieceView 상위 추상 클래스
ChessPieceView
하위 클래스
RookView, BishopVIew...
하위 구현 클래스
RookViewConsole,
RookViewGUI2D...
각 체스 말을 어떻게 그릴지에 대한 정보 저장 체스 말 그리기 관찰자 패턴
Player 상위 추상 클래스
Player
하위 구현 클래스
PlayerConsole,
PlayerGUI2D...
없음 사용자에게 말 이동 정보를 입력받고 이동할 위치가 룰에 맞으면 말을 이동시킴 중재자 패턴
ErrorLogger ErrorLogger 클래스 메시지가 저장되는 큐 메시지를 버퍼에 담아두었다가 로그파일에 쓰기 ErrorLogger객체가 단하나만 존재해도 되도록 단일 객체 패턴 사용

 

이 단계의 디자인 문서에는 각 클래스의 실제 인터페이스까지 만드는 것이 보통이지만 , 그정도로 자세한 부분까지는 생략한다.

 

클래스를 디자인하고, 데이터구조, 알고리즘 패턴을 선택하는 것은 쉽지 않은 일이다. 앞에서 설명한 추상화와 재사용성 원칙을 항상 염두에 두어야 한다. 추상화를 고려할 때 기본은 인터페이스와 구현을 분리해서 생각하는 것이다. 먼저 사용자 입장에서의 인터페이스를 구체화한다. 그 컴포넌트가 무엇을 해야 할지 결정한 다음, 그것을 어떻게 수행할지 데이터 구조와 알고리즘을 선택한다. 이떄 C++ 표준 라이브러리와 동료가 작성한 코드 등 재사용 가능한 코드의 활용도 고려한다.


4.7.2.5 각 서브시스템의 에러 처리

이 디자인 단계에서는 각 서브시스템의 에러 처리 방법에 대하여 기술한다. 에러 처리는 메모리 할당 실패와 같은 시스템 에러는 물론 사용자의 잘못된 입려과 같은 사용자 에러 모두 포함된다. 각 서브시스템별로 에러 처리에 익셉션을 사용할 것인지, 사용한다면 어떤 익셉션을 사용할 것인지 결정해야 한다.

 

서브시스템 시스템 에러 처리 사용자 에러 처리
GmaePlay 메모리 할당에 실패하면 ErrorLogger를 통해 에러 내용을 기록하고 사용자에게 알려준 후 자연스럽게 프로그램을 종료한다. 해당사항 없음.(사용자와 직접적인 인터페이스가 없다.)
ChessBOard
ChessPiece
메모리 할당에 실패하면 ErrorLogger로 에러 내용을 기록하고 익셉션을 발생시킨다. 해당사항 없음.(사용자와 직접적인 인터페이스가 없다.)
ChessBoardView
CHessPieceView
텍스트 또는 그래픽 그리기 작업 도중에 문제가 발생하면 ErrorLogger로 에러 내용을 기록하고 익셉션을 발생시킨다. 해당사항 없음.(사용자와 직접적인 인터페이스가 없다.)
Player 메모리 할당에 실패하면 ErrorLogger로 에러 내용을 기록하고 익셉션을 발생시킨다. 사용자가 입력한 말 이동 위치가 체스보드를 벗어나지 않는지 검사하고, 벗어났으면 재입력을 요청한다. 말을 이동시키기 전에 말의 이동이 룰에 어긋나지 않는지 검사하고 룰에 어긋나면 재입력을 요청한다.
ErrorLogger 메모리 할당에 실패하면 에러 기록을 시도한 후 사용자에게 알려준 뒤 자연스럽게 프로그램을 종료한다. 해당사항 없음.(사용자와 직접적인 인터페이스가 없다.)

 

에러처리에 있어 일반 원칙은 발생 가능한 모든 종류의 에러에 대응하는 것이다.

한가지라도 빠트린다면 프로그램의 버그가 된다! 어떤 경우도 그저 예상치 못한 문제로 취급해서는 안된다.

 

메모리 할당 실패, 잘못된 사용자 입력, 네트워크 실패 등 모든 가능성을 상상해보아야 한다. 그런데 위 테이블에서 볼 수 있듯이 사용자 에러에 대해서는 다른 방식으로 처리해야 한다.

 

사용자가 말의 이동 위치를 잘못 입력했을 떄는 다시 입력하도록 기회를 주어야지 프로그램을 종료해버리면 안된다.


4.8 요약

그 어떤 프로그래밍 프로젝트든 그 첫 단계로 소프트웨어 디자인이 수행되어야 함을 이해하여야 한다.

C++의 어떤 특성은 디자인 단계를 다소 어렵게 만들기도 한다. C++표준 라이브러리 전반에 걸쳐 적용된 객체지향 철학과 범용적으로 적용 가능하게 설계된 방식이 그러한 예 이다.

 

지금까지 소개된 디자인 테마는 총 두개로

첫번쨰로 추상화 개념은 인터페이스와 구현을 분리해서 생각하는 것이다.

두 번째로 재사용 개념은 코드는 물론 아이디어까지 포함하는것으로 실제 프로젝트에서 자주 부딛히는 이슈다. C++디자인은 재사용을 기본적으로 포함하게 되는데, 그형태는 라이브러리나 프레임워크 같은 코드가 될 수 있고 패턴이나 테크닉같은 아이디어 일 수도 있다.

 

코드를 작성할 떄는 최대한 재사용 가능한 형태로 작성하는 것이 좋다.

 

재사용은 그편의성에 따른 반대급부도 있어서, 비용과 이익을 잘 따져보아야 한다. 코드를 재사용할 떄는 그기능과 제약사항, 성능 분석, 라이선싱 및 기술 지원 문제, 가용 플랫폼, 프로토타이핑, 그리고 도움을 얻는 방법과 같은 가이드라인이 있다는 것을 기억하라.

 

더불어서 성능 분석에 필요한 Big-O표기법또한 기억해야 한다.

728x90