C#에서 상속은 정말 중요한 개념입니다. 많이 사용되기도 하며, 잘 사용한다면 코드의 양을 많이 줄일 수있기 때문입니다. 그 뿐만 아니라 프로젝트의 생산성과 확장성이 올라가게 됩니다. 이번 시간은 왜 상속이 중요한지, 생산성과 확장성이 왜 올라가는지 등을 알아겠습니다. 또한 개념과 사용법, 장점, 단점 등을 알아보고, 예시코드와 용도까지 살펴보는 시간을 갖겠습니다.
[상속의 개념]
상속은 객체지향 프로그래밍에서 사용되는 개념으로, 한 클래스가 다른 클래스의 특성과 동작을 물려받는 것을 의미합니다. 간단하게 말하자면, 부모 클래스(또는 기본 클래스)가 가지고 있는 특성과 동작을 자식 클래스(또는 파생 클래스)가 상속받아 사용할 수 있다는 것입니다.
RPG(롤플레잉 게임)을 예시로 들어보겠습니다. RPG 게임에서 주인공이 여러 가지 무기를 사용하고, 여러 종류의 적들을 상대합니다. 이때, 주인공과 적들 사이에는 공통된 특성과 동작이 있을 것입니다.
- 캐릭터 클래스:
- 속성: 이름, 체력, 공격력
- 메서드: 이동하기, 공격하기, 아이템 사용하기
- 주인공 클래스:
- 캐릭터 클래스를 상속받음
- 추가 속성: 경험치, 레벨
- 추가 메서드: 경험치 획득, 레벨업
- 적 캐릭터 클래스:
- 캐릭터 클래스를 상속받음
- 추가 속성: 종류(예: 고블린, 슬라임 등)
- 추가 메서드: 공격 패턴 실행
위는 주인공 클래스와 적 캐릭터 클래스의 공통된 속성을 캐릭터 클래스로 정의한 것 입니다. 즉, 주인공과 적은 캐릭터 클래스를 상속 받아 사용할 수 있다는 것입니다.
[사용법]
기본 클래스(또는 부모 클래스)를 정의하고, 이를 상속받아 파생 클래스(또는 자식 클래스)를 만드는 것입니다. 그러면 파생 클래스는 기본 클래스의 모든 멤버(속성, 메서드)를 상속받아 사용할 수 있습니다.
동물”이라는 기본 클래스를 만들고, 이를 상속받아 “고양이”와 “개”라는 파생 클래스를 만들어 보겠습니다.
// 동물 클래스 정의
class Animal {
public void Eat() {
Console.WriteLine("먹는 중입니다.");
}
public void Sleep() {
Console.WriteLine("잠을 자고 있습니다.");
}
}
고양이” 클래스와 “개” 클래스를 만들어보겠습니다. 이 클래스들은 “동물” 클래스를 상속받을 겁니다.
// 고양이 클래스 정의
class Cat : Animal {
public void Meow() {
Console.WriteLine("야옹~");
}
}
// 개 클래스 정의
class Dog : Animal {
public void Bark() {
Console.WriteLine("멍멍!");
}
}
각각의 클래스를 사용해보겠습니다.
class Program {
static void Main(string[] args) {
Cat myCat = new Cat();
myCat.Eat(); // 동물 클래스의 메서드 호출
myCat.Sleep(); // 동물 클래스의 메서드 호출
myCat.Meow(); // 고양이 클래스의 메서드 호출
Dog myDog = new Dog();
myDog.Eat(); // 동물 클래스의 메서드 호출
myDog.Sleep(); // 동물 클래스의 메서드 호출
myDog.Bark(); // 개 클래스의 메서드 호출
}
}
위의 예시에서 보듯이, “고양이” 클래스와 “개” 클래스는 각각 “동물” 클래스를 상속받아서 만들어진 것입니다. 따라서 각각의 클래스는 “동물” 클래스의 메서드를 사용할 수 있으면서도, 각자의 특징인 “Meow”와 “Bark” 메서드를 추가로 가지고 있습니다.
이제, 개념에서 설명했던 캐릭터, 주인공, 적을 코드로 작성 해보겠습니다.
using System;
// 캐릭터 클래스 정의
class Character {
public string Name { get; set; }
public int Health { get; set; }
public int Attack { get; set; }
public void Move() {
Console.WriteLine("이동합니다.");
}
public void AttackEnemy() {
Console.WriteLine("적을 공격합니다.");
}
public void UseItem() {
Console.WriteLine("아이템을 사용합니다.");
}
}
// 주인공 클래스 정의
class Hero : Character {
public int Experience { get; set; }
public int Level { get; set; }
public void GainExperience(int exp) {
Experience += exp;
Console.WriteLine("{0}의 경험치를 획득했습니다.", exp);
}
public void LevelUp() {
Level++;
Console.WriteLine("레벨이 올랐습니다! 현재 레벨: {0}", Level);
}
}
// 적 캐릭터 클래스 정의
class Enemy : Character {
public string Type { get; set; }
public void ExecuteAttackPattern() {
Console.WriteLine("{0}의 공격 패턴을 실행합니다.", Type);
}
}
class Program {
static void Main(string[] args) {
// 주인공 생성 및 사용
Hero hero = new Hero();
hero.Name = "용사";
hero.Health = 100;
hero.Attack = 20;
hero.Move();
hero.AttackEnemy();
hero.UseItem();
hero.GainExperience(50);
hero.LevelUp();
// 적 생성 및 사용
Enemy enemy = new Enemy();
enemy.Name = "고블린";
enemy.Health = 50;
enemy.Attack = 10;
enemy.Type = "근접 공격";
enemy.Move();
enemy.AttackEnemy();
enemy.ExecuteAttackPattern();
}
}
[장점]
- 코드의 재사용성: 상속을 통해 기존에 작성된 클래스의 특성과 동작을 새로운 클래스에서 그대로 사용할 수 있습니다. 즉, 비슷한 기능을 가진 클래스를 반복해서 작성할 필요 없이, 이미 작성된 클래스를 상속받아 필요한 부분만 추가하거나 수정하여 새로운 클래스를 만들 수 있습니다. 이는 코드의 재사용성을 높여줍니다.
- 유지보수의 용이성: 상속을 사용하면 기존에 작성된 클래스를 수정하지 않고도 새로운 기능을 추가하거나 수정할 수 있습니다. 기본 클래스를 수정하면 이를 상속받은 모든 클래스에도 변경 사항이 적용되므로, 코드의 유지보수가 훨씬 쉬워집니다. 따라서 코드의 일관성을 유지하면서 변경 사항을 쉽게 관리할 수 있습니다.
- 확장성: 상속을 사용하면 기존의 클래스에 새로운 기능을 추가하여 확장할 수 있습니다. 기본 클래스에 새로운 기능을 추가하거나 변경함으로써 파생 클래스들도 자동으로 그 기능을 상속받게 되므로, 기능을 확장하는 과정이 간편해집니다.
[단점]
- 강한 결합: 상속을 사용하면 부모 클래스와 자식 클래스가 강하게 결합될 수 있습니다. 부모 클래스의 변경이 자식 클래스에 영향을 미치며, 자식 클래스를 변경하면 부모 클래스에도 영향을 줄 수 있습니다. 이는 코드의 유연성을 떨어뜨릴 수 있고, 코드를 변경할 때 예상치 못한 부작용이 발생할 수 있습니다.
- 계층 구조의 복잡성: 상속을 남용하면 복잡한 계층 구조가 형성될 수 있습니다. 부모 클래스가 여러 번 상속되거나 다중 상속이 발생할 경우 코드의 이해와 관리가 어려워질 수 있습니다. 특히 상속이 깊게 중첩된 경우 클래스 간의 관계를 파악하기 어려울 수 있습니다.
- 메모리 낭비: 상속을 사용하면 자식 클래스가 부모 클래스의 모든 멤버를 상속받게 됩니다. 때로는 자식 클래스가 사용하지 않는 부분까지 상속받는 경우가 있을 수 있으며, 이는 메모리를 낭비할 수 있습니다. 또한, 상속을 통해 생성된 객체는 부모 클래스의 멤버와 함께 메모리를 차지하므로, 메모리 사용량이 증가할 수 있습니다.
- 의존성 증가: 상속을 사용하면 클래스들 간에 강한 의존성이 생길 수 있습니다. 자식 클래스가 부모 클래스에 의존하게 되면 부모 클래스의 변경이 자식 클래스에 영향을 줄 수 있습니다. 이는 코드의 유연성을 떨어뜨릴 수 있고, 클래스들 간의 결합도가 높아져서 코드의 재사용성과 유지보수성을 저하시킬 수 있습니다.
[상속을 잘못 사용하는 예시 코드]
아래의 코드는 상속을 여러 번 사용하여 의존성과 강한 결합으로 코드의 유연성을 저하시키는 예시입니다.
using System;
// 부모 클래스
class Animal {
public virtual void Eat() {
Console.WriteLine("동물이 먹는다.");
}
}
// 자식 클래스
class Dog : Animal {
public override void Eat() {
Console.WriteLine("개가 먹는다.");
}
}
// 더 자식 클래스
class Bulldog : Dog {
public override void Eat() {
Console.WriteLine("불독이 먹는다.");
}
}
// 더더 자식 클래스
class BulldogWithSpikes : Bulldog {
public override void Eat() {
Console.WriteLine("가시가 있는 불독이 먹는다.");
}
}
class Program {
static void Main(string[] args) {
Animal animal = new BulldogWithSpikes();
animal.Eat();
}
}
위의 코드에서는 Animal 클래스를 상속하여 Dog 클래스를 만들고, 다시 Dog 클래스를 상속하여 Bulldog 클래스를 만들었습니다. 그리고 Bulldog 클래스를 상속하여 BulldogWithSpikes 클래스를 만들었습니다. 이렇게 여러 번 사용하면 클래스 간의 의존성이 높아지고, 각 클래스 간의 강한 결합이 형성됩니다.
만약 Animal 클래스의 구조가 변경된다면 이는 Dog, Bulldog, BulldogWithSpikes 클래스 모두에게 영향을 미칠 것입니다. 또한, 코드의 계층 구조가 복잡해져서 유지보수가 어려워질 수 있습니다. 이러한 문제들은 코드의 유연성을 저하시키고, 변경에 취약한 코드를 만들게 됩니다. 따라서 이런 깊은 계층 구조는 피하는 것이 좋습니다.
[언제 사용해야 할까?]
상속을 사용해야 하는 시점은 부모 클래스와 자식 클래스 간에 “IS-A” 관계가 성립할 때입니다. 즉, 자식 클래스가 부모 클래스의 특성을 가지고 있고, 이를 확장하거나 변경하여 사용해야 할 때 상속을 사용합니다.
예를 들어, “사람”과 “학생” 클래스를 생각해봅시다. 학생은 사람이므로 “학생은 사람이다”라는 관계가 성립합니다. 이때, “학생” 클래스는 “사람” 클래스를 상속받아 학생만의 특성을 추가하거나 변경할 수 있습니다. 따라서 이런 경우에는 상속을 사용하여 구현하는 것이 적합합니다.
// 부모 클래스인 Person 클래스 정의
class Person {
public string Name { get; set; }
public int Age { get; set; }
public void Speak() {
Console.WriteLine("안녕하세요.");
}
}
// 자식 클래스인 Student 클래스 정의 (Person 클래스를 상속받음)
class Student : Person {
public string School { get; set; }
public int Grade { get; set; }
public void Study() {
Console.WriteLine("공부를 합니다.");
}
}
class Program {
static void Main(string[] args) {
// Person 클래스 사용
Person person = new Person();
person.Name = "홍길동";
person.Age = 20;
person.Speak();
// Student 클래스 사용
Student student = new Student();
student.Name = "김철수";
student.Age = 18;
student.School = "고등학교";
student.Grade = 3;
student.Speak(); // Person 클래스의 메서드 호출
student.Study(); // Student 클래스의 메서드 호출
}
}
위의 예시 코드에서 볼 수 있듯이, “학생” 클래스인 Student 클래스는 “사람” 클래스인 Person 클래스를 상속받아 구현되었습니다. 이렇게 하면 학생 클래스는 사람 클래스의 특성을 모두 상속받으면서, 추가적인 학생만의 특성을 추가할 수 있습니다.
[마무리 글]
잘 사용하면 코드의 가독성도 뛰어나지고, 유연성 및 확장성도 크게 증가하는 c#에서 중요한 역할을 하는 문법입니다. 하지만 잘못 사용하게 되면 가독성도 안 좋아지게 되고, 유연성 및 확장성도 떨어지게 됩니다. 제가 제시해드린 장점과 단점을 잘 이해하시고 언제 사용해야하는지 까지 잘 알아 두셨다가 실제로 사용할 때 적용하시다 보면, 상속을 능숙하게 사용하실 수 있을겁니다.