본문 바로가기
Game DevTip/C++

16. C++ 순수 가상함수(추상 클래스 & 인터페이스)에 대해서

by LIKE IT.라이킷 2025. 7. 26.

 

C++ 추상 인터페이스와 추상 클래스

 

객체 지향에서 추상화는 매우 중요한 핵심 개념중 하나이다. 

C++에서 추상화를 구현하는 주요 방식으로 추상 인터페이스와 추상 클래스가 있는데, 

해당 개념, 그리고 순수 가상함수와 추상 클래스의 차이점에 대해서도 알아보자. 

 

추상 인터페이스의 개념

추상 인터페이스는 클래스의 기능(행위)을 정의하지만

구현은 포함하지 않는 순수 가상 함수(pure virtual function)들의 집합이다.

C++에서 추상 인터페이스는 다음과 같은 특징을 가진다.

 

1. 순수 가상 함수만 포함 하는 클래스

2. 함수에 대한 구현 부분이나 멤버 변수가 존재하지 않음. 

3. 헤더파일에 선언되며, public 메서드로만 선언된 클래스. 

4. 인터페이스 클래스는 종종 'I' 접두어를 사용(예시 : IGameAI)

5. 추상 인터페이스를 상속 받은 후에 관련 구현은 CPP 소스 파일에서 이뤄진다.

 

추상 인터페이스의 주요 목적은 클래스가 뭘 할 수 있는지(what)만 정의. 

어떻게(how)에 대해서는 서브 클래스에 위임하여 하는 것. 

이는 외부 사용자가 실제 구현 없이도 클래스의 인터페이스만으로도 사용 가능하게 한다. 

 

C++에서 인터페이스의 필요성

C++ 에서는 클래스 헤더 파일에 인터페이스 뿐만 아니라 클래스의 모든 멤버 변수, 

추가적인 헤더 파일 포함등이 노출된다. 

 

때문에 아래와 같은 문제가 발생할 가능성이 높다. 

 

1. private으로 선언한 멤버들도 어떤 변수들이 있는지 외부에 노출된다. 

2. 구현 세부사항 까지 모든 사용자에게 공개된다. 

3. 인터페이스 변경 시 의존하는 모든 코드를 다시 컴파일 해야한다. 

 

다만 그렇다고 해도 추상 인터페이스를 사용하면 클래스의 구현과 인터페이스 선언을 분리해서

더 효과적인 캡슐화가 가능해진다. 

 


순수 가상 함수와 일반 가상 함수

C++에서 가상 함수(virtual function)는 다형성을 구현하는 핵심 기능이다.

가상 함수에는 두 가지 유형이 존재 한다. 

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

virtual void myFunction() = 0;

  • = 0이 붙은 가상 함수
  • 구현부가 없는 함수 선언
  • 이 함수를 상속받은 자식 클래스는 반드시 이 함수를 오버라이드(구현)해야 함
  • 순수 가상 함수가 하나라도 있는 클래스는 추상 클래스가 되며, 객체를 직접 생성할 수 없음

2. 일반 가상 함수 (Virtual Function)

virtual void myFunction() { /* 기본 구현 */ }

  • 기본 구현을 가지는 가상 함수
  • 자식 클래스에서 재정의(오버라이드)할 수 있지만, 필수는 아님
  • 자식 클래스가 재정의하지 않으면 부모 클래스의 구현이 사용됨

주요 차이점

특성 순수 가상함수  일반 가상함수 
구현부 없음 (= 0) 있음 (기본 구현)
자식 클래스 구현 필수 선택적
클래스 성격 포함 시 추상 클래스가 됨 일반 클래스로 유지 가능
객체 생성 클래스 직접 인스턴스화 불가 클래스 직접 인스턴스화 가능

 


추상 클래스 vs 인터페이스

C++에서 추상 클래스와 인터페이스 개념을 구분짓는 것이 굉장히 중요하다. 

Java나 C#과 다르게 C++에는 interface 키워드가 없지만, 관행적으로는 구분해서 사용한다. 

 

추상 클래스(Abstract Class)

1. 하나 이상의 순수 가상함수를 포함하는 클래스

2. 멤버 변수, 생성자, 일반 메서드 등을 포함 가능. 

3. 자식 클래스에 일 부 구현을 제공할 수 있다. 

4. 주로 'is-a' 관계에서 사용(예시 : Vehicle은 추상클래스, Car와 Motorcycle은 이를 상속한다.)

5. 단일 상속만 가능(C++에서는 다중 상속도 가능 하지만 매우 권장하지 않음.)

 

- 미완성된 설게또

- 상속을 통해서 완전한 클래스로 구현

- 부모-자식 관계강조

 

 

인터페이스(Interface)

1. 오직 순수 가상함수만 포함하는 클래스.

2. 멤버 변수나 구현된 메서드가 없다. 

