2025-08-09 12:42
Tags:
다형성(Polymorphism) 핸드북
1. 만들어진 이유: “하나의 이름, 다양한 모습”
프로그래밍의 초기 시절, 코드는 매우 경직되어 있었음. A
라는 함수는 오직 A
타입의 데이터만 처리할 수 있었고, 만약 B
타입의 데이터를 처리하려면 B
를 위한 새로운 함수를 만들어야만 했음. 이는 코드의 중복을 야기하고, 새로운 기능이 추가될 때마다 수많은 코드를 수정해야 하는 유지보수의 악몽을 낳았음.
객체 지향 프로그래밍(OOP) 설계자들은 이 문제를 해결하기 위해 고민했음. “하나의 인터페이스(사용법)를 통해 서로 다른 여러 구현(실제 동작)을 다룰 수는 없을까?” 이 질문에 대한 해답이 바로 다형성.
‘다형성(Poly-morphism)‘은 그리스어에서 유래한 말로, ‘많은(poly)‘과 ‘모양(morph)‘의 합성어. 말 그대로 **‘여러 가지 형태를 가질 수 있는 능력’**을 의미함. 이를 통해 개발자는 개별적인 차이점에 얽매이지 않고, 전체적인 관점에서 코드를 유연하고 확장 가능하게 설계할 수 있게 됨.
2. 구조: 상속과 메서드 재정의
다형성은 주로 상속(Inheritance) 관계를 기반으로 구현됨. 부모 클래스에 정의된 기능을 자식 클래스들이 각자의 상황에 맞게 재정의(Overriding)하여 사용하는 것이 핵심 구조.
-
부모 클래스 (Superclass): 여러 자식 클래스가 공유하는 공통적인 속성과 메서드를 정의. 일종의 ‘명세서’나 ‘틀’의 역할을 함.
-
자식 클래스 (Subclass): 부모 클래스로부터 속성과 메서드를 물려받음. 이때, 부모의 메서드를 그대로 사용할 수도 있지만, 자신만의 방식으로 **재정의(Overriding)**하여 동작을 바꿀 수 있음.
-
메서드 재정의 (Method Overriding): 부모 클래스에 있는 메서드와 똑같은 이름, 매개변수, 반환 타입을 가진 메서드를 자식 클래스에서 새롭게 정의하는 것. 이것이 다형적 행동의 핵심.
예시: 동물
이라는 부모 클래스에 소리내기()
라는 메서드가 있다고 가정.
-
강아지
자식 클래스는소리내기()
를 “멍멍!”으로 재정의. -
고양이
자식 클래스는소리내기()
를 “야옹~“으로 재정의.
이제 우리는 동물
이라는 하나의 타입으로 강아지
와 고양이
객체를 모두 다룰 수 있으며, 각 객체의 소리내기()
를 호출하면 실제 객체 타입에 맞는 소리가 나게 됨.
3. 사용법: 하나의 코드로 여러 객체 다루기
다형성의 가장 큰 힘은 **‘부모 클래스 타입의 참조 변수로 자식 클래스 객체를 참조’**할 수 있다는 점에서 나옴.
Java 예시
1. 클래스 구조 정의
// 부모 클래스 (명세서 역할)
abstract class Shape {
abstract void draw(); // 자식들이 반드시 구현해야 할 추상 메서드
}
// 자식 클래스 1
class Circle extends Shape {
@Override // 부모의 draw() 메서드를 재정의
void draw() {
System.out.println("원을 그립니다.");
}
}
// 자식 클래스 2
class Rectangle extends Shape {
@Override // 부모의 draw() 메서드를 재정의
void draw() {
System.out.println("사각형을 그립니다.");
}
}
// 자식 클래스 3
class Triangle extends Shape {
@Override // 부모의 draw() 메서드를 재정의
void draw() {
System.out.println("삼각형을 그립니다.");
}
}
2. 다형적 활용
public class DrawingManager {
public static void main(String[] args) {
// 부모 클래스(Shape) 타입의 배열에 다양한 자식 객체(Circle, Rectangle, Triangle)를 담음
Shape[] shapes = { new Circle(), new Rectangle(), new Triangle() };
// 반복문을 통해 각 도형을 그림
// shape 변수는 Shape 타입이지만, 실제로는 Circle, Rectangle, Triangle 객체를 차례로 가리킴
for (Shape shape : shapes) {
shape.draw(); // 같은 draw() 호출이지만, 실제 객체에 따라 다른 결과가 출력됨
}
}
}
실행 결과:
원을 그립니다.
사각형을 그립니다.
삼각형을 그립니다.
DrawingManager 코드는 개별 도형(
Circle,
Rectangle)이 무엇인지 전혀 알 필요가 없음. 오직
Shape라는 '규칙'에만 의존함. 만약 나중에
Star라는 새로운 도형 클래스를 추가하더라도
DrawingManager`의 코드는 단 한 줄도 수정할 필요가 없음. 이것이 다형성이 제공하는 유연성과 확장성.
4. 심화 내용: 정적 다형성과 동적 다형성
다형성은 크게 두 가지로 나뉨.
-
동적 다형성 (Dynamic Polymorphism):
- 우리가 흔히 다형성이라고 부르는 것. **메서드 재정의(Overriding)**를 통해 구현됨.
- 어떤 메서드가 호출될지 **실행 시점(Runtime)**에 결정됨.
- 위의
Shape
예제에서shape.draw()
가 어떤 객체의draw()
를 호출할지는 프로그램이 실행되어shape
변수에 실제 객체가 할당되는 순간에 결정되므로 동적 다형성에 해당.
-
정적 다형성 (Static Polymorphism):
- **메서드 오버로딩(Overloading)**을 통해 구현됨.
- 오버로딩은 하나의 클래스 내에서 같은 이름의 메서드를 여러 개 정의하되, 매개변수의 개수나 타입을 다르게 하는 것.
- 어떤 메서드가 호출될지 **컴파일 시점(Compile time)**에 이미 결정됨. 컴파일러가 매개변수의 타입을 보고 어떤 메서드를 호출할지 미리 알 수 있기 때문.
오버로딩(Overloading) Java 예시
class Calculator {
// 매개변수가 정수 두 개인 add 메서드
int add(int a, int b) {
return a + b;
}
// 매개변수가 정수 세 개인 add 메서드
int add(int a, int b, int c) {
return a + b + c;
}
// 매개변수가 실수 두 개인 add 메서드
double add(double a, double b) {
return a + b;
}
}
// 호출 시점에 어떤 메서드를 사용할지 명확함
Calculator calc = new Calculator();
calc.add(10, 20); // 첫 번째 add() 호출
calc.add(10, 20, 30); // 두 번째 add() 호출
calc.add(3.14, 2.71); // 세 번째 add() 호출
5. 결론: 유연한 소프트웨어의 초석
다형성은 단순히 코드를 줄여주는 기술이 아님. 객체들의 역할을 분리하고, 객체 간의 결합도를 낮추어 소프트웨어 전체를 유연하고, 확장 가능하며, 유지보수하기 쉽게 만드는 객체 지향의 핵심 철학.
새로운 기능이 추가되거나 변경될 때, 관련된 부분만 수정하면 되고 나머지 부분은 영향을 받지 않는 ‘플러그인’과 같은 구조를 가능하게 함. 잘 설계된 다형적 코드는 시간이 지나도 그 가치를 잃지 않는 튼튼한 소프트웨어의 기반이 됨.
다형성의 개념을 잘 이해하고 활용한다면, 변화에 쉽게 무너지지 않는 견고한 시스템을 설계하는 데 큰 도움이 될 것입니다.
다형성과 함께 자주 언급되는 ‘인터페이스(Interface)‘는 다형성을 어떻게 더 극대화할 수 있을까요?