I'm FanJae.

[C++] Abstract class, Interface 본문

C++/Basic

[C++] Abstract class, Interface

FanJae 2024. 8. 21. 18:51

※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.

 

1. 추상 클래스(Abstract class)

class Shape
{
public:
    virtual ~Shape() {}
    
    virtual void draw() = 0;
};

int main()
{
    Shape  s; // error
    Shape* p; // ok   
}

- draw() 처럼 구현이 없고, =0 으로 끝나는 가상 함수를 Pure Virtual Function(순수 가상 함수)라고 한다.

- 이러한 순수 가상함수가 한 개 이상 있는 클래스를 추상 클래스라고 한다.

 

1-1. 추상 클래스의 특징

- 객체를 생성할 수 없다.

- 포인터 변수는 만들 수 있다.

 

1-2. 추상 클래스로 부터 파생된 클래스

class Shape
{
public:
    virtual ~Shape() {}
    
    virtual void draw() = 0;
};

class Rect : public Shape
{
public:
};

int main()
{
    Rect r; // ??
}

- 위와 같은 경우에는 Shape와 동일하게 객체 생성이 불가능하다.

- 기반 클래스(추상 클래스)가 가진 순수 가상함수의 구현부를 제공하지 않으면 역시 추상 클래스이다.

- 객체를 생성할 수 있게 하려면 반드시 순수 가상함수를 override 해서 구현부를 제공해야 한다.

class Rect : public Shape
{
public:
    virtual void draw() {}
};

- 즉, 추상 클래스의 의도는 특정 멤버 함수를 반드시 만들라고 지시하기 위한 것이다.

 

1-3. 순수 가상함수와 가상 함수의 차이점

#include <iostream>
#include <vector>

class Shape
{
    virtual ~Shape() {}
    
    virtual void draw() { std::println("draw Shape");
}

class Rect : public Shape
{
	public:
        void draw() { std::cout << "draw rect" << std::endl;}
}

class Circle : public Shape
{
	public:
        void draw() { std::cout << "draw circle" << std::endl;}
}


int main()
{
    Rect r;
    r.draw();
}

- 도형이라는 것을 그릴 수 있을까?

- 사각형(Rect)와 원(Circle)은 실제로 존재하는 개념이므로, 그릴 수 있다.

- 하지만, 도형(Shape)는 추상적인 개념으로 그리는 것은 불가능하다. 논리적으로 이것이 맞지 않다.

 

① Draw가  가상함수일때 이런 문제가 발생한다.

- Rect가 draw를 Override 하지 않는다면, Shape의 기본 구현을 물려 받는다.

- 하지만 논리적으로 이것이 맞는 동작일까? -> 절대 그렇지 않다는 것이다.

 

※ 즉, 가상 함수와 순수 가상 함수의 차이는 파생 클래스가 재정의를 해야 하는가 여부에 따라 갈린다.

#include <iostream>
#include <vector>

class Shape
{
    virtual ~Shape() {}
    
//  virtual void draw() { std::println("draw Shape");
    virtual void draw() = 0;
}

class Rect : public Shape
{
	public:
        void draw() { std::cout << "draw rect" << std::endl;}
}

class Circle : public Shape
{
	public:
        void draw() { std::cout << "draw circle" << std::endl;}
}


int main()
{
    Rect r;
    r.draw();
}

2. Interface

#include <iostream>

class Camera
{
public:
    void take() 
    {
         std::cout << "take picture" << std::endl; 
    }
};

class People
{
public:    
    void useCamera(Camera* p) { p->take();}
};

int main()
{
    People p;
    Camera c1;
    p.useCamera(&c1);
}

- 초기에 People 클래스에서 사람이 Camera를 사용하는 기능이 있었다고 가정하자.

- 근데 시간이 지나서 HDCaemra가 추가되면 다음과 같이 처리할 수 있다.

#include <iostream>

class Camera
{
public:
    void take() 
    {
         std::cout << "take picture" << std::endl; 
    }
};
class HDCamera
{
public:
    void take() 
    {
         std::cout << "take picture HD" << std::endl; 
    }
};

class People
{
public:    
    void useCamera(Camera* p) { p->take();}
    void useCamera(HDCamera* p) { p->take();}
};

int main()
{
    People p;
    Camera c1;
    p.useCamera(&c1);
    
    HDCamera hd;
    p.useCamera(&hd);
}

- 문제는 새롭게 추가되는 요소 때문에 기존 요소(People)의 코드를 수정하는 것은 좋지 않다.

※ 개방-폐쇄 원칙(OCP, Open-Closed Principle)

- 소프트웨어 개체(클래스, 모듈, 함수등)은 확장에 대해 열려있어야 하고 수정에 대해서는 닫혀 있어야 한다.

- 즉, 새로운 요소가 추가되어도 기존 요소가 변경되지 않도록 설계해야 한다는 원칙이다.

 

class People
{
public:    
    void useCamera(Camera* p) { p->take();}
    void useCamera(HDCamera* p) { p->take();}
};

int main()
{
    People p;
    Camera c1;
    p.useCamera(&c1);
    
    HDCamera hd;
    p.useCamera(&hd);
}

- 이 코드는 OCP를 지킬 수 없게 설계되어있다.

- 새로운 카메라가 등장하면 항상 people의 수정을 진행해야 하기 때문이다.

 

① 해결방법

- 카메라 사용자와 제작자 사이에 지켜야 하는 규칙을 설계한다. -> 추상 클래스를 사용한다.

- 카메라 사용자(People) -> 특정 제품이 아닌 규칙의 이름(추상 클래스)만 사용한다.

- 다양한 카메라 제품 -> 반드시 규칙을 지켜야 한다. 규칙을 담은 추상 클래스로부터 파생되어야 한다.

#include <iostream>

class ICamera
{
public:
    virtual ~ICamera() { }
    
