C# 프로그래밍 12번째 시간 - 클래스의 상속
유니티의 열두걸음, C# 공부시작 49일차
오늘은 클래스의 상속에 대해서 배우겠습니다. 여기서 상속이란 우리가 흔히 아는 그 ‘상속’입니다.
시작해보겠습니다!
1. 클래스의 상속
상속의 정확한 정의는 일정한 친족적 관계가 있는 사람 사이에 한 쪽이 사망하거나 법률상의 원인이
발생하였을 때 재산적 또는 친족적 권리와 의무를 계승하는 제도를 의미합니다.
부모님이 돌아갔을 때 그 유산을 자식이 받는다고 간단하게 생각하시면 될 것 같네요.
클래스의 상속도 마찬가지로, 부모가 자식에게 물려주는 것입니다.
객체 지향 프로그래밍에서는 부모 클래스와 자식 클래스가 있는데,
부모 클래스는 자식 클래스의 기반이 된다하여 기반 클래스라 부르고
자식 클래스는 부모 클래스로부터 파생이 되어 파생 클래스라고도 부릅니다.
C#에서 클래스를 다른 클래스로 상속하려면 이와 같은 방식을 사용해야 됩니다.
class 부모 클래스
{
// ...
}
class 자식 클래스 : 부모 클래스
{
// 부모 클래스의 모든 상태와 행동이 전달 됨.
}
클래스 이름 뒤에 콜론(:)을 추가하고 상속하려는 클래스의 이름을 덧붙이면 됩니다.
부모 클래스를 상속받은 자식 클래스는 부모 클래스의 모든 멤버를 호출할 수 있습니다!
(단, 생성자는 상속되지 않고, 객체 생성 시 부모 클래스의 생성자가 자동으로 호출됩니다.)
[부모 클래스 -> 자식 클래스 그림]
이때 private으로 선언된 멤버는 상속할 수 없는데, 이를 이용한 예제를 만들어보겠습니다.
[상속의 예제]
using System;
namespace Study
{
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 입니다.
코드를 하나하나 뜯어보겠습니다. 맨첨에 Parent라는 클래스가 등장하는데,
이는 num이라는 멤버 변수 생성자가 있습니다. 그리고 Parent 클래스를 Child 클래스에 상속시켰습니다.
Child 클래스에서는 생성자와 DisplayValue() 메소드가 있습니다.
생성자를 보면 매개변수 하나를 받고, 부모 클래스로 부터 물려받은
멤버 변수 num을 매개변수의 값으로 초기화시킵니다.
그리고 객체를 생성한 후 그 객체의 DisplayValue() 메소드를 호출하였습니다.
DisplayValue() 메소드를 보면 num의 값을 출력하는 코드가 있습니다.
결과를 보면 num의 값은 20이라고 하는데 이는 부모 클래스의 멤버 변수 num의 값을 출력시키는 것과 같습니다.
그리고 생성자의 호출 순서를 보았을 때
부모 클래스의 생성자가 먼저 호출되고, 자식 클래스의 생성자는 그다음 호출되었습니다.
여기서 생성자의 호출 순서가 부모 클래스, 자식 클래스 순으로 가는 것을 확인할 수 있습니다.
반대로 소멸할 때는 자식 클래스부터 부모 클래스로 소멸자가 호출됩니다.
여기서 이상한게 있는데, this 키워드를 이용했다는 것을 확인할 수 있습니다.
this 키워드를 사용하여 부모 클래스의 멤버 변수에 접근할 수 있는데,
자식 클래스에도 num멤버 변수가 존재할때는 부모 클래스의 멤버 변수인 num에 접근할 수 없습니다.
이때는 this 키워드가 아닌 base 키워드를 사용합니다. base 키워드 사용은 이와 같습니다.
..
public Child(int num)
{
base.num = num;
Console.WriteLine("자식 클래스의 생성자가 호출되었습니다.");
}
..
이렇게 사용하면 부모 클래스에 접근할 수 있습니다. 형태는 this 키워드와 비슷한 것 같습니다.
2. sealed
이 키워드는 클래스 명에다 붙이는데, 이것을 사용하면 클래스를 상속시킬 수 없습니다.
따라서 그 클래스는 다른 클래스의 부모가 될 수 없다는 의미입니다.
앞의 예제에서 Parent 클래스 앞에 sealed 키워드를 붙여보겠습니다.
[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);
}
}
..
이런 식으로 컴파일을 하면 오류가 발생합니다.
‘sealed 형식인 Parent 클래스로부터 파생될 수 없다’라는 에러입니다.
sealed 키워드를 사용하면 의도하지 않은 상속을 불가능하게 만들 수 있습니다!!
3. set, get
set, get 접근자는 각각 속성을 읽거나, 새 값을 할당할 때 사용됩니다.
객체 지향 프로그래밍에서는 클래스 내부에서만 활용할 수 있도록 private으로 접근을 제한합니다. 정보 보안 때문이죠.
우리가 항상 public으로 설정하게 된다면, 외부에서 수정을 할 수 있는 확률이 높아집니다.
그런데 프로그램을 만들다 보면 내부에 있는 변수를 고쳐야 되는 상황이 벌어집니다.
이때 사용하는 것이 set, get 접근자입니다.
get 접근자는 읽을때, set 접근자는 쓸때 사용합니다. get, set을 이용하여 예제를 만들어 보겠습니다.
[get,set 활용 예제]
using System;
namespace Study
{
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
처음부터 볼 때, name 속성이 private으로 접근을 제한시켰습니다.
그리고 get, set 접근자를 활용하여 Name이란 이름으로 name에 접근할 수 있습니다.
get 영역에서는 name의 값을 반환하고, set 영역에서는 name 속성에 value 값으로 초기화하였습니다.
여기서 value는 Name으로 넘어온 값입니다.
그래서 출력값 처음에는 mc.Name이 아직 John인 상태에서 한번 출력하고
그 다음 Bree로 바뀐 후 Bree로 또 출력이 되었습니다.
이를 또 활용하여 get,set 접근자가 value에 변화를 줄 수도 있고 안 줄 수도 있습니다.
추가 예제를 만들어보겠습니다.
[get, set 접근자 추가 예제]
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보다 크거나 같게 되면 새 값을 할당하지 않고 John 그대로 값을 유지합니다. 새로 입력하려는게 Kelley로
길이가 6이어서 초기화 되지않고 빠져나가는 모습을 볼 수 있습니다!
4. 메소드 재정의
부모 클래스의 메소드를 자식 클래스에서 다시 정의할 때는 virtual, override 키워드를 활용합니다.
virtual 키워드는 자식 클래스에서 메소드를 재정의하고 싶을때 재정의할 부모 클래스의 메소드에,
override 키워드는 부모 클래스에서 virtual로 선언된 메소드를 재정의한다는 표시를 할 때 사용합니다.
예제를 한번 만들어보겠습니다.
[메소드 재정의 예제]
using System;
namespace Study
{
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() 메서드 호출!
여기서 메소드 재정의를 활용하는 키워드를 사용하였습니다.
메소드를 재정의하려면 virtual 키워드가 붙어 있어야한다는 것을 알 수 있습니다.
만약 virtual 키워드를 붙이지 않으면 오류가 발생합니다.
5. 멤버 숨기기 (new)
new 지정자는 부모 클래스의 멤버를 숨길 수 있게 해줍니다.
원래 부모 클래스에서 멤버 변수의 이름이 자식 클래스에서도 똑같은 것이라면
new 지정자를 딱히 사용하지 않아도 숨길 수 있는데,
이 멤버가 숨겨진다는 경고가 나옵니다. 대신 new 지정자를 사용하면 오류가 사라집니다.
예제를 만들어 보겠습니다.
[new 지정자를 활용한 예제]
using System;
namespace Study
{
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. 업캐스팅과 다운캐스팅
이 내용을 설명하기 전 C#의 특징 중 하나인 다형성에 대해서 설명하겠습니다.
<다형성?>
-> 말 그대로 다양한 형태를 띄는 성질이라고 보시면 됩니다. 객체 지향 언어에서는
“하나의 객체가 여러 개의 형태를 가질 수 있는 능력”이라고 말합니다.
메소드의 오버로딩이나 오버라이딩도 다형성의 일부라고 할 수 있습니다!!
업캐스팅은 자식 클래스의 객체가 부모 클래스의 형태로 변환되는 것을 말하고
다운캐스팅은 부모 클래스의 객체가 자식 클래스의 형태로 변환되는 것을 말합니다.
이 둘은 다형성과 밀접한 관계입니다! 예시를 만들어 보겠습니다.
[업캐스팅과 다운캐스팅 활용 예시]
class Animal { }
class Dog : Animal { }
Dog dog = new Dog();
Animal animal = dog; // 업캐스팅
Dog sameDog = (Dog)animal; // 다운캐스팅
여기서 개는 동물의 상태와 행동을 지니므로,
Dog 클래스의 객체를 Animal 클래스의 형태로 바꿀 수 있습니다.
근데 Animal 클래스의 객체를 Dog 클래스의 형태로 바꾸면 문제가 발생합니다.
Dog 클래스가 가지고 있는 필드와 메소드는 Animal 클래스의 객체는 가지고 있지 않기 때문입니다.
class Animal { }
class Dog : Animal { }
Animal animal = new Animal();
Dog dog = (Dog)animal; // InvalidCastException 예외 발생!
이런식으로 부모 객체를 자식 객체로 바꾸면 오류가 발생합니다. 변환이 불가능하기 때문입니다.
마치며
오늘은 클래스의 상속에 대해서 알아보았습니다! 다음에는 확장 메소드, 중첩 클래스, 분할 클래스에 대해서 배우겠습니다!
Comments