← C# 강의 목록으로
🧩
객체지향
객체지향 · 선수: 프로퍼티

08. 상속

이미 있는 클래스의 멤버를 물려받아 새로운 클래스를 만드는 것이 상속입니다. base·virtual·override·sealed·protected 를 익혀 'is-a' 관계를 코드로 표현합니다.

C#.NET 8객체지향상속
소요 시간
약 1~1.5시간
난이도
📊 중급
선수 조건
🎯 프로퍼티와 캡슐화
결과물
이미 있는 클래스의 멤버를 물려받아 새로운 클래스를 만드는 것이 상속입니다. base·virtual·override·sealed·protected 를 익혀 'is-a' 관계를 코드로 표현합니다.

이 강의에서 배우는 것

  • 1`: base` 표기로 클래스를 상속할 수 있다
  • 2자식 생성자에서 `: base(...)` 로 부모 생성자를 호출할 수 있다
  • 3`virtual` / `override` / `sealed override` 의 차이를 안다
  • 4`new` 키워드로 메서드를 가린(hide) 결과가 어떻게 다른지 안다

소개

이미 잘 만들어진 클래스가 있을 때, 비슷한 클래스를 처음부터 다시 짜는 건 낭비입니다. **상속(inheritance)** 으로 부모 클래스의 멤버를 그대로 물려받고 필요한 부분만 덧붙이거나 바꿀 수 있습니다.

핵심 개념

1) `: base` 표기

csharp
class Animal
{
    public string Name = "";
    public void Eat() => Console.WriteLine($"{Name} 먹는다");
}

class Dog : Animal       // Animal 을 상속
{
    public void Bark() => Console.WriteLine($"{Name} 멍멍!");
}

`Dog` 는 `Animal` 의 `Name`, `Eat()` 을 그대로 쓸 수 있습니다. C#은 **단일 상속**만 허용 (한 부모만).

2) 자식 생성자에서 `: base(...)` 호출

부모에 매개변수 있는 생성자가 있다면 자식이 직접 호출해 줘야 합니다.

csharp
class Animal
{
    public string Name;
    public Animal(string name) => Name = name;
}

class Dog : Animal
{
    public Dog(string name) : base(name) { }   // 부모 생성자 호출
}

3) `virtual` 과 `override`

부모가 `virtual` 로 표시한 메서드를 자식이 `override` 로 다시 정의(재정의)할 수 있습니다.

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

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

변수의 타입이 `Animal` 이라도, 실제 객체가 `Dog` 면 `Dog.Speak()` 가 실행됩니다(=다형성, 다음 단원).

4) `sealed override`

"이 메서드는 더 이상 자식이 override 할 수 없다"는 표시.

csharp
class Puppy : Dog
{
    public sealed override void Speak() => Console.WriteLine("깨갱!");
}
// Puppy 를 상속한 클래스는 Speak 를 또 override 못 함

5) `new` 키워드로 hide

`override` 가 아니라 `new` 를 쓰면 "부모 메서드를 가린다" 는 의미가 됩니다. 다형성이 작동하지 않으니 주의.

csharp
class Animal { public virtual void Speak() => Console.WriteLine("Animal"); }
class Cat : Animal { public new void Speak() => Console.WriteLine("Cat"); }

Animal a = new Cat();
a.Speak();   // "Animal" 이 나옴 — Cat 의 new 메서드는 무시됨

`override` 는 가상 디스패치 테이블을 갈아끼우지만, `new` 는 단순히 같은 이름을 새로 만든 별도 메서드입니다.

6) `protected`

부모의 멤버에 자식만 접근하게 하고 싶을 때 쓰는 접근 제한자.

csharp
class Animal { protected int HungerLevel = 0; }
class Dog : Animal { public void Feed() => HungerLevel--; }

핵심 예제

예제 1 — `AnimalDog` : 단순 상속

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

var d = new Dog();
d.Name = "초코";
d.Eat();    // 부모에게서 물려받음
d.Bark();   // 자식 고유
csharp
// Animal.cs
namespace CodingNow.Lecture.Oop08;

internal class Animal
{
    public string Name = "";
    public void Eat() => Console.WriteLine($"{Name} 먹는다");
}

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

**실행 결과**

text
초코 먹는다
초코 멍멍!

**메모:** 자식은 부모의 `Name`, `Eat()` 를 따로 작성하지 않고도 그대로 씁니다.

예제 2 — `BaseCall` : 자식 생성자 + `base.Method()`

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

var d = new Dog("초코", "푸들");
d.Introduce();
csharp
// Animal.cs
namespace CodingNow.Lecture.Oop08;

internal class Animal
{
    public string Name;

    public Animal(string name)
    {
        Name = name;
    }

    public void Introduce()
    {
        Console.WriteLine($"나는 동물 {Name}.");
    }
}

// Dog.cs
internal class Dog : Animal
{
    public string Breed;

    public Dog(string name, string breed) : base(name)   // 부모 생성자 먼저 호출
    {
        Breed = breed;
    }

    public new void Introduce()
    {
        base.Introduce();   // 부모 버전을 먼저 호출
        Console.WriteLine($"종은 {Breed}야.");
    }
}

**실행 결과**

text
나는 동물 초코.
종은 푸들이야.

**메모:** `base.Method()` 는 부모 클래스의 같은 이름 메서드를 그대로 부르는 문법입니다.

예제 3 — `VirtualOverride` : 다형성의 출발점

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

Animal a1 = new Animal();
Animal a2 = new Dog();
Animal a3 = new Puppy();

a1.Speak();
a2.Speak();
a3.Speak();
csharp
// Animal.cs
namespace CodingNow.Lecture.Oop08;

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

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

// Puppy.cs
internal class Puppy : Dog
{
    public sealed override void Speak() => Console.WriteLine("깨갱!");
}

**실행 결과**

text
...
멍멍!
깨갱!

**메모:** 변수의 타입은 모두 `Animal` 이지만, 실제 객체에 맞는 `Speak()` 가 호출됩니다. `Puppy.Speak()` 는 `sealed` 라서 그 아래에서는 더 못 바꿉니다.

예제 4 — `NewKeyword` : `override` vs `new` 차이

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

Animal a = new Cat();   // 변수 타입은 Animal, 실제 객체는 Cat
a.Speak();              // new 라서 부모 버전("Animal")이 호출됨

Cat c = new Cat();
c.Speak();              // 이건 Cat 버전 호출
csharp
// Animal.cs
namespace CodingNow.Lecture.Oop08;

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

// Cat.cs
internal class Cat : Animal
{
    // override 가 아니라 new — 단순히 같은 이름의 새 메서드를 정의한다.
    public new void Speak() => Console.WriteLine("Cat");
}

**실행 결과**

text
Animal
Cat

**메모:** `new` 는 "부모 메서드와 무관한 새 메서드"로 동작합니다. 다형성이 필요한 경우는 거의 항상 `override` 가 정답.

전체 예제 코드 (src/)

src/AnimalDog/Animal.cs

csharp
namespace CodingNow.Lecture.Oop08;

internal class Animal
{
    public string Name = "";

    public void Eat() => Console.WriteLine($"{Name} 먹는다");
}

src/AnimalDog/AnimalDog.csproj

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

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

</Project>

src/AnimalDog/Dog.cs

csharp
namespace CodingNow.Lecture.Oop08;

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

src/AnimalDog/Program.cs

csharp
using CodingNow.Lecture.Oop08;

var d = new Dog();
d.Name = "초코";

d.Eat();    // 부모(Animal)에게서 물려받은 메서드
d.Bark();   // 자식(Dog) 고유 메서드

src/BaseCall/Animal.cs

csharp
namespace CodingNow.Lecture.Oop08;

internal class Animal
{
    public string Name;

    public Animal(string name)
    {
        Name = name;
    }

    public void Introduce()
    {
        Console.WriteLine($"나는 동물 {Name}.");
    }
}

src/BaseCall/BaseCall.csproj

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

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

</Project>

src/BaseCall/Dog.cs

csharp
namespace CodingNow.Lecture.Oop08;

internal class Dog : Animal
{
    public string Breed;

    // 부모 생성자에 name 을 넘겨준다. 자식 생성자 본문보다 base(...) 가 먼저 실행됨.
    public Dog(string name, string breed) : base(name)
    {
        Breed = breed;
    }

    // 부모의 Introduce 를 가리고(new), 안에서 base.Introduce() 로 부모 버전을 호출한다.
    public new void Introduce()
    {
        base.Introduce();
        Console.WriteLine($"종은 {Breed}야.");
    }
}

src/BaseCall/Program.cs

csharp
using CodingNow.Lecture.Oop08;

var d = new Dog("초코", "푸들");
d.Introduce();

src/NewKeyword/Animal.cs

csharp
namespace CodingNow.Lecture.Oop08;

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

src/NewKeyword/Cat.cs

csharp
namespace CodingNow.Lecture.Oop08;

internal class Cat : Animal
{
    // override 가 아니라 new — 다형성이 작동하지 않는다.
    // 같은 이름의 "별도 메서드" 가 정의됐을 뿐.
    public new void Speak() => Console.WriteLine("Cat");
}

src/NewKeyword/NewKeyword.csproj

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

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

</Project>

src/NewKeyword/Program.cs

csharp
using CodingNow.Lecture.Oop08;

// new 키워드로 hide 한 경우: 변수 타입에 따라 어떤 메서드가 호출되는지 달라진다.
Animal a = new Cat();
a.Speak();      // Animal 타입으로 호출 → "Animal"

Cat c = new Cat();
c.Speak();      // Cat 타입으로 호출 → "Cat"

src/VirtualOverride/Animal.cs

csharp
namespace CodingNow.Lecture.Oop08;

internal class Animal
{
    // virtual: 자식이 override 로 재정의할 수 있다.
    public virtual void Speak() => Console.WriteLine("...");
}

src/VirtualOverride/Dog.cs

csharp
namespace CodingNow.Lecture.Oop08;

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

src/VirtualOverride/Program.cs

csharp
using CodingNow.Lecture.Oop08;

// 변수의 타입은 모두 Animal 이지만 실제 객체에 맞는 Speak() 가 호출된다.
Animal a1 = new Animal();
Animal a2 = new Dog();
Animal a3 = new Puppy();

a1.Speak();
a2.Speak();
a3.Speak();

src/VirtualOverride/Puppy.cs

csharp
namespace CodingNow.Lecture.Oop08;

internal class Puppy : Dog
{
    // sealed override: 더 이상 자식이 Speak 를 override 할 수 없게 막는다.
    public sealed override void Speak() => Console.WriteLine("깨갱!");
}

src/VirtualOverride/VirtualOverride.csproj

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

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

</Project>

자주 하는 실수

  1. 부모 생성자에 인자가 있는데 자식이 `: base(...)` 호출을 안 한다 — 컴파일 에러.
  2. `virtual` 안 붙은 메서드를 자식에서 `override` 하려고 한다 — 컴파일 에러.
  3. `override` 자리에 무심코 `new` 를 써서 다형성이 안 먹는다 — 디버깅 어려움.
  4. `private` 멤버를 자식이 직접 쓰려고 한다 — 막힘. 자식에게 보여주려면 `protected`.
  5. 단일 상속이라는 사실을 잊고 `class A : B, C` 처럼 부모 둘을 적는다 — 컴파일 에러 (다중 상속 X). 인터페이스는 여러 개 OK (10편).

정리

  • `class Child : Parent` 로 단일 상속. `base(...)`/`base.M()` 으로 부모를 호출.
  • `virtual` + `override` 가 다형성의 기본 골조. `new` 로 hide 는 가능하나 권장 X.
  • `sealed override` 로 추가 재정의 차단, `protected` 로 자식에게만 노출.

과제

**과제 - 08. 상속**

문제 1 — `Shape` → `Circle`/`Rectangle`

  • 프로젝트 폴더: `Homework01/`
  • 핵심 개념: 상속, `virtual` / `override`

요구사항

  • 부모 `Shape` 클래스에 `virtual double Area()` 메서드를 만든다(기본 반환 0).
  • 자식 `Circle` 은 반지름을 받아 `Area()` 를 `π × r²` 로 override.
  • 자식 `Rectangle` 은 가로/세로를 받아 `Area()` 를 `w × h` 로 override.
  • `Program.cs` 에서 `Circle`, `Rectangle` 객체를 만들고 `Area()` 출력.

