CodeGym /행동 /C# SELF /상속과 오버라이딩에서 자주 하는 실수들

상속과 오버라이딩에서 자주 하는 실수들

C# SELF
레벨 25 , 레슨 1
사용 가능

1. 소개

초보자가 상속이라는 단어를 들으면, 그냥 기존 클래스 하나 가져와서 조금만 확장하거나 바꾸면 끝! 이렇게 생각할 수 있어. 근데 실제로는 생각보다 복잡한 부분이 많아. 상속에서 실수하면 여러 가지로 나타나: 잘못된 시그니처, 키워드 빼먹기, 계층 구조 설계가 엉망이면 진짜 골치 아픈 버그가 생길 수 있지.

어떤 경우엔 실수가 바로 드러나서 코드가 컴파일이 안 돼. 근데 또 어떤 경우엔, 버그가 런타임에야 나타나서, 네 SuperMegaLogger가 유저가 기대한 걸 안 쓰거나, 아예 아무것도 안 쓸 수도 있어. 슬픔, 실망, 긴 디버깅 시간... 다들 한 번쯤 겪어봤지?

자, 이제 상속과 메서드 오버라이딩에서 자주 하는 실수들을 같이 짚어보고, 코드를 좀 더 건강하게 만들어보자!

2. virtual 빼먹음: 왜 메서드를 오버라이드 못할까?

문제

C#에서는 override를 하려면, 부모 클래스에서 그 메서드가 virtual이나 abstract 또는 상속 체인에서 override로 선언되어 있어야 해. 만약 그 단어가 없으면, 자식 클래스에서 override 쓰면 컴파일 에러가 나.


class Animal
{
    public void Speak()
    {
        Console.WriteLine("동물이 뭔가 말한다.");
    }
}

class Cat : Animal
{
    // 컴파일 에러! 부모 클래스 메서드가 virtual, abstract, override가 아님.
    public override void Speak()
    {
        Console.WriteLine("야옹!");
    }
}

에러 메시지는 대충 이런 식이야: "'Cat.Speak()': cannot override inherited member 'Animal.Speak()' because it is not marked virtual, abstract, or override".

이런 실수 안 하려면?

오버라이드하려면, 부모 클래스에서 해당 메서드를 꼭 virtual로 선언해야 해. 그리고 클래스를 확장할 생각이면, 어떤 메서드를 오버라이드할 수 있게 할지 미리 고민하는 게 좋아.


class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("동물이 소리를 낸다.");
    }
}

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

이게 왜 필요할까?
메서드를 virtual로 선언하면, 자식들이 그 동작을 바꿀 수 있게 명시적으로 허락하는 거고, 컴파일러가 실수로 "엉뚱한" 메서드를 오버라이드하지 못하게 막아줘.

3. overridenew 키워드 실수

가끔 반대 상황도 있어: 자식 클래스에서 메서드를 "오버라이드"하고 싶은데, 부모 클래스에 virtual이 없어. 이럴 땐 컴파일러가 override는 못 쓰게 하고, new는 허용해. 근데 이건 완전히 다른 동작이야!


class Dog : Animal
{
    // 이건 override가 아니라 부모 메서드를 숨기는(hiding) 거야.
    public new void Speak()
    {
        Console.WriteLine("멍멍!");
    }
}

이렇게 만든 메서드를 Dog 타입으로 호출하면 잘 돼:


Dog dog = new Dog();
dog.Speak(); // "멍멍!"

근데 부모 타입으로 접근하면, 부모 메서드가 호출돼:


Animal dog2 = new Dog();
dog2.Speak(); // "동물이 소리를 낸다."

설명:
new는 메서드를 오버라이드하는 게 아니라, 부모 걸 가리는 거야. 이걸 "hiding"이라고 해. 이렇게 하면, 다형성(polymorphism)이 기대한 대로 안 돌아가서 헷갈릴 수 있어.

new vs override 실수 피하려면?

진짜 다형성(polymorphism)이 필요하면 virtual/override를 써. 완전히 새로운 기능을 추가하거나, 일부러 부모 메서드 동작을 숨기고 싶으면(진짜 조심해서!) new를 써.

상황 키워드 다형성 동작? 부모 타입으로 접근 시 동작
동작 변경 override 자식 클래스 메서드 호출
메서드 숨기기/대체 new 아니오 부모 클래스 메서드 호출

4. 메서드 시그니처 불일치

초보자뿐 아니라 경력자도, 자식 메서드에서 파라미터 타입이나 반환 타입을 바꿔서 실수하는 경우가 많아. 예를 들어, 부모 메서드가 public virtual void Print(string message)인데, 자식에서 public override void Print(object message)로 "오버라이드"하면, 이건 override가 아니라 그냥 새로운 메서드야.


class Printer
{
    public virtual void Print(string msg)
    {
        Console.WriteLine("기본 프린터: " + msg);
    }
}

class SmartPrinter : Printer
{
    // 컴파일 에러! 시그니처가 부모 메서드와 다름.
    public override void Print(object msg)
    {
        Console.WriteLine("스마트 프린터: " + msg);
    }
}

팁:
메서드 이름, 반환 타입, 파라미터(타입, 개수, 순서) 모두 똑같아야 해.

파라미터 타입을 실수로 바꾸거나 이름을 오타내면, 컴파일러가 경고해줄 거야.

5. 접근 제한자 불일치

또 하나 자주 하는 실수는 접근 제한자 문제야. 자식 메서드는 부모보다 더 좁은(더 private한) 접근 제한자를 가질 수 없어. 이런 코드는 안 돼:


public class Vehicle
{
    public virtual void StartEngine() { /* ... */ }
}

public class Car : Vehicle
{
    // 에러! 'private'는 부모의 'public'보다 더 제한적임.
    private override void StartEngine() { /* ... */ }
}

어떻게 해야 할까?
자식 메서드의 접근 제한자는 부모와 같거나 더 넓어야 해. 대부분 public이나 protected를 써.

6. 추상 메서드 빼먹거나 잘못 선언함

부모 클래스에서 abstract로 메서드를 선언하면, 자식 클래스에서 반드시 override로 구현해야 해. 안 그러면 그 클래스도 추상 클래스가 돼서 인스턴스를 만들 수 없어.


abstract class Shape
{
    public abstract double Area();
}

class Circle : Shape
{
    // 에러! 추상 메서드 Area()를 구현 안 함
}

해결법:
이렇게 구현해줘야 해:


class Circle : Shape
{
    public override double Area()
    {
        return 3.14 * 2 * 2; // 대충...
    }
}

7. 부모 구현 호출: base.Method()

가끔 메서드 전체를 바꾸는 게 아니라, 부모 동작에 뭔가 추가하고 싶을 때가 있어. 이럴 때 자식 메서드에서 base 키워드로 부모 구현을 호출할 수 있다는 걸 까먹거나 모르는 경우가 많아.


class Logger
{
    public virtual void Log(string msg)
    {
        Console.WriteLine("기본 로그: " + msg);
    }
}

class FancyLogger : Logger
{
    public override void Log(string msg)
    {
        // "멋진" 기능 추가하고, 부모 메서드 호출:
        Console.WriteLine("[FANCY] " + msg);
        base.Log(msg);
    }
}

설명:
base.Log(msg)를 호출하지 않으면, 부모 클래스에 있던 로직이 완전히 사라져버려.

8. 부모 생성자(base) 호출 빼먹음

부모 클래스 생성자에 필수 파라미터가 있으면, 자식 클래스에서 base로 명시적으로 부모 생성자를 호출해야 해.


class Engine
{
    public Engine(int cylinders)
    {
        Console.WriteLine("Engine 실린더 수: " + cylinders);
    }
}

class RaceEngine : Engine
{
    // 컴파일 에러! Engine에 기본 생성자가 없음.
    public RaceEngine() { }
}

// 수정된 버전:
class RaceEngine2 : Engine
{
    public RaceEngine2() : base(8) // 부모 생성자 명시적으로 호출
    {
        Console.WriteLine("레이싱 엔진 준비 완료!");
    }
}

9. sealed 키워드 빼먹음

가끔 자식 클래스에서 더 이상 오버라이드 못 하게 막고 싶을 때가 있어 — 이럴 땐 C#에서 sealedoverride를 같이 써. 이걸 안 쓰면, 누군가 네 메서드를 오버라이드해서 네가 기대한 동작이나 로직이 깨질 수 있어.


class Hero
{
    public virtual void Attack() => Console.WriteLine("영웅이 공격한다!");
}
class Warrior : Hero
{
    public sealed override void Attack() => Console.WriteLine("전사가 공격한다!");
}
class Mutant : Warrior
{
    // 에러! Attack 메서드는 위에서 sealed로 막음.
    // public override void Attack() { ... }
}

설명:
이렇게 하면, 해당 레벨에서 구현이 "봉인"돼서, 아래 클래스들은 더 이상 바꿀 수 없어.

10. 오버라이드 대신 잘못된 오버로딩

가끔 개발자들이 오버로딩(overloading)과 오버라이딩(overriding)을 헷갈려. 오버로딩은 같은 이름에 시그니처만 다른 메서드를 여러 개 만드는 거고, 다형성과는 아무 상관 없어.


class Animal
{
    public virtual void Eat()
    {
        Console.WriteLine("동물이 먹는다.");
    }
}

class Panda : Animal
{
    // 이건 override가 아님! 그냥 새로운 오버로딩 메서드.
    public void Eat(string what)
    {
        Console.WriteLine("판다가 먹는다: " + what);
    }
}

...

Animal a = new Panda();
a.Eat(); // 오버라이드하면 자식 구현이 호출됨. 아니면 부모 거 씀
// a.Eat("대나무"); // 컴파일 에러: Animal에 그런 메서드 없음

다형성(polymorphism) 동작을 원하면, 꼭 오버라이딩을 해야 해. 오버로딩만으론 안 돼.

11. 설계상의 실수들 (형식적/비형식적)

클래스 설계가 엉망이면, 상속 구조가 진짜 헷갈려져:

  • base.를 제대로 안 쓰고 override만 계속 도는 "회전목마" 구조,
  • 접근 제한자 사용이 일관성 없음,
  • 너무 깊거나 직관적이지 않은 계층 구조,
  • 메서드 로직이 여러 상속 단계에 나눠져서 어디서 뭘 하는지 모름,
  • virtual/abstract 메서드에 주석이 없음,
  • 부모 생성자에서 virtual 메서드 호출해서, 자식이 아직 완전히 초기화 안 됐을 때 이상한 부작용이 생김.

팁:
헷갈리면, 종이에 직접 상속 구조 그려보고, 어디에 어떤 메서드가 있고, 누가 오버라이드하고, 어디서 base.를 호출해야 하는지 표시해봐(진짜 효과 있음!).
실제 프로젝트에서는, 어떤 메서드를 오버라이드해야 하는지 문서에도 꼭 남겨두는 게 좋아.

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