CodeGym /행동 /C# SELF /폴리모피즘 개념과 OOP에서의 역할

폴리모피즘 개념과 OOP에서의 역할

C# SELF
레벨 21 , 레슨 0
사용 가능

1. 들어가기

상상해봐: 집에 TV, 오디오, 에어컨, 스마트 전구가 있어. 그리고 각각 자기 리모컨이 따로 있지. TV 켜려면 TV 리모컨 들고 "켜기" 버튼을 누르고, 오디오 켜려면 오디오 리모컨 들고 똑같이 "켜기" 버튼을 눌러. 상상돼? 완전 혼란이지?

근데 만약 만능 리모컨이 있다면 어떨까? 그걸 들고 "켜기" 버튼만 누르면, 얘가 알아서 지금 어떤 기기를 켜야 하는지 판단해서 제대로 명령을 보내주는 거야. 그리고 리모컨 자체는 어떻게 TV가 켜지는지, 어떻게 오디오가 켜지는지 몰라도 돼. 그냥 모든 기기에 "켜기"라는 공통 기능이 있다는 것만 알면 돼.

이게 바로 폴리모피즘이랑 똑같은 거야!

폴리모피즘

"폴리모피즘"이라는 단어는 그리스어 poly (많은)와 morph (형태)에서 왔어. 직역하면 "많은 형태"라는 뜻이지. OOP에서 이건 객체가 여러 형태를 가질 수 있다는 거고, 더 정확히는 같은 메서드가 어떤 객체에서 호출되냐에 따라 다르게 동작할 수 있다는 거야.

C#에서는 주로 상속이랑 virtual, override 메서드를 써서 폴리모피즘을 구현해.

폴리모피즘의 핵심 아이디어는 업캐스팅(upcasting)이야. 이게 뭔지 볼까?

아까 AnimalDog, Cat 계층을 다시 보자. 우린 "강아지는 동물이다", "고양이는 동물이다"라는 걸 알아. 이게 "is-a" 관계고, 상속의 기본이야.


public class Animal
{
    public virtual void MakeSound()
    {
        Console.WriteLine("어떤 소리...");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("멍멍!");
    }
}

public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("야옹!");
    }
}

"is-a" 관계 덕분에, 파생 클래스 객체를 부모 클래스 변수에 넣을 수 있어. 이걸 업캐스팅(upcasting)이라고 해. 즉, 객체를 더 일반적인 타입으로 "올려서" 다루는 거지. C# 컴파일러는 이걸 자동으로 해줘:


// 구체적인 강아지가 있어
Dog myDog = new Dog();

// Dog 객체를 Animal 타입 변수에 넣을 수 있어!
// 이게 바로 업캐스팅(upcasting)이야.
// 오른쪽은 구체적인 Dog, 왼쪽은 더 일반적인 Animal.
Animal generalAnimal = myDog;

// 그리고 MakeSound()도 호출할 수 있어!
generalAnimal.MakeSound(); // "멍멍!" 출력됨 ("어떤 소리..."가 아니라 진짜 "멍멍!"이야!)               

여기서 무슨 일이 일어난 걸까?

  1. Animal generalAnimal = myDog;를 썼을 때, 새로운 동물을 만든 게 아니야. 그냥 myDog 객체(실제로는 Dog)를 Animal이라는 "박스"에 넣은 거지.
  2. 컴파일 단계(코드가 바이트코드로 바뀔 때)에서 generalAnimal 변수는 Animal 타입이야. 그래서 컴파일러는 이 변수로 Animal 클래스에 있는 메서드와 속성만 쓸 수 있다고 알아.
  3. 근데 실행 단계(runtime)에서 generalAnimal.MakeSound()를 호출하면, .NET 런타임은 변수 타입(Animal)이 아니라 실제 객체 타입(Dog)을 봐! 그리고 MakeSound()Animal에서 virtual이고 Dog에서 override로 되어 있으니까, Dog의 구현이 호출되는 거야.

이렇게 메서드 동작이 실행 중 실제 객체 타입에 따라 결정되는 걸 동적 디스패치 또는 런타임 폴리모피즘이라고 해.

박스를 상상해봐: 겉에는 "과일"(Animal 타입 변수)이란 라벨이 붙어있고, 안에는 사과(Dog 객체)가 들어있어. "과일아, 소리 내봐!"(MakeSound() 호출)라고 하면, 박스는 안에 사과가 들어있는 걸 알고 "아삭" 소리를 내지, 그냥 "과일 소리"를 내는 게 아니야.

2. 폴리모피즘 실전 데모

폴리모피즘을 진짜 이해하려면, 여러 객체가 있을 때 실제로 어떻게 동작하는지 보는 게 제일 좋아.

"내 작은 동물원" 앱을 확장해보자. 우리가 동물병원에서 일한다고 치고, 여러 동물이 진료를 받으러 온다고 해보자. 각 동물마다 진료 방식은 다르지만, 진료 호출 코드는 공통으로 만들고 싶어.

먼저, Animal 기본 클래스와 Dog, Cat 파생 클래스에 새로운 virtual 메서드 Examine()을 추가해보자:


public class Animal
{
    public string Name { get; set; }
    public Animal(string name) { Name = name; }
    public virtual void MakeSound() { Console.WriteLine($"{Name} 어떤 소리..."); }
    public virtual void Examine()
    {
        Console.WriteLine($"{Name} 진료:");
        Console.WriteLine("  - 호흡 체크.");
        Console.WriteLine("  - 체온 측정.");
    }
}

public class Dog : Animal
{
    public Dog(string name) : base(name) { }
    public override void MakeSound() { Console.WriteLine($"{Name} 말한다: 멍!"); }
    public override void Examine()
    {
        base.Examine();
        Console.WriteLine("  - 이빨 체크.");
        Console.WriteLine("  - 광견병 예방접종.");
    }
}

public class Cat : Animal
{
    public Cat(string name) : base(name) { }
    public override void MakeSound() { Console.WriteLine($"{Name} 말한다: 야옹!"); }
    public override void Examine()
    {
        base.Examine();
        Console.WriteLine("  - 발톱 체크.");
        Console.WriteLine("  - 구충제 알약.");
    }
}

이제 강아지랑 고양이 각각에 맞는 Examine()이 생겼어. base.Examine();을 오버라이드 메서드에서 쓰는 거 봤지? 이건 기본 진료 절차(Animal에서 정의된)를 먼저 실행하고, 그 다음에 각 동물에 맞는 추가 절차를 넣는 거야. 진짜 편하지!

이제 우리 동물병원을 상상해보자. 진료받으러 온 동물 리스트가 있어:


class Program
{
    static void Main()
    {
        Animal[] animals = {
            new Dog("렉스"),
            new Cat("무르카"),
            new Animal("토끼")
        };

        Console.WriteLine("--- 동물 진료 ---");
        foreach (Animal animal in animals)
        {
            animal.Examine();
            Console.WriteLine();
        }

        Console.WriteLine("--- 소리 시간 ---");
        foreach (Animal animal in animals)
            animal.MakeSound();
    }
}

결과:


--- 동물 진료 ---
렉스 진료:
  - 호흡 체크.
  - 체온 측정.
  - 이빨 체크.
  - 광견병 예방접종.

무르카 진료:
  - 호흡 체크.
  - 체온 측정.
  - 발톱 체크.
  - 구충제 알약.

토끼 진료:
  - 호흡 체크.
  - 체온 측정.

--- 소리 시간 ---
렉스 말한다: 멍!
무르카 말한다: 야옹!
토끼 어떤 소리...

이게 바로 폴리모피즘의 힘이야! 똑같은 foreach (Animal animal in animals) 루프를 썼는데, 매번 animal.Examine() (또는 animal.MakeSound())을 호출할 때마다 딱 맞는 구현이 실행돼. 코드는 심플하고, 깔끔하고, 범용적이고, 구현 세부사항은 각 클래스 안에 숨겨져 있지.

내일 햄스터 Hamster가 오면? 그냥 Hamster : Animal 클래스를 만들고 Examine()만 오버라이드하면, foreach 루프는 아무것도 안 바꿔도 돼! 이게 진짜 폴리모피즘의 파워야.

3. OOP에서 폴리모피즘의 역할

폴리모피즘은 그냥 멋진 단어나 코드 트릭이 아니야. 이건 프로그램을 유연하고, 확장 가능하고, 유지보수 쉽게 만들어주는 핵심 원칙이야. 어떤 역할을 하는지 보자:

유연성과 확장성 (Open/Closed Principle)

이게 아마 제일 중요한 장점일 거야. 폴리모피즘 덕분에 확장에는 열려 있고, 변경에는 닫혀 있는 코드를 쓸 수 있어. 이게 무슨 뜻이냐면:

  • 확장에 열려 있음: 새로운 동물 타입(Bird, Fish, Hamster 등)을 추가하고 싶으면, Animal을 상속받아서 필요한 메서드만 오버라이드하면 돼.
  • 변경에 닫혀 있음: 이미 Animal[]로 동작하는 기존 코드는 건드릴 필요 없어. foreach에서 animal.Examine()만 호출하면, 동물 종류가 늘어나도 루프는 그대로야.

