💻 프로그래밍

Polymorphism

다형성

같은 인터페이스로 다른 동작을 수행하는 OOP 핵심 원칙. 코드의 유연성과 확장성을 높이며, 전략 패턴과 의존성 주입의 기반이 됩니다.

📖 상세 설명

다형성(Polymorphism)은 그리스어로 "많은 형태"를 의미하며, 객체지향 프로그래밍(OOP)의 4대 핵심 원칙 중 하나입니다. 같은 인터페이스나 메서드 시그니처를 통해 서로 다른 타입의 객체들이 각자의 방식으로 동작할 수 있게 해줍니다. 예를 들어, draw() 메서드를 호출하면 Circle은 원을, Rectangle은 사각형을 그리는 것처럼, 호출하는 코드는 동일하지만 실제 동작은 객체의 타입에 따라 달라집니다.

다형성은 크게 세 가지 종류로 분류됩니다. 서브타입 다형성(Subtype Polymorphism)은 상속을 통해 부모 클래스 타입으로 자식 클래스 객체를 다루는 것으로, 가장 일반적인 형태입니다. 파라메트릭 다형성(Parametric Polymorphism)은 제네릭(Generic)을 통해 타입을 매개변수화하는 것으로, List<T>처럼 어떤 타입이든 담을 수 있는 컬렉션이 대표적입니다. Ad-hoc 다형성은 메서드 오버로딩을 통해 같은 이름의 메서드가 다른 매개변수를 받는 것입니다.

오버라이딩(Overriding)오버로딩(Overloading)은 다형성의 대표적인 구현 방식입니다. 오버라이딩은 자식 클래스가 부모 클래스의 메서드를 재정의하는 것으로, 런타임에 어떤 메서드가 호출될지 결정됩니다(동적 바인딩). 반면 오버로딩은 같은 클래스 내에서 같은 이름의 메서드를 매개변수만 다르게 정의하는 것으로, 컴파일 타임에 호출할 메서드가 결정됩니다(정적 바인딩). Python은 오버로딩을 직접 지원하지 않고 기본값 매개변수나 *args로 유사한 효과를 냅니다.

실무에서 다형성은 전략 패턴(Strategy Pattern)의존성 주입(Dependency Injection)의 핵심입니다. 전략 패턴은 알고리즘을 인터페이스로 정의하고 런타임에 구현체를 교체할 수 있게 해줍니다. 의존성 주입은 구체 클래스가 아닌 인터페이스에 의존하게 하여, 테스트 시 Mock 객체로 쉽게 교체할 수 있습니다. Spring Framework, React의 Context API, Python의 추상 클래스 등 현대 프레임워크들은 다형성을 적극 활용하여 확장 가능하고 테스트하기 쉬운 구조를 만듭니다.

💻 코드 예제

Java - 서브타입 다형성과 전략 패턴
// 1. 인터페이스 정의 (다형성의 기반)
interface PaymentStrategy {
    void pay(int amount);
}

// 2. 다양한 구현체 (같은 인터페이스, 다른 동작)
class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;

    public CreditCardPayment(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(int amount) {
        System.out.println("카드 " + cardNumber + "로 " + amount + "원 결제");
    }
}

class KakaoPayPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("카카오페이로 " + amount + "원 결제");
    }
}

// 3. 다형성 활용 (구체 클래스가 아닌 인터페이스에 의존)
class ShoppingCart {
    private PaymentStrategy paymentStrategy;  // 인터페이스 타입

    // 의존성 주입 (런타임에 구현체 교체 가능)
    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout(int amount) {
        paymentStrategy.pay(amount);  // 어떤 구현체든 동일하게 호출
    }
}

// 사용 예시
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("1234-5678"));
cart.checkout(50000);  // 카드 결제

cart.setPaymentStrategy(new KakaoPayPayment());
cart.checkout(30000);  // 카카오페이 결제
Python - Duck Typing과 추상 클래스
from abc import ABC, abstractmethod

# 1. 추상 클래스 정의
class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass

    @abstractmethod
    def draw(self) -> str:
        pass

# 2. 구체 클래스들 (같은 메서드, 다른 동작)
class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius ** 2

    def draw(self) -> str:
        return f"반지름 {self.radius}인 원을 그립니다"

class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

    def draw(self) -> str:
        return f"{self.width}x{self.height} 사각형을 그립니다"

# 3. 다형성 활용 (타입에 관계없이 동일한 방식으로 처리)
def print_shape_info(shape: Shape):
    """어떤 Shape든 동일하게 처리 가능"""
    print(f"면적: {shape.area():.2f}")
    print(shape.draw())

