Brise

C++ Design Pattern 본문

프로그램/C,C++

C++ Design Pattern

naudhizb 2022. 4. 7. 15:14
반응형

220404~220407 C++ Design pattern 교육

C++ 언어 내용 요약

  • Public : 기반 클래스, 파생 클래스, 외부에 모두 공개
  • protected : 기반 클래스, 파생 클래스에 공개
    • 생성자를 protected에 놓는 경우: 추상적인 개념을 모델링 할 때 생성자를 직접 호출 불가능하게 만들고 파생되는 실체 클래스가 생성자를 호출할 수 있도록 하기 위함. (Abstract class)
  • private : 기반 클래스에게만 공개

upcasting: 객체지향 언어 특성 상 파생클래스의 포인터를 기반 클래스의
객체로 캐스팅할 수 있다. (파생클래스는 기반클래스에서 정의하는 모든 기능을 지원함)
단, 기반클래스 형태일 때 파생클래스에서 정의한 고유 멤버에 접근이 불가능하며 파생클래스에서 정의하는 고유 멤버에 접근하기 위해서는 명시적인 캐스팅이 필요함.

  • static_cast : 컴파일 시간에 캐스팅을 수행
  • dynamic_cast : 런타임 시간에 캐스팅을 수행( validation을 위해서 VTable이 필요함)
    • dynamic_cast를 사용하기 위해서는 가상함수가 필요. (런타임에 캐스팅하여 적절한 함수호출을 수행하기 위해서는 VTable이 작성되어 있어야 하기 때문)

C++은 객체지향언어로 상속을 지원하기 때문에 기반클래스와 상속클래스에 대하여 같은 함수의 동작이 다를 수 있음. 클래스 포인터를 사용할 때 가상 함수(virtual function)이 아니면 클래스 포인터 타입에 따라 호출되는 함수가 다르고, 가상 함수를 사용하는 경우 클래스 포인터에 관계없이 선언된 클래스의 함수를 호출함(VTable 이용)

모든 기반클래스의 소멸자는 가상함수(virtual function) 이어야 함. (Why??? --> 아래에서 설명)
C++의 VTable 구조와 원리에 대하여.. 어떻게 동작하는가?(Study Point)

생성자의 경우 직접 파생클래스의 생성자를 이용하여 생성을 수행하게 되지만,
사용과 소멸의 경우 기반 클래스를 이용하여 사용할 수 있다. 소멸 시 소멸자가 virtual function이 아니게 되면, 소멸 시 파생 클래스의 소멸자가 불리지 않을 수 있기 때문에 적절한 소멸자를 찾을 수 있도록 기반 클래스의 소멸자를 virtual function으로 선언하여야 한다.
(단, 소멸 시 적절한 소멸자를 직접 찾을 수 있다면, 굳이 virtual function을 사용하지 않아도 된다. virtual function도 오버헤드가 존재하기 때문임.)
--> virtual function을 사용하지 않고 컴파일 단계에서 위의 오류를 막기 위해서는 기반 클래스의 소멸자를 protected로 지정하면 컴파일 단계에서 잘못된 소멸자를 호출하는 것을 막을 수 있다. 하지만, 다단 상속의 경우 대응하기가 어려우므로 객체 설계가 잘 이루어져야 한다.)

virtual function의 경우 기반 클래스와 파생 클래스에서 같은 함수를 여러번 선언하여 사용할 수 있다는 장점이 있다. 하지만, 만약 사용자가 함수 선언시 오타나 잘못된 타입을 사용하게 되면 컴파일 시간에 오류를 잡아내기 어렵다. 때문에 C++11 부터는 virtual function의 선언 뒤에 override 키워드를 사용하여 컴파일 시간에 virtual function이 기반 클래스에 선언되어 있을 경우에만 컴파일 되도록 할 수 있다.

OOP(객체 지향 프로그래밍)

  • 모든 파생 클래스에 공통적인 기능이 있다면, 그 기능은 기반 클래스에도 구현되어야 한다.

  • 다형성(polymorphism)은 같은 함수 동작이 객체에 따라 달라져 사용 코드를 변경하지 않아도 된다는 장점이 있다.

  • 객체지향 설계의 5원칙 : SRP, OCP, LST, ISP, DIP

  • SRP(단일 책임의 원칙, Single Responsibility Principle)

    THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE.

    • 작성된 클래스는 하나의 기능만 가진다
    • 제공하는 모든 서비스는 하나의 축(책임)의 변화(axis of change)를 수행하는데 집중되어야 한다.
    • 어떤 기능의 변화로 클래스를 변경하는 이유는 오직 하니여야 한다.
  • OCP(개방 폐쇄의 원칙, Open Closed Principle)

    YOU SHOULD BE ABLE TO EXTEND A CLASS BEHAVIOR, WITHOUT MODIFYING IT.

    • 기능 확장에 열려있고(Open, 클래스가 나중에 추가되어도)

    • 코드 수정에는 닫혀 있어야(Closed, 기존 코드는 수정되지 않도록)

    • 해야한다는 원칙(Principle)

    • 변경을 위한 비용은 줄이고, 확장을 위한 비용은 극대화 해야한다

    • 추상클래스의 의도는 클라이언트가 서버의 클래스 이름을 직접 호출하지 않게 하여 관계를 느슨하게(loosely coupled) 만드는데 있다.

    • 객체가 다른 객체를 서로 정확히 알고 연관되어 있는 경우 관계가 경직되어 있고, 서로가 유연하지 않은, 즉 tightly coupled(강한 결합) 이 되어 있다고 볼 수 있고, 이는 좋지 않은 디자인이라고 볼 수 있다. (유지 보수 시 코드 변경 포인트가 많으므로)

    • 두 객체 사이의 관계를 loosely coupled한 관계로 바꾸기 위해서는 객체를 먼저 만드는 것이 아닌 객체 사이에 지켜야 할 규칙인 Interface(인터페이스)를 먼저 정하여야 한다.

    OCP를 만족하기 위한 여러 방법들이 있는데 아래와 같다.

    1. 변경되는 부분을 virtual 함수로 분리
      --> template method pattern, 상속 기반 패턴, compile time, 정책 독점
    2. 변경되는 부분을 클래스로 분리
      --> strategy pattern, 포함 기반 패턴, runtime, 정책 공유
  • LSP(리스코브 치환의 원칙, The Liskov Substitution Principle)

    FUNCTION THAT USE POINTER OF REFERENCES TO BE CLASSES MUST BE ABLE TO USE OBJECTS TO OF DERIVED CLASSES WITHOUT KNOWING IT.

    • 서브 타입은 언제나 기반 타입으로 교체될 수 있어야 한다.
    • 서브 타입과 기반 타입은 호환되어야 하며, 공통 기능에 대해 별개의 동작을 하도록 설계되어서는 안된다.
  • ISP(인터페이스 분리의 원칙, Interface Segregation Priciple)

    CLIENTS SHOULD NOT BE FORCED TO DEPEND UPON INTERFACES THAT THEY DO NOT USE.

    • 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다.
  • DIP(의존성 역전의 원칙, Dependancy Inversion Principle)

    A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS.
    B. ABSTRACTION SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS.

    • 하위 레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하지 않아야 한다.
    • 실제 사용 관계는 바뀌지 않으나, 추상을 매개로 메시지를 주고 받음으로써 관계를 느슨하게 한다.

도형 편집기 예제

  1. 각 도형을 타입화 한다.
  2. 기반 클래스를 도입한다.
  3. 파생 클래스의 공통 특징은 기반클래스에 있어야 한다.
  4. 파생 클래스가 override 하게 되는 함수는 반드시 가상함수로 만들어야 한다.
  5. 공통성과 가변성의 분리

디자인 전략

  • Policy Base Design(단위 전략 디자인)
    • Template method : 변하는 부분을 가상함수 또는 템플릿으로 분리 (템플릿인 경우 inline으로 실행시간 감소)
    • Strategy Pattern : 변하는 부분을 클래스로 분리

Design Pattern

생성, 구조, 행위 패턴으로 구성되며 총 23가지의 패턴이 있다.
패턴에 있어 같은 UML로 표현하면 같은 구조를 갖는 경우가 있으나, 의도에 따라 패턴의 이름이 정해지므로 같은 구조라도 다른 이름을 갖는다.

생성 패턴

객체를 만드는데 어떠한 방법으로 만드는 지에 대한 디자인 패턴으로 총 5가지로 구성되어 있음.

Abstract Factory

상세화된 서브클래스를 정의하지 않고도 서로 관련성이 있거나 독립적인 여러 객체의 군을 생성하기 위한 인터페이스를 제공한다.

공장을 짓자, 공장을 바꿀수 있게 하자

Factory Method

객체를 생성하기 위해 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할 지에 대한 결정은 서브클래스가 한다. Factory Method 패턴에서는 클래스의 인스턴스를 만드는 시점을 서브클래스로 미룬다.

제품은 만드는데 어떤걸 만들지는 하위 객체에서 결정하도록 하자.