3. 클래스가 어떤 기능을 제공해야 하는지에 대해서만 정의. 

4. 주로 'can-do' 관계에서 사용한다.(예시 : IDrawalbe은 그릴 수 있는 모든 것에 구현이 가능.)

5. 다중 상속이 가능하다.(여러 인터페이스 구현 가능,)

 

- 기본 설계도

- 다중 상속이 가능하여 더 유연한 디자인 제공

- 구현 클래스 간 관계가 약함

 

 

인터페이스 설계 시 고려사항

효과적인 인터페이스 설계를 위해서는 인터페이스의 사용자와 용도를 고려해야 한다.

 

1. 개인 사용

 - 설계에 대한 빈번한 수정이 가능. 

 - 유연성을 최우선으로 고려

 

2. 팀 내부 사용

 - 요구 기능에 대한 팀 논의가 필요

 - 버전 관리, 코딩 규칙, 데이터 타입 등에 대한 표준화

 - 문서화와 주석 중요

 

3. 외부 협업 사용

 - 외부 요구사항에 맞는 명확한 인터페이스 정의 

 - 확장성에 대한 사전 고려 필수

 - 상세한 문서화와 예제 코드 제공 필요

 

 

용도에 따른 고려사항

1. 컴포넌트 인터페이스 

 - 팀 내부에서 사용되는 작은 규모 인터페이스

 - 다른 개발자들도 쉽게 이해하고 사용할 수 있도록 설계

 

2. API 

 - 다른 시스템이나 외부 개발자에게 제공되는 인터페이스

 - 사용성과 호환성, 확장성에 대한 고려 필요

 - 버전 관리 중요

 

3. 라이브러리 

 - 범용적인 기능을 제공하는 인터페이스 

 - 다양한 환경과 요구사항에 적용 가능하도록 설계

 - 테스트와 문서화가 철저히 필요. 

 

 

실전 예제: 게임 AI 인터페이스

#include <iostream>
#include <string>
using namespace std;

// 게임 AI를 위한 추상 인터페이스
class GameAi
{
public:
    GameAi() {}
    virtual ~GameAi() {}

    // 순수 가상 함수들 - 모든 AI가 구현해야 함
    virtual void takeTurn() = 0;
    virtual void collect_resources() = 0;
    virtual void buildStructures() = 0;
    virtual void attack() = 0;
    virtual void sendScouts(int position) = 0;
    virtual void sendWarriors(int position) = 0;
};

// 오크 AI 구현
class OrcsAi : public GameAi
{
private:
    int resources;
    int warriors;
    int scouts;

public:
    OrcsAi() : resources(0), warriors(0), scouts(0) {}
    ~OrcsAi() {}

    void takeTurn() override {
        cout << "오크AI 턴:" << endl;
        collect_resources();
        buildStructures();
        buildUnits();
        attack();
    }

    void collect_resources() override {
        cout << "자원을 수집합니다..." << endl;
        resources += 10;
    }

    void buildStructures() override {
        cout << "오크 막사와 대장간을 건설합니다." << endl;
    }

    void attack() override {
        cout << "공격을 시작합니다:" << endl;
        sendScouts(1);
        sendWarriors(2);
    }

    void sendScouts(int position) override {
        cout << "Wolf Rider를 정찰 보냅니다." << endl;
    }

    void sendWarriors(int position) override {
        cout << "OrcWarrior 분대를 공격 보냅니다." << endl;
    }

    // 추가 기능 (인터페이스에 없는 메서드)
    void buildUnits() {
        cout << "OrcWarrior와 Raider를 훈련시킵니다." << endl;
        warriors += 5;
        scouts += 2;
    }
};

// 인간 AI 구현
class HumanAi : public GameAi
{
private:
    int resources;
    int infantry;
    int archers;

public:
    HumanAi() : resources(0), infantry(0), archers(0) {}
    ~HumanAi() {}

    void takeTurn() override {
        cout << "인간AI 턴:" << endl;
        collect_resources();
        buildStructures();
        buildUnits();
        attack();
    }

    void collect_resources() override {
        cout << "자원을 수집합니다..." << endl;
        resources += 8;
    }

    void buildStructures() override {
        cout << "마을 회관과 대장간을 건설합니다." << endl;
    }

    void attack() override {
        cout << "공격을 시작합니다:" << endl;
        sendScouts(1);
        sendWarriors(2);
    }

    void sendScouts(int position) override {
        cout << "정찰병을 보냅니다." << endl;
    }

    void sendWarriors(int position) override {
        cout << "기사 부대를 공격 보냅니다." << endl;
    }

    // 추가 기능 (인터페이스에 없는 메서드)
    void buildUnits() {
        cout << "보병과 궁수를 훈련시킵니다." << endl;
        infantry += 4;
        archers += 3;
    }
};

int main()
{
    OrcsAi orc;
    HumanAi human;

    orc.takeTurn();
    cout << endl;
    human.takeTurn();

    return 0;
}

 

예제 분석으로 추상 인터페이스의 장점

 

1. 통일된 인터페이스 : GameAi 인터페이스는 모든 Ai 유형이 구현해야 하는 공통 행동을 정의 

2. 다형성 : 서로 다른 AI 구현체들이 동일한 인터페이스를 통해 접근 가능. 

3. 확장성 : 새로운 Ai 유형(예 : ElfAi, UndeadAi)을 쉽게 추가 가능. 

4. 책임 분리 : 각 AI 구현은 자신만의 특성을 가지면서 공통 인터페이스 준수

 

해당 방식은 게임엔진이 Ai 유형에 관계 없이 일관된 방식으로 Ai와 상호작용 가능. 

 

 

인터페이스를 활용한 플랫폼 독립적인 코드 작성

멀티 플랫폼 애플리케이션을 개발할 때, 플랫폼별 구현을 효과적으로 분리하는 것이 중요하다.

전통적인 방법과 인터페이스를 활용한 방법을 비교해 보자.

전통적인 방법: 전처리기 조건문 사용

class InputSystem
{
public:
    void setInputInfo(string inputString);
};

void InputSystem::setInputInfo(string inputString) {
#ifdef __WIN32__
    // Windows 관련 구현
#else
    // 다른 플랫폼 구현
#endif
}

 

해당 방식의 약간의 문제점이 있다. 

 

1. 가독성 저하 : 조건문이 많아지면 코드 이해가 어려울수 있다. 

2. 확장성 부족 : 새 플랫폼 추가시 모드 #ifdef 블록을 수정해야하기 때문. 

3. 유지보수가 어렵다 : 플랫폼 코드가 한 파일에 존재. 

 

개선된 방법: 인터페이스 활용

// 인터페이스 정의
class InputSystem
{
public:
    virtual void setInputInfo(string inputString) = 0;
};

// Windows 구현
class WindowsInputSystem : public InputSystem
{
public:
    void setInputInfo(string inputString) override
    {
        // Windows 환경 처리
    }
};

// iOS 구현
class IOSInputSystem : public InputSystem
{
public:
    void setInputInfo(string inputString) override
    {
        // iOS 환경 처리
    }
};

 

장점을 분석해보자면, 

 

1. 코드 분리 : 플랫폼 별 코드가 별도 클래스로 구현된다. 

2. 가독성 향상 : 각 플랫폼 구현이 명확하게 분리되어 구분된다. 

3. 학장성 : 새 플랫폼 추가시 새 클래스만 구현하면 된다. 

4. 테스트 용이성 : 각 플랫폼 구현을 독립적으로 테스트 가능. 

 

결론 및 모범 사례

추상 인터페이스와 추상 클래스는 C++에서 강력한 추상화 도구이며, 다음과 같은 상황에서 특히 유용하다.

 

1. 다양한 구현체가 공통 인터페이스를 공유해야 할 때, 

2. 시스템 구성 요소간의 결합도를 낮추고 싶을 때, 

3. 플랫폼 독립적인 코드를 작성할 때, 

4. 프레임 워크나 라이브러리를 개발 할 때, 

 

추상 인터페이스 / 클래스 설계 모범 사례

1. 인터페이스 분리 원칙 (ISP) 준수

 - 클라이언트가 사용하지 않는 인터페이스에 의존하지 않아야 한다. 

 - 큰 인터페이스보다 작고 특화된 여러 인터페이스가 더 좋다. 

 

2. 명확한 책임 정의 

 - 각 인터페이스는 단일 책임을 가져야 한다. 

 - 인터페이스 이름은 그 책임을 명확하게 반영해야 한다. 

 

3. 인터페이스 안정성 유지 

 - 한번 공개된 인터페이스는 쉽게 변경하지 말 것. 

 - 확장은 새 메서드 추가로, 기존 메서드 시그니처 변경은 피해야 한다. 

 

4. 적절한 추상화 수준 선택

 - 너무 추상적이면 구현이 어려워지고, 너무 구체적이면 유연성이 떨어져 의미가 퇴색. 

 - 도메인과 요구사항에 맞는 적절한 추상화 수준을 선택할 것. 

 

5. 문서화 예제 제공

 - 인터페이스의 의도와 사용법을 명확히 문서화 해야한다. 

 - 구현 예제를 통해서 올바른 사용법 안내를 할 것. 

반응형

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

18. C++ 템플릿 활용  (4) 2025.07.28
17. C++ 람다(Lamda) 표현식  (3) 2025.07.27
15. C++ 예외 처리 Try Catch문  (0) 2025.07.25
14. C++ 이동의미론과 rvalue & lvalue  (0) 2025.07.22
13. C++ 스마트 포인터에 대해서  (1) 2025.07.19

댓글