상속

이정현
∙∙조회 5

20261013 상속

개요

비슷한 클래스를 여러 개 만들다 보면 같은 코드가 반복됨. 상속은 이미 만든 클래스의 멤버를 물려받아 새 클래스를 만드는 문법임. 물려받은 쪽은 차이 나는 부분만 적으면 됨.

Mermaid
렌더링 중...

Dog와 Cat은 Name과 Eat()을 적지 않았지만 가지고 있음. Animal에서 물려받았기 때문임.

상속의 필요

같은 코드가 반복되는 자리

게임에 등장하는 캐릭터 셋을 클래스로 만들면 아래와 같음.

csharp
using System;

Warrior warrior = new Warrior();
warrior.Name = "전사";
warrior.ShowStatus();

Mage mage = new Mage();
mage.Name = "마법사";
mage.ShowStatus();

class Warrior
{
    public string Name;
    public int Health = 100;

    public void ShowStatus()
    {
        Console.WriteLine($"{Name} — HP {Health}");
    }

    public void Slash()
    {
        Console.WriteLine($"{Name}이(가) 칼을 휘두름");
    }
}

class Mage
{
    public string Name;
    public int Health = 100;

    public void ShowStatus()
    {
        Console.WriteLine($"{Name} — HP {Health}");
    }

    public void Fireball()
    {
        Console.WriteLine($"{Name}이(가) 불덩이를 던짐");
    }
}

Name, Health, ShowStatus()가 두 클래스에 똑같이 들어 있음. 캐릭터가 다섯 종류가 되면 다섯 벌이 되고, 체력 표시 타입을 바꾸려면 다섯 곳을 고쳐야 함.

공통부를 따로 떼기

공통 멤버만 담은 클래스를 하나 만들고, 나머지는 그것을 물려받으면 됨.

csharp
using System;

Warrior warrior = new Warrior();
warrior.Name = "전사";
warrior.ShowStatus();
warrior.Slash();

Mage mage = new Mage();
mage.Name = "마법사";
mage.ShowStatus();
mage.Fireball();

class Character
{
    public string Name;
    public int Health = 100;

    public void ShowStatus()
    {
        Console.WriteLine($"{Name} — HP {Health}");
    }
}

class Warrior : Character
{
    public void Slash()
    {
        Console.WriteLine($"{Name}이(가) 칼을 휘두름");
    }
}

class Mage : Character
{
    public void Fireball()
    {
        Console.WriteLine($"{Name}이(가) 불덩이를 던짐");
    }
}

Warrior에는 Name도 Health도 ShowStatus()도 없지만 전부 쓸 수 있음.

상속 문법

콜론으로 물려받기

클래스 이름 뒤에 콜론(:)과 물려받을 클래스 이름을 씀.

perl
class 자식클래스 : 부모클래스
{
    // 차이 나는 부분만
}

물려주는 쪽과 물려받는 쪽은 부르는 이름이 여러가지임. 문서마다 다른 말을 쓰므로 모두 알아 둘 필요가 있음.

물려주는 쪽물려받는 쪽
부모 클래스 (parent class)자식 클래스 (child class)
기반 클래스 (base class)파생 클래스 (derived class)
상위 클래스 (super class)하위 클래스 (sub class)

물려받는 것과 물려받지 않는 것

부모의 필드, 프로퍼티, 메서드는 모두 물려받음. 다만 생성자는 물려받지 않음.

멤버물려받는가
필드물려받음
프로퍼티물려받음
메서드물려받음
인덱서물려받음
생성자물려받지 않음

프로퍼티도 그대로 물려받으므로 set에 넣어 둔 검사가 자식에게도 적용됨.

물려받되 보이지 않는 것

private멤버도 자식 객체 안에 함께 만들어짐. 다만 자식 클래스의 코드에서는 이름으로 닿을 수 없음.

csharp
// 부모 클래스의 private에 자식 클래스가 접근 시도하는 예시
Child child = new Child();
child.Show();

class Parent
{
    private int secret = 42;

    public int GetSecret()
    {
        return secret;
    }
}

class Child : Parent
{
    public void Show()
    {
        // Console.WriteLine(secret);   // 닿지 못함
        Console.WriteLine($"부모가 내준 통로로: {GetSecret()}");
    }
}

secret을 자식 클래스에서 직접 쓰려 하면 컴파일되지 않음.

protected

private은 자식에게도 닫혀 있고 public은 아무에게나 열려 있음. 자식에게만 열고 바깥에는 닫는 중간 단계가 protected임.

csharp
// protected 사용 예시
Child child = new Child();

child.ShowPrivate();      // 부모 자신은 볼 수 있음
child.ShowAll();          // 자식이 볼 수 있는 것

// Console.WriteLine(child.protectedValue);   // 바깥에서는 닿지 못함
Console.WriteLine($"바깥에서 볼 수 있는 것: {child.PublicValue}");

class Parent
{
    private int privateValue = 1;
    protected int protectedValue = 2;
    public int PublicValue = 3;

    public void ShowPrivate()
    {
        Console.WriteLine($"private: {privateValue}");
    }
}

class Child : Parent
{
    public void ShowAll()
    {
        // Console.WriteLine(privateValue);   // 닿지 못함
        Console.WriteLine($"protected: {protectedValue}");
        Console.WriteLine($"public: {PublicValue}");
    }
}

바깥에서 protected멤버를 쓰려 하면 private과 같은 오류가 남.

접근 모디파이어클래스 안자식 클래스바깥
privateOXX
protectedOOX
publicOOO
protected필드는 자식이 직접 값을 바꿀 수 있다는 뜻이므로, 검사가 필요한 값이면 필드를 private으로 두고 protected프로퍼티나 메서드로 통로를 나타내는 편이 안전함.
  • 이 밖에 같은 프로젝트 안에서만 열리는 internal이라는 모디파이어도 있음. 프로젝트를 여럿으로 나눠 쓸 때 의미가 생기는 것이라 여기서는 다루지 않음.

생성자와 상속

생성자는 물려받지 않음

부모에 매개변수가 있는 생성자만 있으면 자식은 그것을 그냥 쓸 수 없음. 자식이 생성자를 따로 정의하고, 부모 목의 초기화는 부모 생성자에게 넘겨야 함.

base()로 부모 생성자 부르기

자식 생성자의 매개변수 목록 뒤에 : base(...)를 붙이면 부모 생성자가 먼저 실행됨.

csharp
// base 사용 예시
Student student = new Student("홍길동", 3);
class Person
{
    public string Name;

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

class Student : Person
{
    public int Grade;

    public Student(string name, int grade) : base(name)
    {
        Grade = grade;
    }
}

Person의 생성자 먼저 실행 후 Student의 생성자가 실행 된다. 자식이 부모 필드를 만지기 전에 부모 쪽이 준비를 끝내야 하기 때문임.

base()를 생략하면

base(...)나 this(...)를 적지 않은 인스턴스 생성자는 부모의 매개변수 없는 생성자를 암시적으로 호출함. 접근 가능한 해당 생성자가 없으면 인수를 받는 부모 생성자를 명시해야 함.

csharp
// base() 생략 예시
Dog dog = new Dog();

class Animal
{
    public Animal()
    {
        Console.WriteLine("Animal 생성자");
    }
}

class Dog : Animal
{
    public Dog()
    {
        Console.WriteLine("Dog 생성자");
    }
}

부모에 매개변수 없는 생성자가 없으면 이 자동 호출이 실패하므로, 그때는 base()를 반드시 적어야 함.

여러 단계의 상속

상속은 여러 단계로 이어질 수 있음. 생성자는 가장 위쪽 부모부터 차례로 실행됨.

csharp
// 여러 단계의 상속 예시
GrandChild g = new GrandChild();

Console.WriteLine("---");

class GrandParent
{
    public GrandParent()
    {
        Console.WriteLine("1) GrandParent");
    }
}

class Parent : GrandParent
{
    public Parent()
    {
        Console.WriteLine("2) Parent");
    }
}

class GrandChild : Parent
{
    public GrandChild()
    {
        Console.WriteLine("3) GrandChild");
    }
}

Mermaid
렌더링 중...
  • 물려받는 멤버는base.없이 이름만으로 씀.base를 반드시 적어야 하는 대표적인 경우가 여기서 다룬base()생성자 호출임

base()와 this()

생성자 앞에 붙일 수 있는 것은 둘 중 하나뿐임.this()는 같은 클래스의 다른 생성자를, base()는 부모 생성자를 부름.

csharp
// base, this 사용 예시
Monster a = new Monster("슬라임");
Console.WriteLine("---");
Monster b = new Monster("드래곤", 500);

class Creature
{
    public string Name;
    public int Health;

    public Creature(string name, int health)
    {
        Name = name;
        Health = health;
        Console.WriteLine($"Creature: {name} / {health}");
    }
}

class Monster : Creature
{
    public Monster(string name) : this(name, 100)
    {
        Console.WriteLine("Monster(이름 하나)");
    }

    public Monster(string name, int health) : base(name, health)
    {
        Console.WriteLine("Monster(이름과 체력)");
    }
}

Monster(string)은 this(name, 100)으로 같은 클래스의 다른 생성자를 부르고, 그 생성자가base()로 부모 생성자를 부름.

상속 계층 설계

is-a 관계

상속은 "자식은 부모의 한 종류다"가 성립할 때 씀. 영어로 "is-a"관계라고 부름.

관계성립 여부
개는 동물이다성립-class Dog : Animal
학생은 사람이다성립-class Student : Person
정사각형은 도형이다성립-class Square : Shape
자동차는 엔진이다성립하지 않음
주문은 상품이다성립하지 않음

말이 되지 않는데도 코드가 겹친다는 이유로 상속하면, 자식이 쓸 일이 없는 멤버를 물려받게 되고 부모를 고칠 때마다 관계없는 자식이 깨짐

상속 대신 조합

"은 이다"가 아니라"은 을 가진다"라면 상속이 아니라 필드로 가지는 것이 맞음. 이것을 조합이라고 부름.

csharp
// 조합 예시
Car car = new Car("소나타");

car.Start();
car.Drive();
car.Stop();

class Engine
{
    private bool running;

    public int Horsepower { get; set; } = 150;

    public void Ignite()
    {
        running = true;
        Console.WriteLine($"엔진 시동 ({Horsepower}마력)");
    }

    public void Shutdown()
    {
        running = false;
        Console.WriteLine("엔진 정지");
    }

    public bool IsRunning
    {
        get { return running; }
    }
}

class Car
{
    private Engine engine = new Engine();

    public string Name { get; }

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

    public void Start()
    {
        Console.WriteLine($"[{Name}]");
        engine.Ignite();
    }

    public void Drive()
    {
        if (engine.IsRunning)
        {
            Console.WriteLine("주행 중");
        }
        else
        {
            Console.WriteLine("시동이 걸려 있지 않음");
        }
    }

    public void Stop()
    {
        engine.Shutdown();
    }
}

Car는 Engine을 물려받지 않고 필드로 가짐. 자동차는 엔진의 한 종류가 아니라 엔진을 부품으로 가진 것이기 때문임. 기등을 여러 부품으로 쪼개 두면, 원하는 부품만 골라 조립해 서로 다른 객체를 만들 수 있음.

판단문법
자식은 부모의 한 종류다(is-a)상속-class Dog : Animal
이것은 저것을 가진다(has-a)조합-private Engine engine;

다중 상속은 되지 않음

C#에서 클래스는 부모를 하나만 가질 수 있음. 여러 부모의 기능이 동시에 필요하면 조합으로 풀거나, 구현을 담지 않고 약속만 모으는 다른 문법을 씀. 그 문법은 여기서 다루지 않음.

sealed클래스

더 이상 물려줄 일이 없는 클래스에는 sealed를 붙여 상속을 막을 수 있음.

csharp
// sealed 사용 예시
Detail d = new Detail();

sealed class Receipt
{
}

class Detail : Receipt
{
}

sealed 클래스를 물려받으려 하면 컴파일되지 않음.(컴파일에러 발생)

  • 계산 규칙이 바뀌면 안되는 클래스에 씀
  • 상속을 염두에 두지 않고 만들었다는 설계 의도를 코드로 남기는 수단이기도 함.

네임스페이스

이름이 겹치는 문제

프로그램이 커지면 서로 다른 목적의 클래스가 같은 이름을 원하게 됨. 캐릭터를 뜻하는 Player와 소리를 재생하는 Player가 한 프로젝트에 함께 필요할 수 있음. 네임스페이스는 타입을 묶어 이름의 범위를 나누는 상자임. 다른 상자에 들어있으면 이름이 같아도 됨.

csharp
// 네임스페이스 사용 예시
Character.Player hero = new Character.Player();
hero.Health = 100;
Console.WriteLine($"체력: {hero.Health}");

Audio.Player sound = new Audio.Player();
sound.Play("bgm.mp3");

namespace Character
{
    class Player
    {
        public int Health;
    }
}

namespace Audio
{
    class Player
    {
        public void Play(string file)
        {
            Console.WriteLine($"재생: {file}");
        }
    }
}

Character.Player처럼 네임스페이스를앞에 붙여 적은 이름을 정규화된 이름이라고 함.

using으로 짧게 쓰기

정규화된 이름을 매번 적으면 길어짐. using으로 네임스페이스를 가져오면 짧은 이름으로 쓸 수 있음. Console을 그냥 쓸 수 있었던 것도 같은 원리임. Console의 전체 이름은 System.Console이고, System이 가져와져 있기 때문에 짧게 쓸 수 있는 것임. 닷넷의 타입은 모두 네임스페이스 안에 들어있음.

네임스페이스들어 있는 것
SystemConsole, Math, String, Int32
System.TextStringBuilder
System.IO파일 입출력
System.Collections.Generic여러 값을 담는 자료 구조

계층 구조

네임스페이스는 점으로 이어 계층을 만듦. 폴더 구조와 같은 방식임. 이름은 회사명.제품명.기능형태로 짓고PascalCase를 씀. 폴더 구조와 네임스페이스를 맞춰 두면 파일을 찾기 쉬워짐.

arduino
MyGame/
├── Characters/
│   ├── Hero.cs        → namespace MyGame.Characters
│   └── Enemy.cs       → namespace MyGame.Characters
└── Items/
    └── Sword.cs       → namespace MyGame.Items

별칭

긴 네임스페이스나 겹치는 이름은 별칭을 붙여 줄일 수 있음.

csharp
// 별칭 사용 예시
using Hero = Game.Characters.Player;
using Speaker = Game.Audio.Player;

Hero hero = new Hero();
hero.Show();

Speaker speaker = new Speaker();
speaker.Show();

namespace Game.Characters
{
    class Player
    {
        public void Show()
        {
            Console.WriteLine("캐릭터 Player");
        }
    }
}

namespace Game.Audio
{
    class Player
    {
        public void Show()
        {
            Console.WriteLine("오디오 Player");
        }
    }
}

using static

정적 멤버만 쓰는 클래스는using static으로 클래스 이름까지 생략할 수 있음.

csharp
// using static 사용 예시
using static System.Math;

double result = Sqrt(16);
Console.WriteLine($"제곱근: {result}");
Console.WriteLine($"최댓값: {Max(10, 20)}");
Console.WriteLine($"절댓값: {Abs(-7)}");
Console.WriteLine($"원주율: {PI:F4}");

  • 주의사항: 클래스 이름이 사라지면 그 함수가 어디서 왔는지 읽는 사람이 알기 어려워짐. Math처럼 뜻이 분명한 것에만 쓰는 편이 나음.