← C# 강의 목록으로
🧩
객체지향
객체지향 · 선수: 상속

09. 다형성

같은 호출 코드가 실제 객체 타입에 따라 서로 다르게 동작하는 것이 다형성입니다. virtual/override, abstract 클래스, 업캐스팅·다운캐스팅을 익힙니다.

C#.NET 8객체지향다형성
소요 시간
약 1~1.5시간
난이도
📊 중급
선수 조건
🎯 상속
결과물
같은 호출 코드가 실제 객체 타입에 따라 서로 다르게 동작하는 것이 다형성입니다. virtual/override, abstract 클래스, 업캐스팅·다운캐스팅을 익힙니다.

이 강의에서 배우는 것

  • 1업캐스팅과 다운캐스팅의 차이를 이해한다
  • 2`is` 패턴 매칭으로 안전한 다운캐스팅을 한다
  • 3`abstract` 클래스와 `abstract` 메서드의 쓰임을 안다
  • 4`List<Animal>` 같은 컬렉션에 자식 객체들을 담고 일괄 처리한다

소개

같은 이름의 메서드 호출이 객체의 실제 타입에 따라 다른 동작을 하는 것 — 이게 **다형성(polymorphism)** 입니다. 이전 단원에서 본 `virtual`/`override` 가 그 핵심 도구였고, 이 단원에서는 업/다운캐스팅, `is` 패턴, `abstract` 클래스까지 묶어 본격적으로 다룹니다.

핵심 개념

1) 업캐스팅(upcast)

자식 → 부모 방향. 항상 안전, 명시적 캐스트도 필요 없습니다.

csharp
Animal a = new Dog();    // Dog 객체를 Animal 변수에 담음 (업캐스팅)
a.Speak();               // virtual/override 라면 Dog.Speak() 가 실행됨

변수의 타입은 `Animal` 이라도 실제 객체는 `Dog`. 가상 메서드는 실제 타입에 맞춰 호출됩니다.

2) 다운캐스팅(downcast)

부모 → 자식 방향. 실제 객체가 그 자식이 맞을 때만 성공.

csharp
Animal a = new Dog();
Dog d = (Dog)a;          // 실제로 Dog 라서 OK
d.Bark();

Animal a2 = new Animal();
Dog d2 = (Dog)a2;        // 런타임 InvalidCastException 발생

3) `is` 패턴 매칭으로 안전하게

csharp
if (animal is Dog dog)
{
    dog.Bark();          // animal 이 Dog 일 때만 들어옴, dog 는 이미 캐스팅됨
}

조건 검사와 변수 선언을 한 번에. 캐스트 실패 위험이 없습니다.

`as` 키워드도 비슷하게 쓸 수 있습니다.

csharp
Dog? d = animal as Dog;  // 실패 시 null

4) `abstract` 클래스

"이 클래스만으로는 객체를 만들지 못한다. 자식이 반드시 구체화하라"는 의미.

csharp
abstract class Shape
{
    public abstract double Area();   // 본문 없음, 자식이 반드시 구현
    public void Print() => Console.WriteLine($"넓이={Area()}");
}

class Circle : Shape
{
    public double Radius;
    public Circle(double r) => Radius = r;
    public override double Area() => Math.PI * Radius * Radius;
}

// var s = new Shape();   // 컴파일 에러: abstract 는 직접 인스턴스화 못함

`abstract` 메서드는 자동으로 `virtual` 이며, 자식은 반드시 `override` 해야 합니다.

5) 가상 디스패치(virtual dispatch)

변수의 정적 타입이 아니라 **실제 객체의 동적 타입** 으로 메서드를 찾아 호출하는 것. 이것이 다형성의 작동 원리입니다.

csharp
List<Animal> zoo = [new Dog(), new Cat(), new Cow()];
foreach (var a in zoo)
{
    a.Speak();   // 각각 다른 결과: 멍멍/야옹/음매
}

호출하는 코드는 단 한 줄(`a.Speak()`)인데, 객체마다 다른 동작이 나옵니다. 이것이 OOP의 큰 장점.

핵심 예제

예제 1 — `UpcastDowncast` : 캐스팅 방향

csharp
// Program.cs
using CodingNow.Lecture.Oop09;

