
'다형성'이라는 단어는 "Poly(많은)" + "Morph(형태)"가 합쳐진 말로,
글자 그대로 "많은 형태를 가질 수 있는 능력"을 의미합니다.
한마디로 "하나의 이름으로 여러 가지 동작을 실행하는 능력"입니다.
객체가 무엇이냐에 따라 다른 행동을 하는 것이 다형성의 핵심입니다.
상속 관계에서 부모 클래스의 메서드를 자식 클래스에서
재정의(Overriding)하여 사용하는 것이 대표적인 다형성 구현 방법입니다.
1. virtual키워드는 부모 클래스의 메서드에 붙여줍니다.
"이 메서드는 자식 클래스에서 마음대로 바꿀 수 있어!"라고 허락해 주는 거예요.
2. override키워드는 자식 클래스의 메서드에 붙여줍니다.
"네, 부모님! 이 메서드는 제 방식대로 새롭게 만들겠습니다!"라고 응답하는 거죠.
[코드]
using System;
using System.Collections.Generic;
class Animal
{
// virtual: 이 메서드는 자식 클래스에서 재정의될 수 있음을 허용
public virtual void MakeSound()
{
Console.WriteLine("동물이 소리를 냅니다.");
}
}
// 자식 클래스 1
class Dog : Animal
{
// override: 부모의 MakeSound()를 Dog에 맞게 재정의
public override void MakeSound()
{
Console.WriteLine("멍멍!");
}
}
// 자식 클래스 2
class Cat : Animal
{
// override: 부모의 MakeSound()를 Cat에 맞게 재정의
public override void MakeSound()
{
Console.WriteLine("야옹~");
}
}
class Program
{
static void Main()
{
Animal happy = new Dog();
Animal choco = new Cat();
happy.MakeSound(); // 멍멍!
choco.MakeSound(); // 야옹~
Console.WriteLine();
// 부모 형식의 배열에 자식 객체를 담습니다.
Animal[] animals = { new Dog(), new Cat() };
foreach (Animal animal in animals)
{
animal.MakeSound();
}
Console.WriteLine();
// 부모 형식의 리스트에 자식 객체를 담습니다.
List<Animal> animalList = new List<Animal>
{
new Dog(),
new Cat()
};
foreach (Animal animal in animalList)
{
animal.MakeSound();
}
}
}
[실행 결과]
멍멍!
야옹~
멍멍!
야옹~
멍멍!
야옹~
공통된 행동의 규칙만 부여하고 싶을 때는 인터페이스가 더 적합합니다.
인터페이스는 마치 '자격증'이나 '계약서'와 같아요.
[코드]
using System;
// '움직일 수 있음'이라는 행동을 정의하는 인터페이스
public interface IMovable
{
void Move(); // 이 인터페이스를 구현하는 클래스는 반드시 Move 메서드를 가져야 함
}
// Player 클래스는 IMovable 인터페이스를 구현(자격증 취득!)
public class Player : IMovable
{
public void Move()
{
Console.WriteLine("플레이어가 앞으로 이동합니다.");
}
}
// Enemy 클래스도 IMovable 인터페이스를 구현
public class Enemy : IMovable
{
public void Move()
{
Console.WriteLine("적이 플레이어를 향해 다가옵니다.");
}
}
class Program
{
static void Main()
{
IMovable arthur = new Player();
IMovable goblin = new Enemy();
arthur.Move();
goblin.Move();
}
}
[실행 결과]
플레이어가 앞으로 이동합니다.
적이 플레이어를 향해 다가옵니다.
모든 도형은 '면적을 계산한다'라는 공통된 기능을 가져야 하지만,
면적을 계산하는 방식은 도형마다 다릅니다. 이럴 때 추상 클래스가 유용합니다.
[코드]
using System;
// "미완성 설계도"인 추상 클래스 Shape 정의
public abstract class Shape
{
// 일반 속성: 모든 도형은 이름을 가질 수 있음
public string Name { get; set; }
// 일반 메서드: 모든 자식 클래스가 공통으로 사용하는 완성된 기능
public void DisplayInfo()
{
Console.WriteLine($"이 도형의 이름은 '{Name}'입니다.");
}
// 추상 메서드: 구현부({})가 없음. 자식 클래스에서 반드시 재정의(override)해야 함!
public abstract double CalculateArea();
}
// Shape 추상 클래스를 상속받아 "설계도를 완성"하는 Circle 클래스
public class Circle : Shape
{
private double _radius;
public Circle(double radius)
{
Name = "원";
_radius = radius;
}
// 부모의 추상 메서드를 반드시 override 키워드로 구현해야 함
public override double CalculateArea()
{
return Math.PI * _radius * _radius;
}
}
// Shape 추상 클래스를 상속받아 "설계도를 완성"하는 Rectangle 클래스
public class Rectangle : Shape
{
private double _width;
private double _height;
public Rectangle(double width, double height)
{
Name = "사각형";
_width = width;
_height = height;
}
// 부모의 추상 메서드를 반드시 override 키워드로 구현해야 함
public override double CalculateArea()
{
return _width * _height;
}
}
class Program
{
static void Main()
{
// Shape 형식의 리스트에 자식 객체들을 담음 (다형성!)
Shape[] shapes = new Shape[]
{
new Circle(1),
new Rectangle(5, 2)
};
foreach (Shape shape in shapes)
{
shape.DisplayInfo();
// 같은 shape.CalculateArea() 호출이지만,
// 실제 객체가 Circle이냐 Rectangle이냐에 따라 다른 메서드가 실행됨
Console.WriteLine($"면적: {shape.CalculateArea():F2}");
Console.WriteLine();
}
}
}
[실행 결과]
이 도형의 이름은 '원'입니다.
면적: 3.14
이 도형의 이름은 '사각형'입니다.
면적: 10.00
왜 private메서드는 재정의(Overriding)할 수 없을까요?
우선 자식 클래스는 부모 클래스의 메서드가 어떻게 생겼는지 알아야 합니다.
하지만 private메서드는 "이건 나만의 비밀!"이라고 선언한 것과 같습니다.
자식 클래스 입장에서는 부모에게 그런 메서드가 존재하는지조차 알 수 없습니다.
존재조차 모르는 것을 어떻게 새롭게 정의(Overriding)할 수 있을까요?
이것이 바로 private메서드를 오버라이딩할 수 없는 이유입니다.