---
title: "상속은 물려받아 덧붙이는 것이다"
description: "겹치는 부분을 부모에 두고 다른 것만 자식에 적습니다. 자식을 부모로 보는 것은 언제나 안전합니다."
author: "유니얼"
pubDate: 2026-09-09
category: "programming/csharp/basics"
tags: ["C#", "기초"]
canonical: https://blog.unialgames.com/posts/csharp-inheritance/
---
게임에 전사와 마법사를 만든다고 해 봅시다.

```csharp
class Warrior
{
    private string name;
    private int hp;

    public void TakeDamage(int amount) { ... }
    public bool IsAlive() { ... }
    public void Slash() { ... }
}

class Mage
{
    private string name;
    private int hp;

    public void TakeDamage(int amount) { ... }
    public bool IsAlive() { ... }
    public void Fireball() { ... }
}
```

`name`, `hp`, `TakeDamage`, `IsAlive` 가 똑같습니다. 다른 것은 공격 방법뿐입니다.

앞에서 같은 코드가 두 번 보이면 메서드로 묶을 때라고 했습니다.
여기는 코드 덩어리가 아니라 **클래스가 통째로** 겹칩니다.

> **겹치는 부분을 부모에 두고, 자식은 다른 것만 적습니다.**

## 상속은 이렇게 생겼다

```csharp
class Character
{
    protected string name;
    protected int hp;

    public void TakeDamage(int amount) { ... }
    public bool IsAlive() { ... }
}

class Warrior : Character
{
    public void Slash() { ... }
}

class Mage : Character
{
    public void Fireball() { ... }
}
```

콜론 뒤에 적은 것이 부모입니다. `Warrior` 는 `Character` 를 물려받았습니다.

```csharp
Warrior w = new Warrior();
w.TakeDamage(30);      // 물려받은 것
w.Slash();             // 자기 것
```

`Warrior` 안에 `TakeDamage` 를 안 적었는데 씁니다. 물려받았으니까요.

`protected` 가 여기서 쓰입니다. `private` 이면 자식도 못 씁니다.
부모의 필드를 자식에서 써야 하면 `protected` 입니다.

## 물려받은 것은 사라지지 않는다

상속을 "복사해서 붙인다" 로 생각하면 안 됩니다.
`Warrior` 객체 안에는 `Character` 부분이 **실제로 들어 있습니다.**

<figure class="diagram">
<svg viewBox="0 0 680 200" role="img" aria-label="Character 를 물려받은 Warrior 객체 안에 Character 부분과 Warrior 부분이 함께 들어 있는 그림">
<g fill="var(--bg)" stroke="var(--border)">
<rect x="24" y="30" width="230" height="150" rx="8"></rect>
<rect x="380" y="30" width="230" height="150" rx="8"></rect>
</g>
<rect x="40" y="52" width="198" height="66" rx="6" fill="var(--accent-soft)" stroke="var(--accent)"></rect>
<rect x="396" y="52" width="198" height="66" rx="6" fill="var(--accent-soft)" stroke="var(--accent)"></rect>
<g font-family="var(--font-mono)" font-size="12">
<text x="54" y="74" fill="var(--accent)">name, hp</text>
<text x="54" y="96" fill="var(--accent)">TakeDamage, IsAlive</text>
<text x="54" y="150" fill="var(--text)">Slash</text>
<text x="410" y="74" fill="var(--accent)">name, hp</text>
<text x="410" y="96" fill="var(--accent)">TakeDamage, IsAlive</text>
<text x="410" y="150" fill="var(--text)">Fireball</text>
</g>
<g font-size="12" fill="var(--text-muted)">
<text x="40" y="46">Character 에서 물려받은 부분</text>
<text x="40" y="172">Warrior 가 더한 것</text>
<text x="396" y="46">Character 에서 물려받은 부분</text>
<text x="396" y="172">Mage 가 더한 것</text>
</g>
<text x="24" y="20" font-size="13" fill="var(--text)">Warrior 객체</text>
<text x="380" y="20" font-size="13" fill="var(--text)">Mage 객체</text>
</svg>
<figcaption>물려받은 부분은 객체 안에 실제로 들어 있다.</figcaption>
</figure>

그래서 `Warrior` 는 `Character` **이기도 합니다.** 이 사실이 다음 이야기의 전부입니다.

## 부모의 생성자가 먼저 실행된다

자식 객체를 만들면 부모 부분부터 만들어집니다.

```csharp
class Character
{
    public Character(string name)
    {
        this.name = name;
    }
}

class Warrior : Character
{
    public Warrior(string name) : base(name)
    {
    }
}
```

`: base(name)` 은 **부모 생성자를 먼저 부른다**는 뜻입니다.
앞에서 본 `: this(...)` 와 같은 자리인데, 가리키는 곳이 부모입니다.

부모에 매개변수 없는 생성자가 있으면 `base(...)` 를 안 적어도 그게 자동으로 불립니다.
없으면 반드시 적어야 합니다. 부모 부분을 만들지 않고 자식만 만들 수는 없습니다.

