Computer >> 컴퓨터 >  >> 프로그래밍 >> C#

리스코프 치환 원칙(LSP)이란 무엇이며, C#에서 구현하는 방법

리스코프 치환 원칙(Liskov Substitution Principle)의 핵심은 파생 타입(자식 클래스)은 기반 타입(부모 클래스)을 완전히 대체할 수 있어야 한다는 것입니다.

정의

리스코프 치환 원칙(LSP)은 SOLID 객체 지향 설계 원칙 중 하나로, 자식 클래스를 부모 클래스인 것처럼 자유롭게 사용할 수 있어야 한다는 개념입니다.

쉽게 말해, 모든 파생 클래스는 부모 클래스가 제공하는 기능을 그대로 유지해야 하며, 부모의 동작을 임의로 대체하거나 깨뜨려서는 안 됩니다. 이 원칙이 지켜지지 않으면 부모 타입을 사용하는 코드가 자식 타입이 들어오는 순간 비정상적으로 동작하게 됩니다.

리스코프 치환 원칙 적용 전 — 문제가 되는 코드

다음 예제는 타원(Ellipse)과 원(Circle)의 관계를 통해 LSP 위반이 어떻게 발생하는지 보여줍니다. 수학적으로 원은 타원의 특수한 형태이기 때문에 상속 관계로 표현하기 쉽지만, 실제 구현에서는 예상치 못한 버그를 만들어냅니다.

public class Ellipse {
    public double MajorAxis { get; set; }
    public double MinorAxis { get; set; }

    public virtual void SetMajorAxis(double majorAxis){
        this.MajorAxis = majorAxis;
    }
    public virtual void SetMinorAxis(double minorAxis){
        this.MajorAxis = minorAxis;
    }
    public virtual double Area() {
        return MajorAxis * MinorAxis * Math.PI;
    }
}
public class Circle : Ellipse {
    public override void SetMajorAxis(double majorAxis) {
        base.SetMajorAxis(majorAxis);
        this.MinorAxis = majorAxis; // 원에서는 두 축의 길이가 항상 같습니다
    }
}

public class Result {
    public void Method1() {
        Circle circle = new Circle();
        circle.SetMajorAxis(5);
        circle.SetMinorAxis(4);
        var area = circle.Area(); // 5*4 = 20이 계산되지만, 우리가 기대한 값은 5*5 = 25입니다
    }
}

문제의 원인은 Circle 클래스가 부모인 Ellipse의 동작을 제대로 대체하지 못한다는 점에 있습니다. SetMajorAxis를 호출하면 장축과 단축이 함께 변경되어 버리므로, 이후 SetMinorAxis를 호출하면 의도와 다른 결과가 나옵니다. 이처럼 자식 클래스가 부모의 계약(contract)을 위반하면 리스코프 치환 원칙을 어기게 됩니다.

리스코프 치환 원칙 적용 후 — 올바른 설계

이 문제를 해결하는 방법은 강제로 상속 관계를 유지하는 것이 아니라, 각 타입이 독립적으로 자신의 책임을 수행하도록 분리하는 것입니다. 아래 코드에서는 원을 타원의 자식으로 두지 않고, 반지름(Radius)만 가지는 독립적인 Circle1 클래스로 정의하여 LSP 위반을 제거했습니다.

internal class Program {
    private static void Main() {
    }
}
public class Ellipse {
    public double MajorAxis { get; set; }
    public double MinorAxis { get; set; }

    public virtual void SetMajorAxis(double majorAxis) {
        MajorAxis = majorAxis;
    }
    public virtual void SetMinorAxis(double minorAxis) {
        MajorAxis = minorAxis;
    }
    public virtual double Area() {
        return MajorAxis * MinorAxis * Math.PI;
    }
}
public class Circle : Ellipse {
    public override void SetMajorAxis(double majorAxis) {
        base.SetMajorAxis(majorAxis);
        this.MinorAxis = majorAxis; // 원에서는 두 축의 길이가 항상 같습니다
    }
    public override void SetMinorAxis(double minorAxis) {
        base.SetMinorAxis(minorAxis);
        this.MajorAxis = minorAxis;
    }
    public override double Area() {
        return base.Area();
    }
}

public class Circle1 {
    public double Radius { get; set; }
    public void SetRadius(double radius) {
        this.Radius = radius;
    }
    public double Area() {
        return this.Radius * this.Radius * Math.PI;
    }
}

Circle1은 반지름 하나만으로 면적을 계산하므로, 타원의 동작을 오버라이드하면서 발생하는 모순 없이 자신만의 명확한 규칙을 따릅니다. 이렇게 하면 어떤 곳에서 해당 객체를 사용하더라도 예측 가능한 결과를 얻을 수 있습니다.

핵심 정리

리스코프 치환 원칙은 단순히 상속을 금지하라는 의미가 아니라, 자식 클래스가 부모 클래스의 역할을 온전히 수행할 수 있도록 설계하라는 뜻입니다. "원은 타원이다"처럼 개념적으로는 성립하는 관계라도, 구현 관점에서 부모의 동작을 깨뜨린다면 상속 대신 별도의 클래스로 분리하는 것이 더 올바른 설계입니다. LSP를 준수하면 다형성을 활용한 코드의 신뢰성과 유지보수성이 크게 향상됩니다.