상속으로 클래스 확장하기

  • 43 minutes to read

클래스 간에는 부모와 자식 간의 관계를 설정할 수 있습니다. 이러한 내용을 개체 관계 프로그래밍(object relationship programming)이라고도 합니다. 상속은 부모 클래스에 정의된 기능을 다시 사용하거나 확장 또는 수정하여 자식 클래스로 만드는 것을 말합니다. 특정 클래스에서 만든 기능을 다른 클래스에 상속을 통해 물려주면 여러 가지 장점이 있습니다. 이번에는 클래스(개체)들 간의 관계를 따져보는 내용을 묶어서 살펴보겠습니다.

> // 상속: 부모 클래스에 정의된 기능을 다시 사용, 확장 및 수정하여 자식 클래스로 만들기

클래스 상속

개체 지향 프로그래밍의 장점 중의 하나는 이미 만들어져 있는 클래스를 재사용하는 것입니다. 이때 재사용에 대한 핵심 개념이 바로 상속입니다. 부모의 재산을 자식에게 상속하듯이 부모 클래스(기본 클래스)의 모든 멤버를 자식 클래스(파생 클래스)에게 재사용하도록 허가하는 기능을 의미합니다. 여러 클래스들 간의 관계를 설정할 때 수평 관계가 아닌 부모와 자식 간의 관계처럼 계층적인 관계를 표현하고자 할 때 사용하는 개념을 상속이라 합니다. 클래스 상속은 단일 상속(single inheritance)과 다중 상속(multiple inheritance)으로 구분할 수도 있습니다. 단, C#의 클래스 상속은 단일 상속만을 지원합니다. 다중 상속은 앞으로 다룰 인터페이스를 통해서 지원받을 수 있습니다.

클래스 상속 구문

C#에서는 다음과 같이 클래스명 뒤에 콜론(:) 기호를 붙이고 부모가 되는 클래스명을 붙입니다.

public class 기본클래스이름
{
    // 기본 클래스의 멤버 정의
}

public class 파생클래스이름 : 기본클래스이름
{
    // 기본 클래스의 멤버를 포함한 자식 클래스의 멤버정의
}

System.Object 클래스

System.Object 클래스는 모든 클래스들의 부모 클래스입니다. 닷넷에서 가장 높은 층에 속하는 시조(조상) 클래스라고 볼 수 있습니다. 새롭게 만들어지는 C#의 모든 클래스들은 Object 클래스로부터 상속을 자동으로 받기에 Object 클래스를 상속하는 코드는 생략 가능합니다.

기본(Base) 클래스

다른 클래스의 부모(Parent) 클래스가 되는 클래스를 기본 클래스라고 합니다. 기본 클래스는 다른 말로 Base 클래스, Super 클래스로 표현합니다.

파생(Derived) 클래스

다른 클래스의 자식(Child) 클래스가 되는 클래스를 파생 클래스라고 합니다. 파생 클래스는 다른 클래스로부터 멤버를 물려받은 클래스이고 파생 클래스는 Derived 클래스, Sub 클래스 또는 자식(Child) 클래스로 표현합니다.

부모 클래스와 자식 클래스

프로그래밍에서 상속을 표현할 때 상속을 주는 클래스를 부모 클래스라 하고 상속을 받는 클래스를 자식 클래스라고 합니다. 콜론(:) 기호로 상속 관계를 지정하면 부모 클래스의 public 멤버들은 자식 클래스에서 그대로 물려받아 사용할 수 있게 됩니다. 뒤에서 다루겠지만 public을 포함하여 protected로 선언된 멤버들도 자식 클래스에서 사용이 가능합니다. 그럼 상속을 다루는 예제를 만들어보겠습니다.

코드: InheritanceDemo.cs

//[?] 상속(Inheritance): 부모 클래스의 기능을 자식 클래스에서 물려받아 사용
using System;

namespace InheritanceDemo
{
    //[1] 부모 클래스 선언
    class Parent
    {
        public void Foo() => Console.WriteLine("부모 클래스의 멤버 호출");
    }

    //[2] 자식 클래스 선언
    class Child : Parent
    {
        public void Bar() => Console.WriteLine("자식 클래스의 멤버 호출");
    }

    class InheritanceDemo
    {
        static void Main()
        {
            // 자식 클래스의 인스턴스 생성
            var child = new Child();
            child.Foo(); // 부모 클래스의 멤버 호출
            child.Bar(); // 자식 클래스의 멤버 호출
        }
    }
}
부모 클래스의 멤버 호출
자식 클래스의 멤버 호출

[1]번 코드 영역의 Parent 클래스를 정의하는 코드의 뒤에는 다음 샘플 코드와 같이 Object 클래스로부터 상속받는 코드가 생략되어 있습니다. 이처럼 C#의 조상(시조) 클래스인 Object 클래스를 상속하는 코드를 직접 입력하여 클래스를 선언하지는 않겠지만 내부적으로 이렇게 되어 있다는 정도만 기억하고 있으면 됩니다.

class Parent : Object

[2]번 코드처럼 부모 클래스의 기능을 자식 클래스에서 재사용하려면 콜론 기호를 붙여 상속의 관계를 설정할 수 있습니다.

class Child : Parent

Child 클래스에서는 Parent 클래스에 구현된 public, protected 멤버들을 상속해서 사용할 수 있습니다. 부모 클래스에 선언된 Foo() 메서드는 자식 클래스에서 따로 선언하지 않아도 자식 클래스의 인스턴스 생성 후 부모 클래스의 멤버에 접근이 가능합니다. 이처럼 상속은 공통적인 기능은 부모 클래스에 선언하고 이를 재사용하는 느낌으로 자식 클래스에서 가져다 사용하는 기능을 제공합니다. 부모 클래스의 멤버를 자식 클래스에서 사용할 수 있도록 하려면 public 또는 protected 액세스 한정자를 붙여야 합니다.

InheritanceDemo.py

코드: InheritanceDemo.py

# [?] 상속(Inheritance): 부모 클래스의 기능을 자식 클래스에서 물려받아 사용

# 부모: Base
class Parent():
    def Say(self):
        print("부모 말하다.")

# 자식: Derived
class Child(Parent):
    #pass
    def Say(self):
        Parent.Say(self) 
        print("자식 말하다.")

# 자식 클래스의 인스턴스 생성
child = Child()
child.Say() # 부모로부터 상속
부모 말하다.
자식 말하다.

Base 클래스와 Sub 클래스

부모 클래스, 기반 클래스, 기본 클래스, 슈퍼 클래스가 같은 개념이고, 자식 클래스, 파생 클래스, 서브 클래스가 같은 개념입니다. 자식 클래스에서는 아무런 멤버를 구현하지 않고 부모 클래스의 내용만을 물려받는 모양을 살펴보겠습니다.

코드: BaseSub.cs

// 
> // 부모 클래스
> public class BaseClass
. {
.     public void Do() => Console.WriteLine("Base 클래스에 정의된 메서드");
. }
> 
> // 자식 클래스
> public class SubClass : BaseClass { } // 자식 클래스는 빈 클래스로 구현 
> 
> var sub = new SubClass();
> sub.Do(); // Base 클래스에 정의된 public 또는 protected 멤버 사용 가능
Base 클래스에 정의된 메서드

사실 이번 예제처럼 만들 일은 없습니다. 기반과 서브의 개념으로 부모와 자식의 관계처럼 부모의 기능을 자식에서 사용하는 개념이 바로 상속인 것입니다.

Super 클래스와 Sub 클래스

부모와 자식을 나타내는 클래스를 Super 클래스와 Sub 클래스로 구분하기도 합니다.

Object 클래스 상속

이미 여러 번 언급한 내용이지만, 부모 자식 간의 관계가 아닌 모든 클래스는 내부적으로 Object 클래스를 상속받습니다. 다음 코드처럼 Main 메서드를 포함하고 있는 클래스 뒤에 콜론 기호를 붙이고 System.Object를 명시할 수 있습니다. 물론 이러한 코드는 앞으로도 생략해서 사용할 것입니다.

코드: ObjectClass.cs

> public class Parent : System.Object
. {
.     public static void Hi() => Console.WriteLine("안녕하세요.");
. }
> 
> public class Child : Parent
. {
.     public static void Hello() => Console.WriteLine("반갑습니다.");
. }
> 
> Child.Hi();
안녕하세요.
> Child.Hello();
반갑습니다.

부모 클래스를 포함한 모든 클래스들은 기본적으로 상속 구문을 지정하지 않으면 Object 클래스를 상속하게 됩니다. 그러면 Object 클래스에 정의되어 있는 기본 기능들을 모든 클래스가 물려받아 사용이 가능합니다.

부모 클래스 형식 변수에 자식 클래스의 개체 할당

