Menu
Coddy logo textTech

컴파일 타임 vs 런타임 다형성

Coddy C++ 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 104개 중 56번째.

다형성(Polymorphism)은 "많은 형태"를 의미하며, 객체들이 서로 다르게 행동하면서도 통일된 방식으로 다뤄질 수 있게 하는 핵심적인 OOP 개념입니다. C++는 두 가지 뚜렷한 유형의 다형성을 지원하며, 각각은 프로그램 실행의 서로 다른 단계에서 결정됩니다.

컴파일 타임 다형성(정적 다형성이라고도 함)은 프로그램이 실행되기 전에 컴파일러에 의해 결정됩니다. 컴파일러는 함수 시그니처를 기반으로 호출할 함수를 정확히 결정합니다. 여기에는 함수 오버로딩과 템플릿이 포함됩니다:

void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }

print(5);      // 컴파일러가 print(int)를 선택합니다
print(3.14);   // 컴파일러가 print(double)를 선택합니다

런타임 다형성(동적 다형성이라고도 함)은 프로그램이 실행되는 동안 결정됩니다. 어떤 함수를 호출할지에 대한 결정은 포인터나 참조 타입이 아니라 실제 객체 타입에 따라 달라집니다. 이는 가상 함수를 통해 구현됩니다:

class Shape {
public:
    virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};

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

Shape* s = new Circle();
s->draw();  // 런타임에 결정됨: "Drawing circle"

핵심적인 트레이드오프: 컴파일 타임 다형성은 결정이 컴파일 중에 이루어지기 때문에 런타임 오버헤드가 전혀 없는 반면, 런타임 다형성은 약간의 비용(vtable 조회)이 추가되지만 실행 전까지 타입을 알 수 없는 객체를 다룰 때 더 큰 유연성을 제공합니다.

challenge icon

챌린지

쉬움

두 가지 유형의 다형성을 나란히 보여주는 계산기 시스템을 구축해 보겠습니다. 함수 오버로딩을 통해 컴파일 타임 다형성이 서로 다른 입력 타입을 처리하고, 런타임 다형성을 통해 서로 다른 계산 전략을 동적으로 교체할 수 있는 시스템을 만들 것입니다.

코드는 세 개의 파일로 구성됩니다:

  • Calculator.h: 모든 계산 전략을 나타내는 기본 Calculator 클래스를 정의합니다:
    • int를 반환하고 "Base calculation: " <a> " ? " <b>를 출력하는 가상 calculate(int a, int b) 메서드 (0을 반환)
    • 가상 소멸자
  • Operations.h: 계산 동작을 재정의(override)하는 두 개의 파생 계산기 클래스를 정의합니다:
    • Adder: calculate()를 재정의하여 "Adding: " <a> " + " <b>를 출력하고 합계를 반환합니다.
    • Multiplier: calculate()를 재정의하여 "Multiplying: " <a> " * " <b>를 출력하고 곱을 반환합니다.
    두 클래스 모두 override 키워드를 사용해야 합니다.
  • main.cpp: 두 가지 다형성 유형을 모두 보여주는 시스템을 만듭니다. 두 개의 정수 입력을 읽습니다 (각각 별도의 줄에 입력).

    먼저, 세 개의 오버로드된 display() 함수를 만들어 컴파일 타임 다형성을 보여줍니다:

    • display(int x) 출력: "Integer value: " <x>
    • display(double x) 출력: "Double value: " <x>
    • display(const std::string& x) 출력: "String value: " <x>

    그런 다음 기본 Calculator, Adder, Multiplier를 포함하는 Calculator* 포인터 배열을 생성하여 런타임 다형성을 보여줍니다. 루프를 돌며 입력 값으로 각각의 calculate()를 호출하고, 각 계산 후 결과를 출력합니다.

    출력 구조는 다음과 같습니다:

    === Compile-Time Polymorphism ===
    <int, double, string에 대한 display 출력>
    
    === Runtime Polymorphism ===
    <결과를 포함한 calculate 출력>

    컴파일 타임 섹션의 경우, 첫 번째 입력을 정수로, 그 다음에는 double(동일한 값에 .5를 더함)로, 그 다음에는 문자열 "Result"로 display()를 호출합니다. 작업이 끝나면 동적으로 할당된 계산기들을 정리하십시오.

예를 들어, 입력이 103인 경우:

=== Compile-Time Polymorphism ===
Integer value: 10
Double value: 10.5
String value: Result

=== Runtime Polymorphism ===
Base calculation: 10 ? 3
Result: 0
Adding: 10 + 3
Result: 13
Multiplying: 10 * 3
Result: 30

컴파일러가 인자 타입에 따라 올바른 display() 오버로드를 선택하는 방식(컴파일 타임 결정)과, vtable 메커니즘을 통해 런타임에 실제 객체 타입에 따라 올바른 calculate() 메서드가 결정되는 방식의 차이를 확인해 보세요.

치트 시트

C++는 서로 다른 단계에서 해결되는 두 가지 유형의 다형성을 지원합니다:

컴파일 타임 다형성 (정적 다형성)은 실행 전 컴파일러에 의해 해결됩니다. 함수 오버로딩과 템플릿을 포함합니다:

void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }

print(5);      // 컴파일러가 print(int)를 선택함
print(3.14);   // 컴파일러가 print(double)을 선택함

런타임 다형성 (동적 다형성)은 가상 함수를 사용하여 실행 중에 해결됩니다. 실제 객체 유형에 따라 호출되는 함수가 결정됩니다:

class Shape {
public:
    virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};

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

Shape* s = new Circle();
s->draw();  // 런타임에 결정됨: "Drawing circle"

트레이드오프: 컴파일 타임 다형성은 런타임 오버헤드가 전혀 없는 반면, 런타임 다형성은 약간의 비용(vtable 조회)이 추가되지만 더 큰 유연성을 제공합니다.

직접 해보기

#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"

// TODO: 세 개의 오버로드된 display() 함수를 생성하세요:
// 1. display(int x) - "Integer value: <x>"를 출력
// 2. display(double x) - "Double value: <x>"를 출력
// 3. display(const std::string& x) - "String value: <x>"를 출력



int main() {
    // 두 개의 정수 입력을 읽습니다
    int a, b;
    std::cin >> a;
    std::cin >> b;
    
    // === 컴파일 타임 다형성 (Compile-Time Polymorphism) ===
    std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
    // TODO: 다음을 사용하여 display()를 호출하세요:
    // - 정수형 a
    // - double형 a (0.5를 더함)
    // - 문자열 "Result"
    
    
    std::cout << std::endl;
    
    // === 런타임 다형성 (Runtime Polymorphism) ===
    std::cout << "=== Runtime Polymorphism ===" << std::endl;
    // TODO: 3개의 요소를 가진 Calculator* 포인터 배열을 생성하세요:
    // - 기본 Calculator
    // - Adder
    // - Multiplier
    
    // TODO: 배열을 순회하며 각 요소에 대해 calculate(a, b)를 호출하고,
    // 각 계산 후에 "Result: <return_value>"를 출력하세요
    
    
    // TODO: 동적으로 할당된 메모리를 해제하세요
    
    
    return 0;
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