달나라 노트

C# : 상속 (Inheritance, sealed)과 다형성(Polymorphism) 본문

C#/C#

C# : 상속 (Inheritance, sealed)과 다형성(Polymorphism)

CosmosProject 2022. 3. 23. 19:15
728x90
반응형

 

 

 

C#의 class에도 상속(Inheritance)이라는 개념이 있습니다.

 

상속은 어떤 class를 다른 class에 상속하게되는데 상속이라는 개념을 아주아주 심플하게 설명하면 다음과 같습니다.

"내 class에 없는 내용을 다른 class로부터 복사해온다."

 

다음 코드를 보시죠.

 

using System;

public class Dog
{
    public string name = "Dog";

    public void hello()
    {
        Console.WriteLine("Dog Hello!");
    }
}

public class Cat : Dog
{

}

class MyProgram
{
    static void Main()
    {
        Cat test_cat = new Cat();

        Console.WriteLine(test_cat.name);
        test_cat.hello();
    }
}


-- Result
Dog
Dog Hello!

위 예시의 Main method 부분을 보면 test_cat 변수에 Cat class를 호출해서 객체를 생성하고 있습니다.

 

그리고 생성된 test_cat 객체에서 name field를 참조한다던지 hello() method를 호출하고 있습니다.

 

자 그러면 Cat 객체에는 name이라는 field와 hello()라는 method가 이미 정의되어있을거라고 생각됩니다.

 

근데 위 코드에서 Cat class가 선언된 부분을 보면 Cat class는 아무 내용도 없이 생성되고있습니다.

아래 부분이 Cat class가 선언되는 부분만 나타낸 것입니다.

...
public class Cat : Dog
{

}
...

 

근데 어떻게 Cat class를 받은 test_cat 객체가 name이라는 field를 불러올 수 있었고, hello()라는 method를 사용할 수 있었던 걸까요?

 

그것은 바로 Cat class는 Dog class를 상속받았기 때문입니다.

 

Cat class를 선언하는 부분을 보면 Cat class 자체에는 아무런 내용이 없습니다. 중괄호 내부가 비어있죠.

하지만 public class Cat : Dog 이 부분을 보면 Cat class 옆에 Dog이라는 글자가 적혀있습니다.

 

이 말은 "Cat class를 만든 후에 Dog class의 내용을 상속시켜라" 라는 의미입니다.

 

즉, Cat class는 Dog class가 가진 모든 field와 method들을 모두 똑같이 가지게되는 것이죠.

 

개념은 굉장히 간단합니다.

그냥 Cat이라는 비어있는 class를 선언하고 Dog class의 정보를 가져와서 Cat class에 채워넣은겁니다.

 

 

 

 

이제 class의 상속이라는게 그냥 다른 class의 내용을 복사해오는거라는 개념은 이해했을겁니다.

근데 제가 위에서 상속이라는 개념을 간단하게 설명할 때 아래와 같이 말했습니다.

 

"내 class에 없는 내용을 다른 class로부터 복사해온다."

지금부터 얘기하려는 부분을 빨간색으로 표시했습니다.

 

이제 코드를 약간 수정해봅시다.

 

using System;

public class Dog
{
    public string name = "Dog";

    public void hello()
    {
        Console.WriteLine("Dog Hello!");
    }
}

public class Cat : Dog
{
    public string name = "Cat";
}

class MyProgram
{
    static void Main()
    {
        Cat test_cat = new Cat();

        Console.WriteLine(test_cat.name);
        test_cat.hello();
    }
}


-- Result
Cat
Dog Hello!

 

달라진점은 딱 하나입니다.

 

아래처럼 Cat class 생성 부분에 name이라는 변수를 선언하고 Cat이라는 글자를 넣었습니다.

...
public class Cat : Dog
{
    public string name = "Cat";
}
...

 

Cat class가 Dog class의 내용을 상속받는 것도 동일합니다.

 

 

 

 

...
public class Dog
{
    public string name = "Dog";

    public void hello()
    {
        Console.WriteLine("Dog Hello!");
    }
}

public class Cat : Dog
{
    public string name = "Cat";
}
...

Dog class와 Cat class를 보면 이전과는 다르게 Dog class도 name이라는 field를 가지고있고, Cat class도 name이라는 field를 가지고 있는 상황이 되었습니다.

 