Prototype

견본적(prototypical) 인스턴스를 사용하여 생성할 객체의 종류를 명시하고 이렇게 만들어진 견본을 복사하여 새로운 객체를 생성한다.

Builder

복잡한 객체를 생성하는 방법과 표현하는 방법을 정의하는 클래스를 별도로 분리하여 서로 다른 표현이라도 이를 생성할 수 있는 동일한 구축 공정을 제공할 수 있도록 한다.

Singleton

클래스의 인스턴스는 오직 하나임을 보장하며 이에 대한 접근은 어디에서든지 하나로만 통일하여 제공한다.

구조 패턴

Adapter

클래스의 인터페이스를 클라이언트가 기대하는 형태의 인터페이스로 변환한다. Adapter 패턴은 서로 일치하지 않은 인터페이스를 갖는 클래스들을 함께 동작시킨다.

서로 인터페이스가 맞지 않는 두 가지 서로 다른 객체가 있어 사용이 불가능한 경우에 사용
인터페이스를 중계하는 역할을 하는 상속받은 객체를 생성하여 인터페이스를 맞추는 방법

  • 클래스 어답터: 다중 상속이 널리 사용
  • 객체 어답터: 포함 + 인터페이스 상속(요구조건, Shape)

원래 객체의 인터페이스를 사용할 수 있는지 없는지에 따라 클래스 어답터, 객체 어답터가 나뉨
(e.g. 돼지코 어답터를 사용하여 모양을 변경했지만 기존 콘센트 모양도 사용 가능. LifeLine이 다름)

  • 상속: 클래스에 기능 추가, 클래스의 인터페이스를 변경
  • 포함: 포함 + 인터페이스 상속(요구조건, Shape)

Bridge

구현과 추상화 개념을 분리하여 각각을 독립적으로 변형 할 수 있게 한다.

상위와 하위 객체가 서로 변경이 많은 경우 중간에 추상화 객체를 하나 더 두어 유연하게 변경에 대응한다.
추상화(e.g. Point)와 구현(e.g. PointImpl)을 분리하여 구현 변경 시의 의존성 변경사항을 줄여 빠른 컴파일을 수행할 수 있다.

                     ┌───────┐
                     │Point.h├────────────┐
                     └────┬──┘            │
                          │               │
┌──(Shared Library)───────┼───────┐       │
│                         │       │       ▼
│  ┌───────────┐          │       │ ┌───────────────┐
│  │PointImpl.h├──────┐   │       │ │UserApp.cpp ...│
│  └──────┬────┘      │   │       │ └───────────────┘
│         │           │   │       │
│         ▼           ▼   ▼       │
│  ┌─────────────┐   ┌─────────┐  │
│  │PointImpl.cpp│   │Point.cpp│  │
│  └─────────────┘   └─────────┘  │
│                                 │
└─────────────────────────────────┘

( C++ PIMPL 기법 )

위의 경우 Point.h 가 전방선언으로 PointImpl 객체를 Include 없이 선언하여 사용하기 때문에 (Point.h가 PointImpl.h에 의존하지 않는다.)
PointImpl이 변경되더라도 Point를 사용하는 많은 UserApp들이 새로 컴파일 되지 않아 컴파일 시간을 단축 할 수 있다.

Composite

부분과 전체의 계층을 표현하기 위해 복합 객체를 트리 구조로 만든다. Composite 패턴은 클라이언트로 하려면 개별 객체와 복합 객체를 모두 동일하게 다룰 수 있도록 한다.

Decorator

객체에 동적으로 새로운 서비스를 추가 할 수 있게 한다. Decorator 패턴은 기능의 추가를 위해서 서브 클래스를 생성하는 것보다 융통성 있는 방법을 제공한다.

Facade(퍼사드)

서브 시스템을 합성하는 다수의 객체들의 인터페이스 집합에 대해 일관된 하나의 인터페이스를 제공 할 수 있게한다. Facade는 서브시스템을 사용하기 쉽게 하기 위한 포괄적 개념의 인터페이스를 정의한다.

Flyweight

작은 크기의 객체들이 여러 개 있는 경우, 객체를 효과적으로 사용하는 방법으로 객체를 공유하게 한다.

Proxy

다른 객체에 접근하기 위한 중간 다리 역할을 하는 객체를 둔다

함수호출 -- Proxy --> 명령코드 -- Stub --> 함수호출

  • Proxy: 명령코드를 함수호출로 Wrapping
  • Stub : 명령코드를 함수호출로 해석

행위 패턴

Chain of Responsibility

