1. 소개
계층 구조가 없는 세상을 상상해봐: Person, Animal, Vehicle 같은 수천 개의 클래스가 전부 따로따로 있는 거지. 그런 세상에서 개발자들은 점심시간도 못 버티고 완전 헷갈려서 멘붕 올 거야! 실제 프로젝트에서는 여러 객체가 공통적으로 할 수 있는 게 필요할 때가 많아 (예를 들어, 모든 동물은 움직일 수 있음). 근데 각자만의 특징도 있지 (물고기는 수영, 새는 날기).
바로 이런 계층 구조 덕분에 엔티티들 사이의 관계를 표현할 수 있고, 프로그래밍이 창의적인 일이 되지, 복붙 전쟁이 아니게 돼.
예시를 보자. base 클래스 Animal이 있다고 해보자. 모든 동물은 소리를 낼 수 있어. 근데 고양이는 야옹, 개는 멍멍, 앵무새는 심지어 농담도 할 수 있지. 이걸 코드로 계층적으로 표현하고 싶어.
Base 클래스
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// 기본 메서드: 자식에서 override 가능
public virtual void Speak()
{
Console.WriteLine("동물이 어떤 소리를 내고 있어...");
}
}
여기서 virtual 키워드를 Speak() 메서드에 붙였어. 이건 "야, 자식 클래스들아, 필요하면 이 메서드 override 해도 돼"라는 표시야.
계층 구조 만들기: 파생 클래스
이제 Cat 클래스를 만들어서 Animal을 상속받아보자:
public class Cat : Animal
{
public Cat(string name) : base(name) { }
// Speak를 override — 고양이는 그냥 으르렁거릴 수 없지!
public override void Speak()
{
Console.WriteLine($"{Name} 말한다: 야옹!");
}
}
그리고 Dog 클래스:
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} 말한다: 멍멍!");
}
}
만약 말 못하는 평범한 동물이 있다면? 그럼 base 클래스를 그대로 쓰면 돼, override 안 해도 됨.
시각화 — 계층 트리
. Animal
/ \
Cat Dog
- Animal — base 클래스
- Cat, Dog — 자식(파생) 클래스
2. 이런 계층 구조를 사용하는 코드 써보자
우리 콘솔 앱 작업 계속해보자.
동물 컬렉션이 있고, 각각이 자기만의 소리를 내게 하고 싶다고 해보자:
Animal[] zoo = new Animal[]
{
new Cat("Барсик"),
new Dog("Рекс"),
new Animal("수수께끼 생명체")
};
foreach (Animal animal in zoo)
{
animal.Speak();
}
예상 출력:
Барсик 말한다: 야옹!
Рекс 말한다: 멍멍!
동물이 어떤 소리를 내고 있어...
이렇게 계층 구조랑 polymorphism 덕분에 (곧 자세히 볼 거지만, 핵심은 — 객체의 실제 타입에 따라 올바른 메서드가 호출됨), 네 앱이 유연하고 확장 가능해지는 거야.
3. 새로운 메서드와 필드 추가하기
"소리내기"는 이제 끝. 근데 모든 동물이 똑같으면 너무 심심하지. 예를 들어, 고양이는 9개의 생명을 가질 수 있고, 개는 막대기 가져오기 가능.
고유 동작 추가
자식 클래스에서 자기만의 메서드랑 필드 추가할 수 있어:
public class Cat : Animal
{
public int Lives { get; private set; } = 9;
public Cat(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} 말한다: 야옹! 내 생명 {Lives}개야.");
}
public void LoseLife()
{
if (Lives > 0)
{
Lives--;
Console.WriteLine($"{Name} 생명 하나 잃었어. 남은 생명: {Lives}");
}
else
{
Console.WriteLine($"{Name} 이미 모든 생명 다 썼어!");
}
}
}
코드에서 써보기:
var barsik = new Cat("Барсик");
barsik.Speak(); // Барсик 말한다: 야옹! 내 생명 9개야.
barsik.LoseLife(); // Барсик 생명 하나 잃었어. 남은 생명: 8
새 클래스 추가: "동물원" 확장하기
파생 클래스 만드는 법은 이미 알지. 예를 들어, 앵무새 추가해보자:
public class Parrot : Animal
{
public Parrot(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} 말한다: 안녕, 인간!");
}
public void Repeat(string phrase)
{
Console.WriteLine($"{Name} 따라한다: {phrase}");
}
}
이제 기존 코드는 건드릴 필요 없이 시스템을 쉽게 확장할 수 있어:
var keshka = new Parrot("Кеша");
keshka.Speak(); // Кеша 말한다: 안녕, 인간!
keshka.Repeat("공부해, 학생!"); // Кеша 따라한다: 공부해, 학생!
4. 동물 행동 비교
| 타입 | 메서드 Speak() | 고유 필드 | 추가 동작 |
|---|---|---|---|
| Animal | 있음 (virtual) | Name | — |
| Cat | 있음 (override) | Lives | LoseLife() |
| Dog | 있음 (override) | — | — |
| Parrot | 있음 (override) | — | Repeat(string) |
메모리에서 클래스 계층 구조는 이렇게 생겼어 (블록 다이어그램)
Animal (Name)
├── Cat (Lives)
├── Dog
└── Parrot (Repeat)
5. 우리 앱에서 실전 연습
계층 구조 아이디어를 앱에 연결해보자 — 예를 들어, 여러 타입의 할 일이 있다고 해보자:
- Task (base 클래스): 모든 할 일 — 제목이랑 완료 상태가 있음.
- WorkTask (업무용): 추가로 deadline이 있음.
- HomeTask (집안일): 우선순위("매우 중요", "그냥 그래") 가질 수 있음.
base 클래스부터 시작:
public class Task
{
public string Title { get; set; }
public bool IsCompleted { get; private set; }
public Task(string title)
{
Title = title;
}
public virtual void Complete()
{
IsCompleted = true;
Console.WriteLine($"할 일 \"{Title}\" 완료!");
}
}
이제 업무용 할 일 추가:
public class WorkTask : Task
{
public DateTime Deadline { get; set; }
public WorkTask(string title, DateTime deadline)
: base(title)
{
Deadline = deadline;
}
public override void Complete()
{
base.Complete();
Console.WriteLine($"마감일: {Deadline:d}");
}
}
그리고 집안일 할 일:
public class HomeTask : Task
{
public string Priority { get; set; }
public HomeTask(string title, string priority)
: base(title)
{
Priority = priority;
}
// base 클래스의 Complete로 충분하면 override 안 해도 됨
}
프로그램에서 할 일 리스트 만들기:
List<Task> tasks = new List<Task>
{
new WorkTask("보고서 보내기", DateTime.Today.AddDays(2)),
new HomeTask("설거지하기", "매우 중요"),
new Task("상속 강의 읽기")
};
foreach (Task task in tasks)
{
Console.WriteLine($"할 일: {task.Title}");
task.Complete();
}
예상 출력:
할 일: 보고서 보내기
할 일 "보고서 보내기" 완료!
마감일: 2025.07.13
할 일: 설거지하기
할 일 "설거지하기" 완료!
할 일: 상속 강의 읽기
할 일 "상속 강의 읽기" 완료!
봐봐, 얼마나 편한지: 모든 할 일을 한데 모아서 똑같이 처리하고, 필요한 부분에서만 특성이 드러나.
6. 상속 쓸 때 흔한 실수들
실수 1: virtual로 선언 안 된 메서드를 override 하려는 경우.
base 클래스에서 메서드에 virtual 안 붙이면, 파생 클래스에서 override 못 해. 그러면 polymorphism의 유연함이 사라지고, 계층 구조가 무쓸모가 돼.
실수 2: 논리적으로 연결 안 된 엔티티끼리 상속.
객체들이 의미상 연결 안 돼 있으면 상속 쓰지 마. 예를 들어, 원은 진짜 도형이 맞지만, 말이 탈것인 건 좀 억지야. 단, 특정 맥락(예: 중세 게임)에서는 괜찮을 수도 있음.
실수 3: 너무 깊은 계층 구조.
클래스 구조가 5~6단계 이상 깊어지면, 코드 읽기/유지/테스트가 힘들어져. 이럴 땐 composition을 상속 대신 고려해봐.
실수 4: base 생성자 호출 깜빡함.
파생 클래스에 새 속성 추가할 때 base(...) 호출을 빼먹기 쉬워. 그러면 base 부분이 제대로 초기화 안 돼서, 찾기 힘든 버그가 생길 수 있음.
GO TO FULL VERSION