6. Designing for Reuse
이 챕터의 내용
▶ 재사용 철학: 재사용을 위한 코드를 디자인해야 하는 이유
▶ 재사용 가능한 코드를 디자인하는 방법
▶ 추상화를 사용하는 방법
▶ 재사용을 위한 코드 구조화 전략
▶ 사용 가능한 인터페이스를 디자인하기 위한 6가지 전략
▶ 일반성과 사용 편의성을 조화시키는 방법
▶ The SOLID principle (SOLID 원칙, 로버트 마틴의 5가지 기본 원칙, 개체 지향 설계론)
Chapter 4, "Designing Professional C++ Programs"에서 설명한 것처럼, 프로그램에서 라이브러리와 기타 코드를 재사용하는 것은 중요한 디자인 전략이다. 그러나 이는 재사용 전략의 절반에 불과하다. 나머지 절반은 프로그램에서 재사용할 수 있는 고유한 코드를 설계하고 작성하는 것이다. 아마 당신이 발견한 것처럼, 잘 디자인된 라이브러리와 제대로 디자인되지 않은 라이브러리 사이에는 상당한 차이가 있을 것이다. 잘 디자인된 라이브러리는 사용하기 좋은 반면, 잘못 디자인된 라이브러리는 혐오감으로 사용을 포기하고 직접 코드를 작성하도록 유도할 수 있다. 다른 프로그래머가 사용하도록 명시적으로 디자인된 라이브러리를 작성하든, 단순히 클래스 계층 구조를 결정하든, 상관없이 재사용을 염두에 두고 코드를 디자인해야 한다. 다음 프로젝트에서 언제 유사한 기능이 필요할지 알 수 없다.
Chapter 4에서는 재사용이라는 디자인 테마를 소개하고 라이브러리와 기타 코드를 디자인에 통합하여 이 테마를 적용하는 방법을 설명하지만, 재사용 가능한 코드를 디자인하는 방법은 설명하지 않았다. 이는 이 챕터의 주제이다. Chapter 5, "Designing with Objects"에서 설명한 개체 지향 디자인 원칙을 기반으로 한다.
THE REUSE PHILOSOPHY
당신은 자신과 다른 프로그래머가 재사용할 수 있는 코드를 디자인해야 한다. 이 규칙은 다른 프로그래머가 사용하도록 특별히 의도한 라이브러리와 프레임워크뿐만 아니라 프로그램을 위해 디자인한 모든 클래스, 서브시스템이나 컴포넌트에도 적용된다. 당신은 항상 다음과 같은 좌우명을 염두에 두어야 한다:
- 한 번 작성하고, 자주 사용하라.
- 어떤 대가를 치르더라도, 코드 중복을 방지하라.
- DRY - 반복하지 마라.(중복 배제, 소프트웨어 개발 원리의 하나)
여기에는 몇 가지 이유가 있다:
- 코드는 거의 하나의 프로그램에서만 사용되지 않는다. 코드가 어떻게든 다시 사용될 것이라고 확신할 수 있으므로 처음부터 올바르게 디자인한다.
- 재사용을 위한 디자인은 시간과 비용을 절약한다. 나중에 사용할 수 없도록 코드를 디자인하면, 나중에 유사한 기능이 필요할 때 당신이나 당신의 파트너는 시간을 들여 바퀴를 재발명할 수 있다.
- 당신 그룹의 다른 프로그래머는 당신이 작성한 코드를 사용할 수 있어야 한다. 아마도 당신은 프로젝트에서 혼자 작업하지 않을 것이다. 당신의 동료들은 잘 디자인되고 기능이 풍부한 라이브러리와 사용할 수 있는 코드 조각을 제공하기 위한 당신의 노력에 감사할 것이다. 재사용을 위한 디자인을 협력 코딩(cooperative coding)이라고도 한다.
- 재사용이 부족하면 코드 중복이 발생하고, 코드 중복은 유지 보수의 악몽으로 이어진다. 중복된 코드에서 버그가 발견되면, 중복된 모든 위치에서 버그를 수정해야 한다. 코드 조각을 복사하여 붙여넣는 자신을 발견할 때마다, 최소한 도우미 함수나 클래스로 이동하는 것을 고려해야 한다.
- 당신은 자신의 일의 주요 수혜자가 될 것이다. 숙련된 프로그래머는 코드를 버리지 않는다. 시간이 지남에 따라 그들은 진화하는 도구로 개인 라이브러리를 구축한다. 미래에 비슷한 기능이 언제 필요할지 알 수 없다.
주의
회사에서 고용한 직원으로서 코드를 디자인하거나 작성할 때, 일반적으로 회사가 지적 재산권을 소유한다. 회사에서 고용을 종료할 때 디자인이나 코드의 사본을 보관하는 것은 종종 불법이 될 수 있다. 자영업을 하고 고객을 위해 일할 때도 마찬가지이다.
HOW TO DESIGN REUSABLE CODE
재사용 가능한 코드는 두 가지 주요 목표를 달성한다:
- 첫 번째, 약간 다른 목적이나 다른 애플리케이션 도메인에서 사용하기에 충분히 일반적이다. 특정 애플리케이션의 세부 정보가 포함된 프로그램 컴포넌트는 다른 프로그램에서 재사용하기 어렵다.
- 두 번째, 재사용 가능한 코드도 사용하기도 쉽다. 인터페이스나 기능을 이해하는 데 많은 시간이 필요하지 않다. 프로그래머는 이를 애플리케이션에 쉽게 통합할 수 있어야 한다.
클라이언트에게 라이브러리를 제공하는 방법도 중요하다. 소스 형식으로 전달할 수 있으며, 클라이언트는 소스를 프로젝트에 통합하기만 하면 된다. 또 다른 옵션은 클라이언트의 애플리케이션에 링크하기 위한 바이너리를 정적 라이브러리 형태, Windows 클라이언트의 경우 동적 링크 라이브러리(.dll)이나 Linux 클라이언트의 경우 공유 개체(.so) 형태로 제공하는 것이다. 이러한 각각의 전달 메커니즘은 재사용 가능한 코드를 디자인하는 방법에 추가적인 제약을 부과할 수 있다.
참고
이 챕터에서는 인터페이스를 사용하는 프로그래머를 지칭하기 위해 클라이언트라는 용어를 사용한다. 클라이언트와 프로그램을 실행하는 사용자를 혼동하면 안된다. 또한 이 챕터에서는 클라이언트 코드라는 문구를 사용하여 인터페이스를 사용하기 위해 작성된 코드를 나타낸다.
재사용 가능한 코드를 디자인하기 위한 가장 중요한 전략은 추상화이다.
Use Abstraction
추상화의 핵심은 구현으로부터 인터페이스를 효과적으로 분리하는 것이다. 구현이란 달성하려는 작업을 수행하기 위해 작성하는 코드이다. 인터페이스는 다른 사람들이 당신의 코드를 사용하는 방식이다. C에서는 당신이 작성한 라이브러리에서 함수를 기술하고 있는 헤더 파일이 인터페이스이다. 개체 지향 프로그램에서는 클래스에 대한 인터페이스는 공개적으로 액세스할 수 있는 메서드와 속성의 모음이다. 그러나, 좋은 인터페이스는 공개 메서드만 포함되어야 한다. 클래스의 속성은 공개되어서는 안 되지만, getter와 setter라고 하는 공개 메서드를 통해 노출될 수 있다.
Chapter 4에서 추상화의 원리를 소개하고 내부 작동 방식을 이해하지 않고도 인터페이스로 사용할 수 있는 텔레비전의 실제 비유를 제시했다. 마찬가지로 코드를 디자인할 때, 구현에서 인터페이스를 명확하게 분리해야 한다. 주로 클라이언트는 기능을 사용하기 위해 내부 구현의 세부 정보를 이해할 필요가 없기 때문에, 이 분리는 코드를 더 쉽게 사용할 수 있도록 한다.
추상화를 사용하면 당신과 당신의 코드를 사용하는 클라이언트 모두에게 이익이 된다. 클라이언트는 구현의 세부 사항에 대해 걱정할 필요가 없기 때문에 이점이 된다. 그들은 코드가 실제로 어떻게 작동하는지 이해하지 않고도 당신이 제공하는 기능을 이용할 수 있다. 당신은 코드에 대한 인터페이스를 변경하지 않고, 기본(underlying) 코드를 수정할 수 있기 때문에 이점이 있다. 따라서 당신은 클라이언트가 용도를 변경하지 않고도 업그레이드와 수정을 제공할 수 있다. 동적으로 연결된 라이브러리를 사용하면, 클라이언트는 실행 파일을 다시 빌드할 필요조차 없다. 마지막으로 라이브러리 작성자로서 인터페이스에서 정확히 어떤 상호 작용이 기대되고 어떤 기능을 지원해야 하는지 지정할 수 있기 때문에 두 가지 모두 이점이 있다. 문서 작성 방법에 대한 논의는 Chapter 3, "Coding with Style"를 참조한다. 인터페이스와 구현을 명확하게 분리하면 클라이언트가 의도하지 않은 방식으로 라이브러리를 사용하는 것을 방지할 수 있다. 그렇지 않으면 예기치 않은 동작과 버그가 발생할 수 있다.
주의
인터페이스를 디자인할 때, 구현의 세부 정보를 클라이언트에 노출하면 안된다.
때때로 라이브러리는 하나의 인터페이스에서 다른 인터페이스로 정보를 전달하기 위해 반환된 정보를 유지하기 위해 클라이언트 코드를 요구한다. 이 정보는 handle이라고도 하며 호출 사이에 상태를 기억해야 하는 특정 인스턴스를 추적하는 데 자주 사용된다. 라이브러리 디자인에서 핸들이 필요한 경우라도 핸들의 내부를 노출하면 안되다. 그 핸들을 opaque(불투명) 클래스로 만들어야 한다. 여기서 프로그래머는 내부 데이터 멤버에 직접 또는 공개 getter나 setter를 통해 액세스할 수 없다. 이 핸들 내부의 변수를 조정하기 위해 클라이언트 코드는 필요하지 않다. 잘못된 디자인의 예로는 오류 로깅을 켜기 위해 불투명한 핸들에 구조체의 특정 멤버를 설정해야 하는 라이브러리가 있다.
참고
불행하게도, C++는 클래스를 작성할 때 좋은 추상화 원칙에 근본적으로 우호적이지 않다. 구문을 사용하려면 public 인터페이스와 non-public (private이나 protected) 데이터 멤버와 메서드를 결합하여 하나의 클래스로 정의하고, 클래스 내부 구현의 세부 정보 중 일부를 클라이언트에 노출해야 한다. Chapter 9, "Mastering Classes and Objects"에서는 순수한 인터페이스를 제공하기 위해 이 문제를 해결하기 위한 몇 가지 기술을 설명한다.
추상화는 당신의 전체 디자인을 가이드하기 때문에 매우 중요하다. 당신이 내리는 모든 결정의 일부로, 당신의 선택이 추상화의 원칙을 충족하는지 자문해 보기 바란다. 클라이언트의 입장이 되어 인터페이스의 내부 구현에 대한 지식이 필요한지 여부를 결정한다. 이 규칙에 예외를 적용하는 경우는 거의 없다.
추상화를 사용하여 재사용 가능한 코드를 디자인하는 동안, 다음 사항에 중점을 두어야 한다:
- 첫 번째, 당신은 코드를 적절하게 구성해야 한다. 어떤 클래스 계층을 사용할 것인가? 템플릿을 사용해야 하는가? 코드를 어떻게 서브시스템으로 나누어야 하는가?
- 두 번째, 당신은 인터페이스를 디자인해야 한다. 인터페이스는 당신이 제공하는 기능에 액세스하기 위한 라이브러리의 "entries(항목)"이다.
두 항목 모두 다음 섹션에서 설명한다.
Structure Your Code for Optimal Reuse (최적의 재사용을 위한 코드 구조화)
모든 레벨, 즉 단일 함수에서 클래스를 넘어, 전체 라이브러리와 프레임워크에 이르기까지 디자인의 시작부터 재사용을 고려해야 한다. 다음 텍스트에서는 이러한 모든 레벨을 component라고 한다. 다음 전략은 코드를 적절하게 구성하는 데 도움이 된다. 이러한 모든 전략은 코드를 범용으로 만드는 데 중점을 둔다. 사용 편의성을 제공하는 재사용 가능한 코드 다자인의 두 번째 측면은, 인터페이스 디자인과 더 관련이 있으며 이 챕터의 뒷부분에서 설명한다.
Avoid Combining Unrelated or Logically Separate Concepts (관련이 없거나 논리적으로 분리된 개념을 결합하지 않는다)
컴포넌트를 디자인할 때, 단일 작업이나 작업 그룹에 집중해야 한다. 즉 높은 cohesion(응집력)을 위해 노력해야 한다.
이는 single responsibility principle(SRP: 단일 책임 원칙)이라고도 한다. 난수 생성기와 XML 파서 같은 관련 없는 개념을 결합하지 않는다.
재사용을 위해 특별히 코드를 디자인하지 않더라도 이 전략을 염두에 둔다. 전체 프로그램이 자체적으로 재사용되는 경우는 거의 없다. 대신, 프로그램의 일부나 서브시스템이 다른 애플리케이션에 직접 통합되거나 약간 다른 용도로 조정된다. 따라서 논리적으로 분리된 기능을 서로 다른 프로그램에서 재사용할 수 있는 별개의 컴포넌트로 나누도록 프로그램을 디자인해야 한다. 이러한 각 컴포넌트에는 잘 정의된 책임이 있어야 한다.
이 프로그램 전략은 상호 교환 가능한 개별 부품의 실제 디자인 원칙을 모델링한다. 예를들어, Car 클래스를 작성하고 엔진의 모든 속성과 동작을 이 클래스에 넣을 수 있다. 그러나 엔진은 자동차의 다른 측면에 묶여 있지 않은 분리 가능한 컴포넌트이다. 한 차에서 엔진을 제거하고 다른 차에 넣을 수 있다. 적절한 디자인에는 모든 엔진 관련 기능을 포함하는 Engine 클래스가 포함된다. 그러면 Car 인스턴스에는 Engine 인스턴스만 포함된다.
Divide Your Programs into Logical Subsystems (프로그램을 논리적인 서브시스템으로 분할한다)
서브시스템을 독립적으로 재사용할 수 있는 개별 컴포넌트로 디자인해야 한다. 즉, low coupling(낮은 결합)을 위해 노력해야 한다. 예를 들어 네트워크 게임을 디자인하는 경우, 네트워킹과 그래픽 사용자 인터페이스 관점에서 별도의 서브시스템을 유지한다. 이렇게 하면 다른 컴포넌트를 끌어내지 않고 두 컴포넌트 중 하나를 재사용할 수 있다. 예를 들어, 네트워크를 사용하지 않은 게임을 작성하려고 할 수 있다. 이 경우 그래픽 인터페이스 서브시스템을 재사용할 수 있지만, 네트워킹 부분은 필요하지 않다. 마찬가지로 P2P 파일 공유 프로그램을 디자인할 수 있다. 이 경우 네트워킹 서브시스템은 재사용할 수 있지만, 그래픽 사용자 인터페이스 기능은 재사용할 수 없다.
각 서브시스템에 대한 추상화 원칙을 따라야 한다. 각 서스시스템을 일관성 있고 사용하기 쉬운 인터페이스를 제공해야 하는 미니어처 라이브러리로 생각해 본다. 이러한 미니어처 라이브러리를 사용하는 유일한 프로그래머 일지라도, 논리적으로 구별되는 기능을 분리하는 잘 디자인된 인터페이스와 구현의 이점을 누릴 수 있다.
Use Class Hierarchies to Separate Logical Concepts (논리적 개념을 분리하기 위해 클래스 계층을 사용한다)
프로그램을 논리적 서브시스템으로 나누는 것 외에도, 클래스 레벨에서 관련 없는 개념을 결합하는 것은 피해야 한다. 예를 들어, 자율주행차에 대한 클래스를 작성한다고 가정한다. 자동차에 대한 기본 클래스로 시작하여 모든 자율주행 로직을 직접 통합하기로 결정한다. 그러나 프로그램에서 비자율주행인 차를 원한다면 어떨까요? 이 경우 자율주행과 관련된 모든 로직은 쓸모가 없어지고, 프로그램이 피할 수 있는 비전 라이브러리, LIDAR 라이브러리 등과 같은 라이브러리와 링크해야 할 수 있다. 솔루션은 자율주행차가 일반 자동차의 파생 클래스인 클래스 계층 구조(Chapter 5, "Designing with Objects"에서 소개)를 만드는 것이다. 그렇게 하면, 자율주행 기능이 필요하지 않은 프로그램에서 이런 알고리즘의 비용을 들이지 않고도 자동차의 기본 클래스를 사용할 수 있다. Figure 6-1은 이 계층 구조를 보여준다.
이 전략은 자율주행과 자동차 같은 두 가지 논리적 개념이 있을 때 잘 작동한다. 3개나 그 이상의 개념이 있으면 더 복잡해진다. 예를 들어, 트럭과 자동차를 모두 제공하려 한다고 가정해 본다. 각 트럭과 자동차는 자율주행이 가능하거나 불가능할 것이다. 논리적으로 트럭과 자동차는 모두 vehicle(차량)의 특수한 경우이므로, Figure 6-2와 같이 vehicle 클래스의 파생 클래스이어야 한다.
유사하게, 자율주행 클래스는 비자율주행 클래스의 파생 클래스일 수 있다. 선형 계층 구조를 이러한 구분에서 제공할 수 없다. 한 가지 가능성은 자율주행 측면을 대신 mixin 클래스로 만드는 것이다. 이전 챕터에서 다중 상속을 사용하여 C++에서 mixin을 구현하는 한 가지 방법을 보여 주었다. 예를 들어, PictureButton은 Image 클래스와 Clickable mixin 클래스 모두에서 상속할 수 있었다. 그러나 자율주행 디자인의 경우 클래스 템플릿을 사용하는 다른 종류의 mixin 구현을 사용하는 것이 좋을 것이다. 기본적으로, SelfDrivable mixin 클래스는 다음과 같이 정의될 수 있다. 이 예제는 클래스 템플릿 구문에 대해 조금 앞서 설명하는데, Chapter 12, "Writing Generic Code with Templates"에서 자세히 설명하지만, 이러한 세부 사항은 이 설명에서는 중요하지 않다.
template <typename T>
class SelfDrivable : public T
{
};
이 SelfDrivable mixin 클래스는 자율주행 기능을 구현하는 데 필요한 모든 알고리즘을 제공한다. 이 SelfDrivable mixin 클래스 템플릿이 있으면, 다음과 같이 자동차용으로 하나, 트럭용으로 하나를 인스턴스화할 수 있다.
SelfDrivable<Car> selfDrivingCar;
SelfDrivable<Truck> selfDrivingTruck;
이 두 줄의 결과로 컴파일러는 SelfDrivable mixin 클래스 템플릿을 사용할 수 있다. 클래스 템플릿의 모든 T는 Car로 대체되어 Car에서 파생되는 하나의 인스턴스를 생성한다. 다른 하나는 T가 Truck으로 대체되어 Truck에서 파생된다. Chapter 32, "Incorporating Design Techniques and Frameworks"에서 mixin 클래스에 대해 더 자세히 설명한다.
이 솔루션은 4가지 다른 클래스의 작성을 필요로 하지만, 기능을 명확하게 분리하는 것은 노력할 가치가 있다.
마찬가지로, 클래스 레벨뿐만 아니라 다자인의 모든 레벨에서 관련 없는 개념을 결합하는 것, 즉 높은 응집력을 위해 노력하는 것을 피해야 한다. 예를 들어, 메소드 레벨에서 단일 메소드는 논리적으로 관련이 없는 것을 수행하거나, 돌연변이(set)와 검사(get) 등을 혼합해서는 안 된다.
Use Aggregation to Separate Logical Concepts (논리적 개념 분리하기 위해 포함을 사용한다)
Chapter 5에서 설명한 포함은 has-a 관계를 모델링한다. 개체는 기능의 일부 측면을 수행하기 위해 다른 개체를 포함한다. 상속이 적절하지 않은 경우, 포함을 사용하여 관련이 없거나 관련이 있어도 별도의 기능을 분리할 수 있다.
예를 들어, 가족 구성원을 저장하기 위해 Family 클래스를 작성한다고 가정해보자. 분명 트리 데이터 구조는 이 정보를 저장하는 데 이상적이다. Family 클래스에 트리 구조에 대한 코드를 통합하는 대신, 당신은 별도의 Tree 클래스를 작성해야 한다. 그러면 Family 클래스는 Tree 인스턴스를 포함하여 사용할 수 있다. 개체 지향 용어로 사용하면, Family has-a Tree 이다. 이 기술을 사용하면, 트리 데이터 구조를 다른 프로그램에서 더 쉽게 재사용할 수 있다.
Eliminate User Interface Dependencies (사용자 인터페이스 종속성을 제거한다)
라이브러리가 데이터를 조작하는 라이브러리인 경우, 사용자 인터페이스에서 데이터 조작을 분리하기 원할 수 있다. 이는 라이브러리가 사용될 수 있는 사용자 인터페이스 유형을 이런 종류의 라이브러리에서 가정해서는 안 된다는 것을 의미한다. 라이브러리는 cout, cerr, cin 같은 표준 입출력 스트림을 사용하면 안 된다. 라이브러리가 그래픽 사용자 인터페이스의 컨텍스트에서 사용되는 경우, 이러한 스트림이 의미가 없을 수 있기 때문이다. 예를 들어, Windows GUI 기반 애플리케이션은 일반적으로 콘솔 I/O 형식이 없다. 라이브러리가 GUI 기반 애플리케이션에서만 사용될 것이라고 생각하는 경우, 최종 사용자에게 어떤 종류의 메시지 상자나 다른 종류의 알림을 표시해서는 안 된다. 이는 클라이언트 코드의 책임이기 때문이다. 메시지가 사용자에게 표시되는 방식을 결정하는 것은 클라이언트 코드이다. 이러한 종류의 종속성은 재사용 가능성을 낮출 뿐만 아니라, 클라이언트 코드가 오류에 적절하게 반응(예를 들어, 오류를 자동으로 처리)하지 못하게 한다.
Chapter 4에서 소개한 MVC(Model-View-Controller) 패러다임은 데이터 저장과 시각화를 분리하는 잘 알려진 디자인 패턴이다. 이 패러다임을 사용하면, 모델은 라이브러리에 있을 수 있지만 클라이언트 코드는 뷰와 컨트롤러를 제공할 수 있다.
Use Templates for Generic Data Structures and Algorithms (제너릭 데이터 구조와 알고리즘에 템플릿을 사용한다)
C++에는 타입이나 클래스와 관련하여 제너릭한 구조를 만들 수 있는 template이라는 개념이 있다. 예를 들어, 정수형의 배열에 대한 코드를 작성했을 수 있다. 이후에 double형의 배열을 필요로 하면, double과 함께 작동하도록 모든 코드를 다시 작성하고 복제해야 한다. 템플릿의 개념은 타입이 사양에 대한 파라미터가 되고, 모든 타입에서 작동할 수 있는 단일 코드 본문을 만들 수 있다는 것이다. 템플릿을 사용하면 모든 타입에서 작동하는 데이터 구조와 알고리즘을 모두 작성할 수 있다.
이에 대한 가장 간단한 예로 Chapter 1, "A Crash Course in C++ and the Standard Library"에서 C++ Standard Library의 일부인 std::vector 클래스를 소개하였다. 정수형의 vector를 생성하려면 std::vector<int>를 작성하고 double형의 vector를 생성하려면 std::vector<double>를 작성한다. 일반적으로 템플릿 프로그래밍은 매우 강력하지만 매우 복잡할 수 있다. 다행히도 타입에 따라 파라미터화하는 템플릿을 사용하여 다소 간단한 방법으로 생성할 수 있다. Chapter 12와 26, "Advanced Templates"에서는 자신만의 템플릿을 작성하는 기술을 설명하고, 이 섹션에서는 중요한 디자인 측면에 대해 설명한다.
가능하면 특정 프로그램의 세부 사항을 인코딩하는 대신, 데이터 구조와 알고리즘에 대한 제너릭한 디자인을 사용해야 한다. book 개체만 저장하는 균형 이진 트리 구조를 작성하지 않는다. 모든 타입의 개체를 저장할 수 있도록 제너릭화 한다. 그런 식으로 서점, 음악 가게, 운영 체제나 균형 이진 트리가 필요한 모든 곳에서 사용할 수 있다. 이 전략은 모든 타입에서 작동하는 제너릭 데이터 구조와 알고리즘을 제공하는 Standard Library의 기반이 된다.
Why Templates Are Better Than Other Generic Programming Techniques (템플릿이 다른 제너릭 프로그래밍 기법보다 나은 이유)
템플릿은 제너릭 데이터 구조를 작성하기 위한 유일한 메커니즘이 아니다. C와 C++에서 제너릭 구조를 작성하기 위한 또 다른 오래된 방식은 특정 타입의 포인터 대신 void* 포인터를 저장하는 것이다. 클라이언트는 원하는 모든 것을 void*로 캐스팅하여 저장하는 이 구조를 사용할 수 있다. 그러나 이 접근 방식의 주요 문제점은 type safe(타입이 안전)하지 않다는 것이다. 컨테이너는 저장된 요소의 타입을 확인하거나 적용할 수 없다. 구조체에 저장하기 위해 모든 타입을 void*로 캐스트할 수 있으며, 데이터 구조체에서 포인터를 제거할 때는 당신이 생각하는 것으로 다시 캐스트해야 한다. 관련된 검사가 없기 때문에 결과는 재앙이 될 수 있다. 먼저 한 프로그래머가 void*로 캐스팅하여 데이터 구조체에 int에 대한 포인터를 저장하지만, 다른 프로그래머는 이를 Process 개체에 대한 포인터라고 생각하는 시나리오를 상상해 본다. 두 번째 프로그래머는 무심코 void* 포인터를 Process* 포인터로 캐스팅하고 이를 Process* 개체로 사용하려고 한다. 말할 필요도 없이, 프로그램은 예상대로 작동하지 않을 것이다.
제너릭 비-템플릿 기반 데이터 구조체에서 void* 포인터를 직접 사용하는 대신 C++17부터 사용가능한 std::any 클래스를 사용할 수 있다. any 클래스는 Chapter 24, "Additional Library Utilities"에서 설명하겠지만, 여기서는 any 클래스의 인스턴스에 모든 타입의 개체를 저장할 수 있다는 점만 알면 충분하다. std::any의 기본 구현은 특정 경우에 void* 포인터를 사용하지만, 저장된 타입도 추적하므로 모든 것이 type safe(타입이 안전)하게 유지된다.
또 다른 접근 방식은 특정 클래스에 대한 데이터 구조체를 작성하는 것이다. 다형성으로 해당 클래스의 모든 파생 클래스를 구조체에 저장될 수 있다. Java는 이 접근 방식을 극단적으로 사용한다. 즉, 모든 클래스가 Object 클래스에서 직접 또는 간접적으로 파생되도록 지정한다. 이전 버전 Java의 컨테이너는 Object를 저장하므로 모든 타입의 개체를 저장할 수 있었다. 그러나 이 접근 방식도 type safe(타입에 안전)하지 않다. 컨테이너에서 개체를 제거할 때, 개체가 실제로 무엇인지 기억하고 적절한 타입으로 다운캐스팅해야 한다. 다운 캐스팅이란 클래스 계층 구조에서 보다 구체적인 클래스로 캐스팅하는 것, 즉 계층 구조에서 아래쪽으로 캐스팅하는 것을 의미한다.
반면에 템플릿은 올바르게 사용하면 type safe(타입에 안전)하다. 템플릿의 각 인스턴스화는 하나의 타입만 저장한다. 동일한 템플릿 인스턴스화에 다른 타입을 저장하려고 하면, 프로그램이 컴파일되지 않는다. 또한 템플릿을 사용하면 컴파일러가 각 템플릿 인스턴스화에 대해 고도로 최적화된 코드를 생성할 수 있다. void*와 std::any 기반 데이터 구조체와 비교하여 템플릿은 무료 저장소에서 할당을 피할 수 있으므로 성능이 더 좋다. 최신 버전의 Java는 C++ 템플릿과 마찬가지로 type safe(타입이 안전)한 제너릭 개념을 지원한다.
Problems with Templates (템플릿 문제)
템플릿은 완벽하지 않다. 우선, 그 구문은 혼란 스러울 수 있는데, 특히 전에 템플릿을 사용하지 않은 사람에게는 더욱 그럴 수 있다. 두 번째, 템플릿에는 동족 데이터 구조체가 필요하다. 당신은 단일 구조체에 동일한 타입의 개체만 저장할 수 있다. 즉, 균형 이진 트리에 대한 클래스 템플릿을 작성한다면, Process 개체를 저장하는 트리 개체 하나와 int를 저장하는 또 다른 트리 개체를 만들 수 있다. 동일한 트리에 int와 Process 둘다 저장할 수 없다. 이런 제한은 템플릿의 type safe 특성의 직접적인 결과이다. C++17을 시작으로, 이 동질성 제한에 대한 표준화된 방법이 있다. std::variant나 std::any 개체를 저장하기 위해 당신은 데이터 구조체를 작성할 수 있다. std::any 개체는 모든 타입의 값을 저장할 수 있는 반면, std::variant 개체는 선택한 타입의 값을 저장할 수 있다. any와 variant 모두 Chapter 24에서 자세히 설명한다.
템플릿의 또 다른 단점은 최종 바이너리 코드의 크기가 증가하는 이른바 code bloat(코드 부풀림)이다. 각 템플릿 인스턴스화에 대한 고도로 전문화된 코드는 약간 느린 제너릭 코드보다 더 많은 코드를 사용한다. 그러나 일반적으로 요즘에는 code bloat은 그다지 문제가 되지 않는다.
Templates vs. Inheritance (템플릿 대 상속)
때때로 프로그래머는 템플릿을 사용할지 상속을 사용할지 결정하는 것을 까다롭다고 생각한다. 다음은 결정을 내리는 데 도움이 되는 몇 가지 팁이다.
다른 타입에 대해 동일한 기능을 제공하려는 경우 템플릿을 사용한다. 예를 들어, 모든 타입에서 작동하는 일반 정렬 알고리즘을 작성하려면, 함수 템플릿을 사용한다. 모든 타입을 저장할 수 있는 컨테이너를 만들려면, 클래스 템플릿을 사용한다. 핵심 개념은 클래스나 함수 템플릿이 모든 타입을 동일하게 취급한다는 것이다. 그러나 필요한 경우, 특정 타입을 다르게 처리할 수 있도록 템플릿은 특정 타입에 대해 특수화할 수 있다. 템플릿 특수화는 Chapter 12에서 설명한다.
관련 타입에 대해 다른 동작을 제공하려면, 상속을 사용한다. 예를 들어, 도형 그리기 애플리케이션은 원, 사각형, 선 등과 같은 다양한 도형을 지원하기 위해 상속을 사용한다. 그런 다음 특정 도형은 예를 들어 Shape 기본 클래스에서 파생된다.
상속과 템플릿을 결합할 수 있다. 기본 클래스 템플릿에서 파생되는 클래스 템플릿을 작성할 수 있다. Chapter 12에서는 템플릿 구문에 대해 자세히 설명한다.
Provide Appropriate Checks and Safeguards (적절한 확인과 보호 조치 제공)
안전한 코드 작성에는 두 가지 상반된 스타일이 있다. 최적의 프로그래밍 스타일은 아마도 두 가지를 적절히 혼합하여 사용하는 것이다. 첫 번째는 design-by-contract(계약에 의한 디자인)이라고 한다. 즉 함수나 클래스에 대한 문서는 클라이언트 코드의 책임과 함수나 클래스의 책임에 대한 자세한 설명이 포함된 계약을 나타낸다. 계약에 의한 디자인에는 선행 조건, 후행 조건과 불변식이라는 세 가지 중요한 측면이 있다. 선행 조건(precondition)은 클라이언트 코드가 함수나 메서드를 호출하기 전에 충족해야 하는 조건을 나열한다. 후행 조건(postcondition)은 함수나 메서드가 실행을 완료했을 때 충족해야 하는 조건을 나열한다. 마지막으로, 불변식(invariant)은 함수나 메서드의 전체 실행 중에 충족되어야 하는 조건을 나열한다.
계약에 의한 디자인은 종종 Standard Library에서 사용된다. 예를 들어, std::vector는 vector에서 특정 요소를 가져오기 위해 배열 표기법을 사용하기 위한 계약을 정의한다. 계약에는 경계 검사가 수행되지 않지만, 이는 클라이언트 코드의 책임이라고 명시되어 있다. 즉, 배열 표기법을 사용하여 vector에서 요소를 가져오기 위한 선행 조건은 주어진 인덱스가 유효해야 한다는 것이다. 이는 해당 인덱스가 범위 내에 있다는 것을 알고 있는 클라이언트 코드의 성능을 향상시키기 위해 수행된다.
두 번째 스타일은 가능한 안전하게 함수와 클래스를 디자인하는 것이다. 이 가이드라인의 가장 중요한 측면은 코드에서 오류 검사를 수행한다는 것이다. 예를 들어, 난수 생성기에서 시드가 특정 범위에 있어야 하는 경우, 사용자가 유효한 시드를 전달할거라고 신뢰하지 않는다. 전달된 값을 확인하고, 유효하지 않은 경우 호출을 거부한다. 두 번째 예로, vector에서 요소를 검색하기 위한 design-by-contract(계약에 의한 디자인) 배열 표기법 옆에, 경계 검사를 수행하는 동안 특정 요소를 가저오는 at() 메서드도 정의한다. 사용자가 잘못된 인덱스를 제공하는 경우, 메서드에서 예외를 발생한다. 그래서 클라이언트 코드는 경계 검사 없이 배열 표기법을 사용할지 또는 경계 검사가 있는 at() 메서드를 사용할지 선택할 수 있다.
소득세 신고서를 작성하는 회계사를 예로 들어보자. 회계사를 고용하면, 해당 연도의 모든 재무 정보를 회계사에게 제공한다. 회계사는 IRS의 양식을 작성하기 위해 이 정보를 사용한다. 그러나 회계사는 당신의 정보를 맹목적으로 양식에 기입하지 않고, 대신 그 정보가 이치에 맞는지 확인한다. 예를 들어, 주택을 소유하고 있지만 납부한 재산세를 지정하는 것을 잊은 경우, 회계사는 해당 정보를 제공하도록 상기시켜준다. 마찬가지로, 주택담보대출 이자로 $12,000를 지불했지만 총소득은 $15,000에 불과하다고 말하는 경우, 회계사는 정확한 수치를 제공했는지(또는 적어도 더 저렴한 주택을 추천할 것인지) 부드럽게 물을 것이다.
회계사는 입력이 재무 정보이고 출력이 소득세 신고서인 "program"으로 생각할 수 있다. 그러나 회계사에 의해 추가된 가치는 단순히 양식을 작성하는 것만이 아니다. 또한 회계사들이 제공하는 검사와 보호 장치 때문에 그들을 고용하기로 선택한다. 마찬가지로 프로그래밍에서도 구현 시 가능한 많은 검사와 보호 장치를 제공할 수 있다.
안전한 코드를 작성하고 프로그램에 검사와 안전 장치를 통합하는 데 도움이 되는 몇 가지 기술과 언어 기능이 있다. 클라이언트 코드에 오류를 보고하기 위해, 예를 들어 오류 코드, false나 nullptr 같은 고유 값이나 Chapter 1에서 소개한 std::optional을 반환할 수 있다. 또는 예외를 발생시켜 클라이언트 코드에 오류를 알릴 수 있다. Chapter 14, "Handling Errors"에서 예외를 자세히 다룬다.
Design for Extensibility (확장성을 위한 디자인)
클래스에서 다른 클래스를 파생시켜 확장할 수 있는 방식으로 클래스를 디자인해야 하지만, 수정할 수 없도록 폐쇄해야 한다. 즉, 구현을 수정하지 않고도 동작을 확장할 수 있어야 한다. 이를 open/closed principl(OCP: 개방/폐쇄 원칙)이라고 한다.
예를 들어, 그리기 애플리케이션 구현을 시작한다고 가정한다. 첫 번째 버전은 사각형만 지원해야 한다. 디자인에는 Square와 Renderer라는 두 가지 클래스가 포함되어 있다. 전자는 변의 길이와 같은 정사각형의 정의를 포함한다. 후자는 사각형 그리기를 담당한다. 당신은 다음과 같은 것을 생각해낸다:
class Square { /* Details not important for this example. */ };
class Renderer
{
public:
void render(const vector<Square>& square)
{
for (auto& square : squares) { /* Render this square object... */ }
}
};
다음으로, 원에 대한 지원을 추가하여 Circle 클래스를 만든다:
class Circle { /* Details not important for this example. */ };
원을 렌더링하려면, Renderer 클래스의 render() 메서드를 수정해야 한다. 다음과 같이 변경하기로 결정한다:
void render(const vector<Square>& squares,
const vector<Circle>& circles)
{
for (auto& square : squares) { /* Render this square object... */ }
for (auto& circle : circles) { /* Render this circle object... */ }
}
이 작업을 수행하는 동안, 당신이 무언가 잘못되었다고 느꼈다면, 당신이 옳은 것이다. 기능을 확장하여 원에 대한 지원을 추가하려면, render() 메서드의 현재 구현을 수정해야 하는데, 이는 수정에 대해 폐쇄된 것이 아니다.
이 경우 디자인으로 상속을 사용해야 한다. 이 예제는 상속 문법으로 약간 앞서 있다. Chapter 10, "Discovering Inheritance Techniques"에서는 상속에 대해 자세히 설명한다. 그러나 이 예제를 이해하는 데 세부적인 문법은 중요하지 않다. 지금은 다음 구문이 Square가 Shape 클래스에서 파생되도록 지정한다는 점만 알면 된다.
class Square : public Shape {};
다음은 상속을 사용한 디자인이다:
class Shape
{
public:
virtual void render() = 0;
}
class Square : public Shape
{
public:
void render() override { /* Render square... */ }
// Other members not important for this example.
};
class Circle : public Shape
{
public:
void render() override { /* Render circle... */ }
// Other members not important for this example.
};
class Renderer{
public:
void render(const vector<Shape*>& objects)
{
for (auto* object : objects) { object->render(); }
}
};
이 디자인에서 새로운 도형 타입에 대한 지원을 추가하려면, Shape에서 파생되고 render() 메서드를 구현하는 새 클래스를 작성하기만 하면 된다. Renderer 클래스에서 아무것도 수정할 필요가 없다. 따라서, 이 디자인은 기존 코드를 수정하지 않고도 확장할 수 있다. 즉, 확장에는 열려 있고 수정에는 폐쇄되어 있다.
Design Usable Interfaces (사용 가능한 인터페이스 디자인)
재사용을 위한 디자인에서는, 코드를 적절하게 추상화하고 구조화하는 것 외에도 프로그래머와 상호 작용하는 인터페이스에 집중해야 한다. 당신이 가장 아름답고 가장 효율적인 구현을 가지고 있다고 해도, 당신의 라이브러리가 형편없는 인터페이스를 가지고 있다면 아무런 소용이 없을 것이다.
여러 프로그램에서 사용할 의도가 없더라도, 프로그램의 모든 컴포넌트는 좋은 인터페이스를 가져야 한다. 첫 번째, 언제 어떤 것이 재사용될지 절대 알 수 없다. 두 번째, 처음 사용하는 경우에도 좋은 인터페이스가 중요하다. 특히 그룹내에서 프로그램 작업을 하면서 당신이 디자인하고 작성한 코드를 다른 프로그래머가 사용해야 하는 경우 더욱 그렇다.
C++에서 클래스의 속성과 메서드는 각각 public, protected 또는 private로 지정될 수 있다. 속성이나 메서드를 public으로 지정 한다는 것은 다른 코드에서 그것을 액세스할 수 있다는 것을 의미한다. protected는 속성이나 메서드를 다른 코드에서는 액세스할 수 없지만, 파생 클래스에서는 액세스할 수 있다는 것을 의미한다. private은 더 엄격한 컨트롤이다. 즉, 속성이나 메서드가 다른 코드에 대해 잠겨 있을 뿐만 아니라, 파생 클래스에서도 액세스할 수 없다. 액세스 지정자는 개체 레벨이 아니라 클래스 레벨에 있다. 즉, 예를 들어 동일한 클래스의 다른 개체에 대한 private 속성이나 private 메서드에 클래스의 메서드는 액세스할 수 있다.
노출되는 인터페이스를 디자인한다는 것은 무엇을 public 지정 할지 선택하는 것이다. 노출되는 인터페이스 디자인을 프로세스로 보아야 한다. 인터페이스의 주요 목적은 코드를 사용하기 쉽게 만드는 것이지만, 인터페이스의 일부 기술은 일반성의 원칙을 따르도록 당신을 도울 수 있다.
Consider the Audience (사용자를 고려한다)
노출되는 인터페이스를 디자인하는 첫 번째 단계는 누구를 위해 디자인하는지 고려하는 것이다. 사용자는 팀의 다른 구성원입니까? 개인적으로 사용할 인터페이스입니까? 회사 외부의 프로그래머가 사용할 것입니까? 아마도 고객이나 해외의 계약자일까요? 인터페이스에 대한 도움을 요청할 사람을 결정하는 것 외에도, 이를 통해 디자인 목표 중 일부를 밝힐 수 있다.
인터페이스가 자신의 사용을 위한 것이라면, 디자인을 반복할 수 있는 더 많은 자유가 있을 것이다. 인터페이스를 사용하면서 자신의 필요에 맞게 변경할 수 있다. 그러나 엔지니어링 팀의 역할이 변경되고, 엔젠가는 다른 사람들도 이 인터페이스를 사용할 가능성이 높다는 점을 염두에 두어야 한다.
내부의 다른 프로그래머가 사용할 인터페이스를 디자인하는 것은 약간 다르다. 어떤 면에서, 당신의 인터페이스는 그들과의 계약이 된다. 예를 들어, 프로그램의 데이터 저장소 컴포넌트를 구현하는 경우, 다른 사람들은 특정 작업을 지원하기 위해 해당 인터페이스에 의존하게 된다. 나머지 팀원들이 당신의 클래스에서 하기를 바라는 모든 일을 알아내야 한다. 버전 관리가 필요한가? 어떤 타입의 데이터를 저장할 수 있는가? 계약으로서, 인터페이스는 약간 덜 유연하다고 봐야 한다. 코딩이 시작되기 전에 인터페이스가 합의된 경우, 당신이 코드 작성을 완료한 후 변경하기로 결정을 한다면 다른 프로그래머로부터 신음 소리를 듣게 될 것이다.
외부 사용자를 위한 인터페이스를 설계할 때, 이상적으로는 내부 사용자를 위한 인터페이스를 설계할 때와 마찬가지로 외부 사용자는 인터페이스로 노출하는 기능을 지정하는 데 참여해야 한다. 사용자가 원하는 특정 기능과 향후 사용자가 원할 수 있는 기능을 모두 고려해야 한다. 인터페이스에 사용되는 용어는 사용자가 잘 알고 있는 용어와 일치해야 하며, 문서는 해당 사용자를 염두에 두고 작성해야 한다. 내부 농담, 코드네임 그리고 프로그래머의 속어는 디자인에서 제외되어야 한다.
인터페이스를 디자인하는 사용자도 디자인에 투자해야 하는 시간에 영향을 미친다. 예를 들어, 소수의 사용자가 소수의 장소에서만 사용할 몇 가지 방법으로 인터페이스를 디자인하는 경우, 나중에 인터페이스를 수정하는 것이 허용될 수 있다. 그러나 복잡한 인터페이스나 많은 사용자가 사용할 인터페이스를 디자인하는 경우, 디자인에 더 많은 시간을 할애하고 사용자가 사용하기 시작하면 인터페이스를 수정하지 않도록 최선을 다해야 한다.
Consider the Purpose (목적을 고려한다)
인터페이스를 작성하는 데는 여러 가지 이유가 있다. 종이에 코드를 작성하거나 노출할 기능을 결정하기 전에, 인터페이스의 목적을 이해해야 한다.
Application Programming Interface (API)
Application Programming Inteface(API)는 제품을 확장하거나 다른 컨텍스트 내에서 해당 기능을 사용하기 위해 외부에서 볼 수 있는 메커니즘이다. 내부 인터페이스가 계약이라면, API는 기본 법칙에 가깝다. 당신의 회사에서 일하지도 않는 사람들이 당신의 API를 사용하고 있다면, 당신이 그들에게 도움이 될 새로운 기능을 추가하지 않는 한 API를 변경하는 것을 원하지 않을 것이다. 따라서 API를 계획하고 사용자에게 제공하기 전에 사용자과 논의하는 데 주의를 기울어야 한다.
API 디자인의 주요 절충점은 일반적으로 사용 용이성과 우연성이다. 인터페이스의 대상 사용자는 제품의 내부 작업에 익숙하지 않기 때문에, API를 사용하기 위한 학습 곡선은 점진적이어야 한다. 결국 당신의 회사는 이 API가 사용되기를 원하기 때문에 사용자에게 이 API를 노출하고 있다. 사용하기 너무 어렵다면, API는 실패한 것이다. 유연성은 종종 이에 불리하게 작용한다. 당신의 제품은 다양한 용도로 사용될 수 있으며, 당신이 제공해야 하는 모든 기능을 사용자가 활용할 수 있기를 원한다. 그러나 당신의 제품이 할 수 있는 모든 것을 사용자가 할 수 있도록 하는 API는 너무 복잡할 수 있다.
일반적인 프로그래밍 격언처럼, "A good API makes the common case easy and the advanced/unlikely case possible.(좋은 API는 일반적인 케이스를 쉽게 만들고 고급/예상하지 않은 케이스를 가능하게 한다)". 즉, API는 학습 곡선이 단순해야 한다. 대부분의 프로그래머가 하고자 하는 일은 접근 가능해야 한다. 그러나 API는 보다 고급 사용이 가능해야 하며, 일반적인 케이스의 단순성을 위해 복잡성의 드문 경우는 절충하는 것이 허용된다. 이 장 뒷부분의 "Design Interfaces That Are Easy to Use(사용하기 쉬운 디자인 인터페이스)" 섹션에서는 당신의 디자인에서 따라야 할 여러 가지 구체적인 팁과 함께 이 전략에 대해 자세히 설명한다.
Utility Class or Library (유틸리티 클래스나 라이브러리)
종종, 당신의 작업은 애플리케이션의 어느 곳에서나 일반적으로 사용할 수 있는 일부 특정 기능(예로 로깅 클래스)을 개발하는 것이다. 이 경우 인터페이스는 구현에 대해 너무 많은 부담을 주지 않고, 기능의 대부분이나 전부를 노출하는 경향이 있기 때문에 결정하기가 다소 쉽다. 일반성은 고려해야 할 중요한 문제이다. 클래스나 라이브러리는 일반적인 용도이기 때문에, 디자인 시 사용 가능한 사용 사례를 고려해야 한다.
Subsystem Interface (서브시스템 인터페이스)
데이터베이스 액세스 메커니즘과 같은, 애플리케이션의 두 개의 주요 서브시스템 사이의 인터페이스를 디자인할 수 있다. 이러한 경우, 구현에서 인터페이스를 분리하는 것이 여러가지 이유로 가장 중요하다.
가장 중요한 이유 중 하나는 mockability(모의 가능성)이다. 테스트 시나리오에서, 인터페이스의 특정 구현을 동일한 인터페이스의 다른 구현으로 대체하려고 한다. 예를 들어, 데이터베이스 인터페이스에 대한 테스트 코드를 작성할 때 실제 데이터베이스에 액세스하기를 원하지 않을 수 있다. 실제 데이터베이스에 액세스하는 인터페이스 구현은 데이터베이스의 모든 액세스를 시뮬레이트하는 구현으로 대체될 수 있다.
또 다른 이유는 flexibility(유연성)이다. 테스트 시나리오 외에는, 상호 교환적으로 사용할 수 있는 특정 인터페이스의 여러 가지 다른 구현을 제공할 수 있다. 예를 들어, MySQL 서버 데이터베이스를 사용하는 데이터베이스 인터페이스 구현을 SQL Server 데이터베이스를 사용하는 구현으로 대체할 수 있다. 런타임에 서로 다른 구현 사이에 전환할 수도 있다.
덜 중요하지만 또 다른 이유는, 인터페이스를 먼저 완료하여, 다른 프로그래머들이 당신이 구현을 완료하기 전에 이미 당신의 인터페이스에 대한 프로그래밍을 시작할 수 있다는 것이다.
서브시스템에서 작업할 때, 먼저 주요 목적이 무엇인지 생각해 본다. 서브시스템이 담당하는 주요 작업을 확인한 후에는, 특정 용도와 코드의 다른 부분에 어떻게 표시되어야 하는지에 대해 생각한다. 그들의 입장이 되어 구현 세부 사항에 얽매이지 않도록 한다.
Component Interface (컴포넌트 인터페이스)
당신이 정의하는 대부분의 인터페이스는 아마도 서브시스템 인터페이스나 API보다 작을 수 있다. 이것들은 당신이 작성한 다른 코드 내에서 사용하는 클래스일 수 있다. 이러한 경우, 당신의 인터페이스가 점진적으로 진화하고 무질서가 될 때 주요 함정이 발생한다. 이러한 인터페이스가 사용자 자신을 위한 것이라고 해도 그렇지 않은 것처럼 생각해야 한다. 서브시스템 인터페이스와 마찬가지로, 각 클래스의 주요 목적을 고려하고 해당 목적에 기여하지 않는 기능을 노출하지 않도록 주의해야 한다.
Design Interfaces That Are Easy to Use (사용하기 쉬운 인터페이스 디자인)
당신의 인터페이스는 사용하기 쉬어야 한다. 그렇다고 해서 그것들이 사소해야 한다는 의미는 아니지만, 기능이 허용하는 한 간단하고 직관적이어야 한다. 간단한 데이터 구조를 사용하거나 필요한 기능을 얻기 위해 그들의 코드에 왜곡을 수행하여 라이브러리 소비자가 소스 코드 페이지를 해매도록 요구해서는 안 된다. 이 섹션에서는 사용하기 쉬운 인터페이스를 디자인하기 위한 네 가지 구체적인 전략을 제공한다.
Follow Familiar Ways of Doing Things (익숙한 작업 방식 따르기)
사용하기 쉬운 인터페이스를 개발하기 위한 최선의 전략은 표준적이고 익숙한 작업 방식을 따르는 것이다. 사람들이 과거에 사용했던 것과 유사한 인터페이스를 접하게 되면, 그들은 그것을 더 잘 이해하고, 더 쉽게 채택하고, 그것들을 부적절하게 사용할 가능성이 줄어들 것이다.
예를 들어, 당신이 자동차의 스티어링 메커니즘을 디자인한다고 가정해 보자. 조이스틱, 좌우로 움직이는 두 개의 버튼, 슬라이딩 수평 레버나 좋지만 오래된 스티어링 휠 등 다양한 가능성이 있다. 어떤 인터페이스를 사용하는 가장 쉬울 것 같습니까? 어떤 인터페이스가 자동차를 가장 많이 판매할 것 같습니까? 소비자는 스티어링 휠에 익숙하기 때문에, 두 질문에 대한 답은 물로 스티어링 휠이다. 당신이 우수한 성능과 안전성을 제공하는 다른 메커니즘을 개발하더라도, 당신은 사람들에게 사용법을 가르치는 것은 고사하고 제품을 판매하는 데 어려움을 겪을 것이다. 표준 인터페이스 모델을 따르는 것과 새로운 방향으로 확장하는 것 사이에서 선택할 수 있는 경우, 일반적으로 사람들에게 익숙한 인터페이스를 고수하는 것이 더 좋다.
혁신은 물론 중요하지만, 인터페이스가 아닌 기본 구현의 혁신에 집중해야 한다. 예를 들어, 소비자들은 일부 자동차 모델의 혁신적인 완벽한 전기 엔진에 대해 흥분하고 있다. 이 자동차들은 부분적으로 그것들을 사용하기 위한 인터페이스가 표준 가솔린 엔진을 가진 자동차들과 동일하기 때문에 잘 팔리고 있다.
C++에 적용되는, 이 전략은 C++ 프로그래머에게 익숙한 표준을 따르는 인터페이스를 개발해야 한다는 것을 의미한다. 예를 들어, C++ 프로그래머는 클래스의 생성자와 소멸자에서 각각 개체를 초기화하고 정리할 것으로 기대한다(두 가지 모두 Chapter 8, "Gaining Proficiency with Classes and Objects"에서 자세히 설명함). 기존 개체를 "reinitialize(재초기화)"해야 하는 경우, 표준 방법은 새로 생성된 개체를 해당 개체에 할당하는 것이다. 클래스를 디자인할 때, 이러한 표준을 따라야 한다. 프로그래머가 초기화와 정리에 해당하는 기능을 생성자와 소멸자에 배치하는 대신에 initialize()와 cleanup() 메서드를 호출하도록 요구하면, 클래스를 사용하려는 모든 사람을 혼란스럽게 할 것이다. 당신의 클래스는 다른 C++ 클래스와 다르게 동작하기 때문에, 프로그래머들은 클래스의 사용 방법을 배우는 데 시간이 오래 걸릴 것이고 initialize()나 cleanup()을 호출하는 것을 잊어버려 그것을 잘못 사용할 가능성이 더 높아질 것이다.
참고
항상 인터페이스를 사용하는 사용자의 관점에서 인터페이스를 생각해야 한다.
말이 되나요? 그것들이 당신이 기대하는 것입니까?
C++는 당신의 개체에 대해 사용하기 쉬운 인터페이스를 개발하는 데 도움이 되는 연산자 오버로딩(operator overloading)이라는 언어 기능을 제공한다. 연산자 오버로딩을 사용하면 표준 연산자가 int와 double 같은 기본으로 제공되는 형식에서 작동하는 것처럼 클래스를 작성할 수 있다. 예를 들어, 다음과 같이 분수를 더하고 빼는 스트림(cout)할 수 있는 Fraction 클래스를 작성할 수 있다:
Fraction f1 { 3, 4 };
Fraction f2 { 1, 2 };
Fraction sum { f1 + f2 };
Fraction diff { f1 - f2 };
cout << f1 << " " << f2 << endl;
메서드 호출을 사용하는 동일한 동작과 비교한다:
Fraction f1 { 3, 4 };
Fraction f2 { 1, 2 };
Fraction sum { f1.add(f2) };
Fraction diff { f1.subtract(f2) };
f1.print(cout);
cout << " ";
f2.print(cout);
cout << endl;
당신이 보는 것과 같이, 연산자 오버로딩으로 클래스에 사용하기 쉬운 인터페이스를 제공할 수 있다. 그러나 연산자 오버로딩을 남용하지 않도록 주의해야 한다. 빼기를 구현하도록 + 연산자를 오버로드하고, 곱셈을 구현하도록 - 연산자를 오버로드할 수 있다. 이러한 구현은 직관적이지 않다. 이는 각 연산자가 항상 정확히 동일한 동작을 구현해야 한다는 의미는 아니다. 예를 들어, string 클래스는 + 연산자를 구현하여 string을연결한다. 이는 string 연결을 위한 직관적인 인터페이스이다. 연산자 오버로딩에 대한 자세한 내용은 Chapter 9와 15를 참조한다.
Don`t Omit Required Functionality (필수 기능을 생략하지 않는다.)
인터페이스를 디자인할 때, 향후 어떻게 유지할 지 염두해야 한다. 이것은 당신이 몇 년동안 고정해야 할 디자인입니까? 그렇다면, 당신은 플러그인 아키텍처를 제시하여 확장의 여지를 남겨두어야 할 수도 있다. 사람들이 당신의 인터페이스를 설계된 목적 이외의 목적으로 사용하려고 시도한다는 근거가 있습니까? 그들과 이야기하고 사용 사례를 더 잘 이해해야 한다. 대안은 나중에 다시 작성하거나, 더 나쁘게는 우연히 새로운 기능을 추가하여 지저분한 인터페이스로 끝나는 것이다. 하지만 조심해야 한다. 추측 일반성은 또 다른 함정이다. 디자인, 구현 그리고 public 인터페이스를 불필요하게 복잡하게 만들 수 있으므로, 향후 용도가 불분명한 경우, 가장 중요한 로깅 클래스를 디자인하지 않는다.
이 전략은 두 가지이다. 첫 번째, 클라이언트가 필요로 할 수 있는 모든 동작에 대한 인터페이스를 포함한다. 처음에는 분명하게 들릴 수 있다. 자동차 비유로 돌아가서, 운전자가 자신의 속도를 볼 수 있는 속도계 없이는 당신은 자동차를 만들지 못할 것이다. 마찬가지로, 클라이언트 코드가 nominator와 denominator 값에 액세스하는 메커니즘이 없이 Fraction 클래스를 디자인하지 않을 것이다.
그러나 다른 가능한 동작은 더 모호할 수 있다. 이 전략에서는 클라이언트가 당신의 코드를 입력할 수 있는 모든 용도를 예상해야 한다. 특정 방식으로 인터페이스에 대해 생각하고 있다면, 클라이언트가 인터페이스를 다르게 사용할 때 필요할 수 있는 기능을 놓칠 수 있다. 예를 들어, 게임 보드 클래스를 디자인한다고 가정한다. 체스와 체커 같은 일반적인 게임만 고려하고 보드의 지점당 최대 하나의 게임 조각를 지원하도록 결정할 수 있다. 그러나 나중에 보드의 한 지점에 여러 조각을 허용하는 주사위 놀이 게임을 작성하기로 결정했다면 어떻게 될까? 그 가능성을 배제함으로써 당신은 게임 보드를 주사위 놀이 보드로 사용하는 것을 배제한 것이다.
분명히, 불가능하지는 않지만, 라이브러리의 모든 가능한 사용을 예상하는 것은 어렵다. 완벽한 인터페이스를 디자인하기 위해 잠재적인 향후 사용에 대해 고민해야 한다는 강박감을 느낄 필요는 없다. 조금 더 생각하고 최선을 다하면 된다.
이 전략의 두 번째 부분은 구현에 가능한 한 많은 기능을 포함하는 것이다. 구현에서 이미 알고 있거나 다르게 디자인한 경우 알 수 있는 정보를 지정하기 위해 클라이언트 코드를 요구하지 않는다. 예를 들어, 라이브러리에 임시 파일이 필요한 경우, 라이브러리의 클라이언트가 해당 경로를 지정하지 않도록 해야 한다. 그들은 당신이 어떤 파일을 사용하는지 상관하지 않는다. 적절한 임시 파일 경로를 결정하는 다른 방법을 찾는다.
또한, 라이브러리 사용자가 결과를 통합하기 위해 불필요한 작업을 수행하도록 요구하지 않는다. 난수 라이브러리에서 난수의 하위와 상위 비트를 별도로 계산하는 난수 알고리즘을 사용하는 경우, 사용자에게 제공하기 전에 숫자를 결합한다.
Present Uncluttered Interfaces (정돈된 인터페이스 제공)
인터페이스에서 기능 생략을 피하기 위해, 일부 프로그래머는 정반대의 극단으로 나아간다. 즉 상상할 수 있는 모든 가능한 기능을 포함한다. 인터페이스를 사용하는 프로그래머는 작업을 수행할 수 있는 방법이 없이면 절대 남아 있지 않는다. 불행하게도 인터페이스가 너무 복잡해서 그것을 어떻게 사용 하는지 결코 알아내지 못할 수 있다.
인터페이스에 불필요한 기능을 제공하지 않는다. 결점없이 단순하게 유지한다. 처음에는 이 지침이 필요한 기능을 생략하지 않는 이전 전략과 직접적으로 모순되는 것처럼 보일 수 있다. 기능 생략을 피하기 위한 한 가지 전략은 생각할 수 있는 모든 인터페이스를 포함하는 것이지만, 이는 건전한 전략이 아니다. 당신은 필요한 기능을 포함하고 사용하지 않거나 비생산적인 인터페이스는 생략해야 한다.
자동차를 다시 고려해 본다. 스티어링 휠, 브레이크와 가속 페달, 기어 변속 장치, 거울, 속도계와 대시보드의 몇 가지 다른 다이얼과 같은 몇 가지 컴포넌트와 상호 작용하여 자동차를 운전한다. 이제 수백 개의 다이얼, 레버, 모니터와 버튼이 있는 비행기 조정석처럼 보이는 자동차 대시보드를 상상해 본다. 그것은 사용할 수 없을 것이다. 자동차 운전은 비행기를 조정하는 것보다 훨씬 쉽기 때문에 인터페이스가 훨씬 간단할 수 있다. 고도를 확인하거나 관제탑과 통신하거나 날개, 엔진과 랜딩 기어와 같은 비행기의 무수한 컴포넌트를 제어할 필요가 없다.
또한 라이브러리 개발 관점에서 볼 때 라이브러리가 작을수록 유지 관리가 더 쉽다. 모든 사람을 행복하게 하려고 하면 실수할 여지가 더 많아지고, 모든 것이 뒤엉킬 정도로 구현이 복잡하면, 한 번의 실수로도 라이브러리를 쓸모없게 만들수 있다.
불행하게도 정돈된 인터페이스를 설계한다는 아이디어는 이론상으로는 좋아 보이지만 실제로 적용하기는 매우 어렵다. 규칙은 궁극적으로 주관적이다. 필요한 것과 필요하지 않은 것을 결정한다. 물론, 당신의 클라이언트는 당신이 틀렸을 때, 확실히 말할 것이다.
Provide Documentation (문서 제공)
당신의 인터페이스를 사용하기 쉽게 만드는 방법에 관계없이, 그것들을 사용하기 위한 문서를 제공해야 한다. 프로그래머에게 라이브러리를 사용하는 방법을 알려주지 않는 한, 프로그래머가 라이브러리를 제대로 사용하기를 기대할 수 없다. 라이브러리나 코드를 다른 프로그래머가 사용할 수 있는 제품으로 생각해야 한다. 당신의 제품에는 올바른 사용법을 설명하는 문서가 있어야 한다.
당신의 인터페이스에 대해 문서를 제공하는 방법에는 인터페이스 자체의 주석과 외부 문서 두 가지가 있다. 두 가지 모두 제공하기 위해 노력해야 한다. 대부분의 공개 API는 외부 문서만 제공한다. 주석은 많은 표준 Unix와 Windows 헤더 파일에서 부족한 필수품이다. Unix에서 문서는 일반적으로 소위 man pages의 형태로 제공된다. Windows에서 문서는 일반적으로 통합 개발 환경과 함께 제공되거나 인터넷에서 사용할 수 있다.
대부분의 API와 라이브러리는 인터페이스 자체에서 주석을 생략하지만, 실제로는 이 형식의 문서가 가장 중요하다고 생각한다. 코드만 포함된 "naked" 모듈이나 헤더 파일을 제공해서는 안 된다. 당신의 주석이 외부 문서에 있는 내용을 정확히 반복하더라도 코드만 있는 것보다 친근한 주석이 있는 모듈이나 헤더 파일을 보는 것이 덜 위협적이다. 심지어 최고의 프로그래머들도 여전히 서면 언어를 보는 것을 좋아한다. Chapter 3에서는 주석을 달고 작성하는 방법에 대한 구체적인 팁을 제공하며, 인터페이스에 작성한 주석을 기반으로 외부 문서를 작성할 수 있는 도구도 있다고 설명한다.
Design General-Purpose Interfaces (범용 인터페이스 디자인)
인터페이스는 다양한 작업에 적용할 수 있을 만큼 충분히 범용적이어야 한다. 일반적인 인터페이스로 추정되는 한 애플리케이션의 세부 사항을 인코딩하면, 다른 용도로는 사용할 수 없다. 여기에 명심해야 할 몇 가지 지침이 있다.
Provide Multiple Ways to Perform the Same Functionality (동일한 기능을 수행하는 다양한 방법을 제공)
모든 "고객"를 만족시키려면, 동일한 기능을 수행하는 여러 가지 방법을 제공하는 것이 때때로 도움이 된다. 그러나 이 기술은 신중하게 사용해야 한다. 과도한 애플리케이션은 쉽게 복잡한 인터페이스로 이어질 수 있기 때문이다.
자동차를 다시 고려해 본다. 요즈음 대부분의 신차는 키팝(key fob)의 버튼을 눌러 자동차의 잠금을 해제할 수 있는, 원격 키리스 엔트리 시스템을 제공한다. 그러나 이러한 자동차는 예를 들어 키팝의 배터리가 방전된 경우와 같이, 자동차를 물리적으로 잠금 해제하는 데 사용할 수 있는 표준 키를 항상 제공한다. 이 두 가지 방법은 중복되지만, 대부분의 고객은 옵션을 모두 사용할 수 있다는 점을 높이 평가한다.
때때로 인터페이스 디자인에서 비슷한 상황이 있다. 이 장의 앞부분에서 std::vector는 특정 인텍스에서 단일 요소에 액세스하는 두 가지 방법을 제공한다. 범위 검사를 수행하는 at() 메서드나, 범위 검사를 수행하지 않는 배열 표기법을 사용할 수 있다. 인덱스가 유효하다는 것을 알고 있다면, 배열 표기법을 사용하고 경계 검사로 인해 at()에서 발생하는 오버헤드를 무시할 수 있다.
이 전략은 인터페이스 디자인의 "uncluttered(정돈된)" 규칙에 대한 예외로 간주되어야 한다. 예외가 적절한 몇 가지 상황이 있지만, 주로 정돈된 규칙을 따라야 한다.
Provide Customizability (사용자 정의 제공)
인터페이스의 유연성을 높이려면, 사용자 정의 가능성을 제공해야 한다. 사용자 정의 가능성은 클라이언트가 오류 로깅을 켜거나 끄도록 허용하는 것만큼 간단할 수 있다. 사용자 정의 가능성의 기본 전제는 모든 클라이언트에 동일한 기본 기능을 제공할 수 있지만, 클라이언트에 약간 조정할 수 있는 기능을 제공할 수 있다는 것이다.
이를 달성하는 한 가지 방법은 DIP(dependency inversion principle: 의존성 역전 원칙)라고도 하는 의존성 관계를 반전시키기 위한 인터페이스를 사용하는 것이다. 의존성 주입(dependency injection)은 이 원칙의 구현 중 하나이다. Chapter 4, "Designing Professional C++ Programs"에서는 ErrorLogger 서비스의 예를 간략하게 언급한다. ErrorLogger 인터페이스를 정의하고 의존성 주입을 사용하여, 이 인터페이스를 ErrorLogger 서비스를 사용하려는 각 컴포넌트에 주입해야 한다.
당신은 콜백과 템플릿 파라미터로 더 큰 사용자 정의 가능성을 허용할 수 있다. 예를 들어, 클라이언트가 자체 오류 처리 콜백을 설정하도록 허용할 수 있다. Chapter 19, "Function Pointers, Function Objects, and Lambda Expressions"에서는 콜백에 대해 자세히 설명한다.
Standard Library는 이러한 사용자 지정 가능성 전략을 극단적으로 취하고 실제로 클라이언트가 컨테이너에 대한 자체 메모리 할당자를 지정할 수 있도록 한다. 이 기능을 사용하려면, Standard Library 지침을 따르고 필수 인터페이스를 준수하는 메모리 할당자 개체를 작성해야 한다. Standard Library에 있는 대부분의 컨테이너는 할당자를 템플릿 파라미터 중 하나로 사용한다. Chapter 25, "Customizing and Extending the Standard Library"에 자세한 내용이 나와 있다.
Reconciling Generality and Ease of Ues (일반성과 사용 편의성의 조화)
사용 편의성과 일반성이라는 두 가지 목표는 때때로 충돌하는 것처럼 보인다. 종종 일반성을 도입하면 인터페이스의 복잡성이 증가한다. 예를 들어, 도시를 저장하기 위해 지도 프로그램에 그래프 구조체가 필요하다고 가정한다. 일반화를 위해, 당신은 도시뿐 아니라 모든 유형에 대한 일반적인 지도 구조체를 작성하기 위해 템플릿을 사용할 수 있다. 이렇게 하면, 당신의 다음 프로그램에서 네트워크 시뮬레이터를 작성하는 경우, 동일한 그래프 구조체를 사용하여 네트워크에 라우터를 저장할 수 있다. 불행하게도 템플릿을 사용하게 되면, 인터페이스가 좀 어설프게 되고 사용하기 어려워 진다. 특히 잠재적인 클라이언트가 템플릿에 익숙하지 않은 경우 더욱 그렇게 된다.
그러나 일반성과 사용 편의성은 상호 배타적이지는 않다. 경우에 따라 일반성이 증가하면 사용 편의성이 감소할 수 있지만, 범용적이고 사용하기 쉬운 인터페이스를 디자인할 수 있다.
충분한 기능을 제공하면서 인터페이스의 복잡성을 줄이기 위해 여러 개의 개별 인터페이스를 제공할 수 있다. 이는 interface segregation principle(ISP: 인터페이스 분리 원칙)이라고 한다. 예를 들어, 당신은 두 개의 별도 측면으로 일반적인 네트워크 라이브러리를 작성할 수 있다. 하나는 게임에 유용한 네트워크 인터페이스를 제공하고 다른 하나는 웹 브라우즈를 위한 HTTP(Hypertext Transfer Protocol)에 유용한 네트워크 인터페이스를 제공한다. 또한 여러 인터페이스를 제공하면 좀 더 고급 기능에 대한 옵션을 계속 제공하면서, 일반적으로 사용되는 기능을 쉽게 사용할 수 있도록 도움이 된다. 지도 프로그램으로 돌아가서, 당신은 지도 클라이언트에 다른 언어로 된 도시 이름을 지정할 수 있도록 별도의 인터페이스를 제공하는 동시에 영어가 기본 값이 되도록 지정할 수 있다. 그렇게 하면, 대부분의 고객은 언어 설정에 대해 걱정할 필요가 없지만, 원하는 고객은 그렇게 할 수 있을 것이다.
Designing a Successful Abstraction (성공적인 추상화 디자인)
경험과 반복은 좋은 추상화를 위해 필수적이다. 진정으로 잘 디자인된 인터페이스는 수년간의 작성하고 사용한 다른 추상화에서 비롯된다. 또한 표준 디자인 패턴의 형태로 잘 디자인된 기존 추상화를 재사용하여, 다른 사람이 수 년간 작성하고 사용한 추상화를 활용할 수도 있다. 당신이 다른 추상화를 접할 때, 무엇이 효과가 있었고 무엇이 효과가 없었는지 기억하려고 노력해 본다. 지난주에 사용한 Windows 파일 시스템 API에서 부족한 점은 무엇입니까? 동료 대신 네트워크 래퍼를 작성했다면, 당신은 무엇을 다르게 했을까요? 최고의 인터페이스는 종이에 처음 작성되는 인터페이스인 경우가 거의 없으므로, 계속 반복해야 한다. 당신의 디자인을 동료에게 전달하고 피드백을 요청해야 한다. 회사에서 코드 리뷰를 사용하는 경우, 구현을 시작하기 전에 인터페이스 사양 검토부터 시작해야 한다. 코딩이 시작되면 다른 프로그래머들에게 적응하도록 강요하는 것을 의미하더라도, 추상화를 변경하는 것을 두려워하지 않는다. 이상적으로, 그들은 좋은 추상화가 장기적으로 모든 사람들에게 유익하다는 것을 깨닫게 될 것이다.
때로는 다른 프로그래머에게 당신의 디자인을 전달할 때 약간 전도할 필요가 있다. 아마도 나머지 팀은 이전 디자인에서 문제를 발견하지 못했거나 당신의 접근 방식이 너무 많은 작업을 필요로 한다고 느꼈을 것이다. 그러한 상황에서는 자신의 작업을 옹호하고 그들의 아이디어를 적절히 통합할 준비를 한다.
좋은 추상화는 내보낸 인터페이스가 안정적이고 변경되지 않는 공개(public) 메서드만 가지고 있다는 것을 의미한다. 이를 달성하기 위한 특정 기술을 private implementation idiom(개인 구현 관용구) 또는 pimpl idiom(핌플 관용구)이라 하며 Chapter 9에서 설명한다.
단일 클래스 추상화에 주의해야 한다. 당신이 작성 중인 코드에 상당한 깊이가 있는 경우, 기본 인터페이스와 함께 사용할 수 있는 다른 지원 클래스를 고려해야 한다. 예를 들어, 일부 데이터 처리를 수행하기 위해 인터페이스를 노출하는 경우, 결과를 보고 해석하는 쉬운 방법을 제공하는 결과 클래스를 작성하는 것도 고려해야 한다.
항상 속성을 메서드로 전환한다. 즉, 외부 코드가 당신의 클래스 뒤의 데이터를 직접 조작하도록 허용하지 않는다. 부주의하거나 악의적인 프로그래머가 토끼 개체의 높이를 음수로 설정하는 것을 당신은 원하지 않는다. 대신 필요한 범위 검사를 수행하는 "set height" 메서드를 사용한다.
반복은 가장 중요한 포인트이기 때문에 다시 언급할 가치가 있다. 당신의 디자인에 대한 피드백을 찾아서 응답하고, 필요할 때 변경하고, 실수로부터 배운다.
The SOLID Principles (SOLID 원칙)
이 장과 이전 장에서는 개체 지향 디자인의 여러 가지 기본 원칙에 대해 설명한다. 이러한 원칙을 요약하기 위해, 그것들은 기억하기 쉬운 약어인 SOLID로 축약되는 경우가 많다. 다음 표에는 다섯 가지 SOLID 원칙이 요약되어 있다:
S | Single Responsibility Principle (SRP: 단일 책임 원칙) 단일 컴포넌트는 잘 정의된 단일 책임을 가져야 하며 관련 없는 기능을 결합해서는 안 된다. |
O | Open/Closed Principle (OCP: 개방/폐쇄 원칙) 클래스는 확장에는 개방적이여야 하지만(파생에 의해), 수정에는 폐쇄적이어야 한다. |
L | Liskov Substitution Principle (LSP: Liskov 대체 원리) 당신은 개체의 인스턴스를 해당 개체의 하위 유형 인스턴스로 바꿀 수 있어야 한다. Chapter 5에서는 AssociativeArray와 MultiAssociativeArray 사이의 관계가 has-a인지 is-a인지를 결정하는 예제와 함께 "The Fine Line Between Has-A and Is-A(Has-A와 Is-A 사이의 미세한 경계)" 섹션에서 이 원칙을 설명한다. |
I | Interface Segregation Principle (ISP: 인터페이스 분리 원칙) 인터페이스를 깨끗하고 단순하게 유지한다. 광범위하고 범용적인 인터페이스보다 작고 잘 정의된 단일 책임 인터페이스를 많이 갖는 것이 좋다. |
D | Dependency Inversion Principle (DIP: 종속성 반전 원칙) 인터페이스를 사용하여 종속 관계를 반전한다. 종속성 반전 원칙을 지원하는 한 가지 방법은 이 장의 앞 부분과 Chapter 33, "Applying Design Patterns"에서 자세히 설명하는 종속성 주입이다. |
SUMMARY (요약)
이 장을 읽으면서, 재사용 가능한 코드를 디자인하는 방법을 배웠다. "write once, use often(한 번 작성하고 자주 사용)"으로 요약되는 재사용 철학에 대해 읽고, 재사용 가능한 코드는 범용적이고 사용하기 쉬워야 한다는 것을 배웠다. 또한, 재사용 가능한 코드를 디자인하려면 추상화를 사용하고 코드를 적절하게 구조화하고 좋은 인터페이스를 디자인해야 한다는 사실도 발견했다.
이 장에서는 당신의 코드를 구조화하기 위한 특정 팁을 제시했다. 관련이 없거나 논리적으로 분리된 개념의 결합을 피하고, 일반적인 데이터 구조와 알고리즘에 템플릿을 사용하고, 적절한 확인과 안전 장치를 제공하고, 확장성을 고려하여 설계하기이다.
또한, 이 장에서는 인터페이스 디자인을 위한 6가지 전략을 제시했다. 친숙한 작업 방식 따르기, 필요한 기능을 생략하지 않기, 정돈된 인터페이스 제시하기, 문서 제공 하기, 동일한 기능을 수행하는 다양한 방법 제공하기, 사용자 지정 가능성 제공하기이다. 또한, 일반성과 사용 용이성에 대한 자주 상충되는 요구 사항을 조정하는 방법에 대해서도 논의했다.
이 장은 이 장과 다른 장에서 논의된 가장 중요한 설계 원칙을 설명하는 기억하기 쉬운 약어인 SOLID로 결론을 맺었다.
이 책의 두 번째 파트의 마지막 장으로, 더 높은 수준에서 디자인 주제를 논의하는 데 중점을 두었다. 다음 파트에서는 C++ 코딩에 대한 세부 정보와 함께 소프트웨어 엔지니어링 프로세스의 구현 단계를 자세히 살펴본다.
EXERCISES (연습 문제)
다음 연습 문제를 풀면, 이 장에서 설명한 내용을 연습할 수 있다. 모든 연습 문제에 대한 솔루션은 이 책의 웹 사이트(www.wiley.com/go/proc++5e)에서 코드를 다운로드하여 사용할 수 있다. 그러나 연습 문제를 풀다가 막히는 경우, 웹 사이트에서 솔루션을 보기 전에 먼저 이 장의 일부를 다시 읽고 스스로 답을 찾도록 시도해 본다.
Exercise 6-1: 일반적인 경우를 쉽게 만들고, 있을 법하지 않은 경우를 가능하게 만든다는 것은 무엇을 의미합니까?
Exercise 6-2: 재사용 가능한 코드 디자인을 위한 최고의 전략은 무엇입니까?
Exercise 6-3: 사람에 대한 정보로 작업해야 하는 애플리케이션을 작성한다고 가정한다. 애플리케이션의 한 파트는 최근 주문 목록, 로열티 카드 번호 등과 같은 데이터와 함께 고객 목록을 유지해야 한다. 애플리케이션의 또 다른 파트는 직원 ID, 직위 등이 있는 회사 직원을 추적해야 한다. 따라서, 이름, 전화번호, 주소, 최근 주문 목록, 포인트 카드 번호, 급여, 직원 ID, 직함(예: 엔지니어, 수석 엔지니어 등)을 포함하는 Person이라는 클래스를 디자인한다. 그런 클래스에 대해 어떻게 생각하나요? 생각할 수 있는 개선 사항이 있습니까?
Exercise 6-4: 이전 페이지를 다시 보지 않고 SOLID가 무엇을 의미하는지 설명한다.
'Programming Language > Professional C++' 카테고리의 다른 글
Professional C++ - 7. Memory Management (0) | 2022.12.19 |
---|---|
Professional C++ - 5. Designing with Objects (0) | 2022.10.10 |
Professional C++ (0) | 2022.09.08 |
Professional C++ - 4. Designing Professional C++ Programs (0) | 2022.09.08 |
Professional C++ - 3. Coding with Style (0) | 2022.07.21 |