자식 클래스의 인스턴스를 부모 클래스 형식 변수에 담을 수 있습니다. 이는 자식 클래스의 인스턴스와 동일합니다. 클래스의 멤버에 정의된 override 키워드는 Object 클래스의 ToString() 메서드를 재 정의(Override)하겠다는 의미입니다.

코드: InheritancePractice.cs

> class Developer
. {
.     public override string ToString()
.     {
.         return "개발자";
.     }
. }
> 
> class WebDeveloper : Developer
. {
.     public override string ToString() => "웹 개발자";
. }
> 
> class MobileDeveloper : Developer
. {
.     public override string ToString() => "모바일 개발자";
. }
> 
> var web = new WebDeveloper();
> Console.WriteLine(web); // 웹 개발자
웹 개발자
> 
> var mobile = new MobileDeveloper();
> Console.WriteLine(mobile); // 모바일 개발자
모바일 개발자

부모 클래스 변수에 자식 클래스의 인스턴스를 할당하는 것은 프로그래밍에 약간의 융통성을 주는 행위입니다. 예를 들어 미래에 어떤 값이 들어올지 모르는 경우에는 부모 클래스 변수로 메서드의 매개 변수를 만들어 사용하면 해당 부모 클래스를 상속받는 모든 자식 클래스들의 값을 매개 변수로 받을 수 있는 여유가 생깁니다. 이러한 구조는 앞으로 배울 인터페이스 상속에도 그대로 적용됩니다.

상속(Inheritance)은 영어로 is a(is an) 관계를 표현

상속은 영어로 "~ is a ~" 관계를 표현합니다. 예를 들어 "자동차는 운송 수단입니다."는 "Car is a Vehicle" 형태로 Car 클래스는 Vehicle 클래스의 자식 클래스가 됩니다.

코드: IsAn.cs

//[?] 상속(Inheritance)은 영어로 is a(is an) 관계를 표현
using System;

namespace IsAn
{
    class Vehicle { }

    class Car : Vehicle { }

    class Airplane : Vehicle { }

    class IsAn
    {
        static void Main()
        {
            // 운송 수단(탈것) is a Vehicle.
            Vehicle vehicle = new Vehicle();
            // 자동차 is a Vehicle.
            Vehicle car = new Car();
            // 비행기 is a Vehicle.
            Vehicle airplane = new Airplane();

            // 개체를 문자열로 출력하면 ToString() 메서드가 실행됨 
            Console.WriteLine($"{vehicle}, {car}, {airplane}");
        }
    }
}
IsAn.Vehicle, IsAn.Car, IsAn.Airplane

개체를 생성할 때 부모 클래스로 변수를 만들 수 있습니다. 부모 클래스를 상속하는 모든 자식 클래스들은 부모 클래스를 사용하여 개체 변수를 선언할 수 있습니다. 이때 개체의 성질은 뒤에서 지정하는 생성자가 어떤 것인지에 따라 결정이 됩니다. new Vehicle(); 형태는 Vehicle 클래스의 인스턴스가 되는 것이고 new Car(); 형태는 Car 클래스의 인스턴스 그리고 new Airplane(); 형태는 Airplane 클래스의 인스턴스가 되는 것입니다.

this와 this() 그리고 base와 base()

클래스내에서 this는 나 자신을 의미하고 this()는 나 자신의 생성자를 나타냅니다. 마찬가지로 base는 부모 클래스를 의미하고 base()는 부모 클래스의 생성자를 나타냅니다.

클래스 상속을 통한 부모의 protected 멤버에 접근하기

부모 클래스의 public, protected 멤버는 자식 클래스에서 물려받아 사용이 가능합니다. 이미 만들어져 있는 기능을 그대로 다시 사용하고자 한다면 클래스의 상속을 통해서 이를 구현할 수 있습니다. 클래스 상속을 통해서 자식 클래스에서 부모 클래스의 멤버에 접근하는 예를 실습으로 살펴보겠습니다.

코드: ClassInheritance.cs

using System;

namespace ClassInheritance
{
    public class ParentClass : Object //[A] 모든 클래스는 Object 클래스로부터 상속
    {
        protected void Print1() => Console.WriteLine("부모 클래스에서 정의한 내용");
    }

    public class ChildClass : ParentClass //[B] 콜론 기호로 부모 클래스 지정
    {
        public void Print2() => 
            base.Print1(); //[C] 자식에서 base 키워드로 부모 요소에 접근
    }

