반응형

* 창발(Energence): 지속적인 관찰을 통해 특정한 패턴을 발견하는 것

 

1 객체지향

: 1980년대 부터 존재했던 소프트웨어 개발 방법론으로 큰 프로젝트 개발과 유지보수를 보다 쉽게 하기 위해 도입
e.g., 스몰토크, C++, JAVA, PHP

 

2 디자인 패턴(Design Pattern)

: 객체지향 구현 문제를 해결하기 위해 도입됐으며, 해결 방법을 재사용하는 것으로 객체지향 개발에서 디자인 패턴이 해결하는 주요 문제는 객체 간 관계와 소통 처리

 

3 SOLID 설계 원칙

: 유지보수성과 확장성을 극대화하기 위한 객체지향 설계 지침으로, C++에서는 다형성(Polymorphism), 추상 클래스(Abstract Class), 인터페이스 설계 등을 활용해 구현

* 다형성(Polymorphism)

더보기

같은 Interface(기본 Class)를 통해 다양한 구현을 사용하며, 기본 Class에 대한 의존성을 유지하면서도 구현을 독립적으로 관리 가능

 

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

: 하나의 클래스는 하나의 기능(책임)만 담당하며, 변경 이유도 하나여야 함

 

ERROR CASE
: 하나의 Class가 계산, 출력, 저장 등 여러 책임을 가짐

class Invoice{
public:
	void calculateTotal(){
    	// 계산 로직
    }
    
    void printInvoice(){
    	// 출력 로직
    }
    
    void saveToDatabase(){
    	// 저장 로직
    }
};

 

GOOD CASE

: 각각의 책임을 별도의 Class로 분리

class InvoiceCalculator{
public:
	void calculateTotal(){
    	// 계산 로직
    }
};

class InvoicePrinter{
public:
	void printInvoice(){
    	// 출력 로직
    }
};

class InvoiceRepository{
public:
	void saveToDatabase(){
    	// 저장 로직
    }
};

 

3.2 OCP(Open/Close Principle, 개방 폐쇄 원칙)

: 코드는 확장에 열려 있고 수정에는 닫혀있어야 하며, 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있도록 설계

 

ERROR CASE

: 도형의 종류가 늘어날 때 마다 calculateArea method 수정 필요

class Shape{
public:
	std::string type;
};

class AreaCalculator{
public:
	double calculateArea(const Shape& shape){
    	if(shape.type == "circle"){
        	// 원의 면적 계산
        } else if(shape.type == "rectangle"){
        	// 사각형의 면적 계산
        }
        return 0;
    }
};

 

GOOD CASE

: 다형성을 활용해 새로운 도형을 추가할 때 기존 코드를 수정하지 않음

class Shape{
public:
	virtual double calculateArea() const = 0; // 순수 가상함수
	virtual ~Shape() = default;
};

class Circle: public Shape{
public:
	double radius;
	Circle(double r): radius(r){}
	double calculateArea() const override{
		return 3.14 * radius * radius;
    }
};

class Rectangle: public Shape{
public:
	double width, height;
	Rectangle(double w, double h): width(w), height(h){}
	double calculateArea()const override{
		return width * height;
    }
};

void printArea(const Shape& shape){
	std::cout << "Area: " << shape.calculateArea() << std::endl;
}
더보기

-. virtual

: 해당 함수가 가상함수(Virtual Function)임을 의미하며, 기본 Class(Base Class)에서 선언된 이 함수는 파생 Class(Derived Class)에서 재정의 할 수 있음.

동일한 기본 Class를 상속 받더라도 파생 Class 마다 가상함수의 구현부를 다르게 정의할 수 있으며, 이를 통해 다형성 실현

 

-. virtual double calculateArea() const = 0;

: 함수의 구현부가 없음을 명시하며, 이렇게 선언된 함수는 순수 가상함수(Pure Virtual Function)로 선언되어 반드시 파생 Class에서 구현되어야 함

 

-. 순수 가상함수(Pure Virtual Function)

: 기본 Class에서는 Interface만 제공하고 실제 구현은 파생 Class에서 정의하도록 강제함.

이 경우 기본 Class는 추상 Class(Abstract Class)로 변환되며, 추상 Class는 Interface화 할 수 없음

 

-. virtual ~Shape() = default;

: 소멸자(Destructor)로 객체가 삭제되거나 수명이 끝날 때 호출되는 Class의 특별한 member function으로, Class의 자원을 정리하거나 Memory 해제를 위해 사용

소멸자는 함수의 일정이므로 함수 정의처럼 이름 뒤에 ( )를 사용하고, 매개변수를 가질 수 없으므로 항상 빈 괄호로 선언

 

-. override

: 해당 함수가 순수 가상함수인지 여부와 상관 없이 부모 Class에서 선언된 가상함수를 재정의한다는 것을 명시적으로 나타냄

부모 Class에서 virtual로 선언된 함수만 override keyword로 재정의 가능하며, override keyword를 사용하지 않아도 재정의는 가능하나, override를 사용하면 Compiler가 재정의 여부를 확인하고 함수명이나 Signature가 다르면 Compile Error 발생

 

[ 참고 ] 상속과 선언의 상관 관계

더보기

-. 부모 Class에서 virtual로 선언된 함수: 자식 Class에서 선택적으로 재정의 가능하며, 재정의하지 않는 경우 부모 Class의 구현 그대로 사용

-. 부모 Class에서 순수가상함수로 선언된 함수: 자식 Class에서 반드시 선언 및 구현이 필요하며 자식 Class가 이를 구현하지 않으면 자식 Class도 추상 Class가 되며 Instance 화 불가능

class Work{
public:
	virtual void work1(){ // 기본 구현 제공
    	std::cout << "Base work!" << std::endl;
    }
    virtual void work2() = 0;
};

class Human: public Work{
	// work1()을 선언 및 구현하지 않아도 Error 없음
    // work2()는 선언해주지 않으면 compile error 발생
    void work2() override{
    	std::cout << "I am working!" << std::endl;
    }
};

int main(){
	Human h;
    h.work1(); // 부모 Class의 work() 호출: "Base work!"
    h.work2(); // "I am working!"
    return 0;
}

 

3.3 LSP(Liskov Substitution Principle, 리스코프 치환 원칙)

: 자식 Class는 부모 Class의 역할을 대체할 수 있어야 하며, 부모 Class의 동작을 깨뜨리거나 위반하지 않는 설계 필요

 

ERROR CASE

: Penguin Class가 Bird의 역할을 대체하지 못함

class Bird{
public:
	virtual void fly(){
		std::cout << "I can fly!" << std::endl;
	}
};

class Penguin: public Bird{
public:
	void fly() override{
		throw std::logic_error("Penguin can't fly!");
	}
};

 

GOOD CASE

: Bird와 FlyingBird로 역할 분리

class Bird{
public:
	virtual void eat(){
    	std::cout << "I can eat!" << std::endl;
    }
    virtual ~Bird() = default; // 소멸자
};

class FlyingBird: public Bird{
public:
	virtual void fly(){
    	std::cout << "I can fly!" << std::endl;
    }
};

class Penguin: public Bird(){
public:
	void eat() override{
    	std::cout << "I am a penguin and I eat fish!" << std::endl;
    }
};

class Sparrow: public FlyingBird{
public:
	void fly() override{
    	std::cout << "I am a sparrow and I can fly!" << std::endl;
    }
};

[ 참고 ] 소멸자(Destructor)

더보기

객체가 삭제되거나 수명이 끝날 때 호출되는 Class의 특별한 member 함수로, Class의 자원을 정리하거나 memory 해제를 위해 사용

소멸자는 함수의 일종이므로 함수 정의처럼 이름 뒤에 ( )를 사용하고 매개변수를 가질 수 없으므로 항상 빈 괄호로 선언

 

