C# 강좌 13편. 클래스의 상속(Class inheritance) [최근 수정 2017.12.26]
1. 클래스의 상속(Class inheritance)
이번에 배울 건 '클래스의 상속(Class inheritance)'입니다. 어? 상속이란 말을 어디선가 들어본 적이 있는 것 같지 않나요? 짐작하는 그 상속이 맞냐구요? 네 맞습니다. 혹시나 상속이 뭔지 들어보 적 없는 분들을 위해 무엇인지 알려드리려고 합니다. 상속이란 네이버 지식백과를 빌어 다음과 같이 정의되어 있습니다. '일정한 친족적 관계가 있는 사람 사이에 한 쪽이 사망하거나 법률상의 원인이 발생하였을 때 재산적 또는 친족적 권리와 의무를 계승하는 제도'. 즉, 부모님이 돌아가셨다고 할 때 그 유산을 자식이 물려받는 것이라고 할 수 있습니다. 클래스의 상속도 이와 똑같습니다. 객체 지향 프로그래밍에선 부모 클래스와 자식 클래스가 있는데, 부모 클래스는 자식 클래스의 기반이 된다 하여 기반 클래스라고 부르기도 하고, 자식 클래스는 부모 클래스로부터 파생되었다고 해서 파생 클래스라고도 부르기도 합니다.
C#에서, 클래스를 다른 클래스로 상속하려면 다음과 같이 클래스 이름 뒤에 콜론(:)을 추가하고 상속하려는 클래스의 이름을 덧붙이시면 됩니다.
class 부모 클래스
{
// ...
}
class 자식 클래스 : 부모 클래스
{
// 부모 클래스의 모든 상태와 행동이 전달 됨.
}
위 예제를 보시면 자식 클래스에 부모 클래스를 상속시킵니다. 부모 클래스를 상속받은 자식 클래스는 부모 클래스의 모든 멤버를 물려받게 됩니다. (다만, 생성자는 상속이 되지 않으며 객체 생성 시 부모 클래스의 생성자가 자동으로 호출됨)
[그림 1-1. 상속 관계]
여기서 한가지 알아두셔야 할 점은 private로 선언된 멤버는 상속할 수 없습니다. 아래는 상속의 예제입니다.
using System;
namespace ConsoleApplication8
{
class Parent
{
public int num;
public Parent()
{
Console.WriteLine("부모 클래스의 생성자가 호출되었습니다.");
}
}
class Child : Parent
{
public Child(int num)
{
this.num = num;
Console.WriteLine("자식 클래스의 생성자가 호출되었습니다.");
}
public void DisplayValue()
{
Console.WriteLine("num의 값은 {0} 입니다.", num);
}
}
class Program
{
static void Main(string[] args)
{
Child cd = new Child(20);
cd.DisplayValue();
}
}
}
결과:
부모 클래스의 생성자가 호출되었습니다.
자식 클래스의 생성자가 호출되었습니다.
num의 값은 20 입니다.
계속하려면 아무 키나 누르십시오 . . .
코드를 보시면 5행에서 Parent라는 클래스가 등장합니다. Parent 클래스 내에는 num이라는 멤버 변수와 생성자가 있습니다. 그리고 15행을 보시면 Parent 클래스를 Child 클래스에 상속시키고 있는 것을 볼 수 있습니다.
Child 클래스 내부를 보시면 생성자와 DisplayValue()라는 메소드가 존재합니다. 생성자를 살펴보면 매개변수 하나를 받고, 부모 클래스로부터 물려받은 멤버 변수 num을 매개변수의 값으로 초기화시킵니다. 33~35행을 보시면 객체를 생성하고 그 객체의 DisplayValue() 메소드를 호출합니다.
DisplayValue() 메소드를 살펴보면 num의 값을 출력하는 코드가 보이죠? 결과를 보니, num의 값은 20이라고 출력되었네요. 부모 클래스의 멤버 변수 num의 값을 출력시킨 것과 같습니다. 그리고 생성자의 호출 순서를 보니 부모 클래스의 생성자가 먼저 호출되고, 자식 클래스의 생성자는 그 뒤이어 호출됨을 확인할 수 있습니다. 즉, 부모 클래스의 생성자가 먼저 호출되고 자식 클래스의 생성자가 호출됨을 알 수 있습니다. 반대로 소멸할 때에는 역순으로 자식 클래스의 소멸자부터 호출되고, 이어서 부모 클래스의 소멸자가 호출됩니다.
그런데 한가지 이상한 게 보이죠? 19행을 보시면 자신을 가리키는 this 키워드가 사용되었습니다. this 키워드를 사용하여 부모 클래스의 멤버 변수에 접근은 할 수 있습니다. 그러나, 자식 클래스에도 num이라는 멤버 변수가 존재할 때는 부모 클래스의 멤버 변수인 num에 어떻게 접근하여야 할까요? 그럴 때는 this 키워드가 아닌 base 키워드를 사용하시면 됩니다. 다음과 같이 말이죠.
..
public Child(int num)
{
base.num = num;
Console.WriteLine("자식 클래스의 생성자가 호출되었습니다.");
}
..
위와 같이 base 키워드를 사용하면 부모 클래스에 접근할 수 있게 됩니다. this 키워드와 비슷하죠?
2. sealed
클래스명 앞에다 sealed 키워드를 사용하게 되면, 이 클래스를 상속시키는 건 더이상 할 수 없습니다. 즉, 더이상 이 클래스는 다른 클래스의 부모 클래스가 될 수 없습니다. sealed의 이해를 위해 전의 예제에서 Parent 클래스 앞에 sealed 키워드를 달아봅시다. 그리고 곧바로 결과를 확인해봅시다.
..
sealed class Parent
{
public int num;
public Parent()
{
Console.WriteLine("부모 클래스의 생성자가 호출되었습니다");
}
}
class Child : Parent
{
public int num;
public Child(int num)
{
this.num = num;
Console.WriteLine("자식 클래스의 생성자가 호출되었습니다.");
}
public void DisplayValue()
{
Console.WriteLine("num의 값은 {0} 입니다.", num);
}
}
..
컴파일을 시도 했더니, 다음과 같은 에러가 발생했습니다.
오류 1 'ConsoleApplication8.Child': sealed 형식 'ConsoleApplication8.Parent'에서 파생될 수 없습니다. c:\users\h4ckfory0u\documents\visual studio 2012\Projects\ConsoleApplication8\ConsoleApplication8\Program.cs 19 11 ConsoleApplication8
정보 은닉(information hiding)은 무엇인가요?
이는 객체 지향 프로그래밍과 밀접한 관련이 있습니다. 클래스 내부의 필드나 메소드와 같이 객체가 가지고 있는 것들을 외부에서 접근하지 못하도록 숨기는 것을 말합니다.
우리가 클래스를 설계할 때, private나 public 등과 같은 접근 제한자를 통하여 특정 멤버를 공개할 것인지 공개하지 않을 것인지 지정해 줄 수 있었습니다. 왜 이러한 작업을 하는 걸까요?
우리가 클래스를 설계할 때, 수십에서 수백 개에 달하는 필드나 프로퍼티(property)가 존재할 수 있습니다. 그러나 이러한 정보들을 외부로 모두 노출시켜 버리면 우리가 설계한 클래스를 사용하는 사람의 입장에서는 상당히 곤혹스러울 것입니다. 이럴 때는 필요한 정보만을 외부로 노출시킬 필요가 있습니다.
또 다른 이유는 안정성을 위한 것입니다. 객체 내부에서만 사용되는 필드나 메소드는 외부로 공개하면, 외부에서 이를 수정할 수 있기 때문에 안정성이 깨질 우려가 있기 때문입니다.
public class Person {
public String name;
public int age;
public Person() {
// ...
}
// ...
}
위와 같이 Person이란 클래스가 있다면, 외부에서 name과 age를 수정할 수 있습니다. 접근 제한자가 public으로 지정되어 있기 때문이죠. 그럼 우리는 name이나 age의 값을 신뢰할 수 있을까요? 그렇기 때문에 name과 age 필드의 접근 범위를 private로 제한해줄 필요가 있습니다.
using System;
namespace ConsoleApplication14
{
public class MyClass
{
private string name = "John";
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
}
class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass();
Console.WriteLine("mc.Name : {0}", mc.Name);
mc.Name = "Bree";
Console.WriteLine("mc.Name : {0}", mc.Name);
}
}
}
결과:
mc.Name : John
mc.Name : Bree
계속하려면 아무 키나 누르십시오 . . .
코드의 7줄을 보시면 name 속성이 private로 접근이 제한되어 있음을 알 수 있습니다. 그리고 9~19행에서 get, set 접근자가 등장합니다. Name이란 이름으로 get/set 접근자를 통해 name에 접근할 수 있으며, get 영역 내에서는 name의 값을 반환하고, set 영역 내에서는 name 속성에 value 값으로 초기화시킵니다. 여기서 value은 Name으로 넘어온 값이라고 생각하시면 됩니다. 27행에서는 mc.Name이 아직까지는 John이였다가, 29행에서 이름이 "Bree"로 바뀌고, 31행에서 바뀐 이름을 출력하게 됩니다.
그리고 get/set 접근자 내에서 value에 변화를 주거나, 주지 않을 수도 있습니다. 아래는 그 예제입니다.
public class MyClass
{
private string name = "John";
public string Name
{
get
{
return name;
}
set
{
if (value.Length < 5)
name = value;
}
}
}
class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass();
Console.WriteLine("mc.Name : {0}", mc.Name);
mc.Name = "Kelley";
Console.WriteLine("mc.Name : {0}", mc.Name);
}
}
결과:
mc.Name : John
mc.Name : John
계속하려면 아무 키나 누르십시오 . . .
요번에는 set 접근자 영역 내를 잘 보면 value의 길이가 5보다 작아야 새 값을 할당할 수 있습니다. 만약에 5보다 같거나 크면 어떻게 될까요? 이번에는 Name에 Kelley를 넣어본 뒤에, 변화가 있나 결과를 살펴보았습니다. 결과를 봤더니, 새 값이 할당되지 않고 John 그대로 값이 유지되어 있었습니다. Kelley의 길이는 6이므로 초기화 되지않고 빠져나와 버린 것이죠.
4. 메소드 재정의(virtual, override)
부모 클래스의 메소드를 자식 클래스에서 다시 정의하고 싶을때 virtual, override 키워드가 사용됩니다. 자세히 말하자면, virtual 키워드는 자식 클래스에서 메소드를 재정의 하고 싶을때 재정의 될 부모 클래스의 메소드에 사용되며, override 키워드는 부모 클래스 내에서 virtual로 선언된 메소드를 재정의 하겠다는 표시를 하는 것과 같습니다. (이 말고도 추상 구현 등에서 사용되기도 합니다.)
using System;
namespace ConsoleApplication21
{
class Parent
{
public virtual void A()
{
Console.WriteLine("부모 클래스의 A() 메서드 호출!");
}
}
class Child : Parent
{
public override void A()
{
Console.WriteLine("자식 클래스(Child)의 A() 메서드 호출!");
}
}
class Daughter : Parent
{
public override void A()
{
Console.WriteLine("자식 클래스(Daughter)의 A() 메서드 호출!");
}
}
class Program
{
static void Main(string[] args)
{
Parent parent = new Parent();
parent.A();
Child child = new Child();
child.A();
Daughter daughter = new Daughter();
daughter.A();
}
}
}
결과:
부모 클래스의 A() 메서드 호출!
자식 클래스(Child)의 A() 메서드 호출!
자식 클래스(Daughter)의 A() 메서드 호출!
계속하려면 아무 키나 누르십시오 . . .
코드의 7행, 14행, 21행을 보시면 각각 virtual, override, override가 등장했습니다. 알아두셔야 할 점은, 메소드를 재정의 하려면 virtual 키워드가 붙어 있어야 한다는 겁니다. 만약 virtual 키워드가 붙어있지 않다면, 컴파일러는 다음과 같은 에러를 내보냅니다.
오류 1 'ConsoleApplication21.Child.A()': 상속된 'ConsoleApplication21.Parent.A()' 멤버는 virtual, abstract 또는 override로 표시되지 않았으므로 재정의할 수 없습니다. C:\Users\h4ckfory0u\documents\visual studio 2012\Projects\ConsoleApplication21\ConsoleApplication21\Program.cs 18 30 ConsoleApplication21
using System;
namespace ConsoleApplication21
{
class Parent
{
public int x = 100;
public void A()
{
Console.WriteLine("부모 클래스의 A() 메서드 호출!");
}
}
class Child : Parent
{
public new int x = 200;
public new void A()
{
Console.WriteLine("자식 클래스(Child)의 A() 메서드 호출!");
}
}
class Program
{
static void Main(string[] args)
{
Parent parent = new Parent();
parent.A();
Console.WriteLine("x : {0}", parent.x);
Child child = new Child();
child.A();
Console.WriteLine("x : {0}", child.x);
}
}
}
결과:
부모 클래스의 A() 메서드 호출!
x : 100
자식 클래스(Child)의 A() 메서드 호출!
x : 200
계속하려면 아무 키나 누르십시오 . . .
new 키워드를 붙이지 않아도 컴파일 하는데는 지장이 없습니다. 다만 경고가 발생할 뿐입니다.
6. 업캐스팅과 다운캐스팅(Upcasting and Downcasting)
지금 소개할 개념은 객체 지향 프로그래밍의 특징 중 하나인 다형성(polymorphism)과 밀접한 관련이 있습니다.
다형성(polymorphism)은 무엇인가요?
polymorphism에서 poly는 여러, 다양한(many), morph는 변형(change)이나 형태(form)의 의미를 가지고 있습니다. 사전적 정의로만 살펴보면 "여러가지 형태"를 나타내는데, 이를 객체 지향 프로그래밍으로 끌고 온다면 "하나의 객체가 여러 개의 형태를 가질 수 있는 능력"이라 말할 수 있습니다.
우리는 이미 다형성을 접해본 적이 있습니다. 다형성의 일부인 메소드의 오버로딩(이는 ad-hoc polymorphism에 해당)과 오버라이딩(이는 inclusion polymorphism에 해당)에서 말이죠! C#도 객체 지향 프로그래밍에 근간을 두고 있으므로 이 다형성이라는 녀석은 앞으로도 계속 만나볼 것입니다.
우선 업캐스팅(upcasting)부터 살펴보도록 하겠습니다. 업캐스팅이란 말 그대로 자식 클래스의 객체가 부모 클래스의 형태로 변환되는 것을 말합니다. 반대로, 다운캐스팅(downcasting)은 부모 클래스의 객체가 자식 클래스의 형태로 변환되는 것을 말합니다. 직접 예제 코드를 보도록 하겠습니다.
class Animal { }
class Dog : Animal { }
Dog dog = new Dog();
Animal animal = dog; // 업캐스팅
Dog sameDog = (Dog)animal; // 다운캐스팅
개는 기본적으로 동물의 상태와 행동을 모두 가지기 때문에, Dog 클래스의 객체를 Animal 클래스의 형태로 바꾸어도 문제가 없습니다. 따라서, 우리가 "어떤 형태로 변환하겠다"와 같은 별다른 구문을 쓰지 않아도 암시적으로 변환이 됩니다.
그러나, Animal 클래스의 객체를 Dog 클래스의 형태로 바꾸려 하면 문제가 발생합니다. 당연하게도 Dog 클래스만이 가지고 있는 필드나 메소드가 있는데, Animal 클래스의 객체는 이를 가지고 있지 않기 때문입니다. 위 예제의 7행에 쓰인 코드는 애초에 animal이 Dog 클래스의 객체이기 때문에 컴파일 타임에도 런타임에도 오류가 발생하지 않는 것입니다.
class Animal { }
class Dog : Animal { }
Animal animal = new Animal();
Dog dog = (Dog)animal; // InvalidCastException 예외 발생!
런타임에 변환이 가능할 경우 별다른 오류 없이 변환이 되지만, 위 코드와 같이 변환이 불가능할 경우에는 실행 도중 InvalidCastException이란 예외가 발생합니다.
클래스의 상속편은 여기서 마치도록 하겠습니다. 수고하셨습니다.
다음 강좌에서는 확장 메소드, 중첩 클래스, 분할 클래스에 대해 배워보도록 하겠습니다.