일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- security
- docker
- bash
- condition
- C#
- vuetify
- AI
- 앙상블
- BOJ
- Clone
- machine learning
- var
- TypeScript
- dotenv
- C++
- 보안
- npm
- git
- type
- nginx
- webpack
- property
- leetcode
- Python
- JavaScript
- vue.js
- generic
- scss
- VUE
- loop
- Today
- Total
ice rabbit programming
[C++] GoF 패턴 간단 정리(1) - 공통성과 가변성의 분리, 재귀적 포함 본문
지난 정규표현식 간단 정리 글에 이어서 이번에는 디자인 패턴에 관해 강의를 들으며 정리했던 필기를 정리하려고 합니다.
약 4년 반 전에 포스팅했던 Typescript 강의를 듣고 정리했던 글과 비슷하겠네요.
어느새 처음 입사한지 만 5년이 흐른 지금, 기존 코드도 나름 많이 읽었고 선배 및 동료 개발자들과 리뷰를 하면서 더 나은 코드를 작성할 수 있도록 노력해오기도 했습니다. (개인적으로는 아직 많이 부족하다고 생각합니다만...)
C#, 파이썬과 Typescript를 거쳐서 현재는 C++을 주로 사용하고 있어서 GoF 패턴을 C++ 강좌로 접할 기회가 있어 듣고 간략하게 정리한 내용입니다.
대부분 한 번씩 사용해봤을 법한 패턴들입니다. 다만 패턴에 너무 매몰되지 않고 생산성과 현재 프로덕트가 처한 상황에 맞게 적용하면 된다고 생각하고 있습니다. 여러 가지가 중요하긴 하지만 저는 개인적으로는 <코드 중복의 방지>와 <함수와 클래스의 명확함(한 번에 하나의 일)>이 가장 근본적이고, 이후에 필요에 맞게 쌓아 올려지는 것이 아닌가 합니다.
어쨌거나 약 세 편에 나뉘어서 이전에 썼던 필기들을 올려볼까 합니다.
들어가기 전에
Protected
Protected 생성자: 상속받은 자식 클래스로만 인스턴스 생성 가능(부모 클래스 인스턴스 직접 생성 불가)
Protected 소멸자: 객체를 스택에 만들 수 없게 할 때(즉, Heap에만 만들게) 사용한다. 참조 계수 기반의 객체 수명 관리 기법에서 주로 사용한다.
가상 함수
가상 함수로 만들지 않고 부모 타입 포인터에 자식 객체를 넣을 경우 오버라이딩 되지 않음.
가상함수에 =0을 붙이면 순수 가상 함수가 된다. 구현부가 없고 구현 클래스에서 오버라이딩이 필수이다.
추상 클래스: 순수 가상함수를 한 개 이상 가지고 있다. 인스턴스를 생성할 수 없고, 포인터 변수는 생성할 수 있다.
공통성과 가변성의 분리
변하지 않는 전체 흐름 내에 있는 변하는 부분을 분리하여, 정책이 변경되었을 때 코드의 수정을 최소화 하도록 한다.
개방 폐쇄의 법칙(OCP: Open Close Principle)
l 기능 확장(모듈, 클래스, 함수 추가)에 열려 있고 수정(기존 코드 수정)에는 닫혀 있어야 한다는 원칙
l 새로운 클래스가 추가되어도 기존 클래스의 코드를 수정하지 않도록 만들어야 한다.
Template Method 패턴 – 가상 함수로 분리
상속 기반 패턴. 전체 흐름 중 변하는 부분을 가상 함수로 정의한다. 즉, 해당 부분의 변경이 필요하면 파생 클래스를 새로 만들어서 적용한다.
l 컴파일 타임에 정책을 정해야 한다.
l 파생 클래스에서 오버라이딩하므로 해당 클래스에서만 접근이 가능하다.
l 참고로 final 키워드를 붙이면 파생 클래스에서 원 함수를 변경할 수 없다.
Strategy 패턴 – 인스턴스로 교체
변하는 것을 인터페이스(약한 결합)로 설계하여 별도의 클래스로 정의한다. 즉, 해당 부분의 변경이 필요하면 인터페이스를 구현하는 클래스를 만들어서 적용한다.
l 런타임에 정책을 정하므로 실행 시간에도 변경 적용할 수 있다.
l 가상 함수 기반이므로 상대적으로 느리다.
l 별개의 클래스이므로 전혀 다른 클래스에서도 정책을 같이 사용할 수 있다.
Policy Base Design(단위 전략 디자인) – 템플릿 인자로 교체
Template 인자를 사용하여 정책 클래스를 교체하는 기법이다. GoF 패턴에 있지는 않으나 C++에서 널리 사용되는 전략이다.
l <typename ThreadModel>과 같이 사용하여, template 인자로 사용되는 클래스를 바꿀 수 있도록 한다.
l 함수 내에 변하는 것과 변하지 않는 것이 혼재해 있을 경우에도 사용이 가능하다.
l Strategy(전략) 패턴과 비교했을 때 inline 함수를 사용하므로 실행 속도가 조금 더 빠르지만, 컴파일 타임에 정책을 정해야 한다.
Application Framework
Main 함수에 전체적인 흐름을 담아서 라이브러리 내부에 감추는 방식이다.
l 모든 것을 객체로 한다. (단, C++은 Main은 무조건 일반 함수)
l 특정 분야의 프로그램은 전체적 흐름이 항상 유사하다. (GUI, 게임 등)
CWinApp* g_app = 0;
int main() {
if (g_app->InitInstance() == true)
g_app->Run();
g_app->ExitInstance();
}
// 프로그램 전체 흐름 프레임워크
class CWinApp {
public:
CWinApp() { g_app = this; } // 전역 객체에 어플리케이션 정보 저장
virtual bool InitInstance(() { return false; }
virtual bool Run() { return false; }
virtual Int ExitInstance() { return 0; }
)
// 파생 클래스
class MyApp : public CWinApp {
public:
virtual bool InitInstance { … }
virtual int ExitInstance() { … }
}
MyApp theApp; // 전역 객체 생성자가 main보다 먼저 실행된다.
l 프레임워크 정의하는 추상 클래스가 있고 사용자는 그 틀에 맞춰서 파생 클래스를 만든다.
l MFC, QT, WxWidget, iOS, Android 등에서 사용하는 형태이다.
일반 함수와 가변성 – 함수 인자화
정책이 변경될 경우 라이브러리나 일반 함수 내부를 수정하지 않아도 되도록 설계하는 것이 필요하다.
l 멤버 함수가 변할 경우: 가상 함수나 strategy 패턴을 사용할 수 있다.
l 일반 함수의 경우: 변해야 하는 것(정책)을 함수 인자화한다.
함수 포인터 – 코드 메모리가 증가하지 않지만 인라인 치환 불가
함수 객체, 람다 표현식 – 인라인 치환이 되지만 코드 메모리가 증가
State 패턴
상태에 따라 다른 행동을 해야 할 때 구현할 수 있는 방법이다. 아래는 각 기법의 비교이다.
l 분기: 여러 함수에 조건에 따른 분기가 들어갈 경우, 모든 동작 함수에 분기가 필요하고 새 조건 추가 시 모든 곳에 분기가 추가되는 문제가 있다.
l 가상 함수: 처리는 되나 새 인스턴스를 사용한 것으로 처리된다. 즉, 객체의 변화가 아니라 새 인스턴스가 생성된 것이므로 클래스에 의한 변화이다.
l State 패턴: 변경되는 동작들을 다른 클래스로 분리(인터페이스 정의)한다. 즉, 객체의 속성은 유지하면서 동작을 변경할 수 있다. Strategy 패턴과 유사하다.
struct IState {
virtual void run() = 0;
virtual void attack() = 0;
virtual ~IState() {}
};
class NormalState : public IState { … }
State 패턴은 객체 자신의 내부 상태에 따라 행위를 변경, 객체는 마치 클래스를 변경하는 것처럼 동작한다.
Strategy 패턴은 다양한 알고리즘이 존재하면 이들 각각을 하나의 클래스로 캡슐화하여 알고리즘을 대체하는 식으로 동작한다.
ð 클래스 다이어그램은 유사하나 의도가 다르다.
재귀적 포함
Composite 패턴
다형성을 이용하여 실행할 수 있도록 한다.
Leaf node가 다른 composite node들과 같은 layer에 존재하는 tree 구조에서 유용하게 사용할 수 있다.
개별 객체와 복합 객체를 구별하지 않고 동일한 방법으로 다룰 수 있다.
Ex) 메뉴 속 메뉴를 누르거나 메뉴 속 항목을 눌렀을 때, 같은 Base를 가지고 있다면, 즉 공통된 실행 함수(하위 메뉴를 보여 주거나 항목 실행하는 게 동일한 함수라면)가 있다면 타입에 관계없이 호출하면 된다.
Cf) 자식 클래스들 중 한 곳에서만 사용하는 함수이더라도 기반 클래스에 가상 함수로 선언하고 dummy 구현을 해주면 동일하게 사용이 가능하다. 만약 모두가 사용한다면 순수 가상함수로 선언하면 된다.
예시로 든 Menu Event를 살펴보면 다음과 같다.
l 하는 일(변하는 부분)을 가상 함수로 분리한다.
-> 메뉴의 개수만큼 파생 클래스를 만들어야 한다. (너무 많아질 우려)
l 하는 일(변하는 부분)을 다른 클래스로 분리한다. (Strategy 패턴, Listener라는 이름을 사용하는 기술)
-> 어떤 메뉴인지 정보를 전달받는(주로 인자) 절차가 필요하다.
l 메뉴에 객체가 아닌 함수를 연결한다. 즉, 같은 인터페이스를 사용하는 것이 아니라 함수 포인터를 전달 받아 사용한다.
-> 단, 함수 포인터는 일반 함수와 멤버 함수의 포인터를 동시에 담을 수 없다.
-> 각 함수 포인터를 wrapping하는 클래스를 공통의 기반 클래스를 가지도록 선언하여 사용한다. 이 때 함수 템플릿(클래스 템플릿은 C++17~)은 타입을 명시적으로 지정하지 않아도 묵시적 추론이 되므로 생략 가능하다. ~C++14에서는 클래스 템플릿을 생성하는 함수 템플릿을 도움 함수로 만들면 실제 사용 시 일반 함수와 멤버 함수가 통합된 형태로 사용이 가능하다.
Decorator
실행 시간에 객체에 기능을 추가할 때 사용하는 패턴이다.
(반대는 상속을 통한 객체 기능 추가이다. 상속을 통해 추가하는 건 인스턴스가 새로 생성되는 것으로 객체에 기능을 추가한 게 아니라 클래스에 기능을 추가하여 인스턴스를 변경한 개념이다.)
Composition을 통한 기능을 추가하면(새 클래스가 기존 클래스를 멤버 변수로 가짐) 기존 객체에 기능이 추가된다.
비교하자면 상속에 의한 추가는 클래스에 추가가 되고 코드 작성 시 추가의 개념이고, 구성을 통한 추가는 인스턴스에 추가가 되고 런타임에 추가의 개념이다.
기능을 연속해서 추가하기 위해서는 기능이 추가되는 주체 객체와 기능 추가 객체들은 동일한 기반 클래스를 가져야 한다. (ex. Base, Par1, Par2, Par3 등등이 공통의 기반 클래스를 가짐).
여기서 기능 추가 클래스들은 다시 공통의 기반 클래스(IDecotator)로 묶을 수 있다