## 위로 올라가는 것은 언제나 안전하다

`Warrior` 는 `Character` 이기도 하니, `Character` 자리에 넣을 수 있습니다.

```csharp
Character c = new Warrior("전사");
```

이걸 업캐스팅이라고 합니다. **언제나 됩니다.** 아무것도 안 적어도 됩니다.

전사는 캐릭터의 조건을 전부 갖추고 있습니다. `name` 도 `hp` 도 `TakeDamage` 도 있습니다.
캐릭터로 다뤄도 부족한 것이 없습니다.

이게 되면 목록으로 묶을 수 있습니다.

```csharp
List<Character> party = new List<Character>();
party.Add(new Warrior("전사"));
party.Add(new Mage("마법사"));

for (int i = 0; i < party.Count; i++)
{
    party[i].TakeDamage(10);
}
```

전사와 마법사를 한 목록에 담고 같은 코드로 다룹니다.
**부모 타입은 여러 자식을 한 이름으로 부르는 방법**입니다.

대신 `Character` 로 보고 있으니 `Character` 에 있는 것만 쓸 수 있습니다.

```csharp
Character c = new Warrior("전사");
c.Slash();      // 컴파일 에러
```

담긴 것은 전사가 맞습니다. 그런데 **보고 있는 타입**이 `Character` 입니다.
컴파일러는 그 자리에 마법사가 들어올 수도 있다고 봅니다.

## 내려가는 것은 확인해야 한다

다시 `Warrior` 로 보려면 캐스팅합니다. 다운캐스팅입니다.

```csharp
Character c = new Warrior("전사");
Warrior w = (Warrior)c;
w.Slash();
```

앞에서 캐스팅은 "내가 책임진다" 는 표시라고 했습니다. 여기서도 같습니다.
틀리면 실행 중에 터집니다.

```csharp
Character c = new Mage("마법사");
Warrior w = (Warrior)c;      // InvalidCastException
```

그래서 확인하고 내려갑니다. 두 가지 방법이 있습니다.

```csharp
if (c is Warrior)
{
    Warrior w = (Warrior)c;
    w.Slash();
}
```

```csharp
Warrior w = c as Warrior;
if (w != null)
{
    w.Slash();
}
```

`is` 는 맞는지 묻고, `as` 는 시도해 보고 아니면 `null` 을 줍니다.
`as` 는 실패해도 안 터집니다. 대신 `null` 검사를 해야 합니다.

둘을 합친 짧은 문법도 있습니다.

```csharp
if (c is Warrior w)
{
    w.Slash();
}
```

맞으면 `w` 에 담아 주고 참이 됩니다. 요즘은 이걸 많이 씁니다.

**다만 다운캐스팅이 자주 나오면 설계를 의심해야 합니다.**
`is` 로 타입을 물어보고 갈라지는 코드가 늘어난다면,
타입마다 다르게 동작하도록 만드는 쪽이 대부분 더 낫습니다.

## 부모는 하나뿐이다

C# 에서 클래스는 부모를 하나만 가질 수 있습니다.

```csharp
class Duck : Animal, Flyable      // 컴파일 에러
```

부모가 둘이면 양쪽에 같은 이름의 메서드가 있을 때 어느 쪽인지 정할 수가 없습니다.
그 복잡함을 아예 막았습니다.

여러 갈래가 필요하면 인터페이스를 씁니다. 뒤에서 다룹니다.

## 상속이 늘 답은 아니다

상속은 "무엇인가" 관계일 때만 씁니다.

- 전사는 캐릭터**이다** → 상속
- 자동차는 엔진을 **가진다** → 상속이 아니라 필드로 가짐

```csharp
class Car
{
    private Engine engine;      // 이게 맞습니다
}

class Car : Engine              // 자동차는 엔진이 아닙니다
```

코드를 재사용하고 싶다는 이유만으로 상속하면 이상해집니다.
문장으로 읽어 보면 대체로 답이 나옵니다.

상속이 깊어지는 것도 조심해야 합니다.
부모의 부모의 부모까지 올라가야 뭐가 있는지 알 수 있으면 읽기 어려워집니다.

## 정리

- 콜론 뒤에 부모를 적으면 그 클래스의 것을 물려받습니다.
- 물려받은 부분은 자식 객체 안에 실제로 들어 있습니다.
- `protected` 는 자식까지 쓸 수 있는 표시입니다. `private` 은 자식도 못 씁니다.
- 자식을 만들면 부모 생성자가 먼저 실행됩니다. `: base(...)` 로 넘깁니다.
- 자식을 부모 타입으로 보는 것은 언제나 안전합니다. 여러 자식을 한 목록에 담을 수 있습니다.
- 부모로 보고 있으면 부모에 있는 것만 쓸 수 있습니다.
- 다시 내려가려면 캐스팅합니다. `is` 나 `as` 로 확인하고 내려갑니다.
- 부모는 하나뿐입니다. 여러 갈래가 필요하면 인터페이스를 씁니다.
- "무엇인가" 일 때만 상속합니다. "가진다" 면 필드로 둡니다.