// 업캐스팅: 자식 → 부모 (안전, 자동)
Animal a = new Dog();
a.Speak();   // 가상 디스패치 → Dog.Speak()

// 다운캐스팅: 부모 → 자식 (실제 타입이 맞아야 함)
Dog d = (Dog)a;
d.Bark();
csharp
// Animal.cs
namespace CodingNow.Lecture.Oop09;

internal class Animal
{
    public virtual void Speak() => Console.WriteLine("동물 소리");
}

// Dog.cs
internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("멍멍!");
    public void Bark() => Console.WriteLine("왕!왕!");
}

**실행 결과**

text
멍멍!
왕!왕!

**메모:** 변수 타입은 `Animal` 인데도 `Dog.Speak()` 가 호출되는 게 다형성의 본질.

예제 2 — `IsPattern` : 안전한 타입 검사

csharp
// Program.cs
using CodingNow.Lecture.Oop09;

Animal[] animals = [new Dog(), new Cat(), new Animal()];

foreach (var a in animals)
{
    a.Speak();

    if (a is Dog dog)
        dog.Bark();
    else if (a is Cat cat)
        cat.Purr();
}
csharp
// Animal.cs
namespace CodingNow.Lecture.Oop09;

internal class Animal
{
    public virtual void Speak() => Console.WriteLine("동물 소리");
}

internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("멍멍!");
    public void Bark() => Console.WriteLine("왕!왕!");
}

internal class Cat : Animal
{
    public override void Speak() => Console.WriteLine("야옹~");
    public void Purr() => Console.WriteLine("그르릉~");
}

**실행 결과**

text
멍멍!
왕!왕!
야옹~
그르릉~
동물 소리

**메모:** `if (a is Dog dog)` 안에서 `dog` 변수는 이미 `Dog` 타입으로 사용 가능. 캐스트 실패 위험이 없습니다.

예제 3 — `AbstractShape` : 추상 클래스

csharp
// Program.cs
using CodingNow.Lecture.Oop09;

Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);

s1.Print();
s2.Print();

// var s = new Shape();   // 컴파일 에러: abstract 직접 인스턴스화 불가
csharp
// Shape.cs
namespace CodingNow.Lecture.Oop09;

internal abstract class Shape
{
    public abstract double Area();   // 본문 없음

    public void Print() => Console.WriteLine($"넓이 = {Area():F2}");
}

// Circle.cs
internal class Circle : Shape
{
    public double Radius;
    public Circle(double r) => Radius = r;
    public override double Area() => Math.PI * Radius * Radius;
}

// Rectangle.cs
internal class Rectangle : Shape
{
    public double Width;
    public double Height;
    public Rectangle(double w, double h) { Width = w; Height = h; }
    public override double Area() => Width * Height;
}

**실행 결과**

text
넓이 = 78.54
넓이 = 12.00

**메모:** `Shape` 는 추상이라 직접 만들 수 없지만, 공통 코드(`Print`)는 한 군데 모아 둘 수 있습니다.

예제 4 — `PolymorphismList` : 컬렉션 + 일괄 처리

csharp
// Program.cs
using CodingNow.Lecture.Oop09;

List<Animal> zoo = [new Dog(), new Cat(), new Cow()];

foreach (var a in zoo)
{
    a.Speak();   // 같은 호출인데 객체마다 다른 결과
}
csharp
// Animals.cs
namespace CodingNow.Lecture.Oop09;

internal class Animal
{
    public virtual void Speak() => Console.WriteLine("...");
}

internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("멍멍!");
}

internal class Cat : Animal
{
    public override void Speak() => Console.WriteLine("야옹~");
}

internal class Cow : Animal
{
    public override void Speak() => Console.WriteLine("음매~");
}

**실행 결과**

text
멍멍!
야옹~
음매~

**메모:** 새로운 동물을 추가해도 `zoo` 를 순회하는 코드는 그대로 — 다형성의 진정한 가치는 "추가에 닫혀 있고, 확장에 열려 있다"는 것.

전체 예제 코드 (src/)

src/AbstractShape/AbstractShape.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/AbstractShape/Circle.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Circle : Shape
{
    public double Radius;

    public Circle(double radius) => Radius = radius;

    public override double Area() => Math.PI * Radius * Radius;
}

