본문 바로가기

.Net Technology/.NET TDD

(12) TDD를 위한 객체지향 - 개방과 폐쇄 (Open-closed)

개방과 폐쇄라고 해서 단순히 private나 public을 적절히 이용해야 된다고 생각하면 큰 오산이다. 이 개방과 폐쇄의 원칙은 확장에 있어서는 오픈하고 수정에 있어서는 폐쇄하라는 의미이다. 각각의 유닛들이 만들어 질때virtual 메서드를 적절히 이용하거나 상속을 이용할 수 있으면 우리는 보다 객체지향적인 코드를 생성할 수 있다. 
 
예를 들어 보도록 하겠다. 만약 호텔의 고객을 관리한다고 가정할때 고객의 등급이 일반, 실버, 골드로 나누어 진다고 가정해보자. 그리고 각각의 고객별로 다르게 할인율이 적용된다고 가정하겠다. 이때 이 할인율을 계산하는 GetDiscount라는 메서드를 만들어야 한다면 어떻게 클래스를 설계할지 생각해보자. 일반적으로 다형성의 개념을 도입하지 않는다면 아래와 같은 코드가 작성될 것이다. 
 
public enum CustomerType { Ordinary, Silver, Gold }

public class Customer_Bad_Example
{
    private readonly CustomerType _custType;
    public Customer_Bad_Example(CustomerType cType)
    {
        this._custType = cType;
    }

    public decimal GetDiscount(decimal totalSales)
    {
        if (_custType == CustomerType.Gold)
        {
            return totalSales - 100;
        }
        else if (_custType == CustomerType.Silver )
        {
            return totalSales - 50;
        }
        else
        {
            return totalSales;
        }
    }
}
 
어떠한가? 필자는 이 코드가 왠지 고향냄새가 나는 친근한 코드로 느껴진다. 이 코드를 보고 객체지향의 다형성이라는 개념을 적용해야 겠다라는 생각을 품을 수 있으면 어느정도 객체지향에 이해가 있는 독자일 것이다. 하지만 왜 다형성을 도입해야 하는지에 대한 이유를 아는 것이 더 중요하다.
 
다음 코드를 보면서 살펴보도록 하겠다. 먼저 필자는 위의 코드를 아래와 같이 다시 수정하였다. 
 
public class Customer
{
    public virtual double GetDiscount(double totalSales)
    {
        return totalSales;
    }
}

public class SilverCustomer : Customer
{
    public override double GetDiscount(double totalSales)
    {
        return base.GetDiscount(totalSales) - 50;
    }
}

public class GoldCustomer : SilverCustomer
{
    public override double GetDiscount(double totalSales)
    {
        return base.GetDiscount(totalSales) - 100;
    }
}
 
먼저 우리가 다형성을 도입하는 것은 향후 수정사항이 있을 경우에 보다 편하고 안전하게 그 수정사항을 대처할 수 있게 하기 위함이다. 만약 하나의 메서드에 모든 고객들의 로직을 포함시킨다면 향후 수정이 생길 경우에 모든 고객들의 코드에 영향을 줄 수 있는 위험이 있다. 즉, 모든 참조가 그 하나의 메서드에 걸려있게 되면서 만약 실버 고객을 위한 코드를 입력한다고 하더라도 그것이 잘못되면 다른 참조한 메서드들에게도 그 영향이 전달되는 것이다.
 
TDD에 있어서도 하나의 유닛만 테스트 해야 된다는 TDD의 원칙을 벗어나게 된다. 하나의 유닛 즉, 하나의 기능만 테스트 하고 정확하게 어떤 로직에서 에러가 있는지를 알려주는 것이 TDD의 목표이기 때문에 우리는 이 메서드의 기능을 분리 할 필요가 있다.
 
다형성을 통해서 우리는 기존의 기능들이 수정되는 것을 어느 정도 폐쇄시킬 수 있는 것을 살펴보았고 virtual 메서드를 제공함으로써 확장에 대한 가능성은 얼마든지 열어 두었다. 하지만 위의 코드가 완벽한 것은 아니다. 바로 다음에 살펴볼 리스코브의 대입을 완벽히 지원하고 있는 것이 아니기 때문이다.