4. Designing Professional C++ Programs
이 장의 내용은
▶ 프로그래밍 디자인의 정의
▶ 프로그래밍 다자인의 중요성
▶ C++ 고유의 디자인 측면
▶ 효과적인 C++ 디자인을 위한 두 가지 기본 테마: 추상화와 재사용
▶ 재사용 가능한 다양한 유형의 코드
▶ 코드 재사용의 장점과 단점
▶ 재사용할 라이브러리 선택 지침
▶ 오픈 소스 라이브러리
▶ C++ 표준 라이브러리
애플리케이션을 위한 한 라인의 코드라도 작성하기 전에, 프로그램을 디자인해야 한다. 어떤 데이터 구조를 사용할 것인가? 어떤 클래스를 작성할 것인가? 이 계획은 그룹으로 프로그램하는 경우 특히 중요하다. 같은 프로그램을 작업하고 있는 동료가 무엇을 계획하고 있는지 전혀 모든 채 앉아서 프로그램을 작성한다고 상상해 보자! 이 장에서는 C++ 디자인에 Professional C++ 접근 방식을 사용하는 방법을 배운다.
디자인의 중요성에도 불구하고, 아마도 소프트웨어 엔지니어링 프로세스에서 가장 잘못 이해되고 사용되지 않는 측면일 것이다. 너무 자주, 프로그래머는 명확한 계획 없이 애플리케이션에 뛰어든다. 코드로 디자인을 한다. 이 접근 방식은 복잡하고(convoluted) 지나치게 복잡한(complicated) 디자인으로 이어질 수 있다. 또한 개발, 디버깅 및 유지 관리 작업을 더 어렵게 만든다. 직관적이지 않은 것처럼 보이지만, 프로젝트를 시작할 때 추가 시간을 투자하여 적절하게 디자인을 하면 실제로 프로젝트 일정 동안 시간을 절약할 수 있다.
WHAT IS PROGRAMMING DESIGN? (프로그래밍 디자인이란 무엇인가?)
신규 프로그램이나 기존 프로그램에서 신규 기능 추가를 시작할 때, 첫 번재 단계는 요구 사항을 분석하는 것이다. 여기에는 stakeholders(이해 관계자)와 논의하는 것이 포함된다. 이 분석 단계의 중요한 결과는 신규 코드 조각이 수행해야 하는 what(작업)을 정확히 설명하는 functional requirements(기능 요구 사항) 문서이지만, 수행 how(방법)은 설명하지 않는다. 요구 사항 분석으로 인해 최종 시스템이 수행해야 하는 작업과 비교하여 최종 시스템의 상태를 설명하는 non-functional requirements(기능 외 요구 사항) 문서가 생성될 수도 있다. 기능 외 요구 사항의 예로는 시스템이 안전하고 확장 가능해야 하며 특정 성능 기준의 충족 등이 있다.
모든 요구 사항이 수집되면, 프로젝트의 디자인 단계를 시작할 수 있다. Program design(프로 그램 디자인) 또는 Softwre design(소프트웨어 디자인)은 프로그램의 모든 기능과 기능 외 요구 사항을 충족하기 위해 구현할 아키텍처의 사양이다. 비공식적으로 다자인은 프로그램을 작성하는 방법이다. 일반적으로 디자인 문서의 형태로 디자인을 코드로 작성해야 한다. 모든 회사나 프로젝트에는 원하는 디자인 문서 형식의 고유한 변형이 있지만, 대부분의 디자인 문서는 두 가지 주요 부분을 포함하는 동일한 일반 레이아웃을 공유한다:
- 서브 시스템 간의 인터페이스와 종속성, 서브 시스템 간의 데이터 흐름, 각 서브 시스템의 입력과 출력, 일반 스레딩 모델을 포함하여, 프로그램을 서브 시스템으로 총체적 세분화
- 클래스로 세분화, 클래스 계층, 데이터 구조, 알고리즘, 특정 스레딩 모델와 에러 처리 세부 사항을 포함한 각 서브 시스템의 세부 정보
일반적으로 디자인 문서에는 서브 시스템 상호 작용과 클래스 계층 구조를 보여주는 다이어그램과 테이블이 포함된다. UML(Unified Modeling Language)은 이러한 다이어그램에 대한 산업 표준이며 이 장와 이 후 장에서 다이어그램에 사용된다. UML 구문에 대한 간략한 소개는 Appendix D를 참조한다. 즉 디자인 문서의 정확한 형식은 다자인에 대해 생각하는 과정보다 덜 중요하다.
참고
디자인의 목적은 프로그램을 작성하기 전에 프로그램에 대해 생각하는 것이다.
일반적으로 코딩을 시작하기 전에 가능한 좋은 다자인을 만들도록 노력해야 한다. 디자인은 합리적인 프로그래머라면 누구나 애플리케이션을 구현하기 위해 따를 수 있는 프로그램 맵을 제공해야 한다. 물론 코딩을 시작하고 이전에 생각하지 못한 문제가 발생하면 디자인을 수정해야 하는 것은 불가피하다. 소프트웨어 엔지니어링 프로세스는 이러한 변경을 수핼 할 수 있는 유연성을 제공하고록 디자인되어 있다. 애자일 소프트웨어 개발 방법론인 스크럼(Scrum)은 애플리케이션이 스프린트(sprint)로 알려진 주기로 개발되는 반복적인 프로세스의 한 예이다. 각 스프린트에서 디자인을 수정할 수 있고 새로운 요구 사항을 고려할 수 있다. Chapter 28, "Maximizing Software Engineering Methods"에서는 다양한 소프트웨어 엔지니어링 프로세스 모델에 대해 자세히 설명한다.
THE IMPORTANCE OF PROGRAMMING DESIGN (프로그래밍 디자인의 중요성)
가능한 빠르게 프로그래밍을 시작하기 위해 분석과 디자인 단계를 건너뛰거나 피상적으로만 수행하고 싶은 유혹이 생길 수도 있다. 코드가 컴파일되고 실행되는 것이 보여지는 것만큼 작업에 진척이 있다는 인상을 주는 것도 없다. 프로그램을 구성하는 방법을 어느 정도 이미 알고 있는데고 디자인을 공식화하거나 기능 요구 사항을 기록하는 것은 시간 낭비처럼 보여진다. 게다가 디자인 문서를 작성하는 것은 코딩만큼 재미있지 않다. 하루 종일 논문을 쓰고 싶다면, 컴퓨터 프로그래머가 될 수 없다. 나 자신도 프로그래머로서 코딩을 즉시 시작하고 싶은 유혹을 이해하고 때때로 그것에 굴복한 적이 있다. 그러나 가장 단순한 프로젝트를 제외한 모든 프로젝트에서 문제가 발생할 가능성이 높다. 구현 전에 디자인 없이 성공했는지 여부는 프로그래머로서의 경험, 일반적으로 사용되는 디자인 패턴에 대한 숙련도와 C++, 문제 영역와 요구 사항을 얼마나 깊이 이해했는지에 달려 있다.
팀의 각 구성원이 프로젝트의 다른 부분에서 작업하는 경우, 팀의 모든 구성원이 따라야 하는 디자인 문서가 있는 것이 가장 중요하다. 또한 디자인 문서는 신규 사용자가 프로젝트 디자인을 빠르게 이해하는 데 도움이 된다.
일부 회사에는 기능 요구 사항을 작성하는 전담 기능 분석가와 소프트웨어 디자인을 수행하는 전담 소프트웨어 설계자가 있다. 이러한 회사에서 개발자는 일반적으로 프로젝트의 프로그래밍 측면에만 집중할 수 있다. 다른 회사에서는 개발자가 요구 사항을 수집하고 디자인을 직접 수행해야 한다. 일부 회사는 이 두 극단 사이에 있다. 개발자가 더 작은 디자인을 스스로 수행하는 동안 더 큰 아키텍처의 결정을 내리는 소프트웨어 설계자만 있을 수 있다.
프로그래밍 디자인의 중요성을 이해하는 데 도움이 되도록, 집을 지을 토지를 소요하고 있다고 상상해 보자. 건축업자가 나타나면 청사진을 보여달라고 요청한다. "무슨 청사진?" 그는 대답한다. "내가 뭘 하고 있는지 알아요. 나는 모든 작은 세부 사항을 미리 계획할 필요가 없어요. 2층집? 문제 없어요. 나는 몇 달 전에 단층 집을 지었어요. 그 모델로 시작해서 거기에서 작업할 거예요."
당신이 당신의 불신을 접어두고 건축업자가 계속 진행하도록 허용한다고 가정을 한다. 몇 달 후, 배관이 벽 내부가 아니라 집 외부로 흐르는 것처럼 보인다. 이 이상 현상에 대해 건축업자에게 질문하면 그는 이렇게 말한다. "오. 음, 배관 공사를 위해 벽에 공간을 남겨두는 것을 잊었어요. 나는 이 새로운 건식 벽체 기술에 너무 흥분해서 마음이 놓였어요. 하지만 외부에서도 잘 작동하며 기능이 가장 중요해요." 당신은 그의 접근 방식에 대해 의구심을 갖기 시작했지만, 당신의 더 나은 판단에 반해 그가 계속하도록 허용한다.
완성된 건물을 처음 둘러보면, 부엌에 싱크대가 없다는 것을 알 수 있다. 건축업자는 이렇게 변명한다. "싱크대를 놓을 공간이 없다는 것을 깨달았을 때 부엌일이 3분의 2정도가 끝난 뒤 였어요. 다시 시작하는 대신 옆에 별도의 싱크대 룸을 추가했어요. 괜찮죠?"
건축업자의 변명을 소프트웨어 영역으로 번역하면 친숙하게 들립니까? 집 밖에 배관을 설치하는 것과 같은 문제에 대해 "추한(ugly)" 솔루션을 구현한 적이 있습니까? 예를 들어, 여러 스레드 간에 공유되는 대기열(queue) 데이터 구조에 잠금(locking)을 포함하는 것을 잊었을 수 있다. 문제를 인식할 때쯤에는, 대기열이 사용되는 모든 위치에서 수동으로 잠금을 수행하기로 결정한다. 물론, 그것은 추악하지만 작동한다고 말할 수 있다. 즉, 데이터 구조에 잠금이 내장되어 있다고 가정하고 공유 데이터에 대한 액세스에서 상호 배제를 보장하지 않으며 추적하는 데 3주가 걸리는 경쟁 조건 버그를 유발하는 새로운 누군가가 프로젝트에 합류할 때까지이다. 물론 이 잠금 문제는 추악한 해결 방법의 예일 뿐이다. 분명히, Professional C++ 프로그래머는 각 대기열의 액세스에 수동으로 잠금을 수행하기로 결정하지 않고, 대신 대기열 클래스 내부에 잠금을 직접 통합하거나 잠금 없는 방식으로 대기열 클래스를 스레드로부터 안전하게 만들 것이다.
코딩하기 전에 디자인을 공식화하면 모든 것이 어떻게 조화를 이루는지 결정하는 데 도움이 된다. 집의 청사진이 방들이 어떻게 서로 관련되어 있고 집의 요구 사항을 충족시키기 위해 함께 작동하는지 보여주듯이, 프로그램의 디자인은 프로그램의 서브 시스템이 어떻게 서로 관련되고 소프트웨어 요구 사항을을 충족하기 위해 함께 작동하는지를 보여준다. 디자인 계획이 없으면 서브 시스템 간의 연결, 재사용이나 정보 공유의 가능성, 그리고 작업을 수행하는 가장 간단한 방법을 놓치기 쉽다. 디자인이 제공하는 "큰 그림"이 없으면, 개별 구현 세부 사항에 너무 빠져서 전체적인 아키텍처와 목표를 따라가지 못할 수 있다. 또한 디자인은 프로젝트의 모든 구성원이 참조할 수 있는 작성된 문서를 제공한다. 앞서 언급한 애자일 스크럽(Scrum) 방법론과 같은 반복적인 프로세스를 사용하는 경우, 프로세스의 각 주기 동안 디자인 문서를 최신 상태로 유지해야 한다. 그렇게 하면 가치가 더해질 수 있다. 애자일 방법론의 한 축은 "포괄적인 문서보다 작동하는 소프트웨어"를 선호한다고 명시한다. 프로젝트의 더 큰 부분이 함께 작동하는 방식에 대한 디자인 문서를 최소한 유지해야 하지만, 프로젝트의 더 작은 부분에 대한 디자인 문서를 유지 관리하는 것이 미래에 어떤 가치를 더할지는 팀에 달려 있다고 생각한다. 그렇지 않은 경우, 해당 문서를 제거하거나 오래된 문서로 표시한다.
앞의 비유가 코딩하기 전에 디자인을 하도록 설득하지 못했다면, 코딩으로 바로 뛰어드는 것이 최적의 디자인으로 이어지지 못하는 예가 여기 있다. 체스 프로그램을 작성한다고 가정한다. 코딩을 시작하기 전에 전체 프로그램을 디자인하는 대신, 가장 쉬운 부분부터 시작하여 더 어려운 부분으로 천천히 이동하기로 결정한다. Chapter 1, "A Crash Course in C++ and the Standard Library"에서 소개되고, Chapter 5, "Designing with Objects"에서 더 자세히 다루어진 객체 지향적인 관점에 따라 클래스를 사용하여 체스 말을 모델링하기로 결정한다. 폰(pawn: 졸)이 가장 단순한 체스 말이라고 생각하고 거기에서 시작하기로 결정한다. 폰의 기능과 동작을 고려한 후, Figure 4-1의 UML 클래스 다이어그램에 표시된 속성과 메서드를 사용하여 클래스를 작성한다.
이 디자인에서 m_color 속성은 폰이 검은색인지 흰색인지를 나타낸다. promote() 메소드는 보드의 반대쪽에 도달하면 실행된다.
물론, 이 클래스 다이어그램을 실제로 만들지는 않는다. 구현 단계로 바로 이동한다. 그 클래스에 만족하면, 다음으로 쉬운 말인 비숍(bishop)으로 넘어간다. 속성과 기능을 고려한 후 Figure 4-2의 클래스 다이어그램에 표시된 속성과 메소드를 사용하여 클래스를 작성한다.
다시 말하지만, 코딩 단계로 바로 이동했기 때문에 클래스 다이어그램을 생성하지 않았다. 그러나 이 시점에서 당신은 당신이 뭔가 잘못하고 있는 것이 아닐까라는 의심이 들기 시작한다. 비숍과 폰은 비슷하게 생겼다. 실제로 속성은 동일하며 많은 메소드를 공유한다. 이동 메소드의 구현은 폰과 비숍 사이에 다를 수 있지만, 두 말 모두 이동할 수 있는 기능이 필요하다. 코딩을 시작하기 전에 프로그램을 디자인했다면, 다양한 말이 실제로 매우 유사하며 공통 기능을 한 번만 작성하는 방법을 찾아야 한다는 것을 깨달았을 것이다. Chapter 5에서는 이를 위한 객체 지향 디자인 기법을 설명한다.
또한, 체스 말의 여러 측면은 프로그램의 다른 서브 시스템에 따라 다르다. 예를 들어, 체스말 클래스에서 보드를 모델링하는 방법을 모르면 보드의 위치를 정확하게 나타낼 수 없다. 다른 한편으로, 보드가 말의 위치를 알 필요가 없는 방식으로 말을 관리하도록 프로그램을 디자인할 수 있다. 두 경우 모두, 보드를 디자인하기 전에 말 클래스의 위치를 인코딩하면 문제가 발생한다. 다른 예를 들자면, 먼저 프로그램의 사용자 인터페이스를 결정하지 않고 어떻게 말에 대한 그리기 메소드를 작성할 수 있을까? 그래픽 기반일까 또는 텍스트 기반일까? 보드는 어떻게 생겼을까? 문제는 프로그램의 서브 시스템이 따로 존재하지 않고 다른 서브 시스템과 상호 관련되어 있다는 것이다. 대부분의 디자인 작업은 이러한 관계를 결정하고 정의한다.
DESIGNING FOR C++ (C++를 위한 디자인)
C++로 디자인할 때, 염두에 두어야 할 C++ 언어의 여러 측면이 있다:
- C++에는 방대한 기능 세트가 있다. 이는 거의 완벽한 C 언어의 상위 집합이며, 클래스와 객체, 연산자 오버로딩, 예외, 템플릿과 기타 많은 기능을 더한 것이다. 언어의 단순한 크기 때문에 디자인은 힘든 작업이 된다.
- C++는 객체 지향 언어이다. 즉, 디자인에는 클래스 계층, 클래스 인터페이스와 개체 상호 작용이 포함되어야 한다. 이러한 유형의 디자인은 C나 다른 절차적 언어의 전통적인 디자인과 상당히 다르다. Chapter 5에서는 C++의 객체 지향 디자인에 중점을 둔다.
- C++에는 일반적이고 재사용 가능한 코드를 디자인하기 위한 수많은 기능이 있다. 기본 클래스와 상속 외에도, 템플릿과 연산자 오버로딩 같은 다른 언어 기능을 사용하여 효과적인 디자인을 할 수 있다. 재사용 가능한 코드를 위한 디자인 기술은 이 장의 뒷부분과 Chapter 6, "Designing for Reuse"에서 더 자세히 설명한다.
- C++는 유용한 표준 라이브러리를 제공한다. 여기에는 문자열 클래스, I/O 기능, 많은 공통 데이터 구조체와 알고리즘이 포함된다. 이 모든 것은 C++에서 코딩을 용이하게 한다.
- C++는 많은 디자인 패턴, 즉 문제를 해결하는 일반적인 방법을 손쉽게 수용한다.
디자인을 다루는 것은 압도적일 수 있다. 나는 하루 종일 디자인 아이디어를 종이에 낙서하고, 지우고, 더 많은 아이디어를 쓰고, 지우고, 그 과정을 반복하면서 보냈다. 때로는 이 프로세스가 도움이 되며 며칠(또는 몇 주)이 지나면 깨끗하고 효율적인 디자인으로 이어진다. 때로는 실망스럽고 아무데도 이르지 못하지만, 노력은 낭비가 아니다. 깨진 것으로 판명된 디자인을 다시 구현해야 하는 경우 더 많은 시간을 낭비하게 될 가능성이 크다. 당신이 진정한 진전을 이루고 있는지 여부를 계속 인식하는 것이 중요하다. 막힌 경우, 다음 조치 중 하나를 수행할 수 있다:
- 도움을 요청. 동료, 멘토, 책, 뉴스 그룹이나 웹 페이지에 문의한다.
- 잠시 다른 일을 한다. 나중에 이 디자인 선택하기 위해 돌아온다.
- 결정하고 진행한다. 이상적인 솔루션이 아니라도 뭔가를 결정하고 작업을 시도한다. 잘못된 선택이 곧 드러날 것이다. 그러나 수용 가능한 방법으로 판명될 수 있다. 아마도 이 디자인으로 원하는 것을 달성할 수 있는 확실한 방법은 없을 것이다. 때로는 요구 사항을 충족하는 유일한 현실적인 전략인 경우, "추한(ugly)" 솔루션을 받아들어야 한다. 어떤 결정을 내리든 결정을 문서화하여 나중에 자신과 다른 사람들이 왜 그렇게 했는지 알 수 있도록 한다. 여기에는 당신이 거부한 디자인과 거부 사유를 문서화하는 것이 포함된다.
참고
좋은 디자인은 어렵고 제대로 하려면 연습이 필요하다는 것을 기억한다. 하룻밤 사이에 전문가가 될거라고 기대하지 않는다. C++ 코딩보다 C++ 디자인을 마스터하는 것이 더 어렵다고 해도 놀랄 일은 아니다.
TWO RULES FOR YOUR OWN C++ DESIGNS (자신의 C++ 디자인을 위한 두 가지 규칙)
자신의 C++ 프로그램을 디자인할 때 따라야 할 두 가지 기본 디자인 규칙이 있다. 바로 추상화(abstraction)와 재사용(reuse)이다. 이 지침은 이 책의 주제로 간주될 수 있을 정도로 중요하다. 그것들은 텍스트 전체와 모든 영역에서 효과적인 C++ 프로그램 디자인 전반에 걸쳐 반복적으로 나타난다.
Abstraction (추상화)
추상화의 원리는 실세계의 유추를 통해 이해하는 것이 가장 쉽다. 텔레비전은 대부분의 가정에서 볼 수 있는 기술의 일부이다. 당신은 아마도 그 기능에 익숙할 것이다. 켜고 끄고, 채널을 변경하고, 볼륨을 조정할 수 있고 스피커, DVR 및 블루레이 플레이어와 같은 외부 컴포넌트를 추가할 수 있다. 하지만 블랙박스와 같은 내부에서 어떻게 작동하는지 설명할 수 있는가? 즉, 케이블을 통해 신호를 수신하고 변환하여 화면에 표시하는 방법을 알고 있는가? 대부분의 사람들은 확실히 텔레비전이 어떻게 작동하는지 설명할 수 없지만, 충분히 사용할 수 있다. 텔레비전이 내부 구현과 외부 인터페이스를 명확하게 구분하기 때문이다. 우리는 텔레비전과 전원 버튼, 채널 변환기 및 볼륨 컨트롤 등의인터페이스를 통해 상호 작용한다. 우리는 텔레비전이 어떻게 작동하는지 알지 못하고 관심도 없다. 우리는 스크린에 이미지를 생성하기 위해 음극선관을 사용하든 일종의 외계인 기술을 사용하든 상관하지 않는다. 인터페이스에 영향을 미치지 않기 때문에 중요하지 않다.
Benefiting from Abstraction (추상화의 이점)
추상화의 원리는 소프트웨어에서도 유사하다. 기본 구현(underlying implementation)을 몰라도 코드를 사용할 수 있다. 간단한 예로서, 실제로 제곱근 계산에 사용되는 함수에서 무슨 알고리즘을 사용하는지 모른 채, <cmath> 헤더 파일에서 선언된 sqrt() 함수를 호출할 수 있다. 사실, 제곱근 계산의 기본 구현은 라이브러리 릴리스 간에 변경될 수 있으며, 인터페이스가 동일하게 유지되는 한 함수 호출은 계속 작동한다. 추상화의 원리는 클래스에서도 적용된다. Chapter 1에서 소개했듯이, ostream 클래스의 cout 객체를 사용하여 다음과 같이 표준 출력으로 데이터를 스트리밍할 수 있다:
cout << "This call will display this line of text" << endl;
이 명령문에서는 문자 배열과 함께 cout의 문서화된 삽입 연산자(<<) 인터페이스를 사용한다. 그러나 cout이 사용자 화면에 해당 텍스트를 표시하는 방법을 이해할 필요는 없다. 공개(public) 인터페이스만 알면 된다. cout의 기본 구현은 노출된 동작과 인터페이스가 동일하게 유지되는 한 자유롭게 변경할 수 있다.
Incorporating Abstraction in Your Design (디자인에 추상화 통합)
자신과 다른 프로그래머가 기본 구현을 알지 못하거나 의존하지 않고도 사용할수 있도록 함수와 클래스를 디자인해야 한다. 구현을 노출하는 디자인과 인터페이스 뒤에 숨기는 디자인의 차이점을 보려면 체스 프로그램을 다시 생각해 본다. ChessPiece 객체에 대한 2차원 배열 포인터로 체스보드를 구현하고자 할 수 있다. 다음과 같이 보드를 선언하고 사용할 수 있다:
ChessPiece* chessBoard[8][8]{}; // Zero-initialized array.
…
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) const;
bool isEmpty(size_t x, size_t y) const;
private:
// Private implementation details...
};
이 인터페이스는 기본 구현에 대해 어떤한 약속도 하지 않는다. ChessBoard는 쉽게 2차원 배열이 될 수 있지만, 인터페이스에서는 그것이 필요하지 않다. 구현을 변경하기 위해 인터페이스를 변경할 필요는 없다. 또한 구현은 경계 검사와 같은 추가 기능을 제공할 수 있다.
이상적으로, 이 예제를 통해 추상화가 C++ 프로그래밍에서 중요한 기술임을 확신하게 되었다. Chapter 5에서는 객체 지향 디자인을 더 자세히 설명하고, Chapter 6에서는 추상화의 원리를 더 자세히 설명한다. Chapter 8, "Gaining Proficiency with Classes and Objects", Chapter 9, "Mastering Classes and Objects" 그리고 Chapter 10, "Discovering Inheritance Techniques"에서 자신만의 클래스 작성에 대한 모든 세부 정보를 제공한다.
Reuse (재사용)
C++에서 디자인의 두 번째 기본 규칙은 재사용(reuse)이다. 다시 말하지만, 이 개념을 이해하기 위해 실세계의 유추를 검토하는 것이 도움이 된다. 제빵사로 일하기 위해 프로그래밍 경력을 포기했다고 가정해 보자. 첫 출근날, 수석 조리사가 쿠키를 구우라고 한다. 그의 주문을 이행하기 위해 초콜릿 칩 쿠키의 레시피를 찾아 재료를 섞고 쿠키 시트에 쿠키를 만들고 시트를 오븐에 넣었다. 수석 조리사는 결과에 만족한다.
이제 나는 당신을 놀라게 할 너무나 명백한 것을 지적할 것이다. 당신은 쿠키를 구울 오븐을 직접 만들지 않았다. 또한 버퍼를 휘젓거나 밀가루를 직접 갈아서 만들거나 초콜릿 칩을 만들지 않았다. "그건 당연하지"라고 당신의 생각을 들을 수 있다. 그것은 당신이 진짜 요리사라면 사실이지만, 당신이 베이킹 시뮬레이션 게임을 작성하는 프로그래머라면 어떻겠는가? 이 경우, 초콜릿 칩에서 오븐에 이르기까지 프로그램의 모든 컴포넌트를 작성하는 것에 대해 아무 생각도 하지 않을 것이다. 또는 재사용할 수 있는 코드를 찾아서 시간을 절약할 수도 있다. 아마도 사무실 동료가 요리 시뮬레이션 게임을 작성했고 멋진 오븐 코드를 가지고 있을 것이다. 아마도 당신은 필요로 하는 모든 것을 하지 않을 것이다. 그러나 그것을 수정하고 필요한 기능을 추가할 수는 있다.
당신이 당연하게 여겼던 또 다른 것은 당신이 직접 쿠키를 만들지 않고 쿠키 레시피를 따랐다는 것이다. 다시 말하지만, 그것은 말할 필요도 없다(당연한 것). 그러나 C++ 프로그래밍에서는 말할 필요가 있다. C++에서는 계속해서 발생하는 문제에 접근하는 표준 방법이 있지만, 많은 프로그래머는 각 디자인에서 이러한 전략을 계속해서 재창조하며 간다.
기존 코드를 사용한다는 아이디어는 새로운 것이 아니다. cout으로 무언가를 인쇄한 첫날부터 코드를 재사용하고 있다. 실제로 데이터를 화면에 인쇄하는 코드를 작성하지 않았다. 작업을 수행하기 위해 기존 cout 구현을 사용했다.
불행히도 모든 프로그래머가 사용 가능한 코드를 활용하는 것은 아니다. 당신의 디자인은 기존 코드를 고려하여 적절한 경우 그것을 재사용해야 한다.
Writing Reusable Code (재사용 가능한 코드 작성)
재사용이라는 디자인 테마는 사용하는 코드뿐만 아니라 작성하는 코드에도 적용된다. 클래스, 알고리즘과 데이터 구조체를 재사용할 수 있도록 프로그램을 디자인해야 한다. 당신과 당신의 동료는 현재 프로젝트와 앞으로의 프로젝트 모두에서 이러한 컴포넌트를 사용할 수 있어야 한다. 일반적으로 당면한 경우에만 적용할 수 있는 지나치게 구체적인 코드를 디자인하는 것은 피해야 한다.
C++에서 범용 코드를 작성하는 언어 기술 중 하나는 template(템플릿)을 사용하는 것이다. 앞에서 설명한 것처럼, ChessPiece를 저장하는 특정 ChessBoard 클래스를 작성하는 대신에 체스나 체커와 같은 모든 유형의 2차원 보드 게임에 사용할 수 있는 일반 GameBoard 클래스 템플릿 작성을 고려할 수 있다. 인터페이스를 하드 코딩하는 대신에 PieceType이라는 템플릿 파라미터로 저장할 말을 사용하도록 클래스 선언만 변경하면 된다. 클래스 템플릿은 다음과 같이 보일 수 있다. 이전에 이 구문을 본 적이 없더라도 걱정할 필요가 없다. Chapter 12, "Writing Generic Code with Templates"에서는 구문에 대해 자세히 설명한다.
template <typename PieceType>
class GameBoard
{
public:
void setPieceAt(size_t x, size_t y, PieceType* piece);
PieceType* getPieceAt(size_t x, size_t y) const;
bool isEmpty(size_t x, size_t y) const;
private:
// Private implementation details...
};
인터페이스에서 이 간단한 변경으로, 이제 모든 2차원 보드 게임에 사용할 수 있는 일반 게임 보드 클랙스가 생겼다. 코드 변경은 간단하지만, 코드를 효과적이고 효율적으로 구현할 수 있도록 디자인 단계에서 이러한 결정을 내리는 것이 더 중요하다.
Chapter 6에서는 재사용을 염두에 두고 코드를 디자인하는 방법에 대해 자세히 설명한다.
Reusing Designs (다자인 재사용)
C++ 언어를 배우는 것과 훌륭한 C++ 프로그래머가 되는 것은 매우 다른 일이다. 앉아서 C++ 표준을 읽고, 모든 것을 외우면 다른 사람과 마찬가지로 C++를 알게 될 것이다. 그러나 코드를 보고 자신의 프로그램을 작성하여 약간의 경험을 얻을 때까지는 반드시 좋은 프로그래머가 될 수는 없다. C++ 구문은 언어가 로우 형식으로 수행할 수 있는 작업을 정의하지만, 각 기능을 사용하는 방법에 대해서는 아무 것도 말하지 않는 이유 때문이다.
제빵사의 예에서 설명 했듯이, 당신이 만드는 모든 구운 식품에 대한 레시피를 재발명하는 것은 우스꽝스러울 것이다. 그러나 프로그래머는 종종 그들의 디자인에서 동등한 실수를 범한다. 프로그램을 디자인하기 위해 기존의 "레시피"나 pattern을 사용하는 대신 프로그램을 디자인할 때마다 이러한 기술을 재창조한다.
C++ 언어 사용에 대한 경험이 증가함에 따라, C++ 프로그래머는 언어 기능을 사용하는 그들만의 고유한 방법으로 개발한다. 또한 규모가 큰 C++ 커뮤니티는 일부는 공식적이고 일부는 비공식적으로 언어를 활용하는 몇 가지 표준 방법을 구축했다. 이 책 전체에서 디자인 기법(design techniques)과 디자인 패턴(design patterns)으로 알려진 언어의 이러한 재사용 가능한 애플리케이션에 대해 지적한다. 또한 Chapter 32, "Incorporating Design Techniques and Frameworks"과 Chapter 33, "Applying Design Patterns"에서는 거의 독점적으로 디자인 기법과 패턴에 중점을 둔다. 일부는 명백한 솔루션을 단순히 공식화하기 때문에 명백하게 보일 것이다. 다른 것들은 당신이 과거에 직면했던 문제에 대한 새로운 솔루션을 설명한다. 일부는 당신의 프로그램 조직에 대해 완전히 새로운 사고 방식을 제시한다.
예를 들어, 다른 컴포넌트들의 모든 오류를 로그 파일로 직렬화하는 단일 ErrorLogger 개체를 가지도록 체스 프로그램을 디자인할 수 있다. ErrorLogger 클래스를 다자인하려고 할 때, 프로그램에서 ErrorLogger 클래스의 단일 인스턴스만 가져야 한다는 것을 알게 된다. 그러나 프로그램의 여러 컴포넌트들도 이 ErrorLogger 인스턴스를 사용할 수 있기를 원한다. 즉, 이러한 컴포넌트들은 모두 동일한 ErrorLogger 서비스를 사용하려고 한다. 이러한 서비스 메커니즘을 구현하는 표준 전략은 종속성 주입(dependency injection)을 사용하는 것이다. 종속성 주입을 사용하면, 각 서비스에 대한 인터페이스를 만들고 컴포넌트에 필요한 인터페이스를 컴포넌트에 주입하게 된다. 따라서, 이 시점의 좋은 디자인은 종속성 주입 패턴을 사용하도록 지정하는 것이다.
특정 디자인 문제에서 이러한 솔루션 중 하나가 필요할 때를 인식할 수 있도록 이러한 패턴과 기법에 익숙해지는 것이 중요하다. 이 책에서 설명하는 것보다 C++에 적용할 수 있는 기법과 패턴은 더 많다. 여기에는 좋은 선택이 포함되어 있지만, 더 많고 다양한 패턴에 대해 디지인 패턴에 관한 책을 참조할 수 있다. 제안 사항은 Appendix B를 참조한다.
REUSING EXISTING CODE (기존 코드의 재사용)
숙련된 C++ 프로그래머는 프로젝트를 처음부터 시작하지 않는다. 표준 라이브러리, 오픈 소스 라이브러리, 작업 공간의 독점 코드 기반과 이전 프로젝트의 자체 코드 같은 다양한 소스로부터 코드를 구체화한다. 프로젝트에서 코드를 자유롭게 재사용해야 한다. 이 규칙을 최대한 활용하기 위해, 이 섹션에서는 먼저 재사용할 수 있는 다양한 유형의 코드를 설명한 다음, 기존 코드를 재사용하는 것과 직접 작성하는 것 사이의 장단점을 설명한다. 이 섹션의 마지막 부분에서는 코드를 직접 작성하지 않고 기존 코드를 재사용하기로 결정한 후 재사용할 라이브러리를 선택하기 위한 여러 지침에 대해 설명한다.
참고
코드를 재사용한다는 것은 기존 코드를 복사하여 붙여넣는 것을 의미하지 않는다. 사실, 그것은 정반대의 의미로 코드를 복제하지 않고 재사용하는 것이다.
A Note on Terminology (용어에 대한 참고)
코드 재사용의 장단점을 분석하기 전에, 관련 용어를 명시하고 재사용 가능한 코드의 유형을 분류하는 것이 도움이 된다. 재사용할 수 있는 세 가지 범주의 코드가 있다:
- 과거에 직접 작성한 코드
- 동료가 작성한 코드
- 현재 조직이나 회사 외부의 서드 파티가 작성한 코드
재사용하는 코드를 구성할 수 있는 몇 가지 방법도 있다:
- 독립 실행형 함수나 클래스. 자신의 코드나 동료의 코드를 재사용할 때 일반적으로 이러한 다양성을 접하게 된다.
- 라이브러리. 라이브러리는 XML 구문 분석과 같은 특정 작업을 수행하거나 암호화 같은 특정 도메인을 처리하는 데 사용되는 코드 모음이다. 라이브러리에서 일반적으로 발견되는 기능의 다른 예로는 스레드와 동기화 지원, 네트워킹과 그래픽이 있다.
- 프레임워크. 프레임워크는 프로그램을 설계하는 코드 모음이다. 예를 들어, MFC(Microsoft Foundation Classes) 라이브러리는 Microsoft Windows용 그래픽 사용자 인터페이스 애플리케이션을 만들기 위한 프레임워크를 제공한다. 프레임워크는 일반적으로 프로그램의 구조를 결정한다.
- 전체 애플리케이션. 프로젝트에 여러 애플리케이션이 포함될 수 있다. 새로운 전자 상거래 인프라를 지원하기 위해 웹 서버 프런트 엔드가 필요할 수 있다. 웹 서버와 같은 전체 서드 파티 애플리케이션을 소프트웨어와 함께 번들로 제공할 수 있다. 이 접근 방식은 전체 애플리케이션을 재사용한다는 점에서 코드 재사용을 극도로 끌어낸다.
참고
프로그램은 라이브러리를 사용하지만, 프레임워크는 맞춘다. 라이브러리는 특정 기능을 제공하는 반면, 프레임워크는 프로그램 설계와 구조의 기본이다.
자주 나타나는 또 다른 용어로는 application programming interface 또는 API이다. API는 특정 목적을 위한 라이브러리나 코드 본문에 대한 인터페이스이다. 예를 들어, 프로그래머는 종종 소켓 API, 즉 라이브러리 자체 대신 소켓 네트워킹 라이브러리에 대한 노출된 인터페이스를 참조한다.
참고
사람들은 API와 라이브러리라는 용어를 같은 의미로 사용하지만, 동일하지는 않다. 라이브러리는 구현을 참조하는 반면 API는 라이브러리에 게시된 인터페이스를 참조한다.
간결함을 위해, 이 장의 나머지 부분에서는 재사용 가능한 코드가 실제로 라이브러리인지, 프레임워크인지, 전체 애플리케이션인지, 사무실 동료의 임의 함수 모음인지에 관계없이 모든 재사용 가능한 코드를 라이브러리라는 용어를 사용하여 나타낸다.
Deciding Whether to Reuse Code or Write it Yourself (코드를 재사용할지 아니면 직접 작성할지 결정)
코드를 재사용하는 규칙은 추상화에서 이해하기 쉽다. 그러나 세부 사항에 관해서는 다소 모호하다. 코드를 재사용하는 적절한 시기와 재사용할 코드를 어떻게 알 수 있을까? 항상 절충점이 있으며, 결정은 특정 상황에 따라 다르다. 그러나 코드 재사용에는 일반적으로 몇 가지 장점과 단점이 있다.
Advantages to Reusing Code (코드 재사용의 이점)
코드를 재사용하면 당신과 당신의 프로젝트에 엄청난 이점을 제공할 수 있다:
- 필요한 코드를 작성하는 방법을 모르거나 시간을 정당화하지 못할 수 있다. 당신은 형식이 지정된 입력과 출력을 처리하는 코드를 정말로 작성할 것인가? 물론 아닐 것이다. 이것은 표준 C++ I/O 스트림 및/또는 std::format()을 사용하는 이유이다.
- 애플리케이션의 재사용하는 컴포넌트를 디자인할 필요가 없기 때문에 당신의 디자인은 더 간단해진다.
- 재사용하는 코드는 일반적으로 디버깅이 필요하지 않다. 라이브러리 코드는 이미 광범위하게 테스트되고 사용되었기 때문에 버그가 없다고 가정할 수 있다.
- 라이브러리는 코드에 대한 첫 번째 시도(직접 코드 작성)보다 더 많은 오류 조건을 처리한다.
- 프로젝트를 시작할 때 모호한 오류나 극단적인 경우를 잊어버리고 나중에 이러한 문제를 수정하는 데 시간을 낭비할 수 있다. 재사용하는 라이브러리 코드는 일반적으로 광범위하게 테스트되었고 이전에 많은 프로그래머가 사용했으므로 대부분의 오류를 올바르게 처리한다고 가정할 수 있다.
- 라이브러리는 종종 다른 하드웨어, 다른 운영 체제와 운영 체제 버전, 다른 그래픽 카드 등이 있는 광범위한 플랫폼에서 당신이 스스로 테스트할 수 있는 것보다 훨씬 더 테스트된다. 때때로 라이브러리에는 특정 플랫폼에서 작동하도록 하는 해결 방법이 포함되어 있다.
- 라이브러리는 일반적으로 잘못된 사용자 입력을 의심하도록 설계된다. 잘못된 요청이나 현재 상태에 적합하지 않은 요청은 일반적으로 적절한 오류 알림을 발생한다. 예를 들어, 데이터베이스에서 존재하지 않는 레코드를 찾거나 열러 있지 않은 데이터베이스에서 레코드를 읽으려는 요청은 라이브러리에서 잘 지정된 동작을 가질 것이다.
- 도메인 전문가가 작성한 코드를 재사용하는 것이 해당 영역에 대한 자체 코드를 작성하는 것보다 안전하다. 예를 들어, 보안 전문가가 아닌 한 자신의 보안 코드를 작성하려고 해서는 안 된다. 프로그램에 보안이나 암호화가 필요한 경우 해당 라이브러리를 사용한다. 그러한 성격의 코드에서 겉보기에 사소한 많은 세부 사항이 전체 프로그램의 보안을 손상시킬 수 있으며, 잘못 이해하면 전체 시스템이 손상될 수도 있다.
- 라이브러리 코드는 지속적으로 개선되고 있다. 코드를 재사용하면 직접 작업하지 않고도 이러한 개선의 이점을 얻을 수 있다. 실제로 라이브러리 작성자가 인터페이스를 구현과 적절히 분리했다면, 라이브러리와의 상호 작용을 변경하지 않고도 라이브러리 버전을 업그레이드하여 이러한 이점을 얻을 수 있다. 좋은 업그레이드는 인터페이스를 변경하지 않고 기본 구현을 수정한다.
Disadvantages to Reusing Code (코드 재사용의 단점)
불행하게도 코드 재사용에는 몇 가지 단점이 있다:
- 직접 작성한 코드를 사용하면, 작동 방식을 정확히 이해할 수 있다. 직접 작성하지 않은 라이브러리를 사용하는 경우, 인터페이스를 이해하고 올바른 사용법을 이해하는 데 시간을 투자해야 바로 사용할 수 있다. 프로젝트 시작 시, 이 추가 시간으로 인해 초기 설계와 코딩 속도가 느려질 수 있다.
- 자신만의 코드를 작성하면, 원하는 대로 정확하게 작동된다. 라이브러리 코드는 필요한 정확한 기능을 제공하지 않을 수 있다.
- 라이브러리 코드가 정확히 필요한 기능을 제공하더라도 원하는 성능을 제공하지 않을 수 있다. 성능은 일반적으로 좋지 않거나 특정 사용 사례에 적합하지 않거나 완전히 지정되지 않을 수 있다.
- 라이브러리 코드를 사용하면 지원 문제의 Pandora 상자가 나타난다. 라이브러리에서 버그를 발견하면, 어떻게 해야 합니까? 소스 코드에 액세스할 수 없는 경우가 많기 때문에 수정하고 싶어도 수정할 수 없는 경우가 많다. 라이브러리 인터페이스를 학습하고 라이브러리 사용에 이미 상당한 시간을 투자했다면, 해당 라이브러리를 포기하고 싶지는 않겠지만, 라이브러리 개발자를 설득하여 당신의 시간 스케줄에 따라 버그를 수정하도록 하는 것은 어려울 수 있다. 또한 타사 라이브러리를 사용하는 경우, 라이브러리 작성자가 라이브러리에 종속된 제품 지원을 중단하기도 전에 라이브러리에 대한 지원을 중단하면 어떻게 해야 합니까? 소스 코드를 얻을 수 없는 라이브러리를 사용하기로 결정하기 전에 이 문제에 대해 신중하게 생각해야 한다.
- 지원 문제 외에도 라이브러리에는 소스 코드 공개, 재배포 수수료(종종 바이너리 라이선스 수수료하고 함), 기여자 표시(credit attribution)와 개발 라이센스와 같은 항목이 포함된 라이선스 문제가 있다. 예를 들어, 일부 오픈 소스 라이브러리는 당신의 코드를 오픈 소스로 만들 것을 요구한다.
- 코드를 재사용하려면 신뢰 요소가 필요하다. 코드를 작성한 사람이 작업을 잘했다고 가정하여 신뢰해야 한다. 어떤 사람들은 소스 코드의 모든 라인을 포함하여 프로젝트의 모든 측면을 컨트롤하고 싶어한다.
- 라이브러리를 새로운 버전으로 업그레이드하면 문제가 발생할 수 있다. 업그레이드로 인해 버그가 발생할 수 있으며, 이는 제품에 치명적인 결과를 초래할 수 있다. 성능 관련 업그레이드는 특정 경우에 성능을 최적화할 수 있지만, 특정 사용 사례에서는 성능이 저하될 수 있다.
- 바이너리 전용 라이브러리를 사용할 때 컴파일러를 새로운 버전으로 업그레이드하면 문제가 발생할 수 있다. 라이브러리 공급 업체에서 새로운 버전의 컴파일러와 화환되는 바이너리를 제공하는 경우에만 컴파일러를 업그레이드할 수 있다.
Putting It Together to Make a Decision (종합하여 결정)
이제 코드 재사용의 용어, 장점과 단점에 익숙해졌으므로 코드를 재사용할지 여부를 결정할 준비가 되었다. 종종, 그 결정은 명확하다. 예를 들어 Microsoft Windows용 C++에서 그래픽 사용자 인터페이스(GUI)를 작성하려면, MFC나 Qt와 같은 프레임워크를 사용해야 한다. Windows에서 GUI를 생성하기 위한 기본 코드의 작성 방법을 모를 수 있으며, 더 중요한 것은 GUI를 배우기 데 시간을 낭비하고 싶지 않다는 것이다. 이 경우 프레임워크를 사용하면 수년간의 노력을 절약할 수 있다.
그러나 다른 경우에는 그 선택이 덜 명확하다. 예를 들어 라이브러리에 생소하고 간단한 데이터 구조체만 필요한 경우, 며칠 안에 작성할 수 있는 하나의 컴포넌트만 재사용하도록 라이브러리를 배우는 것은 시간 낭비일 수 있다.
궁극적으로, 자신의 특정한 요구 사항에 따라 결정을 내려야 한다. 그것은 종종 스스로 그것을 작성하는 데 걸리는 시간과 문제를 해결하기 위해 라이브러리를 사용하는 방법을 찾고 배우는 데 필요한 시간 사이의 절충점으로 귀결된다. 이전에 나열한 장점과 단점이 특정 사례에 어떻게 적용되는지 신중하게 고려하여 어떤 요소가 당신에게 가장 중요한지 결정한다. 마지막으로 당신이 항상 생각을 바꿀 수 있다는 것을 기억한다. 당신이 추상화를 올바르게 처리했다면 비교적 쉬울 수도 있다.
Guidelines for Choosing a Library to Reuse (재사용할 라이브러리 선택 지침)
라이브러리, 프레임워크, 동료의 코드, 전체 애플리케이션 또는 자신의 코드를 재사용하기로 결정한 경우, 재새용할 올바른 코드를 선택하기 위해 유념해야 하는 몇 가지 지침이 있다.
Understand the Capabilities and Limitations (기능과 제한 사항의 이해)
사간을 내서 코드에 익숙해져야 한다. 그것의 기능과 한계를 모두 이해하는 것이 중요하다. 문서와 게시된 인터페이스나 API로 시작한다. 이상적으로 코드 사용 방법을 이해하는 데 충분하다. 그러나 라이브러리가 인터페이스와 구현을 명확하게 구분하지 않는 경우, 소스 코드가 제공되는 경우 소스 코드 자체를 검토해야 한다. 또한 코드를 사용하고 코드의 복잡성을 설명할 수 있는 다른 프로그래머와 이야기 해야 한다. 기본 기능을 배우는 것부터 시작해야 한다. 라이브러리라면 어떤 기능을 제공하는가? 그것이 프레임워크라면, 코드는 어떻게 적용하는가? 어떤 클래스에서 파생해야 하는가? 어떤 코드를 직접 작성해야 하는가? 또한 코드 유형에 따라 특정 문제를 고려해야 한다.
다음은 라이브러리를 선택할 때 주의해야 할 몇 가지 사항이다:
- 라이브러리는 멀티 스레드에 안전한가?
- 라이브러리가 그것을 사용하는 코드에 특정 컴파일러 설정을 적용해야 하는가? 만약 그렇다면, 당신의 프로젝트에서 그것을 받아들어질 수 있는가?
- 라이브러리가 다른 어떤 라이브러리에 의존하는가?
또한, 특정 라이브러리에 대해 더 자세한 연구를 수행해야 할 수도 있다:
- 어떠한 초기화와 정리에 대한 호출이 필요한가?
- 클래스에서 파생하는 경우, 어떤 생성자를 호출해야 하는가? 어떤 가상 메소드를 재정의해야 하는가?
- 메모리 포인터를 반환하는 호출이라면, 메모리 해제를 누가 담당하는가? 호출자 또는 라이브러리? 라이브러리가 담당하는 경우, 메모리는 언제 해제되는가? 라이브러리에서 할당한 메모리를 관리하기 위해 스마트 포인터(Chapter 7, "Memory Management")를 사용할 수 있는지 알아보는 것이 좋다.
- 모든 호출로부터 반환되는 값(값이나 레퍼런스)은 무엇인가?
- 모든 발생 가능한 예외는 무엇인가?
- 라이브러리 호출은 어떤 오류 조건을 확인하고, 무엇을 가정하는가? 오류는 어떻게 처리되는가? 클라이언트 프로그램은 오류에 대해 어떻게 알림을 받는가? 메시지 상자를 표시하거나, stderr/cerr이나 stdout/cout에 메세지를 출력하거나 프로그램을 종료하는 라이브러리는 사용하지 않는다.
Understand the Learning Cost (학습 비용의 이해)
학습 비용은 개발자가 라이브러리 사용 방법을 배우는 데 걸리는 시간이다. 이는 라이브러리를 사용하기 시작할 때의 초기 비용이 아니라 시간이 지남에 따라 반복되는 비용이다. 새로운 팀원이 프로젝트에 참여할 때마다 해당 라이브러리의 사용하는 방법을 배워야 한다.
특정 라이브러리의 경우 이 비용이 상당할 수 있다. 따라서 잘 알려진 라이브러리에서 필요한 기능을 찾으면 이국적이고 덜 알려진 라이브러리를 사용하는 것보다 잘 알려진 라이브러리를 사용하는 것이 좋다. 예를 들어, Standard Library에서 필요한 데이터 구조체나 알고리즘을 제공하는 경우 다른 라이브러리를 사용하는 대신 해당 라이브러리를 사용하는 것이 좋다.
Understand the Performance (성능의 이해)
라이브러리나 기타 코드가 제공하는 보장하는 성능을 아는 것이 중요하다. 특정 프로그램이 성능에 민감하지 않더라도 사용하는 코드가 특정 용도에 대해 성능이 좋지 않은지 확인해야 한다.
Big-O Notation (빅오 표기법)
프로그래머는 일반적으로 big-O 표기법을 사용하여 알고리즘과 라이브러리 성능에 대해 논의하고 문서화한다. 이 섹션에서는 불필요한 수학을 많이 사용하지 않고 알고리즘 복잡도 분석과 big-O 표기법의 일반적인 개념을 설명한다. 이러한 개념에 이미 익숙하다면 이 섹션을 건너뛸 수 있다.
Big-O 표기법은 절대적 성능이 아닌 상대적 성능을 나태낸다. 예를 들어, 알고리즘이 300밀리초와 같은 특정 시간 동안 실행된다고 말하는 대신, big-O 표기법은 입력 크기가 증가함에 따라 알고리즘이 어떻게 수행하는지를 나타낸다. 입력 크기의 예로는 정렬 알고리즘으로 정렬할 항목 수, 키 조회 중 해시 테이블의 요소 수, 디스크 간에 복사할 파일 크기 등이 있다.
참고
Big-O 표기법은 속도가 입력에 따라 달라지는 알고리즘에만 적용된다. 입력을 받지 않거나 실행 시간이 무작위인 알고리즘에는 적용되지 않는다. 실제로 대부분의 관심 알고리즘의 실행 시간은 입력에 따라 달라지므로 이 제한은 중요하지 않다.
좀 더 형식적으로 말하면, big-O 표기법은 알고리즘의 복잡성이라고도 하는 입력 크기의 함수로 알고리즘의 실행 시간을 나타낸다. 들리는 것처럼 복잡하지 않다. 예를 들어, 알고리즘은 두 배의 요소를 처리하는 데 두 배의 시간이 걸릴 수 있다. 따라서 200개의 요소를 처리하는 데 1초가 걸린다면 400개의 요소를 처리하는 데 2초, 800개의 요소를 처리하느 데 4초가 걸린다. Figure 4-3은 이를 그래픽적으로 보여준다. 이러한 알고리즘의 복잡성은 입력 크기의 선형 함수라고 한다. 왜냐하면 그래픽적으로 직선에 의해 표시되기 때문이다.
Big-O 표기법은 알고리즘의 선형 성능을 O(n)과 같이 요약한다. O는 big-O 표기법을 사용하고 있음을 의미하고 n은 입력 크기를 나타낸다. O(n)은 알고리즘 속도가 직접적으로 입력 크기의 선형 함수임을 나타낸다.
물론 모든 알고리즘이 입력 크기와 관련하여 선형적인 성능을 갖는 것은 아니다. 다음 표에는 성능이 가장 좋은 순서대로 일반적인 복잡성이 요약되어 있다:
알고리즘 복잡성 | BIG-O 표기법 | 설명 | 예제 알고리즘 |
---|---|---|---|
Constant (상수) | $$O(1)$$ | 실행 시간은 입력 크기와 무관하다. | 배열에서 단일 요소에 접근 |
Logarithmic (로그) | $$O(log n)$$ | 실행 시간은 입력 크기의 밑이2인 로그인 함수이다. | 이진 검색을 사용하여 정렬된 목록에서 요소 찾기 |
Linear (선형) | $$O(n)$$ | 실행 시간은 입력 크기에 정비려한다. | 정렬되지 않은 목록에서 요소 찾기 |
Linear Logartithmic (선형 로그) | $$O(nlog n)$$ | 실행 시간은 입력 크기의 로그 함수의 선형 시간 함수의 곱이다. | 병합 정렬 |
Quadratic (2차) | $$O(n^{2})$$ | 실행 시간은 입력 크기의 제곱의 함수이다. | 선택 정렬과 같은 느린 정렬 알고리즘 |
Exponential | $$O(2^{n})$$ | 실행 시간은 입력 크기의 지수 함수이다. | 최적화된 여행 판매원의 문제 |
성능을 절대 숫자 대신에 입력 크기의 함수로 지정하면 두 가지 이점이 있다.
- 플랫폼에 독립적이다. 한 컴퓨터에서 코드 조각을 200밀리초 안에 실행되도록 지정하면, 두 번째 컴퓨터에서는 속도에 대해 아무 것도 말할 수 없다. 또한 동일한 부하로 동일한 컴퓨터에서 실행하지 않고, 두 개의 다른 알고리즘을 비교하는 것도 어렵다. 반면 입력 크기의 함수로 지정된 성능은 모든 플랫폼에 적용할 수 있다.
- 입력 크기의 함수로서 성능은 하나의 사양으로 알고리즘에 대한 모든 가능한 입력을 포함한다. 알고리즘이 실행되는 데 걸리는 특정 시간(초)은 하나의 특정 입력만 다루고 다른 입력에 대해서는 아무 것도 말하지 않는다.
Tips for Understanding Performance (성능 이해를 위한 팁)
이제 big-O 표기법에 익숙해졌으므로 대부분의 성능 문서를 이해할 준비가 되었다. 특히 C++ Standard Library는 big-O 표기법을 사용하여 알고리즘과 데이터 구조 성능을 설명한다. 그러나 big-O 표기법은 때때로 불충분하거나 오해의 소지가 있다. Big-O 성능 사양에 대해 생각할 때마다 다음 사안을 고려한다:
- 알고리즘이 2배의 데이터를 처리하는 데 2배의 시간이 걸린다면, 애초에 얼마나 오래 걸렸는지에 대해서는 어떤것도 말할 수 없다. 알고리즘이 잘못 잘성되었지만 확장성이 좋다고, 여전히 사용하고 싶은 것은 아니다. 예를 들어 알고리즘이 불필요한 디스크 액세스를 수행한다고 가정해보자. 그것은 아마도 big-O 시간에 영향을 미치지 않을 것이지만, 전반적인 성능에는 매우 나쁠 것이다.
- 그런 점에서 두 알고리즘을 동일한 big-O 실행 시간으로 비교하는 것은 어렵다. 예를 들어, 두 개의 다른 정렬 알고리즘이 모두 O(n log n)이라고 주장하는 경우, 자체 테스트를 실행하지 않고는 어느 것이 더 빠른지 말하기 어렵다.
- Big-O 표기법은 입력 크기가 무한대로 증가함에 따라 알고리즘의 시간 복잡도를 점근적으로 설명한다. 작은 입력의 경우 big-O 시간은 큰 오해의 소지를 가질 수 있다. O(n*n) 알고리즘은 실제로 작은 입력 크기에서 O(log n) 알고리즘보다 더 나은 성능을 보일 수 있다. 결정을 내리기 전에 예상되는 입력 크기를 고려한다.
Big-O 특성을 고려하는 것 외에도 알고리즘 성능의 다른 측면을 살펴봐야 한다. 다음은 명심해야 할 몇 가지 지침이다:
- 라이브러리 코드의 특정 부분을 얼마나 자주 사용할 것인지 고려해야한다. 어떤 사람들은 90/10 규칙이 유용하다고 생각한다. 대부분 프로그램 실행 시간의 90%는 코드의 10%만 사용하고 있다(Hennessy and Patterson, Computer Architecture: A Quantitative Approach, Fifth Edition, 2011, Morgan Kaufmann). 사용하려는 라이브러리 코드가 코드에서 자주 실행되는 10% 범주에 속하는 경우 성능 특성을 주의 깊게 분석해야 한다. 반면에 종종 무시되는 코드의 90%에 해당하는 경우 전체 프로그램 성능에 큰 이점이 없기 때문에 성능을 분석하는 데 많은 시간을 할애하지 않아야 한다. Chapter 29, "Writing Efficient C++"에서는 코드에서 성능 병목 현상을 찾는 데 도움이 되는 도구인 프로파일러에 대해 설명한다.
- 문서를 신뢰하지 않는다. 항상 성능 테스트를 실행하여 라이브러리 코드가 허용 가능한 성능 특성을 제공하는지 확인한다.
Understand Platform Limitations (플랫폼 제한의 이해)
라이브러리 코드를 사용하기 전에 코드가 실행되는 플랫폼을 이해해야 한다. 크로스 플랫폼 애플리케이션을 작성하려는 경우, 선택한 라이브러리도 플랫폼 간 이식이 가능한지 확인해야 한다. 당연하게 들릴지 모르지만, 크로스 플랫폼이라고 주장하는 라이브러리라도 플랫폼마다 미묘한 차이가 있을 수 있다.
또한 플랫폼에는 서로 다른 운영 체제 뿐만 아니라 동일한 운영 체제의 다른 버전도 포함된다. 운영 체제 Solaris 8, Solaris 9 그리고 Solaris 10에서 실행되어야 하는 애플리케이션을 작성하는 경우, 사용하는 모든 라이브러리도 해당 릴리스를 모두 지원하는지 확인해야 한다. 운영 체제 버전 간에 상위 버전이나 하위 버전 호환성이 있다고 가정할 수 없다. 즉 라이브러리가 Solaris 9에서 실행된다고 해서 그것이 Solaris 10에서 실행되는 것은 아니고, 그 반대의 경우도 마찬가지이다.
Understand Licensing (라이선스의 이해)
타사 라이브러리를 사용하면 종종 복잡한 라이선스 문제가 발생한다. 때때로 타사 공급업체의 라이브러리 사용에 대해 라이선스 비용을 지불해야 하는 경우도 있다. 수출 제한을 비롯한 기타 라이선스 제한이 있을 수도 있다. 또한 오픈 소스 라이브러리는 오픈 소스가 되기 위해 링크된 코드가 필요한 라이센스에 따라 배포되는 경우가 있다. 오픈 소스 라이브러리에서 일반적으로 사용하는 여러 라이선스를 이 장의 뒷 부분에서 설명한다.
주의 사항
개발한 코드를 배포하거나 판매할 계획이라면 사용하는 타사 라이브러리의 라이선스 제한 사항을 이해해야 한다. 확신이 서지 않으면 지적 재산을 전문으로 하는 법률 전문가에게 문의한다.
Understand Support and Know Where to Find Help (지원의 이해와 도움말 위치 파악)
라이브러리를 사용하기 전에 버그 제출 프로세스를 이해하고 버그를 수정하는 데 시간이 얼마나 걸리는지 알고 있어야 한다. 가능한 경우 라이브러리가 얼마나 오래 지원될 것인지 결정하여 그에 따라 계획을 세울 수 있다.
흥미롭게도, 조직 내에서 라이브러리를 사용하는 경우에도 지원 문제가 발생할 수 있다. 회사의 다른 부서에 있는 동료에게 라이브러리의 버그를 수정하도록 설득하는 것이, 다른 회사의 낯선 사람에게 동일한 작업을 수행하도록 설득하는 것만큼 어려울 수 있다. 사실 당신은 유료 고객이 아니기 때문에 더 어렵다고 생각할 수도 있다. 내부 라이브러리를 사용하기 전에 조직 내의 정치 및 조직 문제를 이해하고 있는지 확인한다.
전체 애플리케이션을 재사용하는 경우, 지원 문제가 훨씬 더 복잡해질 수 있다. 고객이 번들 웹 서버에 문제가 발생하면 귀하나 웹 서버 공급업체에 문의해야 합니까? 소프트웨어를 출시하기 전에 이 문제를 해결했는지 확인한다.
라이브러리와 프레임워크를 사용하는 것이 처음에는 어려울 수 있다. 다행하게도 다양한 지원 방법이 있다. 먼저 라이브러리와 함께 제공되는 설명서를 참조한다. Standard Library나 MFC와 같이 널리 사용되는 라이브러리의 경우, 해당 주제에 대한 좋은 책을 찾을 수 있을 것이다. 사실, Standard Library에 대한 도움을 받으려면 Chapter 16-25를 참조한다. 책과 제품 설명서에서 다루지 않는 특정 질문이 있는 경우, 웹에서 검색해 본다. 즐겨찾는 검색 엔진에 질문을 입력하여 라이브러리에 대해 설명하는 웹 페이지를 찾는다. 예를 들어, introduction to C++ Standard Library라는 문구를 검색하면, C++ 및 Standard Library에 대한 수백 개의 웹 사이트를 찾을 수 있다. 또한 많은 웹 사이트에는 등록할 수 있는 특정 주제에 대한 자체 뉴스 그룹이나 포럼이 포함되어 있다.
주의 사항
웹에서 읽는 모든 것은 믿지 않는다. 웹 페이지는 인쇄된 책 및 문서와 동일한 검토 프로세스를 거칠 필요가 없으며, 부정확한 내용이 포함될 수 있다.
Prototype (프로토타입)
새로운 라이브러리나 프레임워크를 처음 사용하는 경우, 빠르게 프로토타입을 작성하는 것이 좋다. 코드를 시도하는 것은 라이브러리의 기능에 익숙해지는 가장 좋은 방법이다. 라이브러리의 기능과 제한 사항에 대해 잘 알 수 있도록 프로그램 디자인을 다루기 전에도 라이브러리를 실험해 보는 것을 고려해야 한다. 이 경험적 테스트를 통해 라이브러리의 성능 특성도 확인할 수 있다.
프로토타입 애플리케이션이 최종 애플리케이션과 전혀 같아 보이지 않더라도 프로토타이핑에 소요되는 시간은 낭비가 아니다. 실제 애플리케이션의 프로토타입을 작성해야 한다고 강요하지 않는다. 사용하려는 라이브러리 기능을 테스트하는 더미 프로그램을 작성한다. 요점은 라이브러리에 익숙해지는 것뿐이다.
주의 사항
시간 제약으로 인해, 프로그래머는 때때로 그들의 프로토타입이 최종 제품으로 변형되는 것을 발견한다. 만약 당신이 최종 제품의 기반이 충분하지 않은 프로토타입을 함께 해킹했다면 그렇게 사용되지 않도록 해야 한다.
Open-Source Libraries (오픈 소스 라이브러리)
오픈 소스 라이브러리는 점점 더 인기 있는 재사용 가능한 코드 클래스이다. 오픈 소스의 일반적인 의미는 소스 코드를 누구나 볼 수 있다는 것이다. 모든 배포판에 소스 코드를 포함하는 것에 대한 공식적인 정의와 법적 규칙이 있지만, 오픈 소스 소프트웨어에 대해 기억해야 할 중요한 점은 누구나(당신을 포함하여) 소스 코드를 볼 수 있다는 것이다. 오픈 소스는 단순한 라이브러리 이상에 적용된다는 점에 유의한다. 사실 가장 유명한 오픈 소스 제품은 아마도 안드로이드 운영체제일 것이다. Linux는 또 다른 오픈 소스 운영체제이다. Google Chrome과 Mozilla Firefox는 유명한 오픈 소스 웹 브라우저의 두 가지 예이다.
The Open-Source Movements (오픈 소스 운동)
불행하게도, 오픈 소스 커뮤니티에는 용어에 혼란이 있다. 첫째, 운동(일부는 두 개의 분리되지만, 유사한 운동이라고 한다)에 대해 두 개의 경쟁적인 이름이 있다. Richard Stallman과 GNU 프로젝트는 free software라는 용어를 사용한다. free라는 용어가 완제품을 비용 없이 사용할 수 있어야 한다는 것을 의미하지는 않는다. 개발자는 원하는 만큼 비용을 청구할 수 있다. 대신, free라는 용어는 사람들이 소스 코드를 검토하고, 소스 코드를 수정하고, 소프트웨어를 재배포할 수 있는 자유를 의미한다. free beer의 free보다는 free speech의 free를 생각하면 된다. www.gun.org에서 Richard Stallman과 GNU 프로젝트에 대해 자세히 알아볼 수 있다.
Open Source Initiative는 소스 코드를 사용할 수 있어야 하는 소프트웨어를 설명하기 위해 open-source software라는 용어를 사용한다. 자유 소프트웨어와 마찬가지로 오픈 소스 소프트웨어는 제품이나 라이브러리를 비용 없이 사용할 수 있도록 요구하지 않는다. 그러나 자유 소프트웨어와의 중요한 차이점은 오픈 소스 소프트웨어가 이를 사용, 수정 및 재배포할 수 있는 자유를 제공하기 위해 필요하지 않다는 것이다. www.opensource.org에서 Open Source Initiative에 대해 자세히 알아볼 수 있다.
오픈 소스 프로젝트에는 사용할 수 있는 많은 라이선스 옵션이 있다. 예를 들어, 프로젝트는 GPL(GNU Public License) 버전 중 하나를 사용할 수 있다. 그러나 GPL에서 라이브러리를 사용하려면 GPL에서도 자체 제품을 오픈 소스로 만들어야 한다. 반면에 오픈 소스 프로젝트는 Boost Software License, BSD(Berkeley Software Distribution) License, MIT License, Apache License 등과 같은 라이선스를 사용할 수 있으며, 이를 통해 폐쇄된 소스 제품에서 오픈 소스 프로젝트를 사용할 수 있다. 이러한 라이선스 중 일부는 버전이 다르다. 예를 들어, 실제로 BSD 라이선스에는 네 가지 버전이 있다. 오픈 소스 프로젝트의 또 다른 옵션은 Creative Commons License의 6가지 유형 중 하나를 사용하는 것이다.
일부 라이선스의 경우 최종 제품에 라이브러리 라이선스를 포함해야 한다. 일부 라이선스는 라이브러리를 사용할 때 저작자 표시가 필요하다. 결론적으로 모든 라이선스에는 비공개 소스 프로젝트에서 라이브러리를 사용하려는 경우 이해해야 하는 중요한 세부 사항이 포함되어 있다. opensource.org/licenses 웹사이트는 승인된 오픈 소스 라이선스에 대한 철저한 개요를 제공한다.
"open-source"라는 이름이 "free software"보다 덜 모호하기 때문에 이 책에서는 소스 코드를 사용할 수 있는 제품과 라이브러리를 업급하기 위해 "open-source"를 사용한다. 이름의 선택은 자유 소프트웨어 철학보다 오픈 소스 철학을 지지한다는 의미가 아니다. 단지 이해를 돕기 위한 것이다.
Finding and Using Open-Source Libraries (오픈 소스 라이브러리를 찾고 사용하기)
용어에 관계없이 오픈 소스 소프트웨어를 사용하면 놀라운 이점을 얻을 수 있다. 주요 이점은 기능성이다. XML 구문 분석과 플랫폼 간 오류 로깅에서 인공 신경망을 사용한 딥 러닝과 데이터 마이닝에 이르기까지 다양한 작업에 사용할 수 있는 오픈 소스 C++ 라이브러리가 많이 있다.
무료 배포와 라이선스를 제공하기 위해 오픈 소스 라이브러리가 필요한 것은 아니지만 많은 오픈 소스 라이브러리를 금전적 비용 없이 사용할 수 있다. 일반적으로 오픈 소스 라이브러리를 사용하면 라이선스 비용을 절약할 수 있다.
마지막으로, 정확한 요구 사항에 맞게 오픈 소스 라이브러리를 수정할 수 있는 경우가 많지만, 항상 그런 것은 아니다.
대부분의 오픈 소스 라이브러리는 웹에서 사용할 수 있다. 예를 들어, open-source C++ library XML parsing을 검색하면 C와 C++ XML 라이브러리에 대한 링크 목록이 생성된다. 또한 다음을 포함하여 검색을 시작할 수 있는 몇 가지 오픈 소스 포털도 있다:
Guidelines for Using Open-Source Code (오픈 소스 코드 사용을 위한 지침)
오픈 소스 라이브러리는 몇 가지 고유한 문제를 제시하며 새로운 전략을 요구한다. 첫째, 오픈 소스 라이브러리는 일반적으로 "free(자유로운)" 시간에 사람들이 작성하고 있다. 소스베이스는 일반적으로 개발이나 버그 수정에 참여하고 기여하려는 모든 프로그래머에게 제공된다. 훌륭한 프로그래밍 시민으로서 오픈 소스 라이브러리의 이점을 누리고 있다면, 오픈 소스 프로젝트에 기여하려고 노력해야 한다. 회사에서 일하는 경우 회사의 수익으로 직접 연결되지 않기 때문에 경영진으로부터 이 아이디어에 대한 저항을 받을 수 있다. 그러나 회사 이름 노출과 오픈 소스 운동에 대한 회사의 인식된 지원과 같은 간접적인 이점을 통해 이러한 활동을 추구할 수 있어야 한다고 경영진을 설득할 수 있다.
둘째, 개발의 분산된 특성과 단일 소유권 부족으로 인해 오픈 소스 라이브러리는 종종 지원 문제를 야기한다. 라이브러리에서 버그 수정이 절실히 필요한 경우, 다른 사람이 수정하기를 기다리는 것보다 직접 수정하는 것이 더 효율적이다. 버그를 수정하는 경우 해당 수정 사항을 라이브러리의 공개 코드베이스에 다시 저장해야 한다. 일부 라이선스는 그렇게 하도록 요구하기도 한다. 버그를 수정하지 않더라도 발견한 문제를 보고하여, 다른 프로그래머가 같은 문제를 겪으면서 시간을 낭비하지 않도록 해야 한다.
The C++ Standard Library (C++ 표준 라이브러리)
C++ 프로그래머가 사용할 가장 중요한 라이브러리는 C++ Standard Library이다. 이름에서 알 수 있듯이, 이 라이브러리는 C++ 표준의 일부이므로 표준을 준수하는 모든 컴파일러는 이 라이브러리를 포함해야 한다. Standard Library는 모놀리식(monolithic:단일, 하나)이 아니다. 여기에는 여러 개의 상이한 컴포넌트가 포함되어 있으며, 그 중 일부는 이미 사용하고 있다. 심지어 당신은 그것들이 핵심 언어의 일부라고 가정했을 수도 있다. Chapter 16-25까지는 Standard Library에 대해 더 자세히 설명한다.
C Standard Library (C 표준 라이브러리)
C++는 대부분 C의 상위 집합이기 때문에, C Standard Library를 계속 사용할 수 있다. 그 기능에는 abs(), sqrt(), pow()와 같은 수학 함수와 assert(), errno와 같은 오류 처리 헬퍼가 포함된다. 또한 strlen(), strcpy()와 같은 문자열(string)로 문자 배열(character array)을 조작하기 위한 C Standard Library 함수와 printf(), scanf()와 같은 C 스타일 I/O 함수는 모두 C++에서 사용할 수 있다.
참고
C++는 C보다 더 좋은 문자열과 I/O 지원을 제공한다. C++에서 C 스타일 문자열과 I/O 루틴을 사용할 수 있지만, C++ 문자열 및 형식화(Chapter 2, "Working with Strings and String Views")와 I/O 스트림(Chapter 13, "Demystifying C++ I/O")을 선호하기 때문에 이를 피해야 한다..
Chapter 1에서는 C 헤더 파일이 C++에서 서로 다른 이름을 갖는다는 것을 설명한다. 이러한 이름은 이름 충돌을 일으킬 가능성이 적으므로 C 라이브러리 이름 대신 사용해야 한다. 예를 들어, C++에서 C 헤더 파일 <stdio.h>의 기능이 필요한 경우, <stdio.h> 대신에 <cstdio>를 포함하는 것이 좋다. C 라이브러리에 대한 자세한 내용은 Appendix B를 참조하여 Standard Library Reference를 참조한다.
Deciding Whether or Not to Use the Standard Library (표준 라이브러리 사용 여부 결정)
표준 라이브러리는 기능, 성능과 직교성을 우선적으로 고려하여 설계되었다. 그것을 사용하는 이점은 상당하다. 연결 리스트이나 균형 이진 트리의 구현에서 포인터 오류를 추적하거나 제대로 정렬되지 않는 정렬 알고리즘을 디버그해야 한다고 상상해 보라. Standard Library를 올바르게 사용하면, 그런 종류의 코딩을 직접 수행할 필요가 거의 없을 것이다. 또 다른 이점은 대부분의 C++ 개발자가 Standard Library에서 제공하는 기능으로 작업하는 방법을 알고 있다는 것이다. 따라서 프로젝트에서 Standard Library를 사용하면, 새로운 팀 구성원은 상당한 학습 비용이 발생할 수 있는 타사 라이브러리를 사용하는 것보다 더 빨리 적응할 것이다. Chapter 16-25에서는 Standard Library 기능에 대한 자세한 정보를 제공한다.
DESIGNING A CHESS PROGRAM (체스 프로그램 디자인)
이 섹션에서는 간단한 체스 게임 애플리케이션의 맥락에서 C++ 프로그램을 설계하는 체계적인 접근 방식을 소개한다. 완전한 예를 제공하기 위해, 일부 단계는 이후 장에서 다루는 개념을 참조한다. 디자인 프로세스에 대한 개요를 얻으려면 지금 이 예제를 읽어야 하지만, 이후 장을 마친 후에 다시 읽는 것을 고려할 수도 있다.
Requirements (요구 사항)
디자인을 시작하기 전에, 프로그램의 기능과 효울성에 대한 명확한 요구 사항을 보유하는 것이 중요하다. 이상적으로, 이러한 요구 사항은 "요구 사항 명세서"라는 형태로 문서화 된다. 체스 프로그램에 대한 요구 사항에는 다음과 같은 유형의 사양을 포함하지만, 더 상세하고 더 많은 수를 포함할 수 있다:
- 프로그램은 체스의 표준 규칙을 지원해야 한다.
- 프로그램은 두 명의 휴먼 플레이어를 지원해야 한다. 프로그램은 인공 지능 컴퓨터 플레이어를 제공해서는 안된다.
- 프로그램은 텍스트 기반 인터페이스를 제공해야 한다.
- 프로그램은 게임 보드와 체스 말을 일반 텍스트로 렌더링해야 한다.
- 플레이어는 체스판의 위치를 나타내는 숫자를 입력하여, 자신의 움직임을 표현해야 한다.
사용자가 기대하는 대로 작동할 수 있도록 프로그램을 요구 사항에 따라 디자인한다.
Design Steps (디자인 단계)
일반적인 것부터 구체적인 것까지 프로그램을 디자인할 때 체계적인 접근 방식을 취해야 한다. 다음 단계는 모든 프로그램에 항상 적용되는 것은 아니지만, 일반적인 지침을 제공한다. 디자인에는 다이어그램과 테이블이 적절하게 포함되어야 한다. UML은 다이어그램을 만들기 위한 산업 표준이다. 간단한 소개는 Appendix D를 참조할 수 있지만, 간단히 말해서 UML은 클래스 다이어그램, 시퀀스 다이어그램 등과 같이 소프트웨어 디자인을 문서화하는 데 사용할 수 있는 다양한 표준 다이어그램을 정의한다. 해당되는 경우 UML이나 최소한 UML과 유사한 다이어그램을 사용하는 것이 좋다. 그러나 UML 구문을 엄격하게 고수하는 것을 지지하지 않느다. 왜냐하면 명확하고 이해하기 쉬운 다이어그램을 갖는 것이 구문상 올바른 다이어그램을 갖는 것보다 더 중요하기 때문이다.
Divide the Program into Subsystems (프로그램을 서브 시스템으로 분할)
첫 번째 단계는 프로그램을 일반적인 기능의 서브 시스템으로 나누고, 서브 시스템 간에 인터페이스와 상호 작용을 지정하는 것이다. 이 시점에서는, 데이터 구조와 알고리즘 또는 클래스의 세부 사항에 대해 걱정할 필요가 없다. 당신은 단지 프로그램의 다양한 부분과 그 상호 작용에 대한 일반적인 느낌을 얻으려고 노력하면 된다. 서브 시스템의 상위 레벨 동작이나 기능, 서브 시스템에서 다른 서브 시스템으로 노출된 인터페이스, 다른 서브 시스템에서 이 서브 시스템이 소비하거나 사용하는 인터페이스를 나타내는 테이블에 서브 시스템을 나열할 수 있다. 이 체스 게임에 권장되는 디자인은 MVC(Model-View-Controller) 패러다임을 사용하여 데이터를 저장하는 것과 데이터를 표시하는 것을 명확하게 구분하는 것이다. 이 패러다임은 많은 애플리케이션에서 일반적인 데이터 집합, 해당 데이터에 대한 하나 이상의 보기와 데이터 조작을 처리한다는 개념을 모델링한다. MVC에서, 데이터 집합을 모델이라고 하고, 뷰는 모델의 특별한 시각화이며, 컨트롤러는 일부 이벤트에 대한 응답으로 모델을 변경하는 코드 조각이다. MVC의 세 가지 컴포넌트는 피드백 루프로 상호 작용한다. 즉, 컨트롤러가 작업을 처리하여 모델을 조정하고 뷰가 변경된다. 컨트롤러는 UI 요소와 같은 뷰를 직접 수정할 수도 있다. Figure 4-4는 이러한 상호 작용을 시각화한다. 이 패러다임을 사용하면, 다른 컴포넌트를 수정할 필요 없이 한 컴포넌트를 수정할 수 있으므로 서로 다른 컴포넌트를 명확하게 구분할 수 있다. 예를 들어, 기본 데이터 모델이나 로직를 건드릴 필요 없이, 텍스트 기반 인터페이스와 그래픽 사용자 인터페이스 사이나 데스크톱 PC에서 실행하기 위한 인터페이스와 휴대폰 앱으로 실행하기 위한 인터페이스 사이에 쉽게 전환할 수 있다.
다음 테이블은 체스 게임에서 가능한 서브 시스템이 어떻게 보일 수 있는지 보여준다:
SUBSYSTEM NAME |
INSTANCES | FUNCTIONALITY | INTERFACES EXPORTED |
INTERFACES CONSUMED |
---|---|---|---|---|
GamePlay | 1 | Starts game Controls game flow Controls drawing Declears winner Ends game |
Game Over | Take Turn (on Player) Draw (on ChessBoardView) |
ChessBoard | 1 | Stores chess pieces Checks for ties and checkmates |
Get Piece At Set Piece At |
Game Over (on GamePlay) |
ChessBoardView | 1 | Draws the associated ChessBoard |
Draw | Draw (on ChessPieceView) |
ChessPiece | 32 | Move itself Checks for legal moves |
Move Check Move |
Get Piece At (on ChessBoard) Set Piece At (on ChessBoard) |
ChessPieceView | 32 | Draws the associated ChessPiece | Draw | None |
Player | 2 | Interacts with the user by prompting the user for a move, and obtaining the user`s move Moves pieces | Take Turn | Get Piece At (on ChessBoard) Move (on ChessPiece) Check Move (on ChessPiece) |
ErrorLogger | 1 | Writes error messages to a log file | Log Error | None |
이 테이블에서 볼 수 있듯이, 이 체스 게임의 기능적 서브 시스템에는 GamePlay 서브 시스템, ChessBoard와 ChessBoardView, 32개의 ChessPiece와 ChessPieceView, 2명의 Player와 한 개의 ErrorLogger를 포함한다. 그러나 이것이 유일한 합리적인 접근 방식은 아니다. 프로그래밍 자체와 마찬가지로 소프트웨어 디자인에서도, 동일한 목표를 달성하기 위한 다양한 방법이 있다. 모든 솔루션이 동일한 것은 아니다. 어떤 것들은 확실히 다른 것보다 낫다. 그러나, 종종 똑같이 유효한 방법이 몇 가지 있다.
서브 시스템을 잘 나누면 프로그램이 기본 기능 부분으로 분리된다. 예를 들어, Player는 ChessBoard, ChessPiece나 GamePlay와 다른 서브 시스템이다. 논리적으로 분리된 서브 시스템이기 때문에 Player를 GamePlay 서브 시스템으로 묶는 것은 이치에 맞지 않다. 다른 선택은 명확하지 않을 수 있다.
이 MVC 디자인에서, ChessBoard와 ChessPiece 서브 시스템은 모델의 일부이다. ChessBoardView와 ChessPieceView는 뷰의 일부이고 Player는 컨트롤러의 일부이다.
테이블에서 서브 시스템 관계를 시각화하는 것은 종종 어렵기 때문에, 한 서브 시스템에서 다른 서브 시스템으로의 호출을 나타내는 선이 있는 다이어그램에 프로그램의 서브 시스템을 표시하는 것이 일반적으로 도움이 된다. Figure 4-5는 UML 통신 다이어그램을 기반으로 느슨하게 다이어그램으로 시각화된 체스 게임의 서브 시스템을 보여준다.
Choose Threading Models (스레드 모델 선택)
디자인 단계에서 작성할 알고리즘에 특정 루프를 다중 스레드하는 방법을 생각하기에는 너무 이르다. 그러나 이 단계에서는 프로그램의 고급 스레드의 수를 선택하고 상호 작용을 지정한다. 고급 스레드의 예로는 UI 스레드, 오디오 재생 스레드, 네트워크 통신 스레드등이 있다.
멀티 스레드 디자인에서는, 디자인을 더 간단하고 안전하게 만들어야 하므로, 가능한 한 공유 데이터 사용을 피해야 한다. 공유 데이터를 피할 수 없는 경우 잠금 요구 사항을 지정해야 한다.
다중 스레드 프로그램에 익숙하지 않거나 플랫폼에서 다중 스레드를 지원하지 않는 경우, 프로그램을 단일 스레드로 만들어야 한다. 그러나 프로그램에 각각 병렬로 작동할 수 있는 여러 고유한 작업이 있는 경우, 다중 스레드에 대한 좋은 후보가 될 수 있다. 예를 들어, 그래픽 사용자 인터페이스 애플리케이션에는 종종 메인 애플리케이션 작업을 수행하는 하나의 스레드와 사용자가 버튼을 누르거나 메뉴 항목을 선택하기를 기다리는 다른 스레드가 있다. 다중 스레드 프로그래밍은 Chapter 27, "Multithreaded Programming with C++"에서 다룬다.
체스 프로그램은 게임 흐름을 제어하기 위해 하나의 스레드만 필요하다.
Specify Class Hierarchies for Each Subsystem (각 서브 시스템에 대한 클래스 계층 지정)
이 단계에서는, 프로그램에 작성할 클래스 계층을 결정한다. 체스 프로그램은 체스 말을 나타내는 클래스 계층이 필요하다. 이 계층 구조는 Figure 4-6과 같이 작동할 수 있다. 일반 ChesssPiece 클래스는 추상 기본 클래스로 사용된다. ChessPieceView 클래스에도 유사한 계층 구조가 필요하다.
또 다른 클래스 계층은 ChessBoardView 클래스에 사용할 수 있으며, 게임에 텍스트 기반 인터페이스나 그래픽 사용자 인터페이스를 가질 수 있다. Figure 4-7은 체스 보드를 콘솔에 텍스트로 표기하거나 2D나 3D 그래픽 사용자 인터페이스를 사용하여 표시할 수 있는 계층 구조의 예를 보여준다. ChessPieceView 계층의 개별 클래스에도 유사한 계층 구조가 필요하다.
Chapter 5에서는 클래스와 클래스 계층 구조 설계에 대해 자세히 설명한다.
Specify Classes, Data Structures, Algorithms and Patterns for Each Subsystem (각 서브 시스템에 대한 클래스, 데이터 구조체, 알고리즘과 패턴 지정)
이 단계에서는, 더 높은 수준의 세부 사항을 고려하고 각 서브 시스템에 대해 작성할 특정 클래스를 포함하여 각 서브 시스템의 세부 사항을 지정한다. 각 서브 시스템 자체를 하나의 클래스로 모델링하는 것이 좋다. 이 정보를 다시 테이블로 요약할 수 있다.
SUBSYSTEM | CLASSES | DATA STRUCTURES | ALGORITHMS | PATTERNS |
---|---|---|---|---|
GamePlay | GamePlay 클래스 | GamePlay 개체는 하나의 ChessBoard 개체와 두 개의 Player 개체를 포함한다. | 각 플레이어에게 플레이할 차례를 부여한다. | None |
ChessBoard | ChessBoard 클래스 | ChessBoard 개체는 최대 32개의 ChessPiece를 포함하는 2차원 8x8 그리드를 저장한다. | 각각 이동 후 승패를 확인한다. | None |
ChessBoardView | ChessBoardView 추상 기본 클래스 구체적인 파생 클래스 ChessBoardViewConsole, ChessBoardViewGUI2D등 |
체스 보드를 그리는 방법에 대한 정보를 저장한다. | 체스 보드를 그린다. | Observer |
ChessPiece | ChessPiece 추상 기본 클래스 Rook, Bishop, Knight, King, Pawn과 Queen 파생 클래스 |
각 말은 체스 보드의 위치를 저장한다. | 다양한 위치에 있는 말을 체스 보드에 쿼리하여, 말의 적합한 이동을 확인한다. | None |
ChessPieceView | ChessPieceView 추상 기본 클래스 파생 클래스 RookView, BishopView 등 구체적인 파생 클래스 RookViewConsole, RookViewGUI2D 등 |
체스 말을 그리는 방법에 대한 정보를 저장한다. | 체스 말을 그린다. | Observer |
Player | Player 추상 기본 클래스 구체적인 파생 클래스 PlayerConsole, PlayerGUI2D 등 |
None | 사용자에게 이동을 요청하고, 이동이 적합한지 확인하고, 말을 이동한다. | Mediator |
ErrorLogger | 하나의 ErrorLogger 클래스 | 기록할 메세지 큐 | 메세지를 버퍼링하고 로그 파일에 저장한다. | Dependency injection |
이러한 테이블은 이미 소프트웨어 디자인의 다른 클래스에 대한 일부 정보를 제공하지만, 이들 간의 상호 작용을 명확하게 설명하지는 않는다. UML 시퀀스 다이어그램을 사용하여 이러한 상호 작용을 모델링할 수 있다. Figure 4-8은 이전 테이블의 일부 클래스의 상호 작용을 시각화하는 다이어그램을 보여준다.
Figure 4-8의 다이어그램은 GamePlay에서 Player로의 단일 TakeTurn 호출인 단일 반복만을 보여준다(따라서 부분 시퀀스 다이어그램일 뿐이다). TakeTurn 호출이 완료된 후, GamePlay 개체는 자체를 그리도록 ChessBoardView에 요청해야 하며, 차례로 다른 ChessPieceView에 자체를 그리도록 요청해야 한다. 또한, 체스 말이 상대방의 말을 어떻게 가져가는지 시각화하고, 플레이어의 킹과 플레이어의 루크 중 하나가 관련된 이동인 캐슬링 이동에 대한 지원을 포함하도록 시퀀스 다이어그램을 확장해야 한다. 캐슬링은 플레이어가 동시에 두 개의 말을 움직이는 유일한 동작이다.
디자인 문서의 이 섹션은 일반적으로 각 클래스에 대한 실제 인터페이스를 제공하지만, 이 예제에서는 해당 수준의 세부 정보를 생략한다.
클래스를 디자인하고 데이터 구조, 알고리즘, 패턴을 선택하는 것은 까다로울 수 있다. 이 장의 앞부분에서 논의한 추상화와 재사용 규칙을 항상 염두에 두어야 한다. 추상화의 핵심은 인터페이스와 구현을 별도로 고려하는 것이다. 먼저 사용자의 관점에서 인터페이스를 지정한다. 컴포넌트가 수행할 작업을 결정한다. 그런 다음 데이터 구조와 알고리즘을 선택하여 컴포넌트가 이를 수행하는 방법을 결정한다. 재사용을 위해, 표준 데이터 구조, 알고리즘과 패턴에 익숙해지고 C++의 Standard Library와 직장에서 사용할 수 있는 모든 독점 코드에 대해 알고 있어야 한다.
Specify Error Handling for Each Subsystem (각 서브 시스템에 대한 오류 처리 지정)
이 디자인 단계에서는, 각 서브 시스템의 오류 처리를 설명한다. 오류 처리에는 네트워크 액세스 실패와 같은 시스템 오류와 잘못된 항목과 같은 사용자 오류가 모두 포함되어야 한다. 각 서브 시스템이 예외를 사용할지 여부를 지정해야 한다. 이 정보를 다시 테이블로 요약할 수 있다.
SUBSYSTEM | HANDLING SYSTEM ERRORS | HANDLING USER ERRORS |
---|---|---|
GamePlay | ErrorLogger로 오류를 기록하고, 사용자에게 메시지를 표시하며, 예기치 않은 오류가 발생하면 프로그램을 정상적으로 종료한다. | 해당 없음 (직접적인 사용자 인터페이스 없음) |
ChessBoard ChessPiece |
ErrorLogger로 오류를 기록하고 예기치 않은 오류가 발생하면 예외를 발생한다. | 해당 없음 (직접적인 사용자 인터페이스 없음) |
ChessBoardView ChessPieceView |
ErrorLogger에 오류를 기록하고 그리는 동안 문제가 발생하면 예외가 발생한다. | 해당 없음 (직접적인 사용자 인터페이스 없음) |
Player | ErrorLogger로 오류를 기록하고 예기치 않은 오류가 발생하면 예외를 발생한다. | Sanity(정상성)- 사용자 이동 항목이 보드를 벗어나지 않았는지 확인한다. 그런다음 사용자에게 다른 항목을 입력하라는 메세지를 표시한다. 이 서브 시스템은 말을 이동하기 전에 각 이동의 적합성을 확인한다. 적합하지 않으면, 사용자에게 다른 이동을 요청한다. |
ErrorLogger | 오류 기록을 시도한다. 예기치 않은 오류가 발생하면, 사용자에게 알린다. | 해당 없음 (직접적인 사용자 인터페이스 없음) |
오류 처리의 일반적인 규칙은 모든 것을 처리한는 것이다. 가능한 모든 오류 조건에 대해 심도있게 생각한다. 한 가지 가능성을 놓치면, 프로그램에서 버그로 나타난다. "unexpected(예기치 않은)" 오류로 처리하지 않는다. 메모리 할당 실패, 잘못된 사용자 항목, 디스크 오류, 네트워크 오류 등 모든 가능성을 예상한다. 그러나 체스 게임의 테이블에서 볼 수 있듯이 사용자 오류를 내부 오류와 다르게 처리해야 한다. 예를 들어, 사용자가 잘못된 이동을 입력해도 체스 프로그램이 종료되어서는 안된다. Chapter 14, "Handling Errors"에서는 오류 처리에 대해 더 자세히 설명한다.
SUMMARY (요약)
이 장에서는, 디자인에 대한 전문적인 C++ 접근 방식에 대해 알아보았다. 모든 프로그래밍 프로젝트에서 중요한 첫 번째 단계는 소프트웨어 디자인이라는 것을 이 책을 통해 확신하게 되었기를 바란다. 또한 객체 지향 중점, 대규모 기능 세트와 Standard Library, 일반 코드 작성 기능을 포함하여 디자인을 어렵게 만드는 C++의 일부 측면에 대해서도 배웠다. 이 정보를 통해 C++ 디자인을 보다 잘 준비할 수 있다.
이 장에서는 두 가지 디자인 테마를 소개했다. 첫 번째 주제인 추상화 개념이나 구현에서 인터페이스를 분리하는 개념은 이 책에 스며들어 있으며, 모든 디자인 작업의 지침이 되어야 한다.
두 번째 주제인 코드와 디자인의 재사용 개념은 실제 프로젝트와 이 책에서도 자주 등장한다. C++ 디자인은 라이브러리와 프레임워크 형태의 코드 재사용과 기술과 패턴 형태로 아이디어와 디자인 재사용을 모두 포함되어야 한다는 것을 배웠다. 가능한 재사용할 수 있도록 코드를 작성해야 한다. 또한 기능과 제한 사항, 성능, 라이선스, 지원 모델, 플랫폼 제한 사항, 프로토타입핑, 도움말을 검색할 수 있는 위치에 대한 이해를 포함하여 코드 재사용에 대한 특정 지침과 절충 사항에 대해 기억한다. 또한, 성능 분석과 big-O 표기법에 대해서도 배웠다. 이제 디자인의 중요성과 기본 디자인 테마를 이해했으므로, 나머지 Part II에 대한 준비가 되었다. Chapter 5에서는 디자인에서 C++의 객체 지향 측면을 사용하기 위한 전략을 설명한다.
EXERCISES (연습 문제)
다음 연습 문제를 풀면서, 이 장에서 설명하는 내용을 연습할 수 있다. 모든 연습 문제에 대한 솔루션은 책 웹사이트(www.wiley.com/go/proc++5e)에서 코드를 다운로드하여 사용할 수 있다. 그러나 연습 문제가 잘 안풀리는 경우, 웹 사이트에서 솔루션을 찾아 보기 전에 먼저 이장의 일부를 다시 읽고 스스로 답을 찾으십시오.
Exercise 4-1:
C++로 자신만의 다자인을 만들 때, 따라야 할 두 가지 기본 디자인 규칙은 무엇인가?
Exercise 4-2:
다음과 같은 Card 클래스가 있다고 가정한다. 클래스는 카드 데크의 일반 카드만 지원하며, 소위 조커 카드를 지원할 필요가 없다.
class Card
{
public:
enum class Number { Ace, Two, Three, Four, Five, Six, Seven, Eight,
Nine, Ten, Jack, Queen, King };
enum class Figure { Diamond, Heart, Spade, Club };
Card() {};
Card(Number number, Figure figure)
: m_number { number }, m_figure { figure } {}
private:
Number m_number { Number::Ace };
Figure m_figure { Figure::Diamond };
};
카드 한 벌을 나타내기 위해 Card 클래스를 다음과 같이 사용하는 것에 대해 어떻게 생각하는가? 당신이 생각할 수 있는 개선 사항이 있는가?
int main()
{
Card deck[52];
// ...
}
Exercise 4-3:
친구와 함께 모바일 디바이스용 3D 게임을 만들기 위한 좋은 아이디어를 생각해 냈다고 가정해 본다. 당신은 Android 기기를 가지고 있고, 친구는 Apple iPhone을 가지고 있으며, 물론 두 기기 모두에서 게임을 플레이할수 있기를 원한다. 이 두가지 모바일 플랫폼을 처리하는 방법과 게임 개발 시작을 준비하는 방법을 개략적으로 설명해본다.
Exercise 4-4:
O(n), O(nxn), O(log n), O(1)과 같은 big-O 복잡성이 주어지면, 복잡성이 증가에 따라 정렬할 수 있는가? 그들의 이름은 무엇인가? 이보다 더 심각한 복잡성을 생각할 수 있는가?
'Programming Language > Professional C++' 카테고리의 다른 글
Professional C++ - 7. Memory Management (0) | 2022.12.19 |
---|---|
Professional C++ - 6. Designing for Reuse (0) | 2022.11.03 |
Professional C++ - 5. Designing with Objects (0) | 2022.10.10 |
Professional C++ (0) | 2022.09.08 |
Professional C++ - 3. Coding with Style (0) | 2022.07.21 |