본문 바로가기

.Net Technology/.NET TDD

(13) TDD를 위한 객체지향 - 리스코브 대입 (Liskov substitution)

리스코브 대입의 규칙은 이러하다. “프로그램에 존재하는 한 객체는 어떤 추가적인 수정없이 파생된 서브 클래스들로 대입이 가능해야 한다.” 다시 풀어 설명하자면 자식 클래스들이 부모의 기능을 수행하는데 있어서 어떤 문제가 있어서는 안된다는 것이다. 쉽게 이해하기 위해서 바로 앞에서 사용한 예제를 통해서 그 문제와 해결방안에 대해서 자세히 살펴보도록 하자. 
 
먼저 새로운 기능을 추가해보도록 하겠다. 바로 실버와 골드 고객에게만 스페셜 서비스를 주고 싶은 것이다. 즉, 스페셜 서비스를 요청하면 이 요청을 받아서 DB에 저장하는 메서드를 추가하는 것이다. 먼저 실버는 고객 클래스를 골드는 실버 클래스를 상속받고 있는 구조이기 때문에 우리는 부모인 Customer에 SetSpecialService라는 가상 메서드를 만들어 보도록 하겠다.
 
public class Customer
{
    public virtual double GetDiscount(double totalSales)
    {
        return totalSales;
    }
    public virtual void SetSpecialService(string order)
    {
        throw new Exception("It allows only Silver or Gold Customer.");
    }
}
 
 
그리고 Customer 클래스를 상속받은 실버와 골드 고객들은 아래와 같이 메서드를 구현하였다. 
 
public class SilverCustomer : Customer
{
    public override double GetDiscount(double totalSales)
    {
        return base.GetDiscount(totalSales) - 50;
    }
    public  override void SetSpecialService(string order)
    {
        //Save to Database
    }
}

public class GoldCustomer : SilverCustomer
{
    public override double GetDiscount(double totalSales)
    {
        return base.GetDiscount(totalSales) - 100;
    }
    public override void SetSpecialService(string order)
    {
        //Save to Database
    }
}
 
 
실제 DB로 저장한다고 가정만 할 뿐 실제로 코드는 작성하지 않았다. 자, 그럼 이 코드의 문제를 살펴보자. 바로 아래와 같은 코드를 작성할 때 문제가 발생하게 된다. 
 
List<Customer> customers = new List<Customer>();
customers.Add(new SilverCustomer());
customers.Add(new GoldCustomer());
customers.Add(new Customer());

foreach (Customer c in customers)
{
    c.SetSpecialService("Order");
}
 
 
이렇게 코드를 실행할 경우에 우리는 세번째로 추가된 Customer 객체 즉, SetSpecialService를 호출할 권한이 없기 때문에 실제 런타임에서 에러를 발생시키고 만다. 이 것을 해결하기 위해서 어떻게 해야 할까? 아래와 같이 try catch를 넣으면 어떨까? 
 
foreach (Customer c in customers)
{
    try
    {
        c.SetSpecialService("Order");
    }
    catch(Exception ex)
    {
        //...Message ..?
    }
)
 
 
예외는 예상치 못한 상황의 에러가 발생될 때 사용하는 것이지 특정 로직을 이미 알고 있으면서 사용하는 것이 아니다. 지금까지의 예제는 설계부터가 잘못되었기 때문에 완전히 잘못된 방향으로 코드가 흘러가고 있었다. 먼저 필요없는 기능을 부모 클래스나 자식 클래스가 가지게 된 것 부터가 이 규칙을 위반하게 된다. 즉, 이 규칙은 자식들은 부모의 기능을 완전히 대체할 수 있어야 한다는 것인데 부모 클래스에 필요없는 기능을 넣어 준것이 문제가 된다. 그렇다면 이 잘못된 설계를 바로 잡아 보도록 하자. 필자는 위의 구조를 고치기 위해서 ISpecialService라는 인터페이스를 추가했다.
 
public interface ISpecialService
{
    void SetSpecialService(string order);
}
 
 
이제 이 인터페이스를 이용해서 위의 클래스들의 구조를 아래와 같이 변경해 보도록 하자. 
 
public class Customer
{
    public virtual double GetDiscount(double totalSales)
    {
        return totalSales;
    }
}
public class SilverCustomer : Customer, ISpecialService
{
    public override double GetDiscount(double totalSales)
    {
        return base.GetDiscount(totalSales) - 50;
    }
    public virtual void SetSpecialService(string order)
    {
        //Save to Database
    }
}
public class GoldCustomer : SilverCustomer
{
    public override double GetDiscount(double totalSales)
    {
        return base.GetDiscount(totalSales) - 100;
    }
    public override void SetSpecialService(string order)
    {
        //Save to Database
    }
}
 
 
위와 같이 구조를 인터페이스를 이용해서 수정하였다. 이제 다음과 같은 코드를 다시 작성해보도록 하자. 
 
List<ISpecialService> customers = new List<ISpecialService>();
customers.Add(new SilverCustomer());
customers.Add(new GoldCustomer());
customers.Add(new Customer());
 
그럼 Customer 클래스를 추가하는 부분에서 컴파일 에러를 가지게 된다. 즉, 우리는 적어도 구조를 바꿈으로써 런타임 에러를 피할 수 있다. 여기까지가 리스코브 대입에 해당하는 원칙이다. 즉, Customer를 상속받는 자식 클래스들은 충분히 Customer클래스를 대체하여 대입할 수 있다는 부분이다. 그리고 ISpecialService를 상속받은 객체들은 서로 대입이 가능하다.
 
 
팁: 객체의 인터페이스 확인하기
 
하지만 만약 Customer 리스트를 만들고 ISpecialService를 상속받은 객체를 찾고 싶다면 어떻게 해야 할까? 이 경우에 우리는 Type에서 지원하는IsAssignableFrom메서드를 통해서 실제로 해당 인터페이스가 구현되어 있는지의 여부를 가져올 수 있기 때문에 다음과 같은 코드를 작성하는 것이 가능하다. 
 
List<Customer> customers = new List<Customer>();
customers.Add(new SilverCustomer());
customers.Add(new GoldCustomer());
customers.Add(new Customer());

foreach (Customer c in customers)
{
    if (typeof(ISpecialService).IsAssignableFrom(c.GetType()))
    {
        (c as ISpecialService).SetSpecialService("");
    }
}
 
팁: 끝
 
위의 코드가 완벽히 솔리드 원칙들을 지원하고 있는 것이 아니다. 아직 더 다듬어야 하는 작업들이 남아 있다. 그럼 다음 규칙을 살펴 보도록 하자.