상속

상속이란?

상속Inheritance은 기존 클래스를 기반으로 새로운 클래스를 생성할 수 있게 해주는 메커니즘입니다. 상속을 통해 새로운 클래스는 기존 클래스의 속성과 메서드를 물려받으며, 이를 바탕으로 기능을 확장하거나 수정할 수 있습니다. 이러한 기능은 코드의 재사용성을 높이고, 계층 구조를 통해 프로그램을 더 논리적이고 관리하기 쉽게 만드는 데 기여합니다. 상속은 부모와 자식의 관계로 비유할 수 있습니다. 부모 클래스는 공통적인 속성과 행동을 정의하고, 자식 클래스는 이러한 속성과 행동을 물려받아 자신의 고유한 특성을 추가하거나 부모 클래스의 기능을 수정할 수 있습니다. 이를 통해 중복 코드를 줄이고, 보다 일관성 있는 시스템을 설계할 수 있습니다.

상속의 목적 및 장점

코드 재사용성

상속은 기존 클래스의 기능을 새로운 클래스에서 재사용할 수 있게 해줍니다. 이를 통해 중복 코드를 줄이고, 개발 시간을 단축하며, 일관성을 유지할 수 있습니다. 공통된 기능을 부모 클래스에 정의하고, 이를 여러 자식 클래스가 물려받아 사용할 수 있어, 코드의 재사용성이 크게 향상됩니다.

유지보수성 향상

상속을 사용하면 공통된 코드를 한 곳에서 관리할 수 있어, 수정 사항이 발생할 때 부모 클래스만 변경하면 자식 클래스에 자동으로 반영됩니다. 이는 코드의 유지보수를 쉽게 하고, 변경으로 인한 오류 발생을 줄이는 데 도움이 됩니다.

계층적 구조화

상속을 통해 프로그램의 구조를 계층적으로 구성할 수 있습니다. 상위 클래스는 공통 기능을 제공하고, 하위 클래스는 이 기능을 확장하거나 수정하여 자신만의 특화된 기능을 추가할 수 있습니다. 이를 통해 프로그램의 구조를 논리적으로 정리하고, 복잡성을 줄일 수 있습니다.

상속의 구현 방법

상속은 주로 클래스 선언 시, 부모 클래스의 이름을 자식 클래스에 지정함으로써 구현됩니다. .NET에서는 콜론(:)을 사용하여 부모 클래스를 지정합니다.

상속의 기본 구현

public class Car
{
    public string Brand { get; set; }
    public void StartEngine() => Console.WriteLine($"{Brand} engine started.");
}
public class ElectricCar : Car
{
    public void ChargeBattery() => Console.WriteLine($"{Brand} battery is charging.");
}

위 예제에서 ElectricCar 클래스는 Car 클래스를 상속받습니다. 따라서 ElectricCarCar 클래스의 Brand 속성과 StartEngine 메서드를 그대로 사용할 수 있으며, ChargeBattery라는 새로운 메서드를 추가하여 자신만의 기능을 구현할 수 있습니다.

메서드 재정의

상속받은 메서드를 자식 클래스에서 재정의Overriding할 수 있습니다. 이를 메서드 오버라이딩Method Overriding이라고 하며, 부모 클래스에서 정의된 메서드를 자식 클래스에서 새롭게 구현할 때 사용됩니다.

public class Car
{
    public virtual void StartEngine() => Console.WriteLine("The engine is starting.");
}
public class ElectricCar : Car
{
    public override void StartEngine() => Console.WriteLine("The electric engine is starting silently.");
}

위 예제에서 ElectricCar 클래스는 Car 클래스의 StartEngine 메서드를 오버라이딩하여, 전기 자동차에 맞는 동작을 구현합니다.

상속의 종류

단일 상속

.NET에서는 단일 상속만을 지원합니다. 즉, 하나의 자식 클래스는 오직 하나의 부모 클래스만을 상속받을 수 있습니다. 이는 상속 구조를 단순화하고, 다중 상속에서 발생할 수 있는 복잡성을 줄이기 위함입니다.

다중 상속의 대안: 인터페이스

.NET에서는 다중 상속 대신 인터페이스를 사용하여 다중 상속과 유사한 기능을 구현할 수 있습니다. 인터페이스는 클래스가 구현해야 하는 메서드의 집합을 정의하며, 클래스는 여러 개의 인터페이스를 구현할 수 있습니다.

public interface IStartable
{
    void Start();
}
public interface IStopable
{
    void Stop();
}
public class Kia : IStartable, IStopable
{
    public void Start() => Console.WriteLine("Kia car is starting.");
    public void Stop() => Console.WriteLine("Kia car is stopping.");
}

위 예제에서 Kia 클래스는 IStartableIStopable 인터페이스를 구현하여, 자동차의 시작과 정지 동작을 정의합니다.

상속의 한계

캡슐화의 약화

상속은 자식 클래스가 부모 클래스의 내부 구현에 접근할 수 있게 함으로써, 캡슐화의 원칙을 약화시킬 수 있습니다. 이는 부모 클래스의 구현 세부 사항이 자식 클래스에 노출되어, 부모 클래스의 변경이 자식 클래스에 예기치 않은 영향을 미칠 수 있음을 의미합니다.

상속의 남용

상속을 과도하게 사용하면 클래스 간의 의존성이 높아져, 시스템의 유연성과 확장성이 저하될 수 있습니다. 모든 경우에 상속을 사용하는 것이 바람직하지 않으며, 구성과 같은 대안적인 설계 패턴도 고려해야 합니다.

public class Car
{
    public string Brand { get; set; }
    public int Speed { get; set; }
    
    // 기본적으로 모든 차에 적용되는 기능
    public void Drive() => Console.WriteLine($"{Brand} is driving at {Speed} km/h.");
    
    // 이 메서드는 전기차에만 해당됨
    public virtual void ChargeBattery() => Console.WriteLine($"{Brand} is charging the battery.");
}
public class Hyundai : Car
{
    // 전기차가 아니므로, 이 기능은 불필요하지만 상속 구조 때문에 포함됨
}
public class KiaElectric : Car
{
    // 전기차이므로, 부모 클래스의 충전 메서드를 사용할 수 있음
    public override void ChargeBattery() => Console.WriteLine($"{Brand} battery is charging faster.");
}
// 사용 예시
Hyundai hyundai = new Hyundai { Brand = "Hyundai", Speed = 120 };
KiaElectric kiaElectric = new KiaElectric { Brand = "Kia EV", Speed = 100 };
hyundai.Drive();           // "Hyundai is driving at 120 km/h."
hyundai.ChargeBattery();    // 실제로는 필요 없는 기능이 호출됨
kiaElectric.Drive();        // "Kia EV is driving at 100 km/h."
kiaElectric.ChargeBattery(); // "Kia EV battery is charging faster."
  • 불필요한 기능 상속: Hyundai는 전기차가 아니지만, 상속 구조 때문에 ChargeBattery() 메서드를 물려받습니다. 실제로 이 기능은 물리적 자동차에서는 불필요하지만, 강제로 포함되어 사용될 가능성이 생깁니다.
  • 강한 결합: 부모 클래스 Car가 전기차와 물리적 차 모두에 대한 기능을 포함하려고 하면서, 자식 클래스들이 적절하게 구분되지 않고 모든 기능을 상속받아야만 하는 구조적 문제가 발생합니다.

다중 상속의 제한

.NET에서는 단일 상속만을 지원하므로, 복잡한 상속 관계를 설계할 때 다중 상속을 사용할 수 없습니다. 이는 다중 상속으로 인한 복잡성을 피하는 데는 도움이 되지만, 특정 시나리오에서는 제약으로 작용할 수 있습니다.

다른 객체지향 원칙과의 관계

상속과 캡슐화

상속과 캡슐화는 상호 보완적인 개념입니다. 상속을 통해 코드 재사용성을 높이면서, 부모 클래스의 구현 세부 사항을 캡슐화하여 자식 클래스가 필요 이상으로 부모 클래스의 내부에 의존하지 않도록 설계할 수 있습니다.

상속과 다형성

다형성은 상속을 기반으로 구현됩니다. 상속을 통해 자식 클래스가 부모 클래스의 메서드를 오버라이드하여 자신만의 동작을 구현할 수 있으므로, 다형성을 통해 다양한 객체를 일관된 방식으로 처리할 수 있습니다.

상속과 추상화

상속은 추상화를 구현하는 주요 방법 중 하나입니다. 부모 클래스는 공통된 인터페이스나 추상 메서드를 정의하고, 자식 클래스는 이를 구체화하여 다양한 구현을 제공할 수 있습니다. 이를 통해 복잡한 시스템을 보다 간결하게 표현할 수 있습니다.

SOLID 원칙과의 연계

상속은 SOLID 원칙 중 몇 가지와 밀접한 연관이 있습니다.

상속과 단일 책임 원칙

상속을 통해 각 클래스가 하나의 책임에 집중할 수 있도록 설계할 수 있습니다. 부모 클래스는 공통 기능을 제공하고, 자식 클래스는 구체적인 기능을 추가함으로써 단일 책임 원칙을 준수할 수 있습니다.

상속과 개방_폐쇄 원칙

상속은 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있게 하므로, 개방_폐쇄 원칙을 실현하는 데 유용합니다. 자식 클래스를 통해 기존 기능을 확장하면서도 부모 클래스의 코드는 수정하지 않으므로, 코드의 안정성을 유지할 수 있습니다.

상속과 리스코프 치환 원칙

리스코프 치환 원칙은 자식 클래스가 부모 클래스를 대체할 수 있어야 한다는 원칙입니다. 상속을 통해 자식 클래스가 부모 클래스의 인터페이스를 유지하면서도 고유한 동작을 제공할 수 있어, 이 원칙을 충족할 수 있습니다.

맺음말

상속은 객체지향 프로그래밍에서 코드 재사용성과 유지보수성을 높이는 중요한 도구입니다. 이를 통해 공통된 기능을 효율적으로 재사용할 수 있으며, 다형성과 추상화와 함께 사용하면 복잡한 시스템을 단순화할 수 있습니다. 그러나 상속을 남용하면 시스템의 복잡성이 증가할 수 있으므로, 항상 신중하게 적용해야 하며, 필요에 따라 다른 설계 패턴도 고려하는 것이 중요합니다.