src/AbstractShape/Program.cs

csharp
using CodingNow.Lecture.Oop09;

Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);

s1.Print();
s2.Print();

// var s = new Shape();   // abstract 라서 직접 인스턴스화 불가 (주석 풀면 컴파일 에러)

src/AbstractShape/Rectangle.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Rectangle : Shape
{
    public double Width;
    public double Height;

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double Area() => Width * Height;
}

src/AbstractShape/Shape.cs

csharp
namespace CodingNow.Lecture.Oop09;

// abstract: 자체로는 인스턴스화 못 하고, 자식이 구체화해야 한다.
internal abstract class Shape
{
    // 본문 없는 abstract 메서드 — 자식이 반드시 override.
    public abstract double Area();

    // 추상 클래스에도 일반(구체) 메서드를 둘 수 있다.
    public void Print() => Console.WriteLine($"넓이 = {Area():F2}");
}

src/IsPattern/Animals.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Animal
{
    public virtual void Speak() => Console.WriteLine("동물 소리");
}

internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("멍멍!");
    public void Bark() => Console.WriteLine("왕!왕!");
}

internal class Cat : Animal
{
    public override void Speak() => Console.WriteLine("야옹~");
    public void Purr() => Console.WriteLine("그르릉~");
}

src/IsPattern/IsPattern.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/IsPattern/Program.cs

csharp
using CodingNow.Lecture.Oop09;

Animal[] animals = [new Dog(), new Cat(), new Animal()];

foreach (var a in animals)
{
    a.Speak();   // 가상 디스패치

    // is 패턴: 타입 검사 + 변수 선언을 한 번에. 안에서는 이미 캐스팅된 변수 사용.
    if (a is Dog dog)
        dog.Bark();
    else if (a is Cat cat)
        cat.Purr();
}

src/PolymorphismList/Animals.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Animal
{
    public virtual void Speak() => Console.WriteLine("...");
}

internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("멍멍!");
}

internal class Cat : Animal
{
    public override void Speak() => Console.WriteLine("야옹~");
}

internal class Cow : Animal
{
    public override void Speak() => Console.WriteLine("음매~");
}

src/PolymorphismList/PolymorphismList.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/PolymorphismList/Program.cs

csharp
using CodingNow.Lecture.Oop09;

// 자식 타입 객체들을 부모 타입 컬렉션 하나에 모두 담을 수 있다.
List<Animal> zoo = [new Dog(), new Cat(), new Cow()];

foreach (var a in zoo)
{
    a.Speak();   // 같은 호출인데 객체마다 다른 결과 — 이것이 다형성
}

src/UpcastDowncast/Animal.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Animal
{
    public virtual void Speak() => Console.WriteLine("동물 소리");
}

src/UpcastDowncast/Dog.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("멍멍!");
    public void Bark() => Console.WriteLine("왕!왕!");
}

src/UpcastDowncast/Program.cs

csharp
using CodingNow.Lecture.Oop09;

// 업캐스팅: 자식(Dog) → 부모(Animal). 안전하므로 명시적 캐스트 불필요.
Animal a = new Dog();
a.Speak();   // 가상 디스패치: 실제 객체가 Dog 이므로 Dog.Speak() 가 실행됨

// 다운캐스팅: 부모(Animal) → 자식(Dog). 실제 타입이 Dog 가 맞을 때만 OK.
Dog d = (Dog)a;
d.Bark();

src/UpcastDowncast/UpcastDowncast.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

자주 하는 실수

  1. 다운캐스팅에 `(Dog)a` 만 쓰고 안전 검사를 빼서 `InvalidCastException` 발생 → `is` 또는 `as` 사용.
  2. `abstract` 클래스를 `new` 로 직접 만들려 한다 — 컴파일 에러.
  3. `abstract` 메서드를 자식이 `override` 안 하면 자식도 abstract 처리 안 해 둬서 컴파일 에러.
  4. 가상 메서드를 호출했는데 부모 버전이 나오는 줄 안다. 변수 타입과 무관하게 **실제 객체** 의 메서드가 호출됨.
  5. `is` 패턴 안에서 선언한 변수를 `if` 바깥에서 쓰려 한다 — 스코프는 보통 `if` 블록 내부.