가상 소멸자(Virtual Destructor)

: Class가 상속될 때 자식 Class에서 자원을 적절히 해제하도록 설계된 소멸자로, 부모 Class의 소멸자를 virtual로 선언하면 부모 Class를 통해 파생 Class 객체를 삭제할 때 파생 Class의 소멸자도 호출

소멸자를 virtual로 선언하면 상속 받는 Class에서 동적 할당된 객체가 안전하게 삭제되며, 부모 Class의 소멸자가 가상함수가 아니면 부모 Class의 pointer로 파생 Class 객체를 삭제할 때 부모 Class의 소멸자만 호출하고 파생 Class의 소멸자는 호출되지 않아 memory 누수 발생 가능

→ default를 사용해 compiler에게 기본 소멸자를 생성하도록 지시해 개발자가 특별히 소멸자의 구현을 작성하지 않아도 기본 동작(자원 해제)을 자동으로 수행

 

3.4 ISP(Interface Segregation Principle, 인터페이스 분리의 원칙)

: Client는 자신이 사용하지 않는 Method에 의존하지 않아야 하며, 큰 Interface를 작고 구체적인 Interface로 분리

 

ERROR CASE

: 로봇은 eat() Method가 필요하지 않지만 Worker Interface 때문에 구현해야 함

class Worker{
public:
	virtual void work() = 0;
	virtual void eat() = 0;
};

class Robot: public Worker{
public:
	void work() override{
    	std::cout << "I am working!" << std::endl;
    }
    void eat() override{
    	// 로봇은 eat를 사용할 필요가 없음
    }
};

 

GOOD CASE

: Interface를 분리해 필요하지 않은 의존성 제거

class Workable{
public:
	virtual void work() = 0;
    virtual ~Workable() = default;
}

class Eatable{
public:
	virtual eat() = 0;
    virtual ~Eatable() = default;
};

class Human: public Workable, public Eatable{
public:
	void work() override{
    	std::cout << "I am working!" << std::endl;
    }
    void eat() override{
    	std::cout << "I am eating!" << std::endl;
    }
};

class Robot: public Workable{
public:
	void work() override{
    	std::cout << "I am working!" << std::endl;
    }
};

 

3.5 DIP(Dependency Inversion Principle, 의존관계 역전의 원칙)

: 상위 모듈은 하위 모듈에 의존해서는 안되며 둘 다 추상화에 의존해야 하며, 구체 Class 대신 Interface나 추상 Class에 의존하는 설계 지향

 

ERROR CASE

: Computer Class가 특정 구현(Keyboard, Monitor)에 의존

class Keyboard{};
class Monitor{};

class Computer{
private:
	Keyboard keyboard;
    Monitor monitor;
};

 

GOOD CASE

: Interface에 의존하도록 수정

class InputDevice{
public:
	virtual void input() = 0;
    virtual ~InputDevice() = default;
};

class DisplayDevice{
public:
	virtual void display() = 0;
    virtual ~DisplayDevice() = default;
};

class Keyboard: public InputDevice{
public:
	void input() override{
    	std::cout << "Keyboard Input" << std::endl;
    }
};

class Monitor: public DisplayDevice{
public:
	void display() override{
    	std::cout << "Monitor display" << std::endl;
    }
};

class Computer{
// private으로 선언해 캡슐화 함으로써 외부 코드가 inputDevice와 displayDevice 포인터에 직접 접근하거나 변경하지 못하도록 보호
private:
	InputDevice* inputDevice; // 순수가상함수로 정의된 Interface로, 실제로는 이를 구현한 자식 Class(Keyboard)가 할당됨
    DisplayDevice* displayDevice; // 순수가상함수로 정의된 Interface로, 실제로는 이를 구현한 자식 Class(Monitor)가 할당됨
    
public:
	// 생성자에서 두 개의 포인터 매개변수를 받아 초기화하고, 외부에서 장치 객체를 생성해 전달하므로 Computer Class는 특정 입력/출력 장치의 구현에 직접 의존하지 않음
	Computer(InputDevice* input, DisplayDevice* display): inputDevice(input), displayDevice(display){}
    void use(){
    	// inputDevice와 displayDevice의 동작을 호출하며 inputDevice와 displayDevice가 가리키는 구체적인 구현에 따라 use()의 동작이 달라짐
    	inputDevice->input();
        displayDevice->display();
    }
};

