본문 바로가기

오버로딩과 오버라이딩

@iamrain2026. 1. 19. 12:23

1. 개요

오버로딩(Overloading)과 오버라이딩(Overriding)은 객체 지향 프로그래밍(OOP)의 핵심적인 특징 중 하나인 다형성(Polymorphism)을 구현하는 대표적인 기술입니다. 다형성은 "여러 형태를 가질 수 있는 능력"을 의미하며, 하나의 인터페이스가 다양한 방식으로 동작하게 함으로써 코드의 유연성, 재사용성, 가독성을 높이는 데 결정적인 역할을 합니다.

  • 오버로딩: 컴파일 시점에 결정되는 정적 다형성(Static Polymorphism).
  • 오버라이딩: 런타임 시점에 결정되는 동적 다형성(Dynamic Polymorphism).

다형성이라는 공통된 뿌리를 가지지만, 동작 방식과 사용 목적에서 명확한 차이를 보입니다.

2. 다형성 (Polymorphism)

다형성은 크게 정적 다형성과 동적 다형성으로 나뉩니다.

2.1. 정적 다형성 (Static Polymorphism)

정적 다형성은 컴파일 시점(Compile-time)에 호출될 함수가 결정되는 방식입니다. 컴파일러가 함수 호출 코드를 생성할 때, 인자의 개수, 타입, 순서 등의 정보를 분석하여 어떤 함수를 호출할지 명확하게 알고 기계어 코드를 생성합니다. 이 때문에 정적 바인딩(Static Binding) 또는 조기 바인딩(Early Binding)이라고도 합니다.

  • 대표적인 예: 함수 오버로딩, 연산자 오버로딩

2.2. 동적 다형성 (Dynamic Polymorphism)

동적 다형성은 런타임 시점(Run-time)에 호출될 함수가 결정되는 방식입니다. 상속 관계에 있는 클래스 간에 발생하며, 포인터나 참조 변수가 실제로 어떤 객체를 가리키고 있는지에 따라 호출되는 함수가 달라집니다. 이는 동적 바인딩(Dynamic Binding) 또는 지연 바인딩(Late Binding)이라고 하며, 가상 함수(Virtual Function) 메커니즘을 통해 구현됩니다.

  • 대표적인 예: 함수 오버라이딩(메서드 오버라이딩)

3. 함수 오버로딩 (Function Overloading) - 정적 다형성

3.1. 개념

함수 오버로딩은 하나의 범위(Scope, 주로 클래스나 네임스페이스) 내에서 동일한 이름의 함수를 여러 개 정의하되, 매개변수의 목록(개수, 타입, 순서)을 다르게 하는 기술입니다. 컴파일러는 함수를 호출하는 코드의 인자를 보고 어떤 버전의 함수를 호출할지 결정합니다.

주의: 반환 타입만 다른 경우는 오버로딩으로 인정되지 않습니다. 컴파일러가 호출 지점에서 반환값을 어떻게 사용할지 예측할 수 없기 때문입니다.

// C++ 예시
class Printer {
public:
    void print(int i) {
        std::cout << "Printing int: " << i << std::endl;
    }
    void print(double d) {
        std::cout << "Printing double: " << d << std::endl;
    }
    void print(const char* s) {
        std::cout << "Printing string: " << s << std::endl;
    }
};

int main() {
    Printer p;
    p.print(10);       // print(int) 호출
    p.print(3.14);     // print(double) 호출
    p.print("Hello");  // print(const char*) 호출
    return 0;
}

3.2. 구현 원리: 네임 맹글링 (Name Mangling)

