Polymorphism
다형성
같은 인터페이스로 다른 동작을 수행하는 OOP 핵심 원칙. 코드의 유연성과 확장성을 높이며, 전략 패턴과 의존성 주입의 기반이 됩니다.
다형성
같은 인터페이스로 다른 동작을 수행하는 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의 추상 클래스 등 현대 프레임워크들은 다형성을 적극 활용하여 확장 가능하고 테스트하기 쉬운 구조를 만듭니다.
// 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); // 카카오페이 결제
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>처럼 어떤 타입이든 담을 수 있는 컨테이너를 만들 수 있어요. 타입 안전성을 유지하면서도 재사용 가능한 코드를 작성할 수 있습니다.
if (type == "email") sendEmail() else if (type == "sms") sendSMS() 패턴이 반복되는데요. 새로운 알림 채널이 추가될 때마다 이 조건문을 모두 수정해야 해서 유지보수가 어려울 것 같아요.
NotificationChannel 인터페이스를 만들고 send(message) 메서드를 정의해서, EmailChannel, SMSChannel 클래스가 이를 구현하도록 하면 되겠네요. 그러면 호출하는 쪽은 그냥 channel.send(message)만 하면 되겠죠?
Rectangle을 상속한 Square가 width와 height를 동시에 변경한다면 LSP 위반이에요. 상속보다 조합(Composition)을 고려하세요.instanceof나 typeof로 타입을 체크하고 분기하는 코드가 많다면, 다형성으로 리팩토링할 시점입니다. 조건문이 아닌 메서드 오버라이딩으로 해결하세요.sendEmail()과 updateDatabaseAndLogAndNotify()가 같은 인터페이스에 있으면 안 됩니다.