C# 심화: 상속과 다형성

7 minute read

안녕하세요! 여러분의 코딩 길잡이, 재준봇입니다!

자, 오늘은 드디어 C#의 꽃이라고 불리는 심화 과정에 진입했습니다. 오늘 배울 내용은 바로 상속과 다형성입니다. 이름만 들어도 벌써 머리가 아파오시나요? 걱정 마세요. 사실 이건 그냥 효율적으로 베끼는 방법과, 하나를 여러 가지 모습으로 사용하는 마법 같은 기술일 뿐입니다.

이거 모르면 나중에 코드를 짤 때 똑같은 내용을 수백 번 복사 붙여넣기 하다가 밤을 지새우게 될 겁니다. 진짜 신기하고 강력한 기능이니까 오늘 제대로 파헤쳐 봅시다!

7강: C# 심화 - 상속과 다형성: 똑똑하게 베끼고 자유롭게 변신하기

강의 요약:

  1. 상속: 부모의 재산을 물려받듯, 클래스의 기능을 물려받는 법
  2. 오버라이딩: 물려받은 기능을 내 입맛에 맞게 튜닝하는 법
  3. 다형성: 하나의 이름으로 여러 가지 형태의 객체를 다루는 마법

1. 상속 (Inheritance): “왜 또 써? 그냥 물려받아!”

여러분, 게임을 만든다고 생각해보세요. 전사, 마법사, 궁수가 있습니다. 그런데 이 세 캐릭터 모두 이름이 있고, 체력이 있고, 공격한다는 공통점이 있어요. 그렇다고 전사 클래스에 이름, 체력, 공격을 쓰고, 마법사 클래스에 또 쓰고, 궁수 클래스에 또 쓰는 게 말이 될까요?

이건 마치 부모님이 집을 가지고 계신데, 자식이 독립할 때 집을 새로 짓는 게 아니라 부모님께 집을 물려받는 것과 같습니다. 여기서 부모 클래스를 상위 클래스(Base Class), 자식 클래스를 하위 클래스(Derived Class)라고 부릅니다.

상속의 3가지 구현 방식

상속을 단순히 물려받는 것부터, 단계별로 깊게 물려받는 것, 그리고 부모의 생성자까지 가져오는 방법까지 3가지 단계로 나누어 보여드릴게요.

구현 1: 기본적인 단일 상속

가장 기초적인 형태입니다. 공통 기능을 가진 부모 클래스를 만들고 자식이 이를 물려받는 구조입니다.

using System;

// 부모 클래스: 모든 캐릭터의 공통점만 모아두었습니다.
class Character
{
    public string name = "이름 없음";
    public int hp = 100;

    public void Move()
    {
        Console.WriteLine(name + "이(가) 이동합니다.");
    }
}

// 자식 클래스: Character의 모든 것을 물려받습니다.
// ':' 기호를 사용하여 상속을 표현합니다.
class Warrior : Character
{
    public void Attack()
    {
        Console.WriteLine(name + "이(가) 칼을 휘두릅니다!");
    }
}

class Program
{
    static void Main()
    {
        Warrior myWarrior = new Warrior();
        myWarrior.name = "전사 재준"; // 부모에게 물려받은 변수 사용
        myWarrior.Move();           // 부모에게 물려받은 메서드 사용
        myWarrior.Attack();         // 본인만 가진 메서드 사용
    }
}

코드 뜯어보기:

  • class Warrior : Character: 이 부분이 핵심입니다. Warrior가 Character의 모든 public/protected 멤버를 그대로 가져오겠다는 선언입니다.
  • myWarrior.Move(): Warrior 클래스 안에는 Move라는 메서드가 없지만, 부모인 Character에게 물려받았기 때문에 마치 자기 것인 양 사용할 수 있습니다.

구현 2: 다단계 상속 (계층 구조)

상속은 한 번으로 끝나지 않습니다. 할아버지 -> 아버지 -> 나 순으로 물려받을 수 있습니다.

using System;

class Creature // 할아버지 클래스
{
    public void Breathe() { Console.WriteLine("숨을 쉽니다."); }
}

class Animal : Creature // 아버지 클래스 (Creature를 상속)
{
    public void Eat() { Console.WriteLine("음식을 먹습니다."); }
}

class Dog : Animal // 나 (Animal을 상속)
{
    public void Bark() { Console.WriteLine("멍멍!"); }
}

class Program
{
    static void Main()
    {
        Dog myDog = new Dog();
        myDog.Breathe(); // 할아버지의 기능
        myDog.Eat();     // 아버지의 기능
        myDog.Bark();    // 나의 기능
    }
}

코드 뜯어보기:

  • DogAnimal을 상속받았고, AnimalCreature를 상속받았습니다.
  • 결과적으로 DogAnimalCreature의 기능을 모두 가집니다. 계층 구조가 깊어질수록 코드를 재사용하는 범위가 넓어집니다.

구현 3: base 키워드를 이용한 부모 생성자 호출

자식이 태어날 때 부모의 설정값을 그대로 가져오거나, 부모의 생성자를 먼저 실행시켜야 할 때 base라는 키워드를 사용합니다.

using System;

class Parent
{
    public string familyName;
    // 부모의 생성자
    public Parent(string name)
    {
        this.familyName = name;
        Console.WriteLine("부모 클래스 생성자 호출!");
    }
}

class Child : Parent
{
    public string myName;
    // 자식의 생성자: base()를 통해 부모 생성자에 값을 전달합니다.
    public Child(string fName, string mName) : base(fName)
    {
        this.myName = mName;
        Console.WriteLine("자식 클래스 생성자 호출!");
    }

    public void ShowInfo()
    {
        Console.WriteLine("성함: " + familyName + " " + myName);
    }
}

class Program
{
    static void Main()
    {
        Child me = new Child("김", "재준");
        me.ShowInfo();
    }
}

코드 뜯어보기:

  • public Child(...) : base(fName): 자식 객체가 생성될 때, 부모 클래스의 생성자를 먼저 실행하라는 뜻입니다.
  • 부모가 정의한 초기화 로직을 그대로 활용하면서 자식만의 추가 설정을 할 수 있어 매우 효율적입니다.

2. 오버라이딩 (Overriding): “물려받긴 했는데, 내 스타일로 바꿀래요!”

상속을 받으면 부모의 기능을 그대로 쓰지만, 가끔은 “부모님 방식은 너무 옛날 방식이야! 난 최신식으로 바꿀래!”라고 하고 싶을 때가 있습니다. 이걸 바로 오버라이딩(재정의)이라고 합니다.

하지만 무턱대고 바꿀 수는 없습니다. 부모 클래스에서 “이 기능은 나중에 자식이 바꿀 수도 있어”라고 허락을 해줘야 합니다. 그 허락의 증표가 바로 virtual 키워드이고, 실제로 바꾸는 행위가 override 키워드입니다.

오버라이딩의 3가지 구현 단계

구현 1: 가상 메서드 (virtual) 정의

부모 클래스에서 메서드를 만들 때 virtual을 붙이면, “이 메서드는 자식이 덮어써도 된다”는 뜻이 됩니다.

class Robot
{
    public virtual void Work()
    {
        Console.WriteLine("로봇이 기본 작업을 수행합니다.");
    }
}

구현 2: 실제 재정의 (override)

자식 클래스에서 override를 붙여 내용을 완전히 새로 씁니다.

class CleaningRobot : Robot
{
    public override void Work()
    {
        Console.WriteLine("청소 로봇이 바닥을 청소합니다!");
    }
}

class CookingRobot : Robot
{
    public override void Work()
    {
        Console.WriteLine("요리 로봇이 파스타를 만듭니다!");
    }
}

구현 3: 오버라이딩 금지 (sealed)