    class ClassInheritance : Object
    {
        static void Main()
        {
            //[1] 부모 클래스의 인스턴스 생성
            ParentClass p = new ParentClass();
            Console.WriteLine(p.ToString()); // ClassInheritance.ParentClass
            //[2] 자식 클래스의 인스턴스 생성
            ChildClass c = new ChildClass();
            //c.Print1(); // Print1() 메서드는 protected로 설정되어 있어 외부에서 접근 불가 
            c.Print2(); // 자식 클래스에 직접 구현한 기능
        }
    }
}
ClassInheritance.ParentClass
부모 클래스에서 정의한 내용

[A] 코드와 같이 모든 클래스들은 따로 표기를 하지 않아도 Object 클래스로부터 상속을 받기에 ToString() 메서드와 같이 그 기능이 Object 클래스에 정의되어 있는 메서드를 바로 사용할 수 있습니다. [B] 코드처럼 클래스 선언시 클래스명 뒤에 콜론 기호를 붙이고 부모가 될 클래스명을 적어주는 것만으로도 간단히 public 또는 protected로 선언된 모든 멤버에 접근할 수 있는 기능이 바로 상속인 것입니다. [C] 코드처럼 자식 클래스에서 부모 클래스의 public 또는 protected 멤버에 바로 접근할 때에는 base 키워드를 통해서 호출이 가능합니다.

base 키워드를 사용하여 부모 클래스의 생성자 호출

특정 부모 클래스를 상속하는 자식 클래스의 생성자에서 바로 어떤 일을 처리하지 않고 부모 클래스의 생성자에게 전달해 주는 경우가 있습니다. 이때에는 자식 클래스의 생성자에서 콜론 기호 뒤에 base()를 사용하여 부모 클래스의 생성자를 호출합니다. 다음 코드는 자식 클래스의 생성자에서 부모 클래스의 매개 변수가 있는 생성자에게 문자열을 전달하는 내용을 보여줍니다.

코드: ConstrucotrBase.cs

using System;

namespace ConstructorBase
{
    class Parent
    {
        // 매개 변수로 넘어온 값 출력
        public Parent(string message) => Console.WriteLine(message);
    }
    class Child : Parent
    {
        //[1] base()를 사용하여 부모 클래스의 생성자 호출
        public Child(string message) : base(message) { }
    }
    class ConstructorBase
    {
        static void Main()
        {
            string message = "자식 클래스의 생성자 호출 시 부모 클래스의 생성자로 전달";
            var child = new Child(message);
        }
    }
}
자식 클래스의 생성자 호출 시 부모 클래스의 생성자로 전달

자식 클래스의 인스턴스를 생성하면 자식 클래스의 생성자가 호출되는데 이때 자식 클래스의 생성자에서 특정한 기능을 구현하지 않고 부모 클래스의 생성자에서 기능을 구현하고자 할 때에는 자식 클래스의 생성자에서 바로 base() 코드를 사용하여 부모 생성자를 호출할 수 있습니다. 이처럼 생성자도 메서드처럼 부모와 자식 클래스 간에 동일한 기능을 하는 경우라면 부모 클래스에 그 기능을 넣어 놓고 자식 클래스에서 재사용합니다.

base 키워드로 부모 클래스의 멤버에 접근

base 키워드를 사용하여 부모의 멤버에 접근하는 방법을 다시 한번 알아보겠습니다.

코드: BaseKeyword.cs

using System;

namespace BaseKeyword
{
    public class Car
    {
        private string name; 
        public Car(string name)
        {
            this.name = name;
        }
        public void Run() => Console.WriteLine($"{this.name}가 달린다.");
    }
    public class My : Car
    {
        public My() : this("나의 자동차") { }
        public My(string name) : base(name) { }
    }
    public class Your : Car
    {
        public Your() : base("너의 자동차") { }
    }

    class BaseKeyword
    {
        static void Main()
        {
            (new My()).Run();
            (new My("나의 끝내주는 자동차")).Run();
            new Your().Run();
        }
    }
}
나의 자동차가 달린다.
나의 끝내주는 자동차가 달린다.
너의 자동차가 달린다.

C#에서 base 키워드는 현재 클래스의 부모 클래스를 의미합니다. 만약, 따로 상속 구문을 지정하지 않았으면 base 키워드는 Object 클래스를 가리킵니다.

생성자 상속

앞서 알아본 base() 형태를 생성자 상속이라고 합니다. 이번에는 생성자 상속 예제를 만들어보겠습니다. 현재 코드는 부록의 “Visual Studio 디버거 사용하기”를 참고해서 F10을 여러 번 눌러가면서 단계별로 실행되는 순서를 살펴보면 코드를 이해하는데 좀 더 도움이 됩니다.

코드: ConstructorInheritance.cs

using System;

namespace ConstructorInheritance
{
    public class Parent
    {
        public string Word { get; set; }
        public Parent(string word) // 생성자로 속성을 초기화
        {
            this.Word = word;
        }
    }

    public class Child : Parent
    {
        //[1] this() 형태로, 나 자신의 매개 변수가 있는 생성자에게 기본 문자열 전달
        public Child() : this("[1] 매개 변수가 없는 생성자 실행") { }

        //[2] base() 형태로, 매개 변수 값을 부모 클래스의 매개 변수가 있는 생성자에게 전달 
        public Child(string message) : base(message) { }
        public void Say() => Console.WriteLine(base.Word); // 부모의 World 속성 출력
    }

    class ConstructorInheritance
    {
        static void Main()
        {
            (new Child()).Say();
            (new Child("[2] 매개 변수가 있는 생성자 실행")).Say();
        }
    }
}
[1] 매개변수가 없는 생성자 실행
[2] 매개변수가 있는 생성자 실행 

클래스내에서 this()는 나 자신의 매개 변수가 없는 생성자를 의미합니다. 또한 this(매개 변수)는 매개 변수가 있는 생성자를 호출합니다. [1]번 코드는 매개 변수가 없는 생성자가 호출되면 기본 값으로 나 자신의 매개 변수가 있는 생성자를 호출합니다. [2]번 코드는 매개 변수가 있는 생성자에서 받은 message 변수의 값을 다시 부모 클래스의 생성자를 의미하는 base(message)를 호출하여 부모 생성자에게 전달합니다.

봉인(Sealed) 클래스

클래스를 만들었는데, 더 이상 다른 클래스에게 상속을 주지 않고자 할 때 사용하는 클래스를 봉인 클래스라고 합니다. 봉인 클래스는 최종 클래스로도 불리며 클래스 선언부에 sealed 키워드 붙여서 만듭니다. 예를 들어 다음 샘플 코드의 메인 App 클래스의 시그니처는 sealed 키워드가 붙어 더 이상 상속이 불가능합니다.

sealed partial class App : Application

봉인 클래스 만들기

봉인 클래스를 만들어보겠습니다. 다음 내용을 C# 인터렉티브에 입력 후 단계별로 실행해보세요. 프로젝트 기반 소스는 SealedClass.cs 파일에 있습니다.

(1) Animal 클래스를 만들고 Eat() 메서드를 만듭니다.

> class Animal
. {
.     public void Eat() => Console.WriteLine("밥을 먹습니다.");
. }

(2) Animal 클래스를 상속하는 Cat 클래스를 만드는데 sealed 키워드를 붙여 봉인 클래스로 설정합니다.

> sealed class Cat : Animal { }

(3) Cat 클래스를 상속하는 MyCat 클래스를 만들려고 시도합니다. Cat 클래스는 sealed 키워드가 붙은 최종 클래스이기에 상속에 사용될 수 없어 컴파일러 레벨에서 바로 에러를 발생시킵니다.

> class MyCat : Cat
. {
.     // sealed 키워드가 붙은 클래스는 상속할 수 없습니다.
. }
. 
(1,7): error CS0509: 'MyCat': sealed 형식 'Cat'에서 파생될 수 없습니다.

(4) 봉인 클래스 자체는 상속에 사용되지 않을 뿐 일반적인 클래스와 동일하게 사용됩니다.

> Cat cat = new Cat();
. cat.Eat();
밥을 먹습니다.

위 코드처럼 봉인 클래스인 Cat 클래스를 상속하는 MyCat 클래스를 만들게 되면 컴파일러는 자동으로 에러를 발생시킵니다. 봉인 클래스를 일반적으로 만들지는 않지만, 닷넷 프레임워크의 많은 내장 클래스들 중에는 봉인 클래스로 되어 있어 상속을 허용하지 않는 클래스들이 있습니다.

추상(Abstract) 클래스

클래스 선언 시 추가적으로 abstract 키워드를 붙여서 클래스를 선언할 수 있는데 이를 추상(Abstract) 클래스라고 합니다. 이 추상 클래스는 다른 클래스의 부모(Parent) 또는 기본(Base) 클래스 역할을 합니다.

public abstract class AbstractClassDemo
{

} 

추상 클래스를 사용하여 부모 클래스 만들기

추상 클래스는 일반적인 클래스들의 부모 역할을 하는 클래스, 즉, 공통적인 기능들을 모아놓은 클래스로서의 역할을 지닙니다. 추상 클래스는 추상 클래스 자체로서 만으로도 하나의 기능이 완성되지만, 다른 클래스에게 상속을 준 후 추가적인 기능을 하위 클래스에게 구현하도록 시켜주는 강제성을 가지기도 합니다. 추상 클래스의 특징은 다음과 같습니다. 한번 정도 읽어보고 넘어가세요.

  • 다른 클래스에게 상속을 주고자 할 때 사용하는 클래스입니다.
  • 추상 클래스를 사용하여 개체를 만들 수 없습니다. 즉, 추상 클래스와 뒤에서 나올 인터페이스(Interface)는 인스턴스화될 수 없습니다.
  • 클래스 설계 시 부모 클래스 역할을 하면서, 강제로 자식 클래스에게 특정 멤버명을 물려주고자 할 때 사용합니다.
  • 프로젝트 작성시 멤버 이름을 맞추고자 할 때 추상클래스에 먼저 정의 후 자식클래스에서 해당 멤버를 구현합니다.
  • 추상 클래스는 public과 같은 액세스 한정자를 가집니다.
  • 추상 클래스는 멤버로 필드, 속성, 생성자, 소멸자, 메서드, 이벤트, 인덱서를 가집니다.

추상 클래스를 만들고 상속하기

추상 클래스를 만들어보겠습니다. 다음 내용을 C# 인터렉티브에 입력 후 단계별로 실행해보세요. 프로젝트 기반 소스는 AbstractClassNote.cs 파일에 있습니다.

(1) TableBase 이름으로 클래스를 만들고 속성 2개를 추가합니다. 이 클래스는 다른 클래스에게 상속만을 주고자하는 추상 클래스로 만들기 위해서 추가적으로 abstract 키워드를 붙입니다.

> public abstract class TableBase
. {
.     public int Id { get; set; }
.     public bool Active { get; set; }
. }

(2) 추상 클래스인 TableBase 클래스의 인스턴스를 만들려고 시도해 보겠습니다. 추상 클래스는 따로 인스턴스를 만들 수 없어 에러가 발생됩니다.

> TableBase tableBase = new TableBase();
(1,23): error CS0144: 'TableBase' 추상 클래스 또는 인터페이스의 인스턴스를 만들 수 없습니다.

(3) 추상 클래스인 TableBase를 상속하는 Children 클래스를 만들고 추가적인 속성인 Name 속성을 만듭니다. 추상 클래스는 이처럼 다른 클래스의 부모 클래스 역할을 하는데 사용됩니다.

> public class Children : TableBase
. {
.     public string Name { get; set; }
. }

(4) 추상 클래스를 상속한 Children 클래스의 인스턴스 생성 후 개체 이니셜라이저를 통해서 부모에서 물려받은 Id, Active 속성을 설정하고 Children 클래스에서 지정한 Name 속성을 지정한 후 값을 출력해보면 입력한 값이 정상적으로 출력됩니다.

> var child = new Children() { Id = 1, Active = true, Name = "아이" };
. if (child.Active)
. {
.     Console.WriteLine($"{child.Id} - {child.Name}");
. }
. 
1 - 아이

구현(Concrete) 클래스와 추상(Abstract) 클래스

추상 클래스가 아닌 부모 클래스들은 구현(Concrete) 클래스라고 합니다. 구현 클래스(Concrete Class)는 추상 클래스와 달리 다른 클래스의 부모 클래스가 될 수도 있고 인스턴스화 될 수도 있습니다. 이와 달리 추상 클래스(Abstract Class)는 다른 클래스의 부모 클래스로만 사용 가능하며 인스턴스화 될 수 없습니다. 추상 클래스는 하나 이상의 추상(abstract) 멤버를 가질 수 있습니다.

추상 클래스 만들고 상속하기

추상 클래스를 만들고 이를 상속하는 예제를 살펴보겠습니다.

코드: MustInheritDemo.cs