만약 폴리모피즘이 없었다면 if-else ifswitch문을 엄청 많이 써야 했을 거고, 동물 추가할 때마다 계속 코드를 고쳐야 했을 거야. 진짜 골치 아프지.

코드 단순화

폴리모피즘 덕분에 다양한 타입의 객체 컬렉션을 다루는 코드가 훨씬 단순해져. 객체 타입마다 일일이 체크하고 메서드 호출할 필요 없이, 그냥 부모 클래스의 공통 메서드만 호출하면 시스템이 알아서 맞는 구현을 불러줘.

이건 여러 종류의 문을 "열기" 버튼 하나로 여는 것과 똑같아: 일반문, 미닫이문, 회전문 등등. 매번 밀고, 당기고, 돌리고 할 필요 없이 그냥 "열기"만 누르면 각 문이 알아서 열려.

추상화

폴리모피즘은 추상화랑도 밀접하게 연결돼 있어. 추상화는 OOP의 또 다른 기둥인데, 이건 다음 강의에서 자세히 다룰 거야. 폴리모피즘 덕분에 "무엇을" 하는지(예: "소리 내기", "진료받기")에 집중할 수 있고, "어떻게" 하는지는 신경 안 써도 돼. 우리는 "동물"이라는 추상적인 개념으로 작업하지, "강아지"나 "고양이"에만 묶이지 않아. 그래서 고수준, 깔끔한 코드를 만들 수 있고, 구현 세부사항에 덜 의존하게 돼.

코드 재사용

상속만으로도 부모 클래스의 속성과 메서드를 재사용할 수 있지만, 폴리모피즘이 있으면 그 효과가 더 커져. Animal[] 같은 범용 알고리즘이나 자료구조를 만들어서, 부모 클래스를 상속받은 어떤 객체든 중복 없이 다룰 수 있어.

결국 폴리모피즘은 코드를 더 "프로답게" 만들고, 변화에 강하게 해줘. 실제 개발에서는 요구사항이 계속 바뀌니까 이게 진짜 중요해.

4. 꿀팁 & 디테일

폴리모피즘 구조도

classDiagram
    class Animal {
        +MakeSound()
    }
    class Dog {
        +MakeSound()
    }
    class Cat {
        +MakeSound()
    }
    class Parrot {
        +MakeSound()
    }

    Animal <|-- Dog
    Animal <|-- Cat
    Animal <|-- Parrot
폴리모피즘을 위한 클래스 계층 구조도

MakeSound()Animal 타입 참조로 호출하면, 실제 변수에 들어있는 객체의 클래스 메서드가 호출돼.

배열과 컬렉션에서 폴리모피즘 활용

여러 타입의 객체 컬렉션을 돌면서 똑같은 로직을 실행하는 건 진짜 자주 쓰는 패턴이야.


// 우리 연습용 앱에서:
Animal[] zoo = { new Dog("샤릭"), new Cat("바르식"), new Parrot("케샤") };

foreach (Animal animal in zoo)
{
    animal.MakeSound();
}

실제 프로젝트에서는 이벤트 처리, 화면에 그림 그리기(각 도형마다 다르게), 결제 처리(카드/서비스별로 다르게) 등에 이렇게 써.

폴리모피즘이 동작하는 방식

객체 타입 변수 타입 어떤 메서드가 호출됨?
Dog
Animal
Dog.MakeSound()
Cat
Animal
Cat.MakeSound()
Parrot
Animal
Parrot.MakeSound()
Animal
Animal
Animal.MakeSound()

중요한 건 — 메서드는 부모 클래스에서 virtual이나 abstract로, 자식 클래스에서 override로 선언해야 해!

5. 폴리모피즘에서 흔히 하는 실수

실제로 학생들이 자주 하는 실수는 이런 것들이야:

  • 부모 클래스에서 메서드를 virtual로 안 만들어서, 폴리모피즘이 안 되고 항상 한 가지 동작만 나옴.
  • 어떤 클래스가 변수 타입이어야 하는지 헷갈림. 꼭 기억해: 변수는 타입(예: Animal)이고, 거기에 들어가는 객체는 인스턴스(예: new Dog())야.
  • 부모 클래스에 없는 새로운 멤버를 부모 타입 변수로 쓰려고 함. 예를 들어:

Animal pet = new Dog();
pet.Bark(); // 에러! Animal에는 Bark()가 없어

이럴 땐 어떻게? 정말 필요하면 타입 캐스팅을 써도 되지만, 웬만하면 부모 클래스에 있는 메서드만 쓰도록 코드를 짜는 게 좋아.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION