[C#] C#으로 공부하는 객체지향 프로그래밍 4 - 객체지향의 4가지 특성 : 다형성



객체 지향은 4가지의 특성을 가지고 있다.

  1. 추상화 (Abstraction)
  2. 캡슐화 (Encapsulation)
  3. 상속 (Inheritance)
  4. 다형성 (Polymorphism)
이번엔 다형성에 대해서 알아보자.





다형성 (Polymorphism) 이란?

같은 이름의 메소드 호출에 대해 객체에 따라 다른 동작을 할 수 있도록 구현되는 것을 의미한다. 추상화와 상당히 밀접한 관련이 있는데, (사실 객체 지향은 상속이 근본이 되기 때문에 객체지향의 4가지 특성은 서로 밀접한 관련이 있다.) 이전에 봤던 Animal 클래스를 추상화해서 자식 클래스에서 bark와 legCount 메서드를 오버라이딩 했었다. 비록 이것은 추상화를 통해 비어 있는 메서드를 정의한 것이지만, 다형성의 개념과 크게 다르지 않다.

일단 크게 오버라이드 (override) 와 오버로드 (overload) 를 알아야한다.




메서드 오버라이드 (method override)

부모 클래스의 메서드를 재정의해서 사용하는 것이다. 이 때 부모의 메서드는 virtual 예약어를, 자식의 메서드는 override 예약어를 붙이게 된다.  

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

public class Animal : MonoBehaviour
{
    public virtual void bark()
    {
        Debug.Log("울음소리를 낸다.");
    }

}



public class Dog : Animal
{

    public override void bark()
    {
        Debug.Log("짖는다.");
    }

    private void Start()
    {
        bark();
    }
}

이렇게 될 경우 bark 메서드는 재정의 되어 "울음소리를 낸다." 대신 "짖는다 ."  라는 결과가 나오게 된다.
다만 부모 클래스의 메서드가 아닌, 부모 클래스의 메서드와 이름만 같은 완전히 다른 메서드를 만들고 싶을 수도 있다. 비록 이렇게 하면 나중에 헷갈리게 될 수 있지만 간혹 필요한 경우가 있을 수 있다. 가령 부모 메서드의 형태를 유지하면서 자식 메서드를 실행한다던지 말이다. 이 경우엔 부모 메서드의 virtual 예약어와 자식 메서드의 override 예약어를 빼고 자식 메서드에 new 예약어를 걸어두면 된다.

+ base

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

public class Animal : MonoBehaviour
{
    public virtual void bark()
    {
        Debug.Log("울음소리를 낸다.");
    }

}



public class Dog : Animal
{

    public override void bark()
    {
        base.bark();
        Debug.Log("짖는다.");
    }

    private void Start()
    {
        bark();
    }
}

위와 전체적인 내용은 같으며 자식 메서드에 base.bark() 라는 코드가 생겼는데, C#에서 base는 자바의 super와 같이 부모의 메서드에 접근하는 것이다. 위의 코드는 "울음소리를 낸다." "짖는다."  의 2개의 결과가 출력된다.

그렇다면 virtual / override 와 abstract / override 의 차이점은 무엇일까? 둘 다 부모 메서드를 오버라이드해서 사용하는 것이 아닌가? 그렇다면 추상화는 왜 굳이 사용해야하는 것일까? 어차피 virtual / override 와 new 를 이용해서 부모 클래스의 메서드를 적절하게 사용할 수 있는데 말이다. abstract 와 virtual 에는 명확한 차이가 존재하는데, 바로 부모 클래스를 필수적으로 정의해야 하는가, 그럴 필요가 없는가 의 차이다. 추상화 메서드는 자식 클래스들에서 필수적으로 정의를 해줘야만 보기 싫은 빨간 밑줄이 생기지 않지만, virtual 메서드는 자식 클래스에서 재정의를 하든 말든 오류가 발생하지 않는다.

뭔가 명확한 차이를 예시로 설명하기 어렵지만, abstract는 저자가 책의 목차를 적어놓고, 목차대로 책의 내용을 작성하는 느낌이라면, virtual은 독자가 작성 (정의) 된 책의 내용으로 새로운 콘텐츠를 만든다던가, 내용을 수정하는 등 2차 창작의 느낌이라고 볼 수 있지 않을까? 책의 저자는 목차를 반드시 채워야하지만, 2차 창작자는 해도 되고 안해도 되는 느낌으로 말이다. (아님 말고..)





메서드 오버로드 (method overload)

어떤 클래스 내에서 같은 이름의 메서드가 다른 역할을 수행하게끔 만들어주는 것이다. 아래의 예시는 그냥 생각나는대로 작성해보았는데, 코드를 보면 eat 메서드가 2개가 있는데, 하나는 void 형이며 나머지 하나는 string 형 메서드이다. 이처럼 메서드 오버로딩에서 return 타입은 같든 다르든 상관이 없다. 대신 매개변수 형식은 무조건 달라야한다. 가령 void eat (int food)int eat (int food)  처럼 작성하고 eat(5) 를 출력하려고 한다면 컴파일러는 전자의 메서드에 값을 넣어야할지, 후자의 메서드에 값을 넣어야할지 알 수 없게 된다. 따라서 매개변수를 달리하여 두 메서드가 같은 이름의 다른 메서드라는 것을 확실하게 구분해줘야한다.


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

public class Animal : MonoBehaviour
{
    public virtual void bark()
    {
        Debug.Log("울음소리를 낸다.");
    }

    public void eat(string food)
    {
        Debug.Log(food + "음식을 먹었다!");
    }

    public string eat(int food)
    {
        if (food >= 5) return "적당한";
        else return "부족한";
    }
}



public class Dog : Animal
{

    public override void bark()
    {
        base.bark();
        Debug.Log("짖는다.");
    }

    private void Start()
    {
        bark();
        eat(eat(5));
    }
}


메서드 오버로드와 비슷하게 연산자를 오버로드 하는 방식도 있는데, 아래와 같은 형식으로 작성한다.
public static 이름 operator 연산자(변수명 , 변수명)
static과 operator를 사용하여 해당 연산자에 대한 연산을 재정의해준다. 아래는 op를 오버로드하여 op 클래스의 + 연산자에 대한 연산을 재정의 해준 코드이다. 오버로드와 오버라이드는 객체 지향에서 몰라서는 안될 필수적인 내용이므로 반드시 알고 가도록 하자.

public class op : MonoBehaviour
{
    double num;

    public op(double num)
    {
        this.num = num;
    }

    public static op operator +(op op1, op op2)
    {
        return new op(op1.num + op2.num);
    }

}

public class calc : MonoBehaviour
{
    private void Start()
    {
        op op1 = new op(2.0);
        op op2 = new op(3.0);
        op op3 = op1 + op2;

        Debug.Log(op3);
    }
}


댓글