Computer >> 컴퓨터 >  >> 프로그래밍 >> C++

C++에서 파생 클래스 메서드에 더 제한적인 접근 권한을 설정하면 어떻게 될까?

들어가며

C++에서는 파생 클래스가 기본 클래스의 가상 함수를 재정의할 때, 원래보다 더 엄격한 접근 지정자(private 등)를 적용하는 것이 문법적으로 가능합니다. 그렇다면 이런 코드는 실제로 어떻게 동작할까요? 이 글에서는 몇 가지 예제를 통해 파생 클래스 메서드의 접근 권한을 제한했을 때 발생하는 현상과 그 이유를 자세히 분석해 보겠습니다.

예제 1: 파생 클래스에서 display()를 private으로 재정의

먼저 기본 클래스의 display() 함수는 public으로 선언하고, 파생 클래스에서는 같은 함수를 private으로 재정의해 보겠습니다.

#include <iostream>
using namespace std;

class BaseClass {
public:
    virtual void display() {
        cout << "Print function from the base class" << endl;
    }
};

class DerivedClass : public BaseClass {
private:
    void display() {
        cout << "Print function from the derived class" << endl;
    }
};

int main() {
}

여기까지는 아무 문제 없이 컴파일됩니다. 하지만 main 함수 안에서 파생 클래스 객체를 통해 display()를 직접 호출하면 어떻게 될까요?

int main() {
    DerivedClass d;
    d.display();
}

출력 결과

main.cpp: In function 'int main()':
main.cpp:20:15: error: 'virtual void DerivedClass::display()' is private within this context
    d.display();
              ^
main.cpp:13:10: note: declared private here
    void display() {
         ^~~~~~~

컴파일러는 DerivedClass::display()가 private으로 선언되어 있기 때문에 외부에서의 직접 호출을 허용하지 않습니다. 즉, 파생 클래스에서 접근 지정자를 더 제한적으로 설정하면, 해당 객체를 통한 직접 호출은 컴파일 오류가 발생합니다.

예제 2: 기본 클래스 포인터를 통한 호출

그런데 흥미로운 점은, 같은 private 함수라도 기본 클래스 포인터를 통해 호출하면 정상적으로 실행된다는 사실입니다.

#include <iostream>
using namespace std;

class BaseClass {
public:
    virtual void display() {
        cout << "Print function from the base class" << endl;
    }
};

class DerivedClass : public BaseClass {
private:
    void display() {
        cout << "Print function from the derived class" << endl;
    }
};

int main() {
    BaseClass *b = new DerivedClass;
    b->display();
    delete b;
}

출력 결과

Print function from the derived class

왜 이런 결과가 나올까?

위 프로그램에서는 private으로 선언된 DerivedClass::display()가 기본 클래스 포인터를 통해 성공적으로 호출되었습니다. 그 이유는 C++의 접근 검사 방식에 있습니다.

  • 접근 지정자는 컴파일 타임에 검사됩니다. 컴파일러는 포인터의 정적 타입(static type), 즉 BaseClass*를 기준으로 접근 가능 여부를 판단합니다. 기본 클래스에서 display()는 public이므로 호출이 허용됩니다.
  • 실제 실행되는 함수는 런타임에 결정됩니다. display()는 가상 함수이므로, 런타임에는 포인터가 실제로 가리키는 객체의 타입(동적 타입)에 따라 DerivedClass::display()가 호출됩니다. 이 시점에는 접근 지정자가 다시 검사되지 않습니다.

결국 접근 지정자는 어디까지나 컴파일 타임의 개념이며, 가상 함수 디스패치(virtual dispatch)는 이를 우회하게 됩니다. 따라서 기본 클래스 포인터나 참조를 통해서는 파생 클래스의 private 멤버 함수도 호출될 수 있는 것입니다.

마무리 및 주의 사항

이처럼 C++에서는 파생 클래스에서 접근 권한을 축소해도 다형성(polymorphism)을 통한 호출까지 막을 수는 없습니다. 이는 의도하지 않은 동작이나 캡슐화 위반으로 이어질 수 있으므로, 일반적으로는 재정의하는 가상 함수의 접근 지정자를 기본 클래스와 동일하게 유지하는 것이 좋은 습관입니다. 만약 외부 호출을 확실히 차단하고 싶다면, public 비가상 함수가 private 가상 함수를 감싸는 NVI(Non-Virtual Interface) 패턴이나 컴포지션(composition) 같은 다른 설계 방법을 고려하는 것이 바람직합니다.