예상 출력

text
원 넓이: 78.54
사각형 넓이: 12

힌트

  • `Math.PI` 를 그대로 쓰면 된다.
  • 반올림은 `Math.Round(value, 2)` 또는 형식 지정자 `:F2` 사용.

문제 2 — `Employee` → `Manager`

  • 프로젝트 폴더: `Homework02/`
  • 핵심 개념: 자식 생성자에서 `: base(...)` 호출

요구사항

  • `Employee` 클래스: 필드 `Name`(string), `Salary`(int). 생성자에서 둘 다 받음. `PrintInfo()` 로 정보 출력.
  • `Manager : Employee`: 추가 필드 `Bonus`(int). 생성자에서 부모의 두 값 + bonus 를 받아 `: base(name, salary)` 로 부모 생성자 호출.
  • `Manager.PrintInfo()` 는 부모의 PrintInfo 를 호출한 뒤 보너스도 출력.
  • `Program.cs` 에서 두 객체를 만들고 정보 출력.

예상 출력

text
이름: 영희, 급여: 3000000
이름: 철수, 급여: 5000000
보너스: 1000000

힌트

  • 자식 `PrintInfo` 안에서 `base.PrintInfo()` 호출.
  • 자식 메서드를 부모와 같은 이름으로 쓸 땐 `virtual`/`override` 또는 `new` 중 선택.

정답 확인

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

정답 (answer/)

homework/answer/Homework01/Circle.cs

csharp
namespace CodingNow.Lecture.Oop08;

internal class Circle : Shape
{
    public double Radius;

    public Circle(double radius)
    {
        Radius = radius;
    }

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

homework/answer/Homework01/Homework01.csproj

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

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

</Project>

homework/answer/Homework01/Program.cs

csharp
using CodingNow.Lecture.Oop08;

var circle = new Circle(5);
Console.WriteLine($"원 넓이: {circle.Area():F2}");

var rect = new Rectangle(3, 4);
Console.WriteLine($"사각형 넓이: {rect.Area()}");

homework/answer/Homework01/Rectangle.cs

csharp
namespace CodingNow.Lecture.Oop08;

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;
}

homework/answer/Homework01/Shape.cs

csharp
namespace CodingNow.Lecture.Oop08;

internal class Shape
{
    public virtual double Area() => 0;
}

homework/answer/Homework02/Employee.cs

csharp
namespace CodingNow.Lecture.Oop08;

internal class Employee
{
    public string Name;
    public int Salary;

    public Employee(string name, int salary)
    {
        Name = name;
        Salary = salary;
    }

    public virtual void PrintInfo()
    {
        Console.WriteLine($"이름: {Name}, 급여: {Salary}");
    }
}

homework/answer/Homework02/Homework02.csproj

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

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

</Project>

homework/answer/Homework02/Manager.cs

csharp
namespace CodingNow.Lecture.Oop08;

internal class Manager : Employee
{
    public int Bonus;

    // 부모 생성자에 name, salary 를 넘긴다.
    public Manager(string name, int salary, int bonus) : base(name, salary)
    {
        Bonus = bonus;
    }

    public override void PrintInfo()
    {
        base.PrintInfo();   // 부모 버전 호출
        Console.WriteLine($"보너스: {Bonus}");
    }
}

homework/answer/Homework02/Program.cs

csharp
using CodingNow.Lecture.Oop08;

var emp = new Employee("영희", 3000000);
emp.PrintInfo();

var mgr = new Manager("철수", 5000000, 1000000);
mgr.PrintInfo();

직접 해 보기

bash
cd src/AnimalDog
dotnet run

cd ../BaseCall
dotnet run

cd ../VirtualOverride
dotnet run

cd ../NewKeyword
dotnet run

다음 단원

[09_다형성](../09_다형성/) — 상속을 활용해 같은 호출이 다른 동작으로 이어지는 다형성을 본격적으로 다룹니다.

예제 코드 / 강의 자료

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

GitHub에서 보기 ↗