본문 바로가기

.Net Technology/.NET TDD

(15) TDD를 위한 객체지향 - 의존성의 반전 (Dependency inversion)

마지막으로 원칙이 무엇보다도 TDD에서 가장 중요한 부분이다. 다른 원칙들이 지켜지지 않는다고 하더라도 TDD를 진행하는데 불편함이 있을 뿐이지 그렇게 어렵지는 않다. 하지만 만약 의존성 반전이라는 원칙이 지켜지지 않는다면 TDD 자체가 어려워진다. 
 
의존성 반전이라는 것은 쉽게 이야기 해서 객체간의 강한 참조를 없애는 것이다. 즉, 느슨한 결합을 인터페이스를 통해서 실현하는 것을 의미한다. 그럼 첫번째 원칙에서 살펴본 코드의 문제를 살펴보도록 하자.
 
class FileLogger
{
    public void Handle(string error)
    {
        System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
}

class Customer
{
    private FileLogger obj = new FileLogger();
    public virtual void Add()
    {
        try
        {
            //Some codes
        }
        catch (Exception ex)
        {
            obj.Handle(ex.ToString());
        }
    }
}
 
 
위의 코드의 문제를 보다 쉽게 진단하기 위해서 하나의 추가사항을 설정하겠다. 만약 프로젝트에 따라서 로그를 파일에 저장하는 것이 아니라 DB에 저장한다고 가정해보자. 그럼 당연히 인터페이스를 도입하여 아래와 같이 설계를 바꿀 수 있을 것이다.
interface ILogger
{
    void Handle(string error);
}

class FileLogger :ILogger
{
    public void Handle(string error)
    {
        System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
}

class DBLogger : ILogger
{
    public void Handle(string error)
    {
        System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
}
 
이렇게 두개의 객체를 생성한 뒤에 해야할 일은 바로 Customer 클래스에서 이 로그 클래스들을 상황에 따라서 자유롭게 사용이 가능하도록 코드를 작성하는 것이다. 만약 Customer 클래스가 웹 환경일때에는 DB에 저장하고 윈도우 환경일 때에는 DB로 로그를 남겨야 한다면 어떻게 해야할까? 두개의 객체를 모두 가지고 있어야 할까? 그렇지 않다. 우리는 객체가 생성될 때 객체를 넘겨주는 형태로 클래스를 변경할 수 있다. 다음 코드를 살펴보자.
 
class Customer
{
    private readonly ILogger _obj;
    public Customer(ILogger obj)
    {
        _obj = obj;
    }
    public virtual void Add()
    {
        try
        {
            //Some codes
        }
        catch (Exception ex)
        {
            _obj.Handle(ex.ToString());
        }
    }
}
 
모두 인터페이스를 참조함으로써 인터페이스를 구현한 어떤 타입이 넘어 온다고 하더라도 우리는 쉽게 코드를 작성하고 이용하는 것이 가능하다. 예를 들어 아래와 같이 원하는 대로 객체를 생성할 수 있는 것이다. 
 
var customerDB = new Customer(new DBLogger());
var customerFile = new Customer(new FileLogger()));
 
<팁: 시작 – 팩토리 패턴>
 
팩토리 패턴은 의존성 제거를 위해서 뿐만 아니라 보다 적은 코드로 원하는 환경에 따른 객체를 초기화 시켜주기 위해서 이용되는 패턴이다. 앞의 예제에서 또한 팩토리 패턴을 도입할 수 있다. 먼저 다음과 같은 팩토리 클래스를 정의해보자.
 
public class LoggerFactory
{
    public static ILogger GetLogger(string environment)
    {
        if (environment == "Web")
        {
            return new DBLogger();
        }
        else
        {
            return new FileLogger();
        }
    }
}
 
실제 environment 라는 변수를 설정하였고 이 변수를 통해서 원하는 객체를 만들어 넘겨주는 개념이다. 그럼 Customer 클래스에 팩토리 패턴을 이용하는 새로운 생성자를 하나 추가해보자.
 
public class Customer
{
    private readonly ILogger _obj;

    public Customer(string environment)
    {
        _obj = LoggerFactory.GetLogger(environment);
    }
}
 
이렇게 개발할 경우에 우리는 일일히 로그 객체를 생성해서 넘겨 줘야 하는 수고를 덜 수 있다.
 
<팁: 끝>
 
이렇게 하여 다섯가지 원칙들을 모두 살펴보았다. 물론, 객체 지향에 있어서 이 다섯가지로 만족할 수 있는 것은 아니지만 적어도 이 다섯가지 원칙을 지키는 것을 목표로 한다면 충분히 높은 퀄리티의 코드를 만들어 내는데 큰 보탬이 될 것이다.