08. 상속
이미 있는 클래스의 멤버를 물려받아 새로운 클래스를 만드는 것이 상속입니다. base·virtual·override·sealed·protected 를 익혀 'is-a' 관계를 코드로 표현합니다.
이 강의에서 배우는 것
- 1`: base` 표기로 클래스를 상속할 수 있다
- 2자식 생성자에서 `: base(...)` 로 부모 생성자를 호출할 수 있다
- 3`virtual` / `override` / `sealed override` 의 차이를 안다
- 4`new` 키워드로 메서드를 가린(hide) 결과가 어떻게 다른지 안다
소개
이미 잘 만들어진 클래스가 있을 때, 비슷한 클래스를 처음부터 다시 짜는 건 낭비입니다. **상속(inheritance)** 으로 부모 클래스의 멤버를 그대로 물려받고 필요한 부분만 덧붙이거나 바꿀 수 있습니다.
핵심 개념
1) `: base` 표기
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(...)` 호출
부모에 매개변수 있는 생성자가 있다면 자식이 직접 호출해 줘야 합니다.
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` 로 다시 정의(재정의)할 수 있습니다.
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 할 수 없다"는 표시.
class Puppy : Dog
{
public sealed override void Speak() => Console.WriteLine("깨갱!");
}
// Puppy 를 상속한 클래스는 Speak 를 또 override 못 함5) `new` 키워드로 hide
`override` 가 아니라 `new` 를 쓰면 "부모 메서드를 가린다" 는 의미가 됩니다. 다형성이 작동하지 않으니 주의.
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`
부모의 멤버에 자식만 접근하게 하고 싶을 때 쓰는 접근 제한자.
class Animal { protected int HungerLevel = 0; }
class Dog : Animal { public void Feed() => HungerLevel--; }핵심 예제
예제 1 — `AnimalDog` : 단순 상속
// Program.cs
using CodingNow.Lecture.Oop08;
var d = new Dog();
d.Name = "초코";
d.Eat(); // 부모에게서 물려받음
d.Bark(); // 자식 고유// 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} 멍멍!");
}**실행 결과**
초코 먹는다
초코 멍멍!**메모:** 자식은 부모의 `Name`, `Eat()` 를 따로 작성하지 않고도 그대로 씁니다.
예제 2 — `BaseCall` : 자식 생성자 + `base.Method()`
// Program.cs
using CodingNow.Lecture.Oop08;
var d = new Dog("초코", "푸들");
d.Introduce();// 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}야.");
}
}**실행 결과**
나는 동물 초코.
종은 푸들이야.**메모:** `base.Method()` 는 부모 클래스의 같은 이름 메서드를 그대로 부르는 문법입니다.
예제 3 — `VirtualOverride` : 다형성의 출발점
// Program.cs
using CodingNow.Lecture.Oop08;
Animal a1 = new Animal();
Animal a2 = new Dog();
Animal a3 = new Puppy();
a1.Speak();
a2.Speak();
a3.Speak();// 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("깨갱!");
}**실행 결과**
...
멍멍!
깨갱!**메모:** 변수의 타입은 모두 `Animal` 이지만, 실제 객체에 맞는 `Speak()` 가 호출됩니다. `Puppy.Speak()` 는 `sealed` 라서 그 아래에서는 더 못 바꿉니다.
예제 4 — `NewKeyword` : `override` vs `new` 차이
// Program.cs
using CodingNow.Lecture.Oop08;
Animal a = new Cat(); // 변수 타입은 Animal, 실제 객체는 Cat
a.Speak(); // new 라서 부모 버전("Animal")이 호출됨
Cat c = new Cat();
c.Speak(); // 이건 Cat 버전 호출// 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");
}**실행 결과**
Animal
Cat**메모:** `new` 는 "부모 메서드와 무관한 새 메서드"로 동작합니다. 다형성이 필요한 경우는 거의 항상 `override` 가 정답.
전체 예제 코드 (src/)
src/AnimalDog/Animal.cs
namespace CodingNow.Lecture.Oop08;
internal class Animal
{
public string Name = "";
public void Eat() => Console.WriteLine($"{Name} 먹는다");
}
src/AnimalDog/AnimalDog.csproj
<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
namespace CodingNow.Lecture.Oop08;
internal class Dog : Animal
{
public void Bark() => Console.WriteLine($"{Name} 멍멍!");
}
src/AnimalDog/Program.cs
using CodingNow.Lecture.Oop08;
var d = new Dog();
d.Name = "초코";
d.Eat(); // 부모(Animal)에게서 물려받은 메서드
d.Bark(); // 자식(Dog) 고유 메서드
src/BaseCall/Animal.cs
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
<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
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
using CodingNow.Lecture.Oop08;
var d = new Dog("초코", "푸들");
d.Introduce();
src/NewKeyword/Animal.cs
namespace CodingNow.Lecture.Oop08;
internal class Animal
{
public virtual void Speak() => Console.WriteLine("Animal");
}
src/NewKeyword/Cat.cs
namespace CodingNow.Lecture.Oop08;
internal class Cat : Animal
{
// override 가 아니라 new — 다형성이 작동하지 않는다.
// 같은 이름의 "별도 메서드" 가 정의됐을 뿐.
public new void Speak() => Console.WriteLine("Cat");
}
src/NewKeyword/NewKeyword.csproj
<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
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
namespace CodingNow.Lecture.Oop08;
internal class Animal
{
// virtual: 자식이 override 로 재정의할 수 있다.
public virtual void Speak() => Console.WriteLine("...");
}
src/VirtualOverride/Dog.cs
namespace CodingNow.Lecture.Oop08;
internal class Dog : Animal
{
public override void Speak() => Console.WriteLine("멍멍!");
}
src/VirtualOverride/Program.cs
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
namespace CodingNow.Lecture.Oop08;
internal class Puppy : Dog
{
// sealed override: 더 이상 자식이 Speak 를 override 할 수 없게 막는다.
public sealed override void Speak() => Console.WriteLine("깨갱!");
}
src/VirtualOverride/VirtualOverride.csproj
<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>
자주 하는 실수
- 부모 생성자에 인자가 있는데 자식이 `: base(...)` 호출을 안 한다 — 컴파일 에러.
- `virtual` 안 붙은 메서드를 자식에서 `override` 하려고 한다 — 컴파일 에러.
- `override` 자리에 무심코 `new` 를 써서 다형성이 안 먹는다 — 디버깅 어려움.
- `private` 멤버를 자식이 직접 쓰려고 한다 — 막힘. 자식에게 보여주려면 `protected`.
- 단일 상속이라는 사실을 잊고 `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()` 출력.
예상 출력
원 넓이: 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` 에서 두 객체를 만들고 정보 출력.
예상 출력
이름: 영희, 급여: 3000000
이름: 철수, 급여: 5000000
보너스: 1000000힌트
- 자식 `PrintInfo` 안에서 `base.PrintInfo()` 호출.
- 자식 메서드를 부모와 같은 이름으로 쓸 땐 `virtual`/`override` 또는 `new` 중 선택.
정답 확인
직접 풀어 본 후 [`answer/`](./answer/) 폴더의 정답과 비교해 보세요.
정답 (answer/)
homework/answer/Homework01/Circle.cs
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
<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
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
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
namespace CodingNow.Lecture.Oop08;
internal class Shape
{
public virtual double Area() => 0;
}
homework/answer/Homework02/Employee.cs
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
<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
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
using CodingNow.Lecture.Oop08;
var emp = new Employee("영희", 3000000);
emp.PrintInfo();
var mgr = new Manager("철수", 5000000, 1000000);
mgr.PrintInfo();
직접 해 보기
cd src/AnimalDog
dotnet run
cd ../BaseCall
dotnet run
cd ../VirtualOverride
dotnet run
cd ../NewKeyword
dotnet run다음 단원
[09_다형성](../09_다형성/) — 상속을 활용해 같은 호출이 다른 동작으로 이어지는 다형성을 본격적으로 다룹니다.