Cat class와 Dog class 모두 name이라는 field를 가지고 있으니 상속을 진행하면 둘 중 하나는 사라져야합니다.

 

그러면 Cat class에서 선언된 name field가 남아있을까요? 아니면 Dog class에서 선언된 name field가 Cat class의 name field를 덮어씌울까요?

 

정답은 Cat class에서 선언된 name field가 남아있게됩니다.

코드의 결과를 보면 Cat과 Dog Hello!가 출력되기 때문입니다.

이 말은 Cat class에 있는 name field값이 Cat이었고 이것이 그대로 남아있기 때문에 Cat이라는 문자가 출력된것임을 알 수 있습니다.

 

이것이 바로 아까 말했던 

"내 class에 없는 내용을 다른 class로부터 복사해온다."

라는 말의 의미입니다.

 

그러면 왜 굳이 Cat class의 name field를 남기고 Dog class의 name field를 지웠을까요?

 

바로 Cat class를 사용하는 부분에 정답이 있습니다.

 

Cat test_cat = new Cat();

위 부분을 보면 Cat class를 호출해서 test_cat 객체를 만드는 부분입니다.

근데 test_cat 객체 이름 왼쪽에 Cat이라고 적힌게 보이시나요?

 

Cat class를 이용해서 test_cat 객체를 만들건데 test_cat 객체의 main type은 Cat으로 한다는 의미입니다.

 

즉, Cat class가 다른 class로부터 상속받는 상황이고 Cat class와 부모 class가 가진 속성 또는 method중 동일한 이름의 속성/method가 존재한다면 Cat class의 속성/method를 우선적으로 남기라는 의미인 것이죠.

 

 

 

 

 

 

 

using System;

public class Dog
{
    public string name = "Dog";

    public void hello()
    {
        Console.WriteLine("Dog Hello!");
    }
}

public class Cat : Dog
{
    public string name = "Cat";

    public void hello()
    {
        Console.WriteLine("Cat Hello!");
    }
}

class MyProgram
{
    static void Main()
    {
        Cat test_cat = new Cat();

        Console.WriteLine(test_cat.name);
        test_cat.hello();
    }
}


-- Result
Cat
Cat Hello!

위 예시는 Cat class가 Dog class로부터 상속을 받는 동일한 예시입니다.

다만 Dog class는 name field, hello() method를 가지고 있지만, 상속받을 Cat class도 name field, hello() method를 가지고 있습니다.

 

따라서 Cat class의 name field, Dog class의 name field 둘 중 하나는 사라져야합니다.

Cat class의 hello() method, Dog class의 hello() method 둘 중 하나는 사라져야합니다.

 

이에 대한 우선순위는 Cat class를 호출하는 부분에서 알 수 있습니다.

Cat test_cat = new Cat();

 

Cat class를 호출해서 test_cat 객체를 만드는데 객체의 타입이 Cat입니다.

그러면 기본적으로 겹치는 속성/method 중 Cat class의 속성/method를 우선적으로 남겨놓습니다.

 

따라서 Cat class에서 선언된 name, hello()를 우선적으로 남겨두고 Dog class의 name, hello()는 지워버립니다.

 

 

 

 

 

 

 

 

 

using System;

public class Dog
{
    public string name = "Dog";

    public void hello()
    {
        Console.WriteLine("Dog Hello!");
    }
}

public class Cat : Dog
{
    public string name = "Cat";

    public void hello()
    {
        Console.WriteLine("Cat Hello!");
        base.hello();
    }
}

class MyProgram
{
    static void Main()
    {
        Cat test_cat = new Cat();

        Console.WriteLine(test_cat.name);
        test_cat.hello();
    }
}


-- Result
Cat
Cat Hello!
Dog Hello!

위 코드의 Result를 보면 Cat, Cat Hello!, Dog Hello! 가 모두 출력되고있습니다.

 

Dog Hello!가 갑자기 왜 출력된걸까요?

 

...
public class Cat : Dog
{
    public string name = "Cat";

    public void hello()
    {
        Console.WriteLine("Cat Hello!");
        base.hello();
    }
}
...

딱 하나만 빼면 다른 부분은 이전에 봤던 예시와 동일합니다.

 

한 가지 다른 점은 Dog class를 상속받아 Cat class를 생성하는 부분에서 hello() method 선언 부분을 보면 base 키워드가 사용된 것을 볼 수 있습니다.

 

base의 의미는 상속을 해준 부모 class를 의미합니다.

따라서 base.hello()는 상속을 해준 부모 class가 가진 원본 hello() method를 호출하라는 의미입니다.

 

그래서 부모 class인 Dog class가 가진 hello() method가 실행되면서 Dog Hello! 라는 글자가 출력된겁니다.

 

 

 

 

 

 

 


 

 

 

자 이제 상속에 대해 좀 더 디테일한 부분까지 들어가봅시다.

 

using System;

class Dog
{
    public void hello()
    {
        Console.WriteLine("Dog Hello!");
    }
}

class Cat : Dog
{
    public void hello()
    {
        Console.WriteLine("Cat Hello!");
    }
}

class MyProgram
{
    static void Main()
    {
        Dog test_cat = new Cat();

        test_cat.hello();
    }
}



-- Result
Dog Hello!

지금까지 보았던 예시들과 동일하지만 딱 하나의 부분을 변경했습니다.

 

Dog test_cat = new Cat();

바로 이 부분입니다.

 

원래 코드는 아래와 같았습니다.

Cat test_cat = new Cat();

 

test_cat 변수의 자료형을 Cat에서 Dog으로 바꾸니까 test_cat.hello()의 결과가 Dog Hello! 로 바뀌었습니다.

 

어떻게된걸까요?

 

class Cat : Dog

일단 위 부분에서 Dog class가 부모 class이며 Dog class의 정보를 자식 class인 Cat class에 상속시킨다는 것은 변함 없습니다.

 

다만 아래의 두 가지 경우와 같이 Dog class를 상속받은 Cat class를 실제로 호출하여 객체를 생성할 때,

생성할 객체의 자료형을 자식 class(Cat) 또는 부모 class(Dog) 중 어떤걸 적어주냐에 따라 두 class가 공통적으로 소유한 속성값/method의 overwrite 우선순위가 달라집니다.

 

Cat test_cat = new Cat();

자식 class인 Cat으로 자료형을 선언하면 Cat class가 가진 속성값/method가 우선적으로 남아있게 됩니다.

즉, 부모 class인 Dog class에 있는 hello() method는 사라집니다.

 

Dog test_cat = new Cat();

부모 class인 Dog으로 자료형을 선언하면 Dog class가 가진 속성값/method가 우선적으로 남아있게 됩니다.

즉, 자식 class가 가진 hello() method는 부모 class가 가진 hello() method의 내용으로 덮어씌워집니다.

따라서 test_cat.hello() 구문을 이용하면 hello() method가 부모 class의 내용을 출력하죠.

 

 

 

Dog test_cat = new Cat();

그러면 위처럼 test_cat 객체의 타입은 부모 class인 Dog class의 내용을 우선적으로 남겨두도록 선언했지만,

hello() method는 자식 class인 Cat class의 hello() method를 사용하는 방법은 없을까요?

 

물론 있습니다. 아래 코드를 봅시다.

 

using System;

class Dog
{
    public virtual void hello()
    {
        Console.WriteLine("Dog Hello!");
    }
}

class Cat : Dog
{
    public override void hello()
    {
        Console.WriteLine("Cat Hello!");
    }
}

class MyProgram
{
    static void Main()
    {
        Dog test_cat = new Cat();

        test_cat.hello();
    }
}


-- Result
Cat Hello!

결과를 보면 hello() method가 Cat Hello! 를 출력하며 자식 class인 Cat class의 hello() method를 제대로 보유하고 있음을 알 수 있습니다.

 

이를 가능하게한 키워드는 virtual과 override입니다.

 

 

...
class Dog
{
    public virtual void hello()
    {
        Console.WriteLine("Dog Hello!");
    }
}

class Cat : Dog
{
    public override void hello()
    {
        Console.WriteLine("Cat Hello!");
    }
}
...

class의 생성 과정만을 나타낸 부분입니다.

 

보면 부모 class의 hello() method에 virtual이라는 키워드가 붙었습니다.

자식 class의 hello() method에는 override라는 키워드가 붙었구요.

 