    virtual void take() = 0;
};

class People
{
public:    
    // 객체는 못만들어도 포인터는 사용 가능
    // 특정한 제품이 아닌 규칙대로 쓸 것임을 명시 가능.
    void useCamera(ICamera* p) { p->take(); }
};

int main()
{

}

- 이와 같이 구현하면, 추후에 새로운 카메라가 추가되더라도 사용하는데 무리가 없다.

#include <iostream>

class Camera : public ICamera
{
public:
    void take() 
    {
         std::cout << "take picture" << std::endl; 
    }
};
class HDCamera : public ICamera
{
public:
    void take() 
    {
         std::cout << "take picture HD" << std::endl; 
    }
};

class UHDCamera : public ICamera
{
public:
    void take() 
    {
         std::cout << "take picture UHD" << std::endl; 
    }
};

class People
{
public:    
    void useCamera(ICamera* p) { p->take(); }
};

int main()
{
    People p;
    Camera c;
    p.useCamera(&c);
    
    HDCamera hd;
    p.useCamera(&hd);
    
    UHDCamera uhd;
    p.use_camera(&uhd);
}

- 이런식으로 만들어주면, People 코드는 수정되지 않아도 된다.

 

② 추상 클래스 vs 인터페이스

인터페이스 : 지켜야 하는 규칙(순수 가상 함수)만 있는것

추상클래스 : 지켜야 하는 규칙 + 다른 멤버도 있는 경우

class ICamera
{
public:
    virtual ~ICamera() {}
    virtual void take() = 0;
};

- 보통은 이쪽을 인터페이스라고 한다.

class ICamera
{
    int color;
public:
    void foo() {}
    virtual ~ICamera() {}
    virtual void take() = 0;
};

- 이쪽이 추상클래스이다. 지켜야 하는 규칙도 있지만, 상속으로 그외 물려주는 멤버 데이터,함수도 있기 때문이다.

※ C++은 추상 클래스 문법만 존재한다. Java, C# 등에서는 abstract, interface 라는 별도의 키워드가 존재한다.

 

③ 인터페이스를 만들때 유의 사항

class ICamera
{
    virtual ~ICamera() {}
    
    virtual void take() = 0;
};

- 인터페이스를 만들 때 결국 기반 클래스로 사용되므로 반드시 가상 소멸자를 사용한다.

- class 대신 struct를 사용하는 경우도 많다.

 

- 초기 People이 Camera를 사용할 때는 Camera 라는 객체 정보가 반드시 필요했다.

- 이처럼, 객체가 다른 객체와 강하게 결합되어 있는 것을 강한 결합(Tightly coupling)이라고 한다.

- 교체가 불가능하고 확정성 없는 경직된 디자인이다.

 

- 이와 달리, ICamera로 바꾼 이후의 People은 ICamera를 사용하기 때문에 추후 Camera 이외 다른 객체를 사용하더라도, 이 정보를 알 필요가 없다.

- 이처럼 객체가 다른 객체와 약하게 결합되어 있는 것을 약한 결합(Loosely coupling)이라고 한다.

- 이는 보통 인터페이스를 사용하는 것을 의미한다.

- 교체가 가능하고 확장성 있는 유연한 디자인이 특징이다.

 

 

'C++ > Basic' 카테고리의 다른 글

[C++] RTTI, Dynamic Cast  (0) 2024.08.22
[C++] Virtual Function Table  (0) 2024.08.22
[C++] Upcasting, Virtual Function  (0) 2024.08.21
[C++] Inheritance(상속)  (0) 2024.08.20
[C++] Copy Constructor  (0) 2024.08.19
Comments