정리

  • 다형성 = "같은 호출, 다른 동작". `virtual`/`override` 와 가상 디스패치가 그 엔진.
  • 업캐스팅은 자동, 다운캐스팅은 `is` 패턴 또는 `as` 로 안전하게.
  • `abstract` 클래스는 공통 부분만 정의하고 구체적인 부분은 자식에게 위임할 때 쓴다.
  • 컬렉션과 만나면 위력 폭발 — 자식 종류가 늘어도 처리 코드는 그대로.

과제

**과제 - 09. 다형성**

문제 1 — `Vehicle`/`Car`/`Bike` (`Move()` 다형성)

  • 프로젝트 폴더: `Homework01/`
  • 핵심 개념: `virtual` / `override`, 컬렉션에 자식 객체 담기

요구사항

  • 부모 `Vehicle` 클래스에 `virtual void Move()` 가 있다(기본 메시지 출력).
  • 자식 `Car`, `Bike` 가 각각 `Move()` 를 override 해 다른 메시지를 출력한다.
  • `Program.cs` 에서 `List<Vehicle>` 에 객체 3~4 개를 담고 순회하며 `Move()` 호출.

예상 출력

text
탈것 이동
자동차가 부릉~
자전거가 따르릉~
자동차가 부릉~

힌트

  • `List<Vehicle>` 에 `new Car()`, `new Bike()` 를 그대로 담으면 된다(업캐스팅).

문제 2 — `abstract Animal` + 자식들 `Speak()`

  • 프로젝트 폴더: `Homework02/`
  • 핵심 개념: `abstract` 클래스 / `abstract` 메서드, `is` 패턴

요구사항

  • `abstract class Animal` 에 `abstract void Speak()` 를 둔다.
  • 자식 `Dog`, `Cat`, `Cow` 각각 `Speak()` override.
  • `Dog` 에만 추가 메서드 `Bark()` 를 둔다.
  • `Animal[] zoo = [...]` 를 순회하면서 `Speak()` 호출, `is Dog dog` 일 때 `dog.Bark()` 도 호출.

예상 출력

text
멍멍!
왕!왕!
야옹~
음매~

힌트

  • `abstract` 메서드는 본문이 없고 세미콜론으로 끝난다.
  • `var animal = new Animal();` 는 컴파일 에러 (추상 클래스는 직접 못 만든다).

정답 확인

직접 풀어 본 후 [`answer/`](./answer/) 폴더의 정답과 비교해 보세요.

정답 (answer/)

homework/answer/Homework01/Homework01.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

homework/answer/Homework01/Program.cs

csharp
using CodingNow.Lecture.Oop09;

List<Vehicle> vehicles = [new Vehicle(), new Car(), new Bike(), new Car()];

foreach (var v in vehicles)
{
    v.Move();
}

homework/answer/Homework01/Vehicles.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Vehicle
{
    public virtual void Move() => Console.WriteLine("탈것 이동");
}

internal class Car : Vehicle
{
    public override void Move() => Console.WriteLine("자동차가 부릉~");
}

internal class Bike : Vehicle
{
    public override void Move() => Console.WriteLine("자전거가 따르릉~");
}

homework/answer/Homework02/Animals.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal abstract class Animal
{
    // 자식이 반드시 구현해야 한다.
    public abstract void Speak();
}

internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("멍멍!");
    public void Bark() => Console.WriteLine("왕!왕!");
}

internal class Cat : Animal
{
    public override void Speak() => Console.WriteLine("야옹~");
}

internal class Cow : Animal
{
    public override void Speak() => Console.WriteLine("음매~");
}

homework/answer/Homework02/Homework02.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

homework/answer/Homework02/Program.cs

csharp
using CodingNow.Lecture.Oop09;

Animal[] zoo = [new Dog(), new Cat(), new Cow()];

foreach (var a in zoo)
{
    a.Speak();

    if (a is Dog dog)
        dog.Bark();
}

직접 해 보기

bash
cd src/UpcastDowncast
dotnet run

cd ../IsPattern
dotnet run

cd ../AbstractShape
dotnet run

cd ../PolymorphismList
dotnet run

다음 단원

[10_인터페이스](../10_인터페이스/) — 상속 없이 "할 수 있는 능력"만 약속하는 인터페이스를 배웁니다.

예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