안녕하세요!


이번엔 C# 에서의 abstract, virtual 그리고 override 키워드에 대해서 알아보도록 하겠습니다.

나열된 키워드들은 모두 재정의 또는 구현과 관계가 있는데요.

이 키워드들은 언제, 어떤 상황에서 사용되는지 알아볼까요?


1. abstract (추상)

이 키워드를 사용하려면 해당 클래스도 반드시 abstract 클래스여야 합니다.

이 키워드로 표시한 속성이나 메서드는 본문을 정의할 수 없으며 반드시 접근 한정자는 public, internal 또는 protected 중에 하나여야 합니다.


2. virtual (가상)

이 키워드는 정적 클래스(static class)를 제외한 모든 클래스에서 사용이 가능합니다.

이 키워드로 표시한 속성이나 메서드는 본문을 정의할 수 있으며 abstract 와 마찬가지로 접근 한정자는 public, internal 또는 protected 중에 하나여야 합니다.


3. override (우선시하다 / 치환하다) - 백과사전

이 키워드는 abstract 또는 virtual 키워드로 표시된 속성이나 메서드에만 사용이 가능합니다.

그렇기 때문에 이 키워드는 어떤 클래스를 상속하는 하위 클래스에서만 사용이 가능합니다.



아주 짧게, abstract 와 virtual 그리고 override 키워드에 대해서 살펴보았는데요! 여기서 의문점이 하나 생기게 됩니다.

abstract 와 virtual 이 둘의 차이점이 단지 본문을 정의할 수 있느냐 없느냐로 나뉘는 것 같지만 다행히도 abstract 키워드에는 한 가지 제약 조건이 더 있습니다.

바로 "이 키워드를 사용하려면 해당 클래스도 반드시 abstract 클래스여야 한다" 는 것인데요!

abstract 키워드의 강점은 따로 있습니다.

  • abstract 키워드로 표시한 속성이나 메서드는 파생 클래스에서 반드시 구현해야 한다.
  • 일반 클래스의 virtual 속성/메서드를 abstract 로 재정의가 가능하다.
  • abstract 키워드로 표시된 클래스는 파생 클래스의 생성자를 통해서만 사용해서만 개체(Instance) 생성이 가능하다.
  • abstract 키워드를 속성 또는 메서드에 사용하려는 경우, 해당 클래스가 반드시 abstract 로 표시되어 있는 클래스여야 한다.

abstract 키워드로 표시한 속성이나 메서드는 파생 클래스에서 반드시 구현해야 한다
virtual 의 경우는 파생 클래스에서 따로 재정의(구현)를 하지 않아도 상관이 없습니다. 왜냐? 이미 부모 클래스에서 본문을 정의했기 때문입니다. 심지어 본문의 내용이 { } 처럼 비어있을 경우에도 말이죠.
하지만 abstract 의 경우는 다릅니다. 본문이 정의되어 있지 않기 때문에 파생 클래스에서는 반드시 본문을 정의해야 한다는 제약을 걸 수가 있습니다.
파생 클래스에서 본문을 정의하지 않는다면 아래의 오류를 만나실 수 있습니다.



일반 클래스의 virtual 속성/메서드를 abstract 로 재정의가 가능하다
일반 클래스의 virtual 속성/메서드를 abstract 로 재정의
class Root {
    protected virtual void Do() { }
}
abstract class Child : Root {
    protected abstract override void Do();
}

이런식으로 코딩이 가능하다는 것이죠.


abstract 키워드로 표시된 클래스는 파생 클래스의 생성자를 통해서만 개체(Instance) 생성이 가능하다.

abstract 클래스의 개체(Instance) 생성
class Root {
    protected virtual void Do() { }
}
abstract class Child : Root {
    protected int id;
    static int ObjRef = 0;
    protected Child() {
        id = ObjRef++;
    }
    protected abstract override void Do();
}
class Element : Child {
    public Element() : base() {
        Console.WriteLine("Element ID = {0}", id);
    }
}

직접 코딩을 하신 후 테스트해보시면 아시겠지만, abstract 클래스의 경우는 절대 자기 자신의 개체를 생성할 수 없습니다.

이 말은 위 코드에 정의된 클래스 중 abstract 로 표시된 Child 클래스의 개체를 생성하지 못한다는 뜻입니다. 코드로는 Child c = new Child(); 이렇게 표현이 가능한데, 이 경우에 오류가 발생한다는 것입니다. "코드에서 Child 클래스의 생성자의 접근 한정자를 protected 로 선언했기 때문에 안될 수도 있지" 라고 생각하실 수도 있는데, 생성자의 접근 한정자를 protected 가 아닌 public 으로 변경하고 위 코드로 개체를 생성하려고 시도하면 다음과 같은 오류를 만나실 수 있습니다.


abstract의 개체를 직접적으로 생성하는건 불가능하다는 것입니다. - 반드시 파생 클래스를 거쳐야 가능하죠.



abstract 키워드를 속성 또는 메서드에 사용하려는 경우, 해당 클래스가 반드시 abstract 로 표시되어 있는 클래스여야 한다.

추상 클래스의 추상 속성과 일반 클래스의 추상 속성 (C#)
class AbstractPropertyInNormalClass {
    public abstract int Age { get; }
}
abstract class AbstractPropertyInAbstractClass {
    public abstract int Age { get; }
}

이 코드를 실행하면 어떤 결과가 나올까요? 추상 속성이나 메서드는 반드시 추상 클래스에 정의되어야 하므로 AbstractPropertyInNormalClass 에서는 오류가 발생하게 됩니다.


약간 깊게 abstract 에 대해서 살펴보았는데요!! 이제 virtual 에 대해서 살펴보도록 하겠습니다.

virtual 키워드는 클래스에는 사용할 수 없고 오로지 속성과 메서드에만 사용이 가능합니다. 그리고 abstract 와는 다르게 본문을 정의해줄 수가 있죠.

.NET 에서는 최상위 클래스로 System.Object 클래스가 사용됩니다. 이 클래스는 만들어진 모든 클래스가 자동으로 상속하는 클래스입니다. 좀 다르게 말하자면 모든 .NET 클래스는 System.Object 의 파생 클래스인 셈이죠.

이걸 어떻게 입증할거냐구요? 제가 입증하지 않아도 됩니다. 아무 클래스나 만들고, 클래스 안에 override 를 쳐보시면.. 다음 그림처럼 나오는 것을 보실 수 있습니다!


분명 아무런 클래스도 상속하지 않고 있는데 세 개의 메서드(Equals, GetHashCode, ToString)를 재정의할 수 있다고 하네요!?

조금만 더 파고 들어가 보면 다음과 같은 코드를 보실 수 있습니다. (System.Object 클래스를 디컴파일한 결과입니다)



그림이 잘 보이지 않는 분들은 object.cs < 이 링크를 통해서도 소스 코드를 보실 수 있습니다. (마이크로소프트의 공식 사이트입니다. 안전해요!)

소스 코드를 보시면 아시겠지만, 총 세 개의 메서드가 virtual 로 표시되어 있습니다.

그리고 코드에서 보실 수 있듯이, 본문이 정의되어 있습니다!!

  • public virtual string ToString()
  • public virtual bool Equals(object obj)
  • public virtual int GetHashCode()


이 메서드들.. 이름이 익숙한 것 같죠? 네 자주 보던 메서드가 맞습니다. 어떤 자료형을 사용해도! 어떤 형식을 사용해도 계속 보이는 그것!! ToString() 메서드입니다.

ToString() 메서드가 여러분들에게 가장 익숙한 메서드가 아닐까 싶네요. int 부터 시작해서, string까지.. 심지어 내가 만든 클래스에서도 보이는 이 메서드는 System.Object 클래스에 정의되어 있는 메서드입니다.


테스트를 위해 아주 간단한 클래스 하나를 작성해 보도록 하겠습니다. - 단지 정말 "클래스" 이기만 한 클래스를 말이죠.

정말 "클래스" 이기만 한 클래스 (C#)
using System;

public class Program {
    public static void Main(string[] args) {
        MyClass mc = new MyClass();
        Console.WriteLine("{0}", mc.ToString());
    }
}

class MyClass { }
예제 코드 (.NET Fiddle)


위 프로그램의 실행 결과는 과연 어떨까요?


MyClass 라는 클래스의 이름이 나왔습니다! 기본적으로 ToString() 메서드는 값 형식에 대해서는 값 자체를 출력하고, 참조 형식의 경우 해당 형식에 대한 이름만 출력하는 것이 전부입니다. (단, ToString() 메서드를 재정의한 경우는 제외)

그럼 이제 ToString() 메서드를 재정의하면 어떤 결과가 나오는지 보도록 하겠습니다.

ToString() 메서드가 재정의된 클래스 (C#)
using System;

public class Program {
    public static void Main(string[] args) {
        MyClass mc = new MyClass();
        Console.WriteLine("{0}", mc.ToString());
    }
}

class MyClass {
    public override string ToString() {
        return "제가 만든 MyClass 의 ToString() 메서드입니다!";
    }
}
예제 코드 (.NET Fiddle)


결과는! 예상하신 대로입니다.


ToString() 메서드에서 반환한 값이 화면에 출력되고 있습니다. 이렇게 ToString() 메서드는 해당 형식의 내용이나 값을 화면 혹은 사용자에게 알리기 위해서 사용되기도 합니다.

virtual 키워드에 대해서 알아보려고 했는데.. ToString() 메서드를 예로 들면서 다른 이야기를 너무 많이 했네요!!!!

virtual 키워드의 강점에 대해 알아보도록 하겠습니다.

  • 본문을 정의해야 한다.
  • 반드시 구현할 필요가 없다. - 재정의는 본인의 몫.
  • 재정의를 해도 기존의(부모 클래스에 virtual 로 표시된) 속성이나 메서드 등을 호출할 수 있다.


본문을 정의해야 한다
virtual 로 표시된 속성/메서드의 본문 정의 (C#)
class VirtualMethodInClass {
    public virtual void VirtualMethod() {
        Console.WriteLine("VirtualMethod 호출되었습니다!");
    }
}

위 코드처럼 virtual 키워드가 사용된 속성이나 메서드 등에는 본문을 정의하는 것이 가능합니다. 아무 작업도 안하도록 { } 과 같은 빈 본문을 사용할 수도 있지만 그래도 반드시 본문을 정의해야 한다는 것!



반드시 구현할 필요가 없다. - 재정의는 본인의 몫

virtual 키워드는 반드시 구현하지 않아도 된다 (C#)
class ChildClass : VirtualMethodInClass {
}

VirtualMethodInClass 를 상속하고 있으면서, VirtualMethod 메서드는 재정의하지 않은 것을 보실 수 있습니다.

재정의를 해도 되고 안해도 되는 virtual 키워드의 특징을 보실 수 있습니다.


재정의를 해도 기존의(부모 클래스에 virtual 로 표시된) 속성이나 메서드 등을 호출할 수 있다

재정의를 한 후 기존의 메서드를 호출하기 (C#)
class ChildClass : VirtualMethodInClass {
    public override void VirtualMethod() {
        Console.WriteLine("ChildClass 에서 재정의된 VirtualMethod 가 호출되었습니다.");
        base.VirtualMethod();
    }
}

ChildClass 에서는 override 를 이용해 VirtualMethodInClass 클래스의 VirtualMethod 메서드를 재정의하고 있습니다.

이 때, 메서드는 완전히 재정의되는 것이지만 클래스(ChildClass) 내부에서는 원래(재정의되기 전=virtual로 표시된)의 메서드를 호출할 수가 있습니다.

base 키워드를 사용하는 것이죠.


마지막 예시를 든 코드를 한번 실행해 보도록 하겠습니다. 과연 결과는 어떻게 나올까요?

예제 코드 (C#)
public class Program {
    public static void Main(string[] args) {
        ChildClass c = new ChildClass();
        c.VirtualMethod();
    }
}

class VirtualMethodInClass {
    public virtual void VirtualMethod() {
        Console.WriteLine("VirtualMethod 호출되었습니다!");
    }
}
class ChildClass : VirtualMethodInClass {
    public override void VirtualMethod() {
        Console.WriteLine("ChildClass 에서 재정의된 VirtualMethod 가 호출되었습니다.");
        base.VirtualMethod();
    }
}

실행 결과입니다!


ChildClass 에서 재정의한 대로 "ChildClass 에서 재정의된 VirtualMethod 가 호출되었습니다." 라는 문자열과 기존 VirtualMethodInClass 클래스에 정의된 VirtualMethod 메서드가 호출되어 "VirtualMethod 호출되었습니다!" 라는 문자열을 보실 수 있습니다.


abstract 및 virtual 키워드를 설명하면서 override 키워드를 보셨었는데요! 이 키워드는 재정의할 때 사용되지만 추상(abstract) 클래스의 속성이나 메서드 등을 구현하기 위해서 사용되기도 합니다.


이 글을 통해서 C# 및 재정의에 대해 공부하는 것이 도움이 되셨으면 좋겠습니다. 긴 글 읽어주셔서 감사합니다.

+ Recent posts