int main(){
	Keyboard keyboard; // Keyboard 객체 생성
    Monitor monitor; // Monitor 객체 생성
    
    Computer computer(&keyboard, &monitor); // Computer 객체는 Keyboard와 Monitor 객체의 주소를 전달받아 초기화
    computer.use(); // 1) keyboard.input() 실행 -> "Keyboard input" 출력
                   //  2) monitor.display() 실행 -> "Monitor display" 출력
    return 0;
}

* 캡슐화

더보기

Class의 내부 구현(Data와 Method)을 숨기고, 외부에서 접근할 수 있는 공용 Interface (public)만 제공하는 원칙으로 내부 상태를 외부로부터 보호해 Class 사용자가 필요 이상으로 내부 구조를 알 필요 없도록 설계

-. Data 보호: 외부 코드가 inputDevice와 displayDevice Pointer에 직접 접근하거나 변경하지 못하도록 보호하며 잘못된 수정으로 인한 예기치 않은 동작 방지

-. 내부구현 변경에 유연: inputDevice와 displayDevice가 private member라면 내부 구현을 변경해도 외부 코드에 영향을 주지 않음

-. 명확한 책임 분리: Computer Class는 입력 장치와 출력 장치를 관리하고 사용하는 책임을 가지며 외부에서 직접 member 변수에 접근하면 책임이 모호해 질 수 있음

 

[ 참고 ] 객체 생성 시 Life Cycle

더보기

부모 Class의 객체가 별도로 생성되었다면 부모 Class의 Life Cycle은 자식 Class와 독립적이나, 자식 Class 객체는 반드시 부모 Class의 Life Cycle 내에서 동작

 

1) 부모 Class와 자식 Class의 객체가 별도로 생성된 경우

: 두 객체는 서로 다른 Memory 공간에 존재하므로 부모 Class 객체를 삭제해도 자식 Class의 객체에는 아무런 영향을 미치지 않음

#include <iostream>
using namespace std;

class Parent{
public:
	Parent() {cout << "Parent Constructor" << endl;}
    ~Parent() {cout << "Parent Destructor" << endl;}
};

class Child: public Parent{
public:
	Child() {cout << "Child Constructor" << endl;}
    ~Child() {cout << "Child Destructor" << endl;}
};

int main(void){
	Parent* parentObj = new Parent(); // 부모 Class 객체 생성
    Child* childObj = new Child(); // 자식 Class 객체 생성
    delete parentObj; // 부모 객체 삭제(Child는 여전히 유효)
    child Obj -> ~Child(); // 수동으로 자식 객체 소멸
    return 0;
}

 

결과

Parent Constructor
Parent Constructor
Child Contructor
Parent Destructor
Child Destructor
Parent Destructor

 

2) 부모 Class Pointer로 자식 Class를 관리하는 경우

: 부모 Class Pointer를 사용해 자식 Class 객체를 참조하고 있다면 부모 Class Pointer로 객체를 삭제할 때 부모 Class의 소멸자만 호출

이 때, 부모 Class의 소멸자가 가상소멸자(virtual)로 선언되지 않았다면 자식 Class의 소멸자가 호출되지 않아 메모리 누수 발생 가능

int main(){
	Parent* parentPtr = new Child(); // 부모 Pointer로 자식 객체 참조
    delete parentPtr; // 부모 포인터로 객체 삭제
    return 0;
}

 

결과

Parent Constructor
Child Constructor
Parent Destructor

 

