본문 바로가기

C# 프로그래밍/문법 개념

[C#] 다형성 (Polymorphism)

반응형

다형성이란 무엇인가?

공식 문서에서의 설명은 다음과 같다.

여러 추상화에서 다양한 방법으로 상속된 속성 또는 메서드를 구현할 수 있습니다.

 

쉽게 말해 다형성은 부모 클래스를 상속받은 자식 클래스가 다양한 형태로 객체를 가지는 것을 의미한다.

C#과 같은 객체지향 프로그래밍 언어의 특징 중 하나인 다형성은 클래스의 상속으로부터 이루어지는 것이다.

 

다형성은 객체지향 언어의 가장 큰 특징인 추상화, 캡슐화, 상속, 다형성 중 하나이고 추상화와 상속은 모두 다형성을 나타내기 위한 수단 중 일부이다. 따라서 매우 중요하다고 할 수 있다.


상속으로 어떻게 다형성이라는 것을 나타낼 수 있는가?

자식 클래스가 부모 클래스로부터 상속을 받으면 부모 클래스의 특징들은 그대로 간직하고 새로운 특징들을 추가하게 된다. 하나의 부모 클래스로부터 여러 개의 자식 클래스가 있다면 통일되는 특징들을 유지한 채 여러 개의 다양한 형태의 클래스가 탄생되는 것이다. 이를 통해 다형성을 나타낼 수 있다.

 

하지만 클래스가 단순히 상속을 하는것 만으로는 부족하다. 이를 활용하기 위해서는 다운 캐스팅과 업 캐스팅이라는 것을 클래스에 해주어야 한다. 또한 메서드 오버 라이딩도 해주어야 한다.


업 캐스팅과 다운 캐스팅이 무엇인가?

우선 캐스팅(형식 변환이라고도 한다.)이라는 용어는 변수의 형식을 암시적 혹은 명시적으로 변환하는 것을 의미한다. 암시적, 명시적 형 변환에 대해 먼저 알아보자면 다음과 같다.

 

암시적 변환 저장하는 정보의 손실없이 변수에 맞출 수 있는 경우 수행
명시적 변환 변환 수행시에 정보의 손실이 발생할 수 있거나 캐스트가 실패할 수 있음을 컴파일러에 알려야 할 때 수행

 

예를 들어서 int형식을 long형식으로 변환하는 것은 암시적 형 변환이다. 왜냐하면 32비트 정수인 int형에서 64비트 정수인 long으로 담는 것에는 문제가 없기 때문이다. 마찬가지로 자식 클래스를 부모 클래스에 담는 것도 문제가 없다. 자식 클래스는 부모 클래스의 정보가 모두 다 있고 불필요한 것은 버리면 되기 때문이다.

 

반면 long형을 int형으로 변환하는것은 명시적 변환이다. 이때는 데이터의 손실이 발생한다. 마찬가지로 부모 클래스가 자식 클래스로 형 변환할 때도 추가적인 알 수 없는 정보가 발생하므로 캐스트가 실패할 수 있다. 이때는 명시적으로 형 변환을 시켜주어야 한다. 명시적 형 변환은 변환하고자 하는 대상에 괄호를 넣어 컴파일할 때 알려준다.

 

이때 클래스가 암시적 형 변환을 하면 업캐스팅(자식클래스가 부모 클래스에), 명시적 형 변환을 하면 다운 캐스팅(부모 클래스가 자식 클래스에 캐스팅)에 해당한다.

 

자세히 알아보기 위해 다음 소스코드를 살펴보자.

public class Nationality
{
    public string nationName;
    public int population;
    public float gdp;
    public Nationality(string _nationName, int _population, float _gdp)
    {
        nationName = _nationName;
        population = _population;
        gdp = _gdp;
    }
}

public class Asia : Nationality
{
    public bool isHanRiverTouched;
    public int asianGameGoldMedalCount;
    public Asia(string _nationName, int _population, float _gdp, bool _isHanRiverTouched, int _asianGameGoldMedalCount) : base(_nationName, _population, _gdp)
    {
        isHanRiverTouched = _isHanRiverTouched;
        asianGameGoldMedalCount = _asianGameGoldMedalCount;
    }
}

public class Europe : Nationality
{
    public bool isThamesRiverTouched;
    public Europe(string _nationName, int _population, float _gdp, bool _isThamesRiverTouched) : base(_nationName, _population, _gdp)
    {
        isThamesRiverTouched = _isThamesRiverTouched;
    }
}

Asia클래스와 Europe클래스는 모두 Nationality클래스를 상속받고 있다. Asia와 Europe클래스는 분명 서로 다른 클래스이다. 하지만 둘은 Nationality를 상속받았기에 공통점이 존재한다.

 

이 클래스들의 겹치는 변수들을 서로 비교하고 싶다면 어떻게 해야 할까? 

 

각 클래스의 변수에 따로 접근하여 변수를 가져와야 할까? 그건 너무 비효율적이다.

이때 활용 가능한 것이 캐스팅이다.

 

아래의 스크립트를 보자.

void Start()
{
    //국가이름, 인구, GDP, 한강보유여부, 아시안게임 메달 수
    Asia[] asiaCountry = new Asia[3];
    asiaCountry[0] = new Asia("Korea", 10, 100, true, 1000);
    asiaCountry[1] = new Asia("China", 100, 10, false, 1);
    asiaCountry[2] = new Asia("Japan", 10, 1, false, 0);
    //국가이름, 인구, GDP, 템즈강보유여부
    Europe[] europeCountry = new Europe[2];
    europeCountry[0] = new Europe("France", 10, 10, true);
    europeCountry[1] = new Europe("Italy", 20, 20, false);
}

만약 각 나라의 GDP를 비교해서 GDP가 가장 높은 나라를 찾는다고 하자. 이 상태로는 for문을 2번 써야 할 것이다. 그리고 변수가 추가되어야 할 것이고 나라가 추가되면 될수록 스파게티 코드가 될 가능성이 높다.

 

효율적인 코드 관리를 위해 업 캐스팅을 사용한다. 다음 예시를 보자.

void Start()
{
    //국가이름, 인구, GDP, 한강보유여부, 아시안게임 메달 수
    Asia[] asiaCountry = new Asia[3];
    asiaCountry[0] = new Asia("Korea", 10, 100, true, 1000);
    asiaCountry[1] = new Asia("China", 100, 10, false, 1);
    asiaCountry[2] = new Asia("Japan", 10, 1, false, 0);
    //국가이름, 인구, GDP, 템즈강보유여부
    Europe[] europeCountry = new Europe[2];
    europeCountry[0] = new Europe("France", 10, 10, true);
    europeCountry[1] = new Europe("Italy", 20, 20, false);

    List<Nationality> countries = new List<Nationality>();
    countries.AddRange(asiaCountry);
    countries.AddRange(europeCountry);

    float highestGdp = 0;
    Nationality highestNationality = null;
    for (int i = 0; i < countries.Count; i++)
    {
        if (countries[i].gdp > highestGdp)
        {
            highestGdp = countries[i].gdp;
            highestNationality = countries[i];
        }
    }

    Debug.Log(highestNationality.nationName);
}

위는 암시적 형 변환(업 캐스팅)이 이루어진 상태이다. Nationality의 상속을 받는 Asia와 Europe이 Nationality로 형을 변환한 것이다. 이것은 상속을 받은 자식 클래스가 부모 클래스의 모든 멤버를 갖고 있기 때문에 가능하다. 따라서 컴파일에도 아무런 문제가 발생하지 않는다. 자연스럽게 부모 클래스인 Nationality클래스 리스트에 추가를 해주고 for문으로 한 번에 비교하면 된다.

로그 결과

반대로 부모 클래스가 자식 클래스로 형 변환하는 것은 다운 캐스팅(명시적 형 변환)에 해당한다.

Asia firstAsiaCountry = (Asia)countries[0];
Debug.Log(firstAsiaCountry.nationName);

위처럼 코드를 작성하면 정상적으로 로그가 찍힌다. 왜냐면 countries리스트의 첫 번째 클래스는 Asia에 해당하는 값을 넣어주었기 때문이다. 그런데 만약 Asia가 아닌 Europe의 값을 넣어주고 Asia로 변경하면 어떻게 될까?

 

그러면 곧장 다음과 같은 에러가 발생한다.

로그 결과

명시적 형 변환이 적합하지 않다고 에러 메시지가 뜬다.

 

C#에는 이러한 문제점을 방지하기 위해서 is와 as연산자가 존재한다.


is와 as연산자가 무엇일까?

is는 런타임 형식이 지정된 형식과 일치하는지를 비교하여 bool값을 반환한다.

if (countries[3] is Asia)
{
    Debug.Log(countries[3].nationName);
}

해당 코드처럼 사용하면 더 이상 에러 메시지를 나타내지 않는다. 

 

is연산자는 추가적으로 다음과 같이 응용할 수 있다.

if (countries[3] is Asia asia)
{
    asia.gdp += 10;
}

다시 말해, 변수를 생성하고 해당 지역변수를 참조하는 것이 가능하다는 것이다. (물론 if문 안에서만 참조가 가능하다.)

이것은 선언 패턴 이라고도 한다.

 

as는 식의 결과를 지정된 참조 또는 nullable값 형식으로 명시적으로 변환한다. 이때 변환이 되지 않는다면 null값을 반환한다.

 

as연산자를 활용하여 위의 코드를 다시 작성해보면 다음과 같은 코드이다.

Asia asia = countries[3] as Asia;
if (asia != null)
{
    asia.gdp += 10;
}

이렇게 업 캐스팅으로 형을 통일해주었다면 클래스의 다형성을 나타내기 위한 첫 번째 과정을 마친 것이다.

다형성을 표현하기 위해서는 처음 언급한 것처럼 메서드 오버 라이딩까지 해주면 완벽하다.


메서드 오버 라이딩이란?

메서드 오버 라이딩은 부모와 자식 클래스가 있고 자식 클래스가 부모 클래스의 형을 갖고 있을 때 메서드를 호출할 경우 자식 클래스의 메서드가 호출되도록 하는 것이다.

 

코드를 통해 확인하자.

public class Nationality
{
    public string nationName;
    public int population;
    public float gdp;
    
    public void SayCountryInformations()
    {
        Debug.Log(nationName + "'s population is " + population + ".");
    }
}

만약 부모 클래스인 Nationality가 SayCountryInformations라는 함수를 갖고 있다면 자식 클래스들도 모두 같은 함수를 갖게 된다. 이때 자식 클래스의 함수를 부모 클래스와 서로 다르게 작성하고 싶다면 메서드 오버 라이딩을 해야 한다.

public class Nationality
{
    public string nationName;
    public int population;
    public float gdp;

    public virtual void SayCountryInformations()
    {
        Debug.Log(nationName + "'s population is " + population + ".");
    }//virtual키워드를 반드시 써줘야 자식클래스에서 오버라이드가 가능하다.
}

public class Asia : Nationality
{
    public bool isHanRiverTouched;
    public int asianGameGoldMedalCount;

    public override void SayCountryInformations()
    {
        base.SayCountryInformations();
        Debug.Log(nationName + "'s asianGameGoldMedalCount is " + asianGameGoldMedalCount + ".");
    }//override키워드를 통해 부모클래스의 함수를 재정의 한다.
}

로그 결과


다형성은 왜 필요한가?

우선 상속을 하는 이유에 대해 알아보자.

상속을 하는 가장 큰 이유는 코드의 재사용성 때문일 것이다. 여러 클래스에서 동일하게 작성되는 부분은 부모 클래스에서 정의하고 자식 클래스가 상속받아 사용하면 코드를 간결하게 작성할 수 있다.

여타 다른 이유로는 형식을 통일함으로써 배열과 리스트 같은 묶음으로 관리를 용이하게 할 수 있다는 점이다. (if-else를 남발하게 되면 스파게티 코드가 될 가능성이 크다.)

 

특히 인터페이스를 상속받아 형식을 통일하는 경우 관리하기가 매우 수월해지며 다양한 디자인 패턴의 구사가 가능해진다.


어떻게 활용할 수 있는가?

하나의 예를 들어보자.

 

어떠한 회사가 있고 그 회사에서는 기획, 프로그래머, 아트 이렇게 3 분류의 인재를 뽑는다고 가정하자.

세 파트의 분류는 분명 하는 역할이 다르다. 그렇지만 중복되는 특징이 존재할 것이다. 

예를 들어 3가지 분류 모두 연봉을 받는다. 근무 시간도 존재할 것이고 어느 직책을 맡는지에 대한 정보도 있어야 할 것이다. 서로 값은 다르겠지만 회사의 구성원으로서 반드시 있어야 할 변수이다. 이렇게 겹치는 부분은 부모 클래스로 두어 자식 클래스가 상속받도록 하면 알맞다.

 

반대로 하는 역할 자체가 다른 부분도 있을 것이다. 기획과 프로그래머, 아트는 기업 내에서 어떠한 일을 수행하는 것은 같지만 하는 일의 내용은 다르다. 또한 각자 어떤 프로그래밍 언어를 사용하는지에 대한 정보가 반드시 필요하지만 여타 직군은 선택사항이다. 반대로 아트가 어떤 그래픽 툴을 쓰는지는 필수사항이지만 프로그래머는 필수사항이 아니다.

이러한 경우에 다형성을 활용하기 좋다. 자식 클래스에는 각 클래스의 특징들을 추가적으로 서술한다.

 

유니티에서의 사용 예시를 스크립트로 확인하자.

public class Worker : MonoBehaviour
{
    public string m_name;
    public float wallet;
    public float salary;
    public int businessHour;
    public JobPosition jobPosition;

    public virtual void Work()
    {
        TakeSalary();
    }

    private void TakeSalary()
    {
        wallet += salary;
    }
}

public enum JobPosition
{
    Intern,
    Staff,
    Manager,
    Directer,
}

세 직군 모두 일을 하고 월급을 받는 것은 동일하다. 하지만 어떠한 일을 하는지는 서로 다르기 때문에 자식 클래스에서 오버라이드 하도록 가상 함수(virtual)로 둔다.

 

다음은 Worker를 상속받는 Programmer이다.

public class Programmer : Worker
{
    public static float programmingArrivalRate;
    public ProgrammingLanguage programmingLanguage;
    public float skill;
    public string usingIDE;

    void Start()
    {
        skill = Random.Range(5f, 12.5f);
        usingIDE = ((IDE)Random.Range(0, System.Enum.GetValues(typeof(IDE)).Length)).ToString();
        programmingLanguage = (ProgrammingLanguage)Random.Range(0, System.Enum.GetValues(typeof(ProgrammingLanguage)).Length);
    }

    public override void Work()
    {
        base.Work();
        programmingArrivalRate = skill + programmingArrivalRate >= 100 ? 100 : skill + programmingArrivalRate;
        Debug.Log(m_name + " achieved " + skill + "% of the progamming project using " + programmingLanguage.ToString() + " with " + usingIDE
        + ".\n Programming arrival rate is " + programmingArrivalRate + "%.");
    }
}

public enum ProgrammingLanguage
{
    C,
    Cpp,
    CS,
    Python,
    Java,
    Kotlin,
}

public enum IDE
{
    Eclipse,
    VisualStudio2019,
    VisualStudioCode,
    PyCharm,
    JetBrain,
}

 

아래는 Worker를 상속받는 Artist이다.

public class Artist : Worker
{
    public static float artArrivalRate;
    public float skill;
    public string artTool;

    void Start()
    {
        skill = Random.Range(8f, 10f);
        artTool = ((artTool)Random.Range(0, System.Enum.GetValues(typeof(artTool)).Length)).ToString();
    }

    public override void Work()
    {
        base.Work();
        artArrivalRate = skill + artArrivalRate >= 100 ? 100 : skill + artArrivalRate;
        Debug.Log(m_name + " achieved " + skill + "% of the art project using " + artTool
        + ".\n Art arrival rate is " + artArrivalRate + "%.");
    }
}

public enum artTool
{
    Photoshop,
    Illustator,
    Max,
    Maya,
    Blender,
}

 

Worker를 상속받는 ProductManager이다.

public class ProductManager : Worker
{
    public int instructCount = 0;
    public string plan;

    void Start()
    {
        plan = "do work something " + Random.Range(0, 6) + "times";
    }

    public override void Work()
    {
        base.Work();
        InstructSomething();
        Debug.Log(m_name + " instructed the others to " + plan);
    }

    private void InstructSomething()
    {
        instructCount += 1;
    }
}

 

마지막으로 모든 Worker를 관리할 Enterprise클래스이다.

public class Enterprise : MonoBehaviour
{
    [Header("WORKERS")]
    public GameObject productManager;
    public GameObject programmer;
    public GameObject artist;

    [Header("PERSONAL INFORMATIONS")]
    public string[] firstName;
    public string[] lastName;

    private List<Worker> workers = new List<Worker>();
    
    //button
    public void GenerateWorker(string workerType)
    {
        float randomXPos = Random.Range(-5, 5f);
        float randomZPos = Random.Range(-5, 5f);

        GameObject clone = null;
        switch (workerType)
        {
            case "ProductManager":
                clone = productManager;
                break;
            case "Programmer":
                clone = programmer;
                break;
            case "Artist":
                clone = artist;
                break;
        }
        Worker worker = Instantiate(clone, new Vector3(randomXPos, 0, randomZPos), Quaternion.identity).GetComponent<Worker>();
        WorkerInit(worker);
        workers.Add(worker);
    }

    //button
    public void PutToWork()
    {
        workers.ForEach(
            worker => worker.Work()
            );
    }

    private void WorkerInit(Worker newWorker)
    {
        newWorker.m_name = firstName[Random.Range(0, lastName.Length)] + " " + lastName[Random.Range(0, firstName.Length)];
        newWorker.wallet = 0;
        newWorker.salary = Random.Range(100, 150f);
        newWorker.businessHour = Random.Range(6, 10);
    }
}

 

인스펙터 변수 설정

인스펙터 창에서 다음과 같이 설정해 주고 시작 시 Worker의 초기값을 설정해 주도록 한다.

버튼을 누르면 해당 오브젝트가 생성되도록 한다. 원하는 수만큼 버튼을 눌러서 랜덤 한 포지션에 오브젝트들을 생성하고 랜덤한 초기값들을 설정하게 한다. 이 오브젝트들은 업 캐스팅되어 Worker리스트에 모두 담긴다.

게임뷰

DO WORK!! 버튼을 누르면 PutToWork함수가 실행되며 workers리스트의 모든 Worker들이 Work함수를 실행한다.

public void PutToWork()
{
    workers.ForEach(
        worker => worker.Work()
        );
}

로그결과

 

반응형