컴파일러는 오버로딩된 함수들을 어떻게 구분할까요? 바로 네임 맹글링이라는 기술을 사용합니다. 컴파일러는 함수 이름과 매개변수 정보를 조합하여 내부적으로 고유한 이름을 새로 만들어냅니다. 이 과정은 컴파일러마다 규칙이 다릅니다.

  • 계층 구조 및 동작 방식:
    1. 사용자 코드: 개발자가 print(10); 코드를 작성합니다.
    2. 컴파일러 (C++):
      • print(int) 함수를 파싱하면서, 컴파일러는 내부 규칙에 따라 _Z5printi 와 같은 새로운 이름을 생성합니다. (Z5는 이름 길이, iint 타입을 의미)
      • print(double)_Z5printd (ddouble)로 변경합니다.
      • print(10); 호출 코드는 내부적으로 _Z5printi(10);을 호출하는 기계어 코드로 번역됩니다.
    3. 링커: 링커는 컴파일된 목적 파일들(.o, .obj)을 연결할 때, _Z5printi라는 심볼을 찾아 해당 함수의 기계어 코드와 연결합니다. 만약 해당 심볼을 찾지 못하면 "unresolved external symbol" 같은 링커 오류가 발생합니다.

이처럼 네임 맹글링 덕분에 링커는 이름이 같은 함수들을 명확히 구분하고 연결할 수 있습니다.

3.3. 장점과 단점

  • 장점:
    • 가독성 및 일관성: 유사한 기능을 하는 함수에 동일한 이름을 부여할 수 있어 코드의 직관성과 가독성이 높아집니다. add, add_int, add_double처럼 이름을 따로 지을 필요가 없습니다.
    • 사용 편의성: 함수 사용자가 매개변수 타입에 따라 다른 함수 이름을 외울 필요 없이 하나의 이름만 기억하면 됩니다.
  • 단점:
    • 모호성: 암시적 형 변환(Implicit Type Conversion)과 함께 사용될 때, 개발자의 의도와 다른 함수가 호출될 수 있는 모호한 상황이 발생할 수 있습니다.
    • 과용의 위험: 너무 많은 함수를 오버로딩하면 오히려 어떤 함수가 호출될지 예측하기 어려워 유지보수성이 떨어질 수 있습니다.

3.4. 사용 시점

논리적으로 동일한 작업을 수행하지만, 입력받는 데이터의 타입이나 개수가 다양할 때 사용하는 것이 가장 이상적입니다. (예: print, add, calculate 등)

4. 함수 오버라이딩 (Function Overriding) - 동적 다형성

4.1. 개념

함수 오버라이딩은 상속 관계에 있는 클래스 간에, 부모 클래스에 정의된 가상 함수(Virtual Function)를 자식 클래스에서 동일한 시그니처(이름, 매개변수, 반환 타입)로 재정의하는 기술입니다. 이를 통해 부모 클래스 타입의 포인터나 참조가 자식 객체를 가리킬 때, 재정의된 자식 클래스의 함수가 호출되도록 할 수 있습니다.

// C++ 예시
class Animal {
public:
    virtual void speak() const { // 가상 함수로 선언
        std::cout << "An animal speaks." << std::endl;
    }
    virtual ~Animal() {} // 가상 소멸자
};

class Dog : public Animal {
public:
    void speak() const override { // 부모의 함수를 오버라이딩 (override 키워드 권장)
        std::cout << "Woof! Woof!" << std::endl;
    }
};

class Cat : public Animal {
public:
    void speak() const override {
        std::cout << "Meow~" << std::endl;
    }
};

void doSpeak(const Animal& animal) {
    animal.speak(); // 여기서 동적 바인딩이 발생
}

int main() {
    Dog myDog;
    Cat myCat;
    Animal myAnimal;

    doSpeak(myDog);    // "Woof! Woof!" 출력
    doSpeak(myCat);    // "Meow~" 출력
    doSpeak(myAnimal); // "An animal speaks." 출력
    return 0;
}

4.2. 구현 원리: 가상 함수 테이블 (Virtual Function Table, vtable)

동적 다형성의 핵심은 가상 함수 테이블(vtable)가상 포인터(vptr)입니다.

  • 계층 구조 및 동작 방식:
    1. 컴파일러:
      • Animal 클래스에 virtual 함수가 있음을 인지하고, Animal 클래스에 대한 vtable을 정적 데이터 영역에 생성합니다. 이 테이블에는 Animal::speak 함수의 주소가 들어갑니다.
      • Dog 클래스를 컴파일할 때, Animal의 vtable을 복사하여 Dog의 vtable을 만듭니다. 그리고 speak 함수가 오버라이딩되었으므로, vtable 내의 speak 항목을 Dog::speak 함수의 주소로 교체합니다.
      • 가상 함수를 가진 클래스의 객체가 생성될 때, 객체의 메모리 맨 앞에 숨겨진 포인터인 vptr을 추가하는 코드를 생성합니다. 이 vptr은 해당 클래스의 vtable을 가리킵니다.
    2. 런타임:
      • Dog myDog; 코드가 실행되면, Dog 객체가 스택에 생성됩니다. 이 객체는 내부에 Dog의 vtable을 가리키는 vptr을 가집니다.
      • doSpeak(myDog);가 호출되면, animal 참조는 myDog 객체를 가리킵니다.
      • animal.speak(); 호출 시, 시스템은 다음 단계를 따릅니다.
        a. animal이 가리키는 객체(myDog)의 vptr에 접근합니다.
        b. vptr을 통해 Dog의 vtable 주소를 얻습니다.
        c. vtable에서 speak 함수에 해당하는 인덱스를 찾아 함수 포인터를 가져옵니다.
        d. 가져온 함수 포인터(Dog::speak의 주소)를 호출합니다.

이처럼 런타임에 객체의 실제 타입을 확인하고 vtable을 통해 함수를 호출하기 때문에 동적 바인딩이 가능해집니다.

4.3. 장점과 단점

  • 장점:
    • 유연성과 확장성: 부모 클래스 타입의 인터페이스를 통해 다양한 자식 객체를 일관된 방식으로 다룰 수 있습니다. 새로운 자식 클래스가 추가되어도 기존 코드를 수정할 필요가 없습니다(OCP: 개방-폐쇄 원칙).
    • 코드 재사용: 부모 클래스의 공통 기능을 재사용하면서, 각 자식 클래스에 특화된 동작을 추가할 수 있습니다.
  • 단점:
    • 성능 오버헤드: vtable을 통한 간접 호출(indirect call)은 일반 함수 호출보다 약간의 성능 저하가 있습니다. (vptr 참조 -> vtable 주소 획득 -> 함수 주소 획득 -> 호출)
    • 메모리 오버헤드: 모든 객체마다 vptr을 위한 추가 메모리(포인터 크기만큼)가 필요하며, 클래스마다 vtable이 생성됩니다.

4.4. 사용 시점

상속 관계에서 부모 클래스의 동작을 자식 클래스에서 구체화하거나 다르게 행동하도록 만들어야 할 때 사용합니다. 인터페이스와 구현을 분리하여 유연한 설계를 만들고 싶을 때 필수적입니다.

5. 오버로딩 vs 오버라이딩 비교

구분 오버로딩 (Overloading) 오버라이딩 (Overriding)
관련 OOP 특성 다형성 (정적) 다형성 (동적), 상속
관계 동일 클래스 또는 동일 스코프 내 부모-자식 클래스 간 (상속 관계)
함수 시그니처 이름은 같지만, 매개변수 목록이 다름 이름, 매개변수, 반환 타입이 모두 동일 (공변 반환 타입 제외)
바인딩 시점 컴파일 타임 (정적 바인딩) 런타임 (동적 바인딩)
구현 기술 네임 맹글링 (Name Mangling) 가상 함수 테이블 (Virtual Table)
목적 동일 이름으로 다양한 타입의 인자를 처리 (편의성) 부모의 기능을 자식에서 재정의하여 동작 변경 (확장성)
키워드 (C++) (특별한 키워드 없음) virtual, override (권장)

6. 요약

 오버로딩정적 다형성으로, 컴파일 시점에 어떤 함수를 부를지 결정됩니다. 같은 클래스 안에서 똑같은 이름의 함수를 매개변수만 다르게 해서 여러 개 만드는 것으로 컴파일러가 함수를 부르는 쪽의 인자 타입을 보고 어떤 함수를 호출할지 미리 정해놓는 방식입니다. 주로 코드의 편의성을 위해 사용됩니다.

 오버라이딩동적 다형성이고, 프로그램 실행 중에 어떤 함수를 부를지 결정됩니다. 상속 관계에서 부모 클래스의 함수를 자식 클래스에서 똑같은 형태로 다시 만드는(재정의하는) 겁니다. 부모 클래스 타입의 포인터로 자식 객체를 가리키고 함수를 호출해도, 실제로는 자식 클래스에서 재정의한 함수가 실행됩니다. 이건 '가상 함수'와 '가상 테이블'이라는 내부 구조를 통해 동작하며, 코드의 유연성과 확장성을 높이는 역할을 합니다.

7. 꼬리 질문과 답변

Q1: C++에서 virtual 키워드 없이 함수를 재정의하면 어떻게 되나요? 오버라이딩이 아닌가요?

A1: virtual 키워드가 없는 부모 클래스의 함수를 자식 클래스에서 동일한 시그니처로 재정의하는 것은 '오버라이딩'이 아니라 '함수 숨김(Function Hiding)'이라고 부릅니다. 이 경우, 부모 클래스 포인터로 자식 객체를 가리켜 함수를 호출하면 항상 부모 클래스의 함수가 호출됩니다. 동적 다형성이 적용되지 않고, 포인터의 정적 타입에 따라 호출될 함수가 컴파일 시점에 결정되기 때문입니다(정적 바인딩). 자식 클래스에서 재정의된 함수는 자식 클래스 타입의 포인터나 객체를 통해서만 접근할 수 있습니다.

Q2: 생성자(Constructor)도 오버로딩이 가능한데, 소멸자(Destructor)는 왜 오버로딩이 불가능한가요?

A2: 생성자는 객체를 초기화하는 다양한 방법을 제공하기 위해 여러 버전이 필요할 수 있습니다. 예를 들어, 기본값으로 생성하거나, 특정 값들을 인자로 받아 생성하는 경우입니다. 그래서 매개변수를 다르게 하여 오버로딩하는 것이 매우 유용합니다. 하지만 소멸자는 객체가 메모리에서 해제될 때 호출되는 단 하나의 정리 작업을 수행합니다. 이 과정에서는 어떤 인자도 필요 없으며, 소멸 방식이 여러 개일 이유가 없습니다. 따라서 소멸자는 매개변수를 가질 수 없고, 이름도 ~ClassName으로 고정되어 있어 오버로딩이 원천적으로 불가능합니다.

Q3: 가상 함수 테이블(vtable)을 사용하면 성능 저하가 있다고 했는데, 항상 단점으로만 봐야 할까요?

A3: vtable을 통한 함수 호출은 일반 함수 호출에 비해 1) vptr을 통해 vtable 주소를 얻고, 2) vtable에서 실제 함수 주소를 찾는 두 단계의 간접 참조(indirect reference)가 추가되어 약간의 오버헤드가 발생합니다. 하지만 현대 CPU의 발전된 분기 예측(branch prediction) 기능과 캐싱 덕분에 이 오버헤드는 대부분의 애플리케이션에서 무시할 수 있을 정도로 미미합니다. 성능 저하라는 단점보다는, 동적 다형성을 통해 얻는 설계의 유연성, 확장성, 유지보수성의 향상이라는 이점이 훨씬 크기 때문에 적극적으로 사용됩니다. 극도의 성능 최적화가 필요한 게임 엔진의 핵심 렌더링 루프나 실시간 임베디드 시스템의 특정 영역이 아니라면, 오버라이딩의 이점이 단점을 압도한다고 볼 수 있습니다.

'Computer Science' 카테고리의 다른 글

MMU (Memory Management Unit)  (0) 2026.01.16
커널 (Kernel)  (0) 2026.01.15
렌더링 파이프라인  (0) 2026.01.08
페이지(Page)와 프레임(Frame)의 차이  (0) 2026.01.07
다익스트라 (Dijkstra) 알고리즘  (1) 2025.12.22
iamrain
@iamrain :: Annals of Unreal

iamrain 님의 블로그 입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차