> //[?] 추상(Abstact) 클래스: 다른 클래스에게 상속을 지정(강제)하는 클래스
> // 부장님(?) 클래스 생성
> abstract class GeneralManager
. {
.     public abstract void SayHumor(); // 메서드 본문 생략
. }
> 
> // 부장님 클래스를 상속하는 사람 클래스 생성
> class Person : GeneralManager
. {
.     public override void SayHumor()
.     {
.         Console.WriteLine("1+1은? 노가다!");
.     }
. }
> 
> var person = new Person();
> person.SayHumor();
1+1은? 노가다!

현실 세계의 부장님의 아재 개그를 표현해 보았습니다. GeneralManager 클래스를 상속하는 모든 Person 클래스는 SayHumor() 메서드를 강제로 구현해야 합니다.

추상 클래스와 추상 메서드

추상 클래스와 추상 메서드를 사용하는 예제를 다나 더 만들어보겠습니다.

코드: AbstractClassShape.cs

> //[?] 추상 클래스(Abstract Class): 
. //    기본(부모) 클래스 역할을 하여 파생(자식) 클래스에게 
. //    특정 멤버를 반드시 구현하도록 멤버 리스트를 제공
> //[1] 추상 클래스
. public abstract class Shape
. {
.     //[2] 추상 멤버: 추상 메서드
.     public abstract double GetArea();
. }
> 
> //[3] 추상 클래스를 상속하는 클래스
. public class Square : Shape
. {
.     private int _size;
.     public Square(int size)
.     {
.         _size = size;
.     }
.     // 부모 클래스인 Shape 추상 클래스의 추상 멤버인 GetArea() 메서드를 구현
.     public override double GetArea()
.     {
.         return _size * _size;
.     }
. }
> 
> //[A] 자신의 이름으로 인스턴스 생성
> Square square = new Square(10);
> square.GetArea()
100
> //[B] 부모의 이름으로 인스턴스 생성
> Shape shape = new Square(5);
> shape.GetArea()
25

[1]번 코드처럼 abstract 키워드를 붙인 클래스를 추상 클래스라고 합니다. [2]번 코드처럼 abstract 키워드를 붙인 멤버를 추상 멤버라고 합니다. 추상 클래스는 다른 클래스에게 멤버를 상속하여 구현하도록 하는 역할을 합니다. Shape 클래스의 GetArea() 추상 메서드는 Square 클래스에서 구현하여 Main() 메서드에서 Square 클래스의 인스턴스를 Square 형식 변수 또는 부모 클래스인 Shape 형식 변수에 대입해서 사용할 수 있습니다.

자식 클래스에게만 멤버 상속하기

부모 클래스의 특정 멤버 중에서 자식 클래스에게만 상속을 주고자 한다면, 해당 멤버의 액세스 한정자를 public에서 protected로 설정하면 됩니다. 필드와 같이 private으로 주게 되면 해당 클래스에서만 접근 가능한 멤버가 되는 것이고 protected는 자식 클래스들까지 접근하도록 하고 public은 모든 클래스에서 접근하도록 하는 규칙을 가지는 것입니다.

필드 숨기기

이번에는 private 키워드를 사용하여 필드를 숨기거나 protected 키워드를 사용하여 자식 클래스에게만 멤버를 상속하는 예제를 살펴보겠습니다.

코드: FieldHiding.cs

using System;

namespace FieldHiding
{
    class Parent
    {
        //[1] 필드 숨김: 필드는 무조건 private으로 설정
        private string _word;

        //[2] protected는 자식 클래스에서만 호출 가능한 멤버
        protected string Word
        {
            get { return _word; }
            set { _word = value; }
        }
    }

    class Child : Parent
    {
        public void SetWord(string word)
        {
            base.Word = word;
        }
        public string GetWord()
        {
            return Word; // 부모 클래스의 Word 속성 접근
        }
    }

    class FieldHiding
    {
        static void Main()
        {
            Child child = new Child();
            child.SetWord("필드 숨기기 및 자식 클래스에게만 멤버 상속하기");
            Console.WriteLine(child.GetWord());
        }
    }
}
필드 숨기기 및 자식 클래스에게만 멤버 상속하기

필드에 private을 붙이면 해당 필드는 해당 클래스에서만 사용됩니다. 이를 필드 은폐(Field Hiding) 또는 필드 숨기기라고 합니다. 클래스 멤버 중에서 자식 클래스에게만 상속을 주고 외부에는 공개하지 않고 보호를 하려면 public과 private이 아닌 protected 키워드를 붙여 멤버를 만들면 됩니다.

기본 클래스의 멤버 숨기기