virtual 키워드는 virtual 키워드가 적용된 method를 가상의 형태로 만들라는 겁니다.

즉, 언제든 다른 method에게 덮어씌워져서 사라질 수 있도록 하라는 의미이죠.

 

override 키워드는 override 키워드가 적용된 method로 다른 method를 덮어씌울 수 있도록 하라는 것입니다.

즉, Cat class의 hello() method는 override 키워드와 함께 정의되어있으므로 혹시나 class의 상속이 발생하고 부모 class가 hello() method를 가지고있으면 부모 class의 hello() method를 지워버리고 자식 class의 hello() method를 남겨두라는 의미입니다.

 

 

 

 

상속 과정에서 virtual과 override 키워드를 사용한 다양한 경우를 볼 수 있는 예시입니다.

이 예시를 보는 것이 가장 중요합니다.

 

using System;

class Dog
{
    public void hello1()
    {
        Console.WriteLine("Dog Hello!");
    }

    public virtual void hello2()
    {
        Console.WriteLine("Dog Hello!");
    }

    public virtual void hello3()
    {
        Console.WriteLine("Dog Hello!");
    }
}

class Cat : Dog
{
    public void hello1()
    {
        Console.WriteLine("Cat Hello!");
    }

    public void hello2()
    {
        Console.WriteLine("Cat Hello!");
    }

    public override void hello3()
    {
        Console.WriteLine("Cat Hello!");
    }
}

class MyProgram
{
    static void Main()
    {
        Dog test_cat = new Cat();

        test_cat.hello1();
        test_cat.hello2();
        test_cat.hello3();
    }
}



-- Result
Dog Hello!
Dog Hello!
Cat Hello!

 

각 method별로 부모 class의 method와 자식 class의 method 중 어떤 method가 상속이 진행된 후 살아남을지 봐봅시다.

 

 

 

 

상속 결과 부모 class와 자식 class의 겹치는 속성/method의 우선순위를 확인할 때에는 가장 먼저 class를 이용해서 객체를 만드는 부분을 봐야합니다.

 

Dog test_cat = new Cat();

 

test_cat 객체를 만드는데

타입은 Dog class입니다.

그리고 내용은 Cat class의 내용을 가져온다고 보면 됩니다.

 

가장 중요한건 타입입니다.

객체의 타입이 Dog class로 지정되었으므로 일단 Cat과 Dog의 상속 관계에선 Dog class의 내용이 우선적으로 남아있게됩니다.

 

즉, Dog class와 Cat class의 상속이 발생하고, 두 class에 동일한 이름의 속성/method가 존재할 경우 Dog class의 속성/method를 우선적으로 남기고 Cat class의 속성/method는 지운다는 의미입니다.

 

 

 

 

 

 

 

 

using System;

class Dog
{
    public void hello1()
    {
        Console.WriteLine("Dog Hello!");
    }

    ...
}

class Cat : Dog
{
    public void hello1()
    {
        Console.WriteLine("Cat Hello!");
    }

    ...
}

class MyProgram
{
    static void Main()
    {
        Dog test_cat = new Cat();

        test_cat.hello1();
        ...
    }
}


-- Result
Dog Hello!
...

먼저 hello1() method입니다.

 

Cat class에 Dog class를 상속시키고있습니다.

 

두 class 모두 hello1() method를 가지고 있으므로 둘 중 하나는 사라져야합니다.

 

Dog class의 hello1(), Cat class의 hello1() method 모두 virtual, override 등의 키워드가 없으므로 기본적으로 설정된 override rule을 따릅니다.

 

Dog test_cat = new Cat();

객체 생성부분을 보면 Dog 이 객체의 타입으로 지정되어있습니다.

따라서 Dog class의 내용이 우선적으로 남아있게 되므로 Dog class의 hello1() method가 최종적으로 살아남습니다

따라서 hello1()의 결과로 Dog Hello! 가 출력됩니다.

 

 

 

 

 

 

using System;

class Dog
{
    ...

    public virtual void hello2()
    {
        Console.WriteLine("Dog Hello!");
    }

    ...
}

class Cat : Dog
{
    ...

    public void hello2()
    {
        Console.WriteLine("Cat Hello!");
    }

    ...
}

class MyProgram
{
    static void Main()
    {
        Dog test_cat = new Cat();

        ...
        test_cat.hello2();
        ...
    }
}



-- Result
...
Dog Hello!
...

hello2() method입니다.

 

Cat class에 Dog class를 상속시키고있습니다.

 

두 class 모두 hello2() method를 가지고 있으므로 둘 중 하나는 사라져야합니다.

 

Dog class의 hello2()는 virtual 키워드와 함께 선언되었으므로 사라질 준비가 된 method입니다.

Cat class의 hello2() method 는 아무 키워드 없이 일반 method로 선언되었습니다.

 

비록 Dog class의 hello2() method가 virtual 키워드로 선언되었어도 Cat class의 hello2()는 따로 override를 원하지 않습니다.

따라서 기본 설정을 따라갑니다.

 

Dog test_cat = new Cat();

객체 생성부분을 보면 Dog 이 객체의 타입으로 지정되어있습니다.

따라서 Dog class의 내용이 우선적으로 남아있게 되므로 Dog class의 hello2() method가 최종적으로 살아남습니다.

따라서 hello2()의 결과로 Dog Hello! 가 출력됩니다.

 

 

 

 

 

 

using System;

class Dog
{
    ...

    public virtual void hello3()
    {
        Console.WriteLine("Dog Hello!");
    }
}

class Cat : Dog
{
    ...

    public override void hello3()
    {
        Console.WriteLine("Cat Hello!");
    }
}

class MyProgram
{
    static void Main()
    {
        Dog test_cat = new Cat();

        ...
        test_cat.hello3();
    }
}


-- Result
...
Cat Hello!

hello3() method입니다.

 

Cat class에 Dog class를 상속시키고있습니다.

 

두 class 모두 hello3() method를 가지고 있으므로 둘 중 하나는 사라져야합니다.

 

Dog class의 hello3()는 virtual 키워드와 함께 선언되었으므로 사라질 준비가 된 method입니다.

Cat class의 hello3() method 는 override 키워드와 함께 선언되었으므로 상속 과정에서 존재하는 모든 hello3() method를 지워버립니다. Cat class의 hello3() method가 최종적으로 살아남을거라는 의미입니다.

 

한가지 주의할 것은 override 키워드로 덮어씌울 method는 반드시 virtual 키워드로 선언되어야만 합니다.

virtual 키워드는 사라질 준비가 되어있는 method라는 의미이기 때문이죠.

 

Dog test_cat = new Cat();
객체 생성부분을 보면 Dog 이 객체의 타입으로 지정되어있습니다.

 

따라서 기본값은 Dog class의 내용이 우선적으로 남아있게 된다는 의미입니다.

하지만 virtual과 override 키워드로 인해 Cat class의 hello3() method가 Dog class의 hello3() method를 덮어씌워버려서 Cat class의 hello3() method가 최종적으로 살아남습니다.

 

따라서 hello3()의 결과로 Cat Hello! 가 출력됩니다.

 

 

 

지금까지 본 예시처럼 상속의 관계, 객체 생성 시 변수의 type이 부모 class인지 자식 class인지, method들이 virtual/override 키워드로 선언되었는지 등 여러 상황에 따라 같은 이름의 method라도 다른 기능을 나타낼 수 있습니다.

 

이러한 성질을 다형성(Polymorphism)이라고 합니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

상속에서 사용되는 것 중 sealed라는 키워드에 대해 알아봅시다.

 

using System;

sealed class Dog
{
    public string name = "Dog";

    public void hello()
    {
        Console.WriteLine("Dog Hello!");
    }
}

public class Cat : Dog
{
    public string name = "Cat";
}

class MyProgram
{
    static void Main()
    {
        Cat test_cat = new Cat();

        Console.WriteLine(test_cat.name);
        test_cat.hello();
    }
}

위 코드를 실행하면 Error가 발생합니다.

 

그 이유는 Cat class는 Dog class로부터 내용을 상속받도록 코드가 작성되어있는데, Dog class가 sealed 키워드와 함께 선언되었기 때문입니다.

 

sealed 키워드가 붙은 채로 생성된 class는 다른 class에게 상속해줄 수 없습니다.

즉, sealed 키워드가 붙은 채로 생성된 class는 다른 class에게 자신의 정보를 줄 수 없습니다.

 

 

 

 

 

 

728x90
반응형
Comments