티스토리 뷰

 저번 포스팅 때 설명을 못한 상속에서의 유용한 기능이 한 가지 더 존재한다. 먼저 가상적인 상황을 부여해보면, 힐러가 객체를 부여받아 힐을 해주는 기능을 구현했다고 가정하여 보자. 아래가 해당 스크립트이다.

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

// 상속 문법을 사용한 캐릭터 클래스들

// 부모 클래스를 이용한 상속 코드 구현 순서 (일단 중복 해결에 대한 기준으로만)
// 1. 중복되는 멤버 변수와 메소드를 공통(부모 클래스)으로 이동시킴
// 2. 중복된 내용 제거된(부모 클래스로 옮겨진) 자식 클래스에서 부모 클래스를 상속받는 형식의 명세로 변경
// -> public 자식 클래스 : 부모 클래스 { ...중복이 제거되고 남은 코드들... }
// 3. 

// CharacterEx 클래스는 추상메소드(Die)를 가진 클래스로 절대로 객체로 생성되면 안되므로 추상 클래스로 정의해야 함.
public abstract class CharacterEx // 부모 클래스
{
    // * protected : 자식 클래스에서 부모 클래스의 멤버변수(속성) 및 메소드를 접근 허용하고 싶을 때 사용하는 접근 지정자
    // 이름
    protected string name;
    // 이동속도
    protected float speed;
    // 공격력
    protected int damage;
    // 체력
    protected int hp;

    public CharacterEx(string name, float speed, int damage, int hp = 1) // 부모 클래스 생성자
    {
        this.name = name;
        this.speed = speed;
        this.damage = damage;
        this.hp = hp;
    }

    // 자식 클래스에서 Move 메소드를 확장정의/수정정의/재정의할 수 있도록 virtual 키워드를 붙여줘야 함.
    // * 목적 : 부모 클래스로부터 물려받은 메소드가 자식 클래스의 역할/기능에 맞지 않을 경우
    // 이동 메소드
    public virtual void Move()
    {
        Debug.Log("[" + name + "] 캐릭터가 " + speed + "속도로 발바닥에 땀이 나도록 뛰면서 이동을 수행합니다.");
    }

    // 공격 기능
    public virtual void Attack()
    {
        Debug.Log("[" + name + "] 캐릭터가 " + damage + "로 공격을 수행합니다.");
    }

    // 피격 메소드
    public void Hit(int damage)
    {
        hp -= damage;
        Debug.Log("[" + name + "] 캐릭터가 " + damage + "만큼의 공격을 받아 체력이 " + hp + "이 되었습니다.");
        if (hp < 0)
        {
            hp = 0;

            // 부모 클래스에서 메소드를 호출할 때 현재 객체가 자식 객체로 생성됐을 경우에는 자식의 오버라이드된 메소드가 호출된다.
            Die();
        }
    }

    // 외부에서 호출되는 것을 막기위해 protected
    // * 내용이 없는 메소드를 만드는게 의미가 있는가?
    //protected virtual void Die()
    //{
    //    //호출하면 안되는 메소드 자식 클래스들은 각자 구현할 것
    //}

    // abstract 메소드는 추상 메소드로 자식 클래스에서는 반드시 Die 메소드를 재정의(override)해야 함
    protected abstract void Die();

    // 자식 클래스에서 재정의가 가능한 체력 보충 메소드 정의
    public virtual void AddHp(int hp)
    {
        this.hp += hp;

        Debug.Log("[" + name + "] 가 힐러로부터 " + hp + "만큼의 체력을 보충하였습니다.");
    }
}

// NPC 
public class NPCEx : CharacterEx // NPCEx가 CharacterEx 클래스를 상속받음
{
    // 대화내용
    private string talkMessage;

    // * NPC가 생성될 때 넘겨받은 생성자 매개변수의 값을 부모의 생성자에게 넘겨줌
    // -> 문법 : 자식 생성자(매개변수를 ...) : base(부모 생성자 초기화 전달값)
    public NPCEx(string name, float speed, int damage, string talkMessage) : base(name, speed, damage)
    {
        this.talkMessage = talkMessage;
    }

    // 대화 수행 메소드
    public void Talk()
    {
        Debug.Log("[" + name + "] 캐릭터가 " + talkMessage + "로 유저들에게 말을 겁니다.");
    }