3) 가상소멸자 사용하는 경우

: 부모 Class의 소멸자를 가상소멸자(virtual)로 선언하면 부모 Pointer를 사용하더라도 자식 Class의 소멸자가 먼저 호출되며, 상속 관계에서 객체를 안전하게 삭제하기 위한 권장 방법

class Parent{
public:
	Parent() {cout << "Parent Constructor" << endl;
    virtual ~Parent() {cout << "Parent Destructor" << endl;}
};

class Child: public Parent{
public:
	Child() {cout << "Child Constructor" << endl;
    ~Child() {cout << "Child Destructor" << endl;
};

int main(){
	Parent* parentPtr = new Child(); // 부모 Pointer로 자식 객체 참조
    delete parentPtr; // 부모 Pointer로 객체 삭제
    return 0;
}

 

결과

Parent Constructor
Child Constructor
Child Destructor
Parent Destructor

 

4) Smart Pointer를 사용한 객체의 Life Cycle 관리

: C++11 부터 제공되는 Smart Pointer(std::shared_ptr, std::unique_ptr)를 사용하면 객체의 Life Cycle을 안전하게 관리 가능

 

4 GoF(Gang of Four)

: Erich Gamma, Richard Helm, Ralph Johnson, John Vissides의 저서인 'GoF의 디자인패턴'을 가리키는 용어로, 처음 소프트웨어 공학에서 사용되는 패턴을 정리한 사람들의 별칭

객체지향의 문제점을 분석해 24개의 패턴으로 분류

 

5 패턴의 요소

: 24개로 분리된 디자인 패턴은 공통된 4가지 요소를 가짐

 

5.1 이름(Pattern Name)

: 패턴 이름을 통해 피턴의 용도를 직관적으로 이해 가능

 

5.2 문제(Problem)

: 각 패턴은 해결하고자 하는 문제를 갖고 있으며, 이러한 문제들은 패턴 적용을 고려해야 하는 시점을 암시

 

5.3 해법(Solution)

: 문제점을 인식했다면 객체 요소 간 관계를 정리하고 객체들을 추상화 및 나열함으로써 해결 방법을 찾음

 

5.4 결과(Consequence)

: 디자인 패턴은 알려진 문제점들을 해결하는 데 효과적이지만 모든 코드에서 디자인 패턴이 유용하다고 볼 수는 없으므로 꼭 필요한 경우를 생각해 적절히 분배해 사용해야 함

 

6 디자인 패턴의 사용 목적

6.1 통일성

: 디자인 패턴은 개발 방법을 정의함으로써 보다 통일화된 코드를 작성할 수 있음

 

6.2 실체화(Reification)

: 구현(Implementation) 보다는 디자인을 의미하며, 구조만으로 패턴을 파악하는 것은 불가능하므로 의도를 파악하는 것이 중요

 

6.3 유지보수 및 방어적 설계

: 향후 추가되는 코드를 수정하기 쉽게 변경할 수 있도록 구현해야 하며, 오랫동안 문제 없이 유지보수를 하기 위해 변경 가능한 디자인(Design for change)으로 설계해야함

코드를 작성하면서 어떤 부분이 향후 수정될 것으로 예측된다면 해당 기능을 방어적으로 처리할 수 있도록 코드가 설계되어야 하며, 지속적인 리팩토링 작업을 수행해 주어야 함

 

디자인 패턴 vs 처리 성능
성능 최적화를 위해서는 많은 함수 호출과 객체 간 호출이 적을 수록 좋으나 디자인 패턴에서는 코드의 가독성과 유지보수를 위해 객체의 메소드를 분리하며 호출도 자주 발생하게 되므로 패턴을 너무 많이 사용하는 경우 잦은 메소드 호출로 인해 성능이 저하될 수 있음
반응형

'SW > Design Pattern' 카테고리의 다른 글

1. 생성패턴 - Factory Pattern  (0) 2025.02.10

+ Recent posts