요청을 처리할 수 있는 기회를 하나 이상의 객체에 부여함으로써 요청하는 객체와 처리하는 객체 사이의 결합도를 없애려는 것이다. 요청을 해결할 객체를 만날 때까지 고리를 따라서 요청을 전달한다.

Command

요청을 객체로 캡슐화 함으로써 서로 다른 요청으로 클라이언트를 파라미터화하고, 요청을 저장하거나 기록을 남겨서 오퍼레이션의 취소도 가능하게 한다.

명령을 실행 할 때 명령 이력을 관리하기 위해서는 실체가 필요하다. Command 패턴의 경우 명령의 수행을 객체화 하여 명령 수행시 객체를 생성하여 관리한다.

Iterator

복합 객체 요소들의 내부 표현 방식을 공개하지 않고도 순차적으로 접근할 수 있는 방법을 제공한다.

Mediator

객체들 간의 상호작용을 객체로 캡슐화한다. 객체들 간의 참조관계를 객체에서 분리함으로써 상호작용만을 독립적으로 다양하게 해석할 수 있다.

M:N 의 상호관계를 정의하는 경우 복잡도가 증가하기 때문에 변경이 어려움. 이를 개선하기 위해 중간에 조건 변경을 확인하는 중재자를 도입하여 복잡도와 코드 변경 범위를 한정시킬 수 있다.

Memento

캡슐화를 위해하지 않고 객체 내부 상태를 캡슐화하여, 나중에 객체가 이 상태로 복구 가능하게 한다.

Observer

객체 사이의 1:N 종속성을 정의하고 한 객체의 상태가 변하면 종속됙 다른 객체에 통보가 가고 자동으로 수정이 일어나게 한다.

State

객체 자신의 내부 상태에 따라 행위를 변경하도록 한다. 객체는 마치 클래스를 바꾸는 것처럼 보인다.

Strategy

다양한 알고리즘이 존재하면 이들 각각을 하나의 클래스로 캡슐화하여 알고리즘의 대체가 가능하도록 한다. Strategy 패턴을 이용하면 클라이언트와 독립적인 다양한 알고리즘으로 변형할 수 있다. 알고리즘을 바꾸더라도 클라이언트는 아무런 변경을 할 필요가 없다.

Template Method

오퍼레이션에는 알고리즘의 처리 과정만을 정의하고 각 단계에서 수행할 구체적인 처리는 서브클래스에서 정의한다. Template Method 패턴은 알고리즘의 처리과정은 변경하지 않고 알고리즘 각 단계의 처리를 서브클래스에서 재정의 할 수 있게 한다.

  • 알고리즘의 단계를 virtual function을 통해 서브클래스에서 재정의
  • Limitation
    1. 실행시간에 정책을 변경하는 것이 불가능(가상함수를 통해 정책을 변경하는 것은 객체의 변경이 아닌 클래스에 대한 변경임)
    2. 정책을 재사용할 수 없음. 같은 기능을 다른 클래스에 대해 제공해야하는 경우 새 클래스를 작성하여야 함.

Visitor

객체 구조에 속한 요소에 수행될 오퍼레이션을 정의하는 객체. Visitor 패턴은 처리되어야 하는 요소에 대한 클래스를 변경하지 않고 새로운 오퍼레이션을 정의할 수 있게 한다.

  • 반복자 : 복합객체의 모든 요소를 동일한 방식으로 열거
  • 방문자 : 복합객체의 모든 요소를 동일한 방식으로 연산수행
클래스 추가 함수 추가
전통 객체 쉽다 어렵다
(모든 파생 클래스에 재정의)
방문자 패턴 어렵다 쉽다
(방문자 인터페이스 수정)

방문자 패턴의 단점

  • 객체지향의 캡슐화가 깨진다
  • private을 사용하기 어렵다

Interpreter

언어에 따라서 문법에 대한 표현을 정의한다. 또 언어의 문장을 해석하기 위해 정의의 표현에 기반하여 분석기를 정의한다.

반응형

'프로그램 > C,C++' 카테고리의 다른 글

[C++ 기본3] 6. Upcasting  (0) 2022.05.15
[C++ 기본3] 5. 상속  (0) 2022.05.15
[C++ 기본3] 4. 상수멤버함수  (0) 2022.05.15
[C++ 기본 3] 3. this  (0) 2022.04.04
[C++ 기본 3] 2. 정적 멤버  (0) 2022.04.04
[C++ 기본 3] 1. 객체복사  (0) 2022.04.04
[C++ 기본 2] 내용 정리  (0) 2022.04.04
Comments