    protected override void Die()
    {
        Debug.Log("꼴까닥 소리를 내며 쓰러져서 사라집니다.");
    }
}

public class Healer : CharacterEx
{
    private int healPoint; // 체력 보충 수치

    public Healer(string name, float speed, int damage, int healPoint, int hp) : base(name, speed, damage, hp)
    {
        this.healPoint = healPoint;
    }

    protected override void Die()
    {
        Debug.Log("한 생명이라도 더 구하고 떠나야 하는데 ㅠ");
    }

    // 메소드 오버로드를 통해 체력 보충 메소드를 추가함
    public void Healing(KnightEx knight)
    {
        knight.AddHp(healPoint);
    }

    public void Healing(MagicianEx magician)
    {
        magician.AddHp(healPoint);
    }

    public void Healing(NPCEx npc)
    {
        npc.AddHp(healPoint);
    }

    public void Healing(Healer healer)
    {
        healer.AddHp(healPoint);
    }
}

// 기사 클래스
public class KnightEx : CharacterEx
{
    // 힘(방패 치기 기술 공격력)
    private int str;

    // 기사 생성자
    public KnightEx(string name, float speed, int damage, int str, int hp) : base(name, speed, damage, hp)
    {
        this.str = str;
    }

    public override void Attack()
    {
        // 자식 클래스에서 재정의(override) 하면서 호출되지 않는 부모의 메소드의 동작이 필요할 경우)
        // 자식 클래스에서 base 키워드를 통해 호출되지 않는 부모의 메소드를 인위적으로 호출해줘야 함
        base.Attack();
        ShieldAttack();
    }

    // 방패 치기 메소드
    private void ShieldAttack()
    {
        Debug.Log("[" + name + "] 캐릭터가 방패로 " + str + "만큼의 힘으로 밀쳐 냅니다.");
    }

    protected override void Die()
    {
        Debug.Log("조국의 영광을 위해 지금 쓰러지지만 후회하지는 않는다.");
    }

    public override void AddHp(int hp)
    {
        base.AddHp(hp);

        // 체력의 10%를 힘으로 보충함
        int addStr = (int)(hp * 0.1f);
        Debug.Log("기사가 힘을 " + addStr + "만큼 추가 보충합니다.");

        str += addStr;
    }
}

// 마법사 클래스
public class MagicianEx : CharacterEx
{
    // 지능(파이어볼 기술 공격력)
    private int inte;

    // 법사 생성자
    public MagicianEx(string name, float speed, int damage, int inte, int hp) : base(name, speed, damage, hp)
    {
        this.inte = inte;
    }

    // 자식 클래스는 부모가 재정의를 허용한 메소드에 대한 재정의를 위해 override 키워드를 넣어 메소드를 정의해줘야함
    public override void Move()
    {
        Debug.Log("[" + name + "] 캐릭터가 " + speed + "속도로 공중부양해서 하늘을 날아다니며 이동을 수행합니다.");
    }

    // 파이어볼 메소드
    public void FireballAttack()
    {
        Debug.Log("[" + name + "] 캐릭터가 마법으로 " + inte + "만큼의 지능으로 파이어볼을 발사합니다.");
    }

    protected override void Die()
    {
        Debug.Log("난 언젠가 마법의 힘으로 부활할거다. I will be back.");
    }
}

public class ExtendComponent : MonoBehaviour
{
    void Start()
    {
        NPCEx npc = new NPCEx("NPC", 10, 5, "찾으시는 물건이 있으십니까?");
        npc.Move();
        npc.Attack();
        npc.Hit(10);
        npc.Talk();

        Debug.Log("------------------------------------------");

        KnightEx knight = new KnightEx("방패기사", 20, 100, 30, 1000);
        knight.Move();          // 기사가 이동을 수행함
        knight.Attack();        // 기사가 공격을 수행함
        knight.Hit(300);        // 기사가 피격당함

        Debug.Log("------------------------------------------");

        MagicianEx magician = new MagicianEx("해리포터마법사", 15, 200, 100, 300);
        magician.Move();          // 법사가 이동을 수행함
        magician.Attack();        // 법사가 공격을 수행함
        magician.Hit(120);        // 법사가 피격당함
        magician.FireballAttack();  // 파이어볼 공격 수행

        Debug.Log("------------------------------------------");

        Healer healer = new Healer("메딕", 10, 0, 50, 50);
        healer.Move();          // 힐러가 이동을 수행함
        healer.Attack();        // 힐러가 공격을 수행함
        healer.Hit(20);        // 힐러가 피격당함
        // * 힐러는 Die 메소드를 재정의하지 않았기 때문에 부모의 비어있는 Die 메소드가 호출됨(문제점)
        // -> 해결법 : 부모 클래스에서 Die 메소드를 자식 클래스에서 반드시 재정의(override)하도록 강제화하면 됨 (abstract)

        // 힐러가 캐릭터마다 힐을 제공함
        healer.Healing(knight);
        healer.Healing(magician);
        healer.Healing(npc);
        healer.Healing(healer);
    }
}

 힐러 클래스 내에서 메소드 오버로드(overload)를 통해 객체에 따라 다른 메소드가 실행되게 healing 기능을 추가해 주었다. 하지만 이 스크립트에서는 심각한 문제점이 하나 있다. 바로, 이후의 클래스가 더 등장하게 될 때마다 해당 클래스의 오버로드 메소드를 매번 추가해 주어야 한다는 것이다.

 

 이런 상황에서 사용할 수 있는 기능이 바로 '업 캐스팅'이다. 업 캐스팅이란 모든 자식 클래스 타입의 객체를 부모 클래스 타입으로 상승하여 캐스팅해서 참조하는 기능을 말한다. 위처럼 메소드를 클래스마다 오버로드하여 정의하면 가독성도 떨어지고 코드가 중복되게 된다.

 이해하기 쉽게 아래의 코드로 설명하겠다.

KnightEx knight = new KnightEx("방패기사", 20, 100, 30, 1000);
CharacterEx character = knight;

 위와 같이 상속 관계에서 부모 위치에 있는 CharacterEx 클래스 객체에 자식인 KnightEx 클래스 객체를 참조시킬 수 있는 것이다. 하 지만 이 업 캐스팅을 사용할 때는 주의할 점이 있다.

업 캐스팅을 사용할 때는 자식의 클래스 내에 있는 멤버 변수나 메소드에 접근할 수 없고, 부모의 멤버 변수나 메소드에만 접근할 수 있다. 단 예외적으로 부모의 클래스에서 virtual이나 abstract되어 자식 클래스에서 override된 메소드들은 접근이 가능하다. 즉, 자식 클래스에만 구현되어 있는 멤버 변수나 메소드들은 제한되고, 부모 메소드에만 구현되어 있거나 virtual / abstract + override를 이용하여 부모와 자식 클래스에서 이미 구현되어 있는 변수나 메소드들은 접근이 가능한 것이다.

 

 업캐스팅을 통하여 Healing 메소드를 아래와 같이 작성할 수 있다.

// * 업캐스팅 : 모든 자식 클래스 타입의 객체를 부모의 클래스 타입으로 캐스팅해서 참조하는 것
public void Healing(CharacterEx character)
{
    character.AddHp(healPoint);

    // * 현재 character 변수의 타입은 업캐스팅 참조가 되어 있는 상태이므로 자식 클래스 내의 멤버변수나 메소드를 접근할 수 없음
}

 위의 코드에서 보이듯이, 이렇게 구현하면 가독성도 매우 좋고 구현하기에도 매우 편리하다.

 

 업캐스팅과는 반대로, 아래의 코드처럼 다운 캐스팅도 가능은 하다.

CharacterEx character = new CharacterEx("캐릭터클래스", 20, 100, 30);
Knight knight = (Knight)character;

 예시로 기사 클래스를 다운 캐스팅했지만, 법사 클래스나 NPC 클래스도 가능하다. 하지만 이 다운 캐스팅은 섣불리 사용하기에는 위험한 접근이다. 위에서도 의문점을 품은 분들도 많이 계시겠지만, 일단 본 스크립트에서 knight 클래스를 포함한 자식 클래스들의 생성자 메소드와 character 클래스의 생성자 메소드는 매개 변수 자체부터가 다르다. 생성자 메소드에서 base로 넘겨준 매개 변수들의 갯수도 각자 다르고, 고유적인 멤버 변수들이나 멤버 메소드들도 다르기 때문에 다운 캐스팅을 사용할 때는 문제가 될 부분을 잘 살펴봐야 한다.