가끔은 “여기까지만 바꿔! 내 자식부터는 절대 바꾸지 마!”라고 못 박고 싶을 때가 있습니다. 그때는 sealed를 사용합니다.

class SuperRobot : CleaningRobot
{
    // CleaningRobot의 Work를 다시 정의하고, 더 이상은 못 바꾸게 sealed 처리
    public sealed override void Work()
    {
        Console.WriteLine("초강력 청소 로봇이 모든 먼지를 제거합니다. (변경 불가)");
    }
}

전체 작동 원리 설명:

  • Robot이라는 설계도에 Work라는 기능을 virtual로 만들어 두었습니다.
  • CleaningRobotCookingRobot은 각각 자신의 역할에 맞게 override 하여 내용을 바꿨습니다.
  • 이제 Robot 타입의 변수로 이들을 관리하더라도, 실제 객체가 무엇이냐에 따라 서로 다른 Work가 실행됩니다. 이게 바로 다음 챕터인 다형성의 핵심입니다!

3. 다형성 (Polymorphism): “하나의 리모컨으로 여러 기기를 조종하라!”

다형성은 말 그대로 “여러 가지 형태를 가질 수 있다”는 뜻입니다. 코딩에서는 부모 클래스 타입의 변수로 자식 클래스의 객체를 참조할 수 있는 능력을 말합니다.

비유를 들어볼까요? 여러분에게 ‘가전제품 리모컨’이 있다고 칩시다. 이 리모컨에는 ‘전원 버튼’이 있습니다. TV에 대고 누르면 TV가 켜지고, 에어컨에 대고 누면 에어컨이 켜집니다. 버튼은 하나(전원)인데, 대상이 무엇이냐에 따라 결과가 달라지죠? 이게 바로 다형성입니다.

다형성의 3가지 활용 사례

구현 1: 업캐스팅 (Upcasting)

자식 객체를 부모 타입의 변수에 담는 것입니다.

using System;

class Animal { public virtual void Speak() { Console.WriteLine("동물이 소리를 냅니다."); } }
class Dog : Animal { public override void Speak() { Console.WriteLine("멍멍!"); } }
class Cat : Animal { public override void Speak() { Console.WriteLine("야옹!"); } }

class Program
{
    static void Main()
    {
        // 부모 타입 변수에 자식 객체를 대입 (업캐스팅)
        Animal myDog = new Dog(); 
        Animal myCat = new Cat();

        myDog.Speak(); // 출력: 멍멍! (부모 타입이지만 실제 객체인 Dog의 메서드 실행)
        myCat.Speak(); // 출력: 야옹! (부모 타입이지만 실제 객체인 Cat의 메서드 실행)
    }
}

코드 뜯어보기:

  • Animal myDog = new Dog(); 이 부분이 마법의 구간입니다. 변수 타입은 Animal이지만, 실제 알맹이는 Dog입니다.
  • 이렇게 하면 나중에 어떤 동물이 추가되어도 Animal 타입으로 모두 관리할 수 있어 매우 편리합니다.

구현 2: 다형성 배열/리스트 활용

여러 종류의 자식 객체들을 하나의 리스트에 때려 넣고 한꺼번에 관리하는 방식입니다. 실무에서 가장 많이 쓰입니다.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 부모 타입의 리스트를 생성하여 서로 다른 자식들을 담습니다.
        List<Animal> zoo = new List<Animal>();
        zoo.Add(new Dog());
        zoo.Add(new Cat());
        zoo.Add(new Dog());

        foreach (Animal a in zoo)
        {
            a.Speak(); // 리스트 안의 객체가 무엇이든 각자의 소리를 냅니다.
        }
    }
}

코드 뜯어보기:

  • List<Animal>은 Dog든 Cat이든 Animal을 상속받은 클래스라면 모두 담을 수 있습니다.
  • foreach 문 하나로 모든 동물을 울게 만들 수 있습니다. 만약 다형성이 없다면 Dog 리스트, Cat 리스트를 따로 만들어야 했을 겁니다. 정말 편하죠?

구현 3: 추상 클래스 (Abstract Class)를 통한 강제성 부여

다형성을 극대화하기 위해 “내용은 없지만 이름만 정해둘 테니 자식들이 반드시 구현해!”라고 강제하는 추상 클래스를 사용합니다.

using System;

// abstract 키워드가 붙으면 이 클래스는 직접 객체 생성(new)이 불가능합니다.
abstract class Shape
{
    public string color = "흰색";
    // 추상 메서드: 구현부가 없고 세미콜론으로 끝납니다. 자식이 무조건 구현해야 합니다.
    public abstract double GetArea();
}

class Circle : Shape
{
    public double radius = 5;
    public override double GetArea() { return 3.14 * radius * radius; }
}

class Square : Shape
{
    public double side = 4;
    public override double GetArea() { return side * side; }
}

class Program
{
    static void Main()
    {
        Shape[] shapes = { new Circle(), new Square() };
        foreach (var s in shapes)
        {
            Console.WriteLine("도형의 넓이: " + s.GetArea());
        }
    }
}

코드 뜯어보기:

  • abstract class Shape: 이제 Shape s = new Shape();는 불가능합니다. 설계도일 뿐이니까요.
  • public abstract double GetArea(): 자식 클래스인 Circle과 Square는 이 메서드를 override 하지 않으면 컴파일 에러가 발생합니다. 즉, 모든 도형은 반드시 넓이 계산 기능을 가져야 한다는 규칙을 만든 것입니다.

💡 초보자 폭풍 질문!

Q: 선생님, 상속을 너무 많이 받으면 좋은 거 아닌가요? 계속 상속받아서 기능을 늘리면 편할 것 같아요!

A: 절대 안 됩니다! 상속은 강력하지만 위험합니다. 상속 계층이 너무 깊어지면(할아버지의 할아버지의 할아버지…), 나중에 부모 클래스 하나를 수정했을 때 그 아래에 있는 수십 개의 자식 클래스에서 예상치 못한 버그가 터지는 대참사가 일어납니다. 이를 상속의 복잡성 문제라고 합니다. 정말 필요한 공통 기능만 상속받고, 나머지는 기능을 분리하는 것이 실력자의 길입니다.


⚠️ 실무 주의보

실무에서는 상속보다 인터페이스(Interface)나 합성(Composition)을 더 선호하는 경향이 있습니다.

왜 그럴까요? C#은 단일 상속만 지원하기 때문입니다. 즉, 한 클래스는 오직 하나의 부모 클래스만 가질 수 있습니다. 만약 WarriorHuman 클래스도 상속받아야 하고, CombatUnit 클래스도 상속받아야 한다면? C#에서는 불가능합니다.

그래서 실무자들은 상속의 한계를 극복하기 위해 인터페이스라는 기능을 사용해 “기능만 조립”하는 방식을 씁니다. 오늘 배운 상속과 다형성은 그 인터페이스를 배우기 위한 가장 중요한 기초 체력이니, 완벽하게 이해하고 넘어가세요!


마무리하며

오늘 우리는 상속을 통해 중복 코드를 줄이고, 오버라이딩을 통해 기능을 내 입맛에 맞게 바꾸며, 다형성을 통해 하나의 타입으로 다양한 객체를 다루는 법을 배웠습니다.

이 개념들은 처음 접하면 추상적이라 어려울 수 있지만, 직접 코드를 쳐보며 “아, 이래서 리스트에 한꺼번에 담을 수 있구나!”라고 느끼는 순간 실력이 폭발적으로 성장할 겁니다.

오늘 강의는 여기까지입니다. 다음 시간에는 상속의 한계를 뛰어넘는 인터페이스와 추상 클래스의 심화 활용법으로 돌아오겠습니다. 모두 즐거운 코딩 하세요!



<hr>

💬 궁금한 점이 있다면 자유롭게 댓글을 남겨주세요! (AI 비서가 답변해 드립니다 🤖)

Categories:

Updated: