상속은 물려받아 덧붙이는 것이다
Esc

    상속은 물려받아 덧붙이는 것이다

    –C#기초

    게임에 전사와 마법사를 만든다고 해 봅시다.

    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 가 똑같습니다. 다른 것은 공격 방법뿐입니다.

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

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

    상속은 이렇게 생겼다

    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 를 물려받았습니다.

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

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

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

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

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

    name, hp TakeDamage, IsAlive Slash name, hp TakeDamage, IsAlive Fireball Character 에서 물려받은 부분 Warrior 가 더한 것 Character 에서 물려받은 부분 Mage 가 더한 것 Warrior 객체 Mage 객체
    물려받은 부분은 객체 안에 실제로 들어 있다.

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

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

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

    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 자리에 넣을 수 있습니다.

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

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

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

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

    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 에 있는 것만 쓸 수 있습니다.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    부모는 하나뿐이다

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

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

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

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

    상속이 늘 답은 아니다

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

    • 전사는 캐릭터이다 → 상속
    • 자동차는 엔진을 가진다 → 상속이 아니라 필드로 가짐
    class Car
    {
        private Engine engine;      // 이게 맞습니다
    }
    
    class Car : Engine              // 자동차는 엔진이 아닙니다

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

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

    정리

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