종제로
종제로 Devlog
종제로
전체 방문자
오늘
어제
  • 분류 전체보기 (43)
    • C, C++ (22)
      • C, C++ (10)
      • Modern C++ (4)
      • 전문가를 위한 C++ (책) (8)
    • DirectX 자체엔진 개발 (8)
    • 자료구조 알고리즘 (10)
      • 공부 (9)
      • 문제풀이 (1)
    • 자기 계발 (1)
    • 기타 (2)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • c++ 11
  • C
  • 자료구조
  • directX
  • C++
  • DirectX11
  • 전문가를 위한 C++
  • c++ 17
  • 알고리즘
  • 모두의C언어

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
종제로

종제로 Devlog

C, C++/전문가를 위한 C++ (책)

전문가를 위한 C++ 4장 : 프로그램 디자인 (1)

2022. 6. 14. 16:23

4장 전문가다운 C++ 프로그램 디자인

본격적으로 코드를 작성하기 전에 반드시 프로그램 디자인부터 해야 한다.

본능을 거스르는 기분이 들더라도 프로젝트를 시작할 때 시간을 충분히 투자해서 제대로 디자인하면 오히려 프로젝트 완료 시점을 앞당길 수 있다.

 

 

1. 프로그램 디자인의 정의

프로젝트를 새로 시작하거나 기존에 구현했던 프로그램을 개선하기 시작할 때 가장 먼저 할 일은 요구사항을 분석하는 것이다. 요구사항은 이해 당사자와 함께 논의한다. 요구사항 분석 단계에서 가장 핵심적인 결과는 기능 요구사항 문서다. 이 문서는 작성할 코드가 정확히 할 일만 표현하고, 그 일을 달성하는 구체적인 방법은 생략한다. 요구사항 분석 과정에서 비기능 요구사항 문서도 나올 수 있다. 이 문서는 최종 결과로 나오는 시스템에 대한 동작이 아닌 속성을 표현한다. 예를 들어 시스템이 보안에 뛰어나고, 확장성도 높고, 일정한 성능 기준을 만족해야 한다는 식으로 표현한다.

 

요구사항을 모두 수집했다면 프로젝트 디자인 단계로 넘어간다. 프로그램 디자인 또는 소프트웨어 디자인이란 앞 단계에서 도출한 (기능 및 비기능) 요구사항을 모두 만족하는 프로그램을 구현하기 위한 구조에 대한 명세서다. 쉽게 말해 디자인이란 프로그램의 구현 계획을 정리한 것이다. 디자인은 일반적으로 문서 형태로 작성한다.

 

 

2. 프로그램 디자인의 중요성

요구사항 분석과 디자인 과정을 건너뛰거나 가볍게 넘기고, 어서 빨리 프로그래밍 단계로 넘어가려는 유혹에 빠지기 쉽다. 코드를 컴파일해서 실행하면 일이 진행된다는 느낌이 확실히 들기 때문이다. 프로그램의 구성 방안이 어느 정도 머릿속에 잡힌 상태라면 기능 요구사항이나 디자인에 대한 문서를 따로 작성하는 것이 시간 낭비처럼 여겨지기도 한다. 게다가 디자인 문서를 작성하는 작업은 코드 작성에 비해 따분한 면도 있다. 하루 종일 문서나 작성하려고 프로그래머가 된 게 아니라고 생각할 수도 있다. 필자도 프로그래머이기에 당장 코딩에 들어가고 싶다는 마음을 누구보다도 알고, 때로는 그런 유혹에 넘어가기도 한다. 하지만 이렇게 하면 아주 간단한 프로젝트에서도 문제가 발생하기 마련이다. 디자인 과정을 거치지 않고도 제대로 구현하려면 프로그래밍 경험이 풍부하고, 주요 디자인 패턴도 자유자재로 적용할 수 있어야 하고, C++뿐만 아니라 주어진 문제의 배경과 요구사항을 깊이 있게 이해해야 하기 때문이다.

 

예를 들어 여러 스레드가 공유하는 큐에 잠금(락, lock) 기능을 깜박하고 구현하지 않았는데, 한참 구현이 진행되고 나서야 발견했고 그냥 큐를 사용할 때마다 직접 잠금/해제 작업을 수행하도록 코드를 작성했을 수도 있다. 좀 어설프지만 실행에는 문제없다고 주장할 것이다. 하지만 프로젝트에 새로 합류한 프로그래머가 큐에 당연히 잠금 기능이 있을 거라고 여기고 한참 작업했는데, 어느 날 공유 데이터에 접근할 때 상호 배제(뮤텍스, mutex)를 보장하지 못해서 경쟁 상태(race condition)가 나타나는 버그가 발생했다. 3주에 걸쳐 원인을 분석한 뒤에야 애초에 잠금 기능이 없었다는 것을 발견할 수도 있다. 물론 이 사례는 어설프게 땜질하듯 구현할 때 발생할 수 있는 수많은 문제 중 하나에 불과하다. 당연히 전문 C++ 프로그래머라면 큐에 접근할 때마다 직접 잠금/해제 작업을 수행하도록 구현하지 않고, 큐 클래스 내부에 잠금 기능을 제공하거나 잠금 기능 없이도 큐 클래스가 스레드에 안전(스레드 세이프)하게 작동하도록 구현한다.

 

 

3. 디자인 방법

디자인 작업은 만만치 않다. 필자는 디자인 방식을 고민하느라 며칠 동안 종이에 끄적였다가 지우고, 새로운 아이디어를 추가했다가 또 지우는 과정을 수없이 반복한다. 이렇게 작업하다가 며칠 또는 몇 주가 지나면 마침내 명확하고 효율적인 디자인이 나올 수도 있다. 반면 시간이 흘러도 도무지 답은 보이지 않고 고통만 늘어날 때도 있다. 그렇다고 헛수고하는 것은 아니다. 한참 지나서야 디자인이 잘못됐다는 것을 깨닫고 처음부터 다시 구현하는 것보다는 훨씬 많은 시간을 절약할 수 있다. 이 과정에서 반드시 작업이 진행되고 있는지 가늠해야 한다.

중간에 막혀서 진척이 없다면 다음과 같은 방법을 취한다.

  • 도움을 요청한다 : 동료나 멘토, 책, 뉴스그룹, 웹 페이지 등에서 조언을 구한다.
  • 잠시 다른 일에 몰두한다 : 나중에 다시 디자인 작업을 이어서 한다.
  • 일단 결정을 내리고 다음 단계로 나아간다 : 차선책이더라도 결정을 내리고 그 방식으로 밀어붙여 본다. 결정이 잘못됐다면 금세 드러나기 마련이다. 하지만 막상 해보니 의외로 좋은 방식일 수도 있다. 어쩌면 완벽한 디자인이란 없을지도 모른다. 당장 요구사항을 만족하면서 실제로 적용할 수도 있는 최적의 방법이 아무리 머리를 쥐어짜도 떠오르지 않는다면 '어설픈' 솔루션이라도 적용한다. 이때 다른 사람뿐만 아니라 본인도 이러한 결정을 내린 이유를 알 수 있도록 구체적인 내용을 문서에 기록해둔다. 생각해봤지만 채택하지 않은 디자인에 대해서도 그 이유와 함께 문서에 남긴다.

결론 : 제대로 디자인하려면 상당한 노력이 들 뿐만 아니라 그 수준에 이르기까지 수많은 훈련이 필요하다. 하룻밤 사이에 전문가가 되겠다는 기대는 일찌감치 접기 바라며, C++로 코드를 작성하는 것보다 C++ 코드에 대해 디자인하기가 훨씬 어렵다는 사실을 깨닫더라도 너무 주눅 들지 말기 바란다.

 

 

4. C++ 디자인에 관련된 두 가지 원칙

 C++ 디자인에서 가장 핵심적인 원칙은 추상화와 재사용이다. 이 원칙은 이 책의 핵심 주제로 삼아도 될 정도로 굉장히 중요하다.

 

(1) 추상화

추상화의 원칙은 현실 상황에 맞게 비유하면 이해하기 쉽다. 요즘은 집집마다 TV가 한 대씩 있어서 TV를 켜거나 끄는 방법, 채널을 변경하는 방법, 볼륨을 조절하는 방법, 스피커/DVR/블루레이 플레이어 등을 연결하는 방법을 비롯한 TV의 기능을 누구나 잘 알고 있다. 하지만 TV 상자 내부에서 구체적으로 어떻게 작동하는지 설명하기는 쉽지 않다. 다시 말해 공중파나 케이블로 전달된 신호를 받아서 적절히 변환한 뒤 화면에 표시하는 과정을 구체적으로 아는 사람은 많지 않다. 하지만 TV의 구체적인 작동 원리는 몰라도 사용하는 데는 아무런 어려움이 없다. TV의 내부 구현과 외부 인터페이스가 명확히 분리되어 있기 때문이다. TV를 사용하려면 전원 버튼, 채널 버튼, 볼륨 컨트롤과 같은 TV의 인터페이스만 알아도 된다. 이 과정에서 내부 작동 원리를 알 필요는 없다. TV에 화면을 표시하는 데 브라운관을 사용하는지, 외계에서 들여온 최신 기술을 사용하는지 전혀 신경 쓰지 않아도 된다. 인터페이스에 아무런 영향을 미치지 않기 때문이다.

 

추상화의 장점

소프트웨어에 적용되는 추상화 원칙도 이와 비슷하다. 내부 구현 방식을 이해하지 않아도 코드를 사용할 수 있다. 간단한 예로 <cmath> 헤더 파일에 선언된 sqrt() 함수를 호출하는 경우를 들 수 있다. 이 함수가 제곱근을 구하는 데 내부적으로 사용하는 알고리즘은 몰라도 된다. 실제로 제곱근 게산에 대한 내부 구현은 라이브러리 버전마다 달라질 수 있는데, 인터페이스만 그대로 유지된다면 기존에 이 함수를 호출한 코드는 버전 변화에 아무런 영향을 받지 않는다.

 

추상화를 적용하여 디자인하기

함수와 클래스를 디자인할 때는 작성자 자신뿐만 아니라 다른 프로그래머가 내부 구현사항을 몰라도 쉽게 사용할 수 있게 구성해야 한다. 구현사항을 드러내는 디자인과 이를 인터페이스 뒤로 숨기는 디자인의 차이를 확실히 보기 위해 체스 프로그램을 살펴보자. 체스보드는 대부분 ChessPiece 객체를 2차원 포인터 배열로 구현한다. 

그래서 보드를 다음과 같이 선언했다.

ChessPiece* chessBoard[8][8];
...
chessBoard[0][0] = new Rook();

하지만 이렇게 하면 추상화 원칙에 어긋난다. 체스보드를 사용하는 프로그래머는 항상 체스보드가 2차원 배열로 구현되었다는 사실을 알아야 한다. 이 구현을 크기가 64인 1차원 vector와 같이 다른 방식으로 변경하면 문제가 발생한다. 프로그램 코드 전체를 뒤져서 체스보드를 사용한 부분을 일일이 수정해야 하기 때문이다. 또한 체스보드를 사용하는 측에서 메모리 조작 과정에 문제가 발생하지 않도록 직접 관리해야 한다. 이렇게 되면 인터페이스와 구현이 확실히 분리되었다고 볼 수 없다.

 

이보다 나은 방법은 체스보드 모델을 클래스로 작성하는 것이다. 그런 다음 인터페이스를 공개하고, 그 뒤에 구현에 대한 세부사항을 숨긴다.
예를 들어 다음과 같이 ChessBoard 클래스를 정의할 수 있다.

class ChessBoard
{
public:
	// 생성자, 소멸자, 대입 연산자 코드는 생략했다.
	void setPieceAt(size_t x, size_t y, ChessPiece* piece);
	ChessPiece* getPieceAt(size_t x, size_t y);
	bool isEmpty(size_t x, size_t y) const;
private:
	// 데이터 멤버 코드는 생략했다.
};

이 인터페이스를 보면 내부 구현에 대한 코드는 한 줄도 나오지 않았다. ChessBoard를 2차원 배열로 구현하더라도 인터페이스에 전혀 어긋나지 않는다. 구현 방식이 바뀌더라도 인터페이스를 그대로 유지할 수 있다. 게다가 구현 코드에서 경계선 검사와 같은 기능도 추가로 제공할 수 있다.

 

(2) 재사용

C++ 디자인에서 두 번째로 중요한 원칙은 재사용이다. C++ 프로그래밍에서 흔히 겪는 문제에 대해 어느 정도 공통적으로 적용할 수 있는 기법이 나와 있긴 하지만 상당수의 프로그래머는 디자인할 때마다 적합한 전략을 직접 마련하길 원한다.

 

기존 코드를 활용한다는 개념은 예전부터 있었다. cout으로 화면에 출력하는 코드도 재사용한 것이다. 화면에 데이터를 출력하는 코드를 직접 작성하지 않고, 그저 이 동작을 수행하도록 기존에 만들어진 cout 코드를 가져다 썼다. 안타깝게도 기존 코드를 제대로 활용하지 않는 프로그래머가 많다. 디자인할 때 기존 코드가 있다면 이를 재사용할 수 있는지 반드시 검토하기 바란다.

 

재사용 가능한 코드 만들기

재사용 원칙은 직접 작성한 코드뿐만 아니라 가져다 쓰는 코드에도 똑같이 적용된다. 프로그램은 클래스, 알고리즘, 데이터 구조를 재사용할 수 있도록 디자인해야 한다. 그래서 프로그램을 작성한 자신뿐만 아니라 다른 동료도 이렇게 작성된 컴포넌트를 현재 프로젝트나 향후 프로젝트에서 활요할 수 있어야 한다. 일반적으로 당장 주어진 문제에만 적용할 수 있도록 너무 특화된 형태로 디자인하지 않는 것이 좋다.

 

C++는 이렇게 코드를 범용적으로 만들 수 있도록 템플릿이라는 기능을 제공한다. 다음 예제는 템플릿을 이용하여 데이터 구조를 만드는 예를 보여준다. 앞에 나온 예제처럼 ChessPiece를 저장하는 ChessBoard를 너무 구체적으로 작성하지 말고, 체스나 체커와 같이 2차원 보드를 사용하는 게임이라면 언제든지 적용할 수 있도록 GameBoard라는 제네릭 템플릿으로 정의한다. 또한 게임에 사용할 말을 인터페이스에 고정시키지 말고 템플릿 매개변수로 전달하도록 클래스 선언을 변경한다. 
이렇게 작성한 코드는 다음과 같다.

template <typename PieceType>
class GameBoard
{
public:
	// 생성자, 소멸자, 대입 연산자 코드는 생략했다.
	void setPieceAt(size_t x, size_t y, PieceType* piece);
	PieceType* getPieceAt(size_t x, size_t y);
	bool isEmpty(size_t x, size_t y) const;
private:
	// 데이터 멤버 코드는 생략했다.
};

이렇게 인터페이스를 살짝 변경하는 것만으로도 2차원 보드를 사용하는 게임이라면 어디서나 적용할 수 있는 제네릭 게임보드 클래스를 만들 수 있다. 여기서 수정된 코드 양은 많지 않지만, 이런 결정을 디자인 단계에서 내리는 것은 중요하다. 그래야 코드를 효과적이면서 효율적으로 구현할 수 있다.

 

디자인 재사용

C++ 언어를 익히는 것과 뛰어난 C++ 프로그래머가 되는 것은 전혀 별개다. 자리에 앉아 C++ 표준을 읽고 세세한 사항을 암기하는 것은 누구나 할 수 있다. 하지만 코드를 읽고 프로그램을 직접 작성해본 경험이 어느 정도 쌓이기 전에는 결코 뛰어난 프로그래머가 될 수 없다. 그 이유는 바로 C++는 다양한 기능을 제공하기만 할 뿐 각 기능을 사용하는 방법을 구체적으로 알려주지 않기 때문이다.

 

많은 프로그래머가 디자인할 때 기존 패턴을 활용하여 프로그램을 디자인하지 않고 프로그램을 디자인할 때마다 비슷한 테크닉을 매번 새로 개발한다.

 

예를 들어 여러분이 체스 프로그램을 디자인할 때 여러 컴포넌트에서 발생하는 에러를 ErrorLogger 객체 하나만으로 로그 파일에  기록(직렬화, serialize)한다고 가정하자. 이렇게 하려면 프로그램에서 ErrorLogger 클래스의 인스턴스를 단 한 개만 만들도록 클래스를 구성해야 한다. 그런데 프로그램에 있는 다른 컴포넌트도 이 ErrorLogger 인스턴스를 사용해야 한다. 다시 말해 모든 컴포넌트가 동일한 ErrorLogger 서비스를 사용하게 만들어야 한다. 이러한 서비스 매커니즘을 구현하기 위한 표준 접근 방식은 의존성 주입(dependency injection)패턴을 적용하는 것이다. 이 패턴은 서비스마다 인터페이스를 만들어서 이 서비스를 사용하는 컴포넌트에 집어넣는다. 따라서 체스 프로그램도 의존성 주입 패턴을 적용해서 디자인하는 것이 바람직하다.

 

 

참고

  • 전문가를 위한 C++ 책
저작자표시 비영리 변경금지 (새창열림)

'C, C++ > 전문가를 위한 C++ (책)' 카테고리의 다른 글

전문가를 위한 C++ : 3장 코딩 스타일  (0) 2022.06.11
전문가를 위한 C++ : 2장 스트링 (2)  (0) 2022.06.09
전문가를 위한 C++ : 2장 스트링 (1)  (0) 2022.06.08
전문가를 위한 C++ : 1장 (4)  (0) 2022.06.07
전문가를 위한 C++ : 1장 (3)  (0) 2022.06.06
    'C, C++/전문가를 위한 C++ (책)' 카테고리의 다른 글
    • 전문가를 위한 C++ : 3장 코딩 스타일
    • 전문가를 위한 C++ : 2장 스트링 (2)
    • 전문가를 위한 C++ : 2장 스트링 (1)
    • 전문가를 위한 C++ : 1장 (4)
    종제로
    종제로

    티스토리툴바