공부 목적으로 만든 블로그입니다 ! 틀린 내용은 이야기해주시면 감사하겠습니다 !

 

가장 먼저 인터페이스와 추상클래스를 왜 사용하는지에 대해 생각을 해봤습니다.

처음 이 개념을 접할때는 단순하게 

" 협업하는 입장에서 상대방이 구현하는 구체클래스의 메서드를 알 수 있다면 

협업의 효율이 올라가겠구나.. "  또는 

" 아하 인터페이스와 추상클래스를 통해 다형성을 구현할 수 있구나 "

라는 생각을 하였고, 추가적으로 모듈간의 의존성이 낮아진다라고 막연히 생각했습니다.

 

하지만 프로젝트를 겪고 난 후 생각이 달라졌습니다. 만일 프로젝트를 하던 도중에 

헤더파일에서 불필요한 포함이 줄어드는 개념을 알았다면 더욱 많은 기능을 구현할 수 있었을 것입니다.

그리고 저는 다음과 같은 점에서  인터페이스와 추상클래스의 구체적인 활용법이 너무 궁금했습니다.

" 헤더파일에서 전방선언만 하는 것이고, 소스파일에서 어차피 include해줘야하는데

왜 이게 의존성이 줄어드는걸까? " ,

" 헤더 파일에서 불필요한 포함을 하지 않게되면 무슨일이 일어나는거지? "

그렇기 때문에 좀 더 인터페이스와 추상클래스의 차이점에 대해 구글링을 해봤습니다.

좀 길어질것 같아 목차를 정리하겠습니다

  1. 각각의 개념 정리
  2. 차이점
  3. 소스파일과 헤더파일에 미치는 영향
  4. 예시

가장 먼저 개념부터 집고 넘어가겠습니다 !

 

1. 인터페이스란 ?


 

인터페이스란 클래스가 반드시 구현해야 할 함수 목록만을 미리 정의해 두고,

실제 함수의 동작은 상속받은 클래스가 작성할 수 있게하는 일종의 계약서 입니다.

즉, "이 함수만은 반드시 만들어야한다!"라는 함수 시그니처만을 정의합니다.

 

또, 인터페이스는 상태(멤버변수) 및 구현부(일반 함수)가 없습니다. 다시 말해 가상 소멸자와 순수가상함수만 포함됩니다.

C++의 경우에서 인터페이스 클래스에는 가상 소멸자가 있어야합니다. 

(가상 소멸자가 선언되어 있지 않으면 기본 클래스의 소멸자를 사용하게 됩니다.)

이유는 자식 클래스의 올바른 소멸자가 호출되어야하기 때문입니다.

 

 

인터페이스를 사용하였을 경우의 이점 : 

 

  •  1. 결합도 감소 : 클래스 간 구체적인 구현을 공유하지 않고, 필요한 함수 목록만 약속하기 때문에 클래스 간                                            의존도가 낮아집니다.
  •  2.  확장성 향상 :  새로운 파생클래스를 만들 때, 인터페이스를 구현하기만 하면 기존 시스템에 쉽게 합쳐질 수 있습니다.
  •  3.  다형성  : 인터페이스 포인터 배열로 관리한다면 아이템 종류가 무엇이든 같은 함수를 호출하여 다룰 수 있습니다.                       예시 ) AnimalInterface * animal ;

 

2. 추상클래스란? 


추상 클래스는 개념적으로 인스턴스화 할 수 없는 클래스. 즉, 인스턴스를 생성할 수 없는 클래스로, 일반적으로 하나 이상의 순수 가상 함수가 있는 클래스를 추상클래스라고 하며, 이 클래스로부터 파생된 모든 클래스는 이 가상함수를 

반드시 재정의 해야합니다. 또, 추상클래스는 상태(멤버변수 및 함)를 포함할 수 있습니다.

추상클래스는 C++에서 다중상속시 여러 문제가 있어 권장하지 않습니다.

마지막으로 추상클래스는 인스턴스화 시킬 수 없습니다.

 

 

3. 인터페이스와 추상클래스의  공통점 및 차이점 (C++)


 

C++은 다중 상속을 지원합니다. 또, C++은 인터페이스를 지원해주지 않습니다.

그렇기 때문에 인터페이스 역할을 하는 순수 추상 클래스(이하 인터페이스)는 데이터 멤버가 없고

함수의 계약만을 제공하기 때문에 다중 상속시 발생할 수 있는 여러 문제 (예시 : 다이아몬드 문제) 를 

상대적으로 쉽게 관리할 수 있습니다.

반면에 추상클래스는 기본 구현이나 데이터 멤버가 포함되어있기 때문에 

다중 상속시 멤버변수 및 함수 가 여러 경로에서 상속될 수 있기 때문에 모호성이나 복잡한 초기화 문제를 

야기할 가능성이 높습니다.

 

또한 추상클래스는 기본 구현이나 데이터 멤버를 포함할 수 있으므로 , 향후 새로운 기능이나 변경 사항을

기본 클래스에 추가하거나 수정할 수 있고,

기본 클래스를 상속받은 모든 파생클래스들이 자동으로 그 변경 사항을 반영하게 됩니다.

(버전화가 용이합니다.)

 

그리고 만약 구현하려는 기능이 서로 관계가 없는 다양한 클래스에서 공통으로 사용될 수 있다면 ,

추상 클래스보다는 인터페이스의 형태로 제공하는 것이 유리합니다.

예시 ) 렌더링 가능한 인터페이슬 기능은 UI,게임,데이터 모델 등 서로 관련이 없는 여러 객체에서 

필요할 수 있습니다.

 

추상 클래스는 보통 공통적인 데이터 멤버나 기본 구현을 포함하므로, 

밀접하게 관련된 객체들에서 사용하기에 적합합니다.

예시 ) 아이템

 

 

반면에 인터페이스는 본질적으로 계약만을 정의하기 때문에 한번 정의를 하게 되면

인터페이스가 바뀌게 될 경우 모든 구현체에 영향을 주기 때문에 변경이 어렵습니다.

 

요약하자면

인터페이스는 다중 상속을 보다 안전하고 명확하게 사용할 수 있게 해줍니다.

 

추상클래스는 버전 관리가 필요하다면, 기본 구현을 포함하는 추상클래스를 사용하는 것이

유지보수의 관점에서 유연합니다.

 

구현하려는 기능이 서로 관계가 없는 클래스들 사이에서 공통으로 사용된다면

인터페이스

 

밀접하게 관련된 객체들 사이에서 사용된다면 추상클래스 

 

 

 

4. 각각의 예시


 

예시 코드 1 )  구체 클래스에 직접 의존하는 경우 

 

// Application.h
#ifndef APPLICATION_H
#define APPLICATION_H

#include "ConcreteLogger.h"  // 구체 클래스에 의존

class Application {
private:
    ConcreteLogger logger;  // ConcreteLogger 타입을 직접 멤버로 사용
public:
    void DoSomething();
};

#endif // APPLICATION_H

 

만일 구체 클래스(ConcreteLogger)의 구현이 변경된다면 이를 포함하고 있는 모든 파일이 

다시 컴파일 되어야합니다. 

즉, 구체 클래스에 직접 의존함으로써 의존성이 높아지고, 결합도가 올라갑니다.

 

예시 코드 2 ) 인터페이스를 사용해 의존성을 줄인 코드 

 

2-1 ) 인터페이스 

// ILogger.h
#ifndef ILOGGER_H
#define ILOGGER_H

#include <string>

class ILogger {
public:
    virtual ~ILogger() = default;
    virtual void Log(const std::string& msg) = 0;
};

#endif // ILOGGER_H

 

2-2 ) 인터페이스를 상속받은 구체 클래스

// ConcreteLogger.h
#ifndef CONCRETE_LOGGER_H
#define CONCRETE_LOGGER_H

#include "ILogger.h"
#include <iostream>

class ConcreteLogger : public ILogger {
public:
    void Log(const std::string& msg) override {
        std::cout << "ConcreteLogger: " << msg << std::endl;
    }
};

#endif // CONCRETE_LOGGER_H

 

2-3 ) App은 인터페이스에 의존

#ifndef APPLICATION_H
#define APPLICATION_H

// ILogger에 대한 전방 선언 (헤더 포함 없이 포인터 선언 가능)
class ILogger;

class Application {
private:
    ILogger* logger;  // ILogger 인터페이스에 의존 (다형성 활용)
public:
    // 생성자: ILogger 포인터를 받아 멤버에 저장
    Application(ILogger* logger);

    // 기능 수행 메서드: 내부에서 logger를 사용하여 메시지 출력
    void DoSomething();
};

#endif // APPLICATION_H

2-3-1 ) App의 Cpp파일

#include "Application.h"
#include "ILogger.h"  // 실제 ILogger 인터페이스 정의가 필요

// 생성자 구현: 전달받은 ILogger 포인터를 멤버 변수에 할당
Application::Application(ILogger* logger) : logger(logger) {}

// DoSomething 메서드 구현
void Application::DoSomething() {
    if (logger) {               // logger가 유효한지 체크
        logger->Log("Hello, world!");  // ILogger 인터페이스를 통한 다형적 호출
    }
}

 

2 - 4 )  인터페이스를 상속한 구체 클래스

 

#ifndef CONCRETE_LOGGER_H
#define CONCRETE_LOGGER_H

#include "ILogger.h"  // ILogger 인터페이스 선언 포함

class ConcreteLogger : public ILogger {
public:
    // 생성자 및 소멸자 선언
    ConcreteLogger();
    virtual ~ConcreteLogger();

    // ILogger 인터페이스의 순수 가상 함수 Log의 오버라이드
    virtual void Log(const std::string& msg) override;
};

#endif // CONCRETE_LOGGER_H

 

2- 4- 2 ) 마지막으로 구체 클래스의 CPP

 

#include "ConcreteLogger.h"
#include <iostream>


ConcreteLogger::ConcreteLogger() 
{
}

// 소멸자 구현
ConcreteLogger::~ConcreteLogger()
{
}

// ILogger의 Log 함수 오버라이드 구현
void ConcreteLogger::Log(const std::string& msg) {
    std::cout << "ConcreteLogger: " << msg << std::endl;
}

 

다음과 같이 설계하여 Main에서 사용한다고 해보겠습니다.

예시 3 ) Main

// main.cpp
#include "ConcreteLogger.h"
#include "Application.h"

int main() {
    // ConcreteLogger 인스턴스를 생성 후 ILogger 포인터에 할당
    ConcreteLogger concreteLogger;
    ILogger* logger = &concreteLogger;
    
    // Application 객체에 logger를 주입
    Application app(logger);
    app.DoSomething();  // "ConcreteLogger: Hello, world!"가 출력됨

    return 0;
}

 

 

5. 요약


 

Main에서 보는것처럼

추상클래스 및 인터페이스에는 다음과 같은 이점이 있습니다 

 

1. 헤더 의존성이 감소합니다.

 - 전방 선언을 통해 구체 클래스인 ConcreteLogger의 헤더 파일을 포함하지 않아도 됩니다.

     -  ConcreteLogger의 변경이 Application.h에 영향을 주지 않습니다. 

     -  인터페이스가 변경된다면 영향을 주게 됩니다. 

 

2. 유연성 및 확장성이 좋습니다.

 - 새로운 로거를 추가한다면 ILogger인터페이스를 파생한 클래스만 구현하면 됩니다.

 

3. 컴파일 시간 단축 

 - 구체적인 구현체의 세부 사항이 헤더에 노출되지 않으므로, 해당 구현체가 변경되어도

   이를 포함하지 않는 다른 파일은 재컴파일 되지 않습니다.

 

4. 인터페이스에서 파생된 구체 클래스들을 필요할때만 include해주면 되어,

   의존성이 줄어들고 컴파일 속도가 올라갑니다. 또, 확장성이 좋습니다.

 

5. 캡슐화 

  -  인터페이스만 헤더에 노출하고, 구체적인 구현은 상속받은 클래스에 숨김으로써,

      다른 모듈에서는 구체적인 구현의 변경이 영향을 주지 않도록 합니다.

 

따라서 헤더 파일 수준에서 불필요한 Include가 줄어들고

구현 세부사항이 캡슐화되며 결합도가 낮아지게 됩니다.

 

 

끝으로 언리얼엔진의 인터페이스와 추상클래스 그리고 가상함수에 대한 내용은 C++과 다른점이 있습니다.

다음에는 언리얼엔진의 인터페이스와 추상클래스를 알아보겠습니다.

 

 

출처 : https://velog.io/@kon6443/C-Abstract-class-VS-Interface-%EC%B0%A8%EC%9D%B4

출처 : https://velog.io/@hyongti/C%EC%B6%94%EC%83%81%ED%81%B4%EB%9E%98%EC%8A%A4-vs.-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%ED%81%B4%EB%9E%98%EC%8A%A4

+ Recent posts