무엇인가면 추상 클래스, 무엇을 할 수 있으면 인터페이스
Esc

    무엇인가면 추상 클래스, 무엇을 할 수 있으면 인터페이스

    –C#기초

    앞 글의 Character 에 이상한 데가 있었습니다.

    class Character
    {
        public virtual void Attack()
        {
            Console.WriteLine("공격");
        }
    }

    Character 의 기본 공격이 “공격” 을 출력합니다. 이게 뭘까요. 전사도 마법사도 아닌 그냥 캐릭터는 어떻게 공격하는지, 답이 없는 질문입니다.

    두 가지 문제가 있습니다.

    Character c = new Character("아무개");    // 이게 만들어집니다

    직업 없는 캐릭터가 만들어집니다. 그리고 새 직업을 만들면서 Attack 갈아 끼우는 것을 잊으면 조용히 “공격” 이 출력됩니다. 아무도 안 막아 줍니다.

    채워야 하는 자리는 비워 두고, 안 채우면 컴파일이 안 되게 하면 됩니다.

    추상 클래스는 반쯤 만든 틀이다

    abstract class Character
    {
        protected string name;
        protected int hp;
    
        public void TakeDamage(int amount) { ... }    // 공통, 구현해 둔다
        public bool IsAlive() { ... }
    
        public abstract void Attack();                // 자식마다 다르다, 비워 둔다
    }

    두 가지가 달라졌습니다.

    class 앞에 abstract 를 붙였습니다. 이러면 이 클래스로는 객체를 만들 수 없습니다.

    Character c = new Character();      // 컴파일 에러

    붕어빵 틀인데 반만 만들어진 틀입니다. 그대로는 못 찍습니다.

    Attack 에도 abstract 를 붙이고 중괄호 없이 세미콜론으로 끝냈습니다. “이런 메서드가 있어야 한다” 만 적고 내용은 안 적었습니다.

    물려받은 자식은 반드시 채워야 합니다.

    class Warrior : Character
    {
        public override void Attack()
        {
            Console.WriteLine("칼로 벤다");
        }
    }

    안 채우면 컴파일 에러입니다. 잊을 수가 없습니다. virtual 은 “갈아 끼워도 된다” 였고, abstract 는 “반드시 채워라” 입니다.

    virtual abstract
    부모에 내용이 있다 없다
    자식이 갈아 끼워도 되고 안 해도 된다 반드시 채워야 한다
    그 클래스로 객체를 만들 수 있다 만들 수 없다

    인터페이스는 약속 목록이다

    추상 클래스는 구현을 물려줍니다. 그런데 물려줄 것이 없고 할 수 있어야 하는 것만 정하고 싶을 때가 있습니다.

    새가 날고 비행기도 납니다. 둘은 아무 관계가 없습니다. 공통 부모를 만들면 FlyingThing 같은 이상한 것이 생깁니다.

    interface IFlyable
    {
        void Fly();
    }
    
    class Bird : Animal, IFlyable
    {
        public void Fly() { Console.WriteLine("날개짓"); }
    }
    
    class Airplane : Vehicle, IFlyable
    {
        public void Fly() { Console.WriteLine("엔진 추력"); }
    }

    인터페이스에는 무엇을 할 수 있어야 하는지만 적습니다. 어떻게 하는지는 안 적습니다. 필드도 없습니다. 상태를 안 가집니다.

    이름 앞에 I 를 붙이는 것이 C# 의 관습입니다. IFlyable, IComparable 처럼요.

    부모는 하나뿐이지만 인터페이스는 여러 개 됩니다.

    class Duck : Animal, IFlyable, ISwimmable
    {
        public void Fly() { ... }
        public void Swim() { ... }
    }

    앞에서 부모를 하나로 제한한 이유가 물려받는 내용이 겹치기 때문이라고 했습니다. 인터페이스는 물려줄 내용이 없으니 겹칠 것도 없습니다. 그래서 여러 개가 됩니다.

    인터페이스도 타입이다

    인터페이스로도 여러 종류를 한 이름으로 부를 수 있습니다.

    List<IFlyable> things = new List<IFlyable>();
    things.Add(new Bird());
    things.Add(new Airplane());
    
    for (int i = 0; i < things.Count; i++)
    {
        things[i].Fly();
    }

    새와 비행기는 부모가 완전히 다른데 한 목록에 담겼습니다. “날 수 있다” 는 공통점 하나로 묶은 것입니다.

    상속으로는 이게 안 됩니다. 상속은 계보를 따라가야 하는데, “날 수 있다” 는 계보와 상관없이 여기저기 흩어져 있습니다.

    무엇으로 고르나

    문장으로 읽어 보면 대체로 답이 나옵니다.

    • 전사는 캐릭터이다 → 추상 클래스
    • 오리는 날 수 있다 → 인터페이스
    추상 클래스 인터페이스
    관계 무엇인가 (is a) 무엇을 할 수 있다 (can do)
    구현을 물려줄 수 있다 못 한다
    필드를 가질 수 있다 못 가진다
    몇 개나 하나만 여러 개

    공통 코드를 물려주려면 추상 클래스, 능력만 약속하려면 인터페이스입니다.

    둘을 같이 쓰는 것이 보통입니다. Character 는 추상 클래스로 두고 공통을 물려주면서, 날 수 있는 직업만 IFlyable 을 붙이는 식입니다.

    class Dragoon : Character, IFlyable
    {
        public override void Attack() { ... }
        public void Fly() { ... }
    }

    우리가 이미 쓰던 것들

    List 를 만들 때 이렇게 쓸 수 있습니다.

    IList<int> nums = new List<int>();

    List 는 IList 를 구현한 클래스입니다. IList 로 받아 두면 나중에 다른 구현으로 바꿔도 쓰는 쪽이 안 깨집니다.

    foreach 로 돌 수 있는 것들은 전부 IEnumerable 을 구현하고 있습니다. 배열도, List 도, Dictionary 도요. 그래서 같은 문법으로 돌 수 있습니다. 그 문법은 뒤에서 다룹니다.

    약속을 정해 두면 서로 모르는 것들이 같은 방식으로 다뤄집니다. 인터페이스가 하는 일이 이것입니다.

    여기까지 정리하면

    객체지향이라고 부르는 것의 네 가지를 다 봤습니다.

    무엇 어디서 봤나
    캡슐화 안을 감추고 창구만 연다 private 과 메서드
    상속 겹치는 것을 물려받는다 Character 와 자식들
    다형성 같은 호출이 다르게 동작한다 virtual 과 override
    추상화 공통점만 남기고 나머지를 지운다 abstract 와 인터페이스

    네 가지가 따로 있는 것이 아닙니다. 무엇이 같고 무엇이 다른가를 나누는 일의 다른 이름들입니다.

    같은 것은 부모에 두고, 다른 것은 자식에 두고, 밖에 안 보여도 되는 것은 감춥니다. 설계란 이 나누기를 하는 일이고, 문법은 그걸 적는 방법입니다.

    처음부터 잘 나눌 수는 없습니다. 고치기 어려워졌다고 느낄 때 다시 나누면 됩니다. 그때 꺼내 볼 기준이 하나 있다면, 한 클래스가 한 가지 일만 하게 두는 것입니다. 바꿔야 할 이유가 두 개 이상이면 나눌 때가 된 것입니다.

    정리

    • abstract 클래스는 그 자체로 객체를 만들 수 없습니다. 반쯤 만든 틀입니다.
    • abstract 메서드는 내용 없이 선언만 합니다. 자식이 반드시 채워야 합니다.
    • virtual 은 갈아 끼워도 된다는 허락, abstract 는 반드시 채우라는 요구입니다.
    • 인터페이스는 무엇을 할 수 있어야 하는지만 적습니다. 구현도 필드도 없습니다.
    • 부모는 하나지만 인터페이스는 여러 개 구현할 수 있습니다.
    • 인터페이스도 타입입니다. 계보가 다른 것들을 능력 하나로 묶을 수 있습니다.
    • “무엇인가” 면 추상 클래스, “무엇을 할 수 있다” 면 인터페이스입니다.
    • 캡슐화, 상속, 다형성, 추상화는 전부 무엇이 같고 다른가를 나누는 일입니다.