# 4. Duck Typing (Python의 특징)
class Triangle:  # Shape를 상속하지 않음
    def area(self) -> float:
        return 10.0

    def draw(self) -> str:
        return "삼각형을 그립니다"

# Duck Typing: "오리처럼 걷고 오리처럼 울면, 그것은 오리다"
shapes = [Circle(5), Rectangle(4, 6), Triangle()]
for shape in shapes:
    print_shape_info(shape)  # Triangle도 동작함!

🗣 실무 대화 예시

🎓 기술 면접: 다형성 종류 설명
👤
면접관
다형성의 종류에 대해 설명해주세요. 서브타입 다형성과 파라메트릭 다형성의 차이점도 알려주시겠어요?
👩
지원자
서브타입 다형성은 상속 관계를 통해 구현됩니다. 부모 클래스 타입으로 자식 클래스 객체를 다룰 수 있어서, Animal animal = new Dog();처럼 사용합니다. 런타임에 실제 객체 타입에 따라 메서드가 호출되죠.
👩
지원자
파라메트릭 다형성은 타입을 매개변수로 받는 제네릭입니다. List<String>이나 Map<K, V>처럼 어떤 타입이든 담을 수 있는 컨테이너를 만들 수 있어요. 타입 안전성을 유지하면서도 재사용 가능한 코드를 작성할 수 있습니다.
👤
면접관
그럼 오버로딩과 오버라이딩의 바인딩 시점 차이는요?
👩
지원자
오버로딩은 컴파일 타임에 결정되는 정적 바인딩입니다. 메서드 호출 시 매개변수의 타입과 개수로 어떤 메서드를 호출할지 컴파일러가 결정해요. 반면 오버라이딩은 런타임에 결정되는 동적 바인딩입니다. 실제 객체의 타입에 따라 호출될 메서드가 결정되죠. 이게 가상 메서드 테이블(vtable)을 통해 구현됩니다.

🔍 코드 리뷰: 다형성 활용
👨‍💻
시니어 개발자 (리뷰어)
이 코드에서 if (type == "email") sendEmail() else if (type == "sms") sendSMS() 패턴이 반복되는데요. 새로운 알림 채널이 추가될 때마다 이 조건문을 모두 수정해야 해서 유지보수가 어려울 것 같아요.
👩‍💻
주니어 개발자
아, NotificationChannel 인터페이스를 만들고 send(message) 메서드를 정의해서, EmailChannel, SMSChannel 클래스가 이를 구현하도록 하면 되겠네요. 그러면 호출하는 쪽은 그냥 channel.send(message)만 하면 되겠죠?
👨‍💻
시니어 개발자 (리뷰어)
정확해요! 그게 OCP(Open-Closed Principle)를 지키는 방법이에요. 새 채널이 추가되면 기존 코드 수정 없이 새 클래스만 추가하면 됩니다. 그리고 팩토리 패턴과 함께 쓰면 채널 생성 로직도 깔끔하게 분리할 수 있어요.

주의사항

🚨 다형성 관련 실무 주의사항
  • LSP(리스코프 치환 원칙) 준수: 자식 클래스는 부모 클래스를 완벽히 대체할 수 있어야 합니다. Rectangle을 상속한 Square가 width와 height를 동시에 변경한다면 LSP 위반이에요. 상속보다 조합(Composition)을 고려하세요.
  • 타입 체크 대신 다형성 활용: instanceoftypeof로 타입을 체크하고 분기하는 코드가 많다면, 다형성으로 리팩토링할 시점입니다. 조건문이 아닌 메서드 오버라이딩으로 해결하세요.
  • 인터페이스 분리 원칙(ISP): 하나의 거대한 인터페이스보다 작고 구체적인 여러 인터페이스가 낫습니다. 클라이언트가 사용하지 않는 메서드에 의존하지 않도록 설계하세요.
  • 추상화 수준 일관성: 같은 계층의 클래스들은 비슷한 추상화 수준을 유지해야 합니다. sendEmail()updateDatabaseAndLogAndNotify()가 같은 인터페이스에 있으면 안 됩니다.
  • 과도한 상속 계층 피하기: 3단계 이상의 상속 계층은 복잡성을 높입니다. 상속보다 인터페이스와 조합을 우선 고려하세요. "is-a" 관계가 명확할 때만 상속을 사용합니다.

🔗 관련 용어

📚 더 배우기