부모 클래스에 만들어져 있는 특정 메서드를 자식 클래스에서 새롭게 정의해서 사용할 때에는 new 키워드를 사용하여 자식 클래스의 메서드를 정의할 수 있습니다. 부모에 “public void Work()”로 정의된 메서드는 자식에서 “public new void Work()”로 재정의 하는 것입니다. 기본 클래스의 멤버를 숨기고 자식 클래스에서 새롭게 정의하는 방법에 대해서 살펴보겠습니다.

코드: MethodNew.cs

> //[1] 부모 클래스 생성
> class Parent
. {
.     public void Work() => Console.WriteLine("프로그래머");
. }
> 
> //[2] 자식 클래스 생성
. class Child : Parent
. {
.     // 기본 멤버 숨기기: new -> 새롭게 정의, 다시 정의, 재정의 
.     public new void Work() => Console.WriteLine("프로게이머");
. }
> //[!] 자식 클래스의 인스턴스 생성
> var child = new Child();
> child.Work(); // "프로게이머"
프로게이머

Parent 클래스에는 Work() 메서드에서 “프로그래머”를 출력합니다. 만약, 자식 클래스인 Child 클래스에서 따로 Work() 메서드를 구현하지 않았다면 “프로그래머”를 그대로 출력하겠지만, 여기서는 새롭게(new) Work() 메서드를 만들고 “프로게이머”를 출력하도록 변경하였습니다. 이렇게 부모에 정의된 메서드를 new 키워드를 사용하여 새롭게 정의해서 사용할 수 있습니다. 이러한 내용을 다른 말로 메서드 오버라이드라고도 합니다. 이 부분에 대해선 다음 장에서 좀 더 자세히 다루어 보겠습니다.

MethodNew.java

코드: MethodNew.java

class Parent {
    public void work() {
        System.out.println("프로그래머");
    }
}

class Child extends Parent {
    // New : 새롭게 정의, 다시 정의
    @Override
    public void work() {
        System.out.println("프로게이머");       
    }
}

public class MethodNew {
    public static void main(String[] args) {
        Child child = new Child();
        child.work(); // 프로게이머 
    }
}
프로게이머

장 요약

개체 지향 프로그래밍의 세 가지 주요 특징은 캡슐화, 다형성 그리고 상속이 있습니다. 이번 강의는 우선 상속을 다루었는데요. 상속을 사용하면 재사용의 느낌을 얻을 수 있습니다. 닷넷에서 제공하는 수많은 API들은 이러한 상속의 관계를 거쳐 여러 API를 재사용 및 확장해서 사용이 가능합니다.

정보처리기사 실기 시험 기출 문제 - 클래스 상속과 메서드 호출

문제

다음 C# 프로그램이 실행되었을 때의 동작을 설명하고, 출력 결과를 예측하시오.

소스 코드 파일명: ClassInheritanceDemo.cs

using System;

class SuperClass {
    int num;
    public SuperClass(int num) { this.num = num; }
    public void Display() { Console.WriteLine($"num={num}"); }
}

class SubClass : SuperClass {
    public SubClass(int num) : base(num) {
        base.Display();
    }
}

class ClassInheritanceDemo {
    static void Main(string[] args) {
        SubClass obj = new SubClass(10);
    }
}

입력 예시

이 프로그램은 사용자로부터 입력을 받지 않습니다.

출력 예시

num=10

해설

이 프로그램은 C#에서 클래스 상속과 생성자, 메서드 호출 메커니즘을 보여줍니다.

  1. SuperClass라는 이름의 부모 클래스는 정수형 멤버 변수 num을 가지고 있으며, 생성자를 통해 이 변수를 초기화합니다. 또한, Display 메서드를 통해 num 변수의 값을 출력합니다.
  2. SubClass라는 이름의 자식 클래스는 SuperClass를 상속받습니다. SubClass의 생성자는 부모 클래스의 생성자를 호출하여 num을 초기화한 후, base.Display()를 호출하여 num의 값을 출력합니다.
  3. ClassInheritanceDemo 클래스의 Main 메서드에서는 SubClass의 인스턴스를 생성하면서 생성자에 정수 10을 전달합니다. 이로 인해 부모 클래스의 num 변수가 10으로 초기화되고, Display 메서드를 통해 num=10이 출력됩니다.

이 프로그램은 개체 지향 프로그래밍의 중요한 개념 중 하나인 상속을 통해 부모 클래스의 기능을 확장하고 재사용하는 방법을 설명합니다.

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com