로버트 마틴이 쓴 의존관계 역전 원칙에서 Button-Lamp 예를 C#으로 이해하기

로버트 마틴[참고 1]이 쓴 의존관계 역전 원칙[참고 2]에서 Button-Lamp 예를 C#으로 이해하기

이철우

이 글은 로버트 마틴[참고 1]이 쓴 - The Dependency Inversion Principle[참고 2] - 안에 작은 제목 'A Simple Example’에 있는 Button-Lamp 예를 C#으로 풀어놓은 것이다. 의존관계 역전 원칙의 자세한 내용에 대해서는 [참고 2]를 참고하기 바란다. 그 원칙은 간략히 다음과 같다.

첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.

둘째, 추상화는 세부사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.

[참고2]의 [그림 4]

전통적인 모듈 또는 라이브러리가 하위 모듈 재활용에 중점을 둔 것이라면, 객체지향의 의존관계 역전 원칙은 열고 닫기 원칙과 더불어 상위 모듈 재활용에 중점을 둔 것이라 한다.

신호 'on’이 들어오면 'Lamp On’을 출력하고, 'off’가 들어오면 'Lamp Off’를 출력한다. 이것을 LampService 라고 부르겠다. 서비스를 정지하려면 'stop’을 입력한다. LampService.cs, Program.cs 이렇게 두 파일로 첫 프로젝트를 구성하며, LampService.cs를 분리해가며 프로젝트를 더 만들 것이다. 세 번째 프로젝트에서 의존관계 역전 원칙을, 네 번째 프로젝트에서 Adapter 패턴을, 다섯 번째 프로젝트에서 열고 닫기 원칙을 적용할 것이다. 프로젝트가 진행될 수록 LampService가 변화에 더 유연해짐을 알 수 있다.

한 파일에 모두

// LampService.cs
public class LampService
{
    private bool _isStopRequired = false;
    public LampService()
    {
    }
    public void Run()
    {
        while (!_isStopRequired)
        {
            var input = Console.ReadLine();
            switch (input)
            {
                case "stop":
                    _isStopRequired = true;
                    break;
                case "on":
                    Console.WriteLine("Lamp On.");
                    break;
                case "off":
                    Console.WriteLine("Lamp Off.");
                    break;
                default:
                    Console.WriteLine(input);
                    break;
            }
        } 
    }
}

// Program.cs
Console.WriteLine("Hello, World!");
new LampService().Run();
Console.WriteLine("Bye.");

버튼과 램프 분리

// NaiveLamp.cs
public class NaiveLamp
{
    public void TurnOn()
    {
        Console.WriteLine("Lamp On.");
    }
    
    public void TurnOff()
    {
        Console.WriteLine("Lamp Off.");
    }
}

// NaiveButton.cs
public class NaiveButton
{
    private readonly NaiveLamp _naiveLamp;
    private string _signal = string.Empty;
    
    public NaiveButton(NaiveLamp naiveLamp)
    {
        _naiveLamp = naiveLamp;
    }
    
    public void Detect(string signal)
    {
        _signal = signal;
        var buttonOn = GetPhysicalState();

        if (buttonOn)
        {
            _naiveLamp.TurnOn();
        }
        else
        {
            _naiveLamp.TurnOff();
        }
    }

    private bool GetPhysicalState()
    {
        return _signal.Equals("on");
    }
}

// LampService.cs
public class LampService
{
    private bool _isStopRequired = false;
    private readonly NaiveButton _button;
    public LampService(NaiveButton button)
    {
        _button = button;
    }
    
    public void Run()
    {
        while (!_isStopRequired)
        {
            var input = Console.ReadLine();
            switch (input)
            {
                case "stop":
                    _isStopRequired = true;
                    break;
                case "on":
                case "off":
                    _button.Detect(input);
                    break;
                default:
                    Console.WriteLine(input);
                    break;
            }
        }
    }
}

// Program.cs
Console.WriteLine("Hello, World!");
new LampService(new NaiveButton(new NaiveLamp())).Run();
Console.WriteLine("Bye.");

의존관계 역전 원칙을 적용

// ILamp.cs
public interface ILamp
{
    void TurnOn();
    void TurnOff();
}

// ButtonAbstract.cs
public abstract class ButtonAbstract
{
    private readonly ILamp _lamp;
    protected string Signal { get; private set; } = string.Empty;
    
    protected ButtonAbstract(ILamp lamp)
    {
        _lamp = lamp;
    }

    public void Detect(string signal)
    {
        Signal = signal;
        var buttonOn = GetPhysicalState();

        if (buttonOn)
        {
            _lamp.TurnOn();
        }
        else
        {
            _lamp.TurnOff();
        }
    }

    protected abstract bool GetPhysicalState();
}

// InvertedLamp.cs
public class InvertedLamp : ILamp
{
    public void TurnOn()
    {
        Console.WriteLine("Lamp On.");
    }
    
    public void TurnOff()
    {
        Console.WriteLine("Lamp Off.");
    }
}

// InvertedButton.cs
public class InvertedButton : ButtonAbstract
{
    public InvertedButton(ILamp lamp) : base(lamp)
    {
    }

    protected override bool GetPhysicalState()
    {
        return Signal.Equals("on");
    }
}

// LampService.cs
public class LampService
{
    private bool _isStopRequired = false;
    private readonly ButtonAbstract _button;
    public LampService(ButtonAbstract button)
    {
        _button = button;
    }

    public void Run()
    {
        while (!_isStopRequired)
        {
            var input = Console.ReadLine();
            switch (input)
            {
                case "stop":
                    _isStopRequired = true;
                    break;
                case "on":
                case "off":
                    _button.Detect(input);
                    break;
                default:
                    Console.WriteLine(input);
                    break;
            }
        }
    }
}

// Program.cs
Console.WriteLine("Hello, World!");
new LampService(new InvertedButton(new InvertedLamp())).Run();
Console.WriteLine("Bye.");

Adapter 패턴 적용

먼저 프로젝트에 OtherLamp, OtherLampAdapter를 추가하고 실행을 위해 Program.cs를 바꾸자.

// OtherLamp.cs
public class OtherLamp
{
    public void SwitchOn()
    {
        Console.WriteLine("Lamp On.");
    }
    
    public void SwitchOff()
    {
        Console.WriteLine("Lamp Off.");
    }

}

// OtherLampAdapter.cs
public class OtherLampAdapter : ILamp
{
    private readonly OtherLamp _lamp = new OtherLamp();
    public void TurnOn()
    {
        _lamp.SwitchOn();
    }

    public void TurnOff()
    {
        _lamp.SwitchOff();
    }
}

// Program.cs
Console.WriteLine("Hello, World!");
new LampService(new InvertedButton(new OtherLampAdapter())).Run();
Console.WriteLine("Bye.");

열고 닫기 원칙을 적용

InvertedButton의 메서드 GetPhysicalState를 더 유연하게 바꾸자.

// IGetPhysicalState.cs
public interface IGetPhysicalState
{
    bool GetPhysicalState(string signal, string criteria);
}

// PhysicalStateGetter.cs
public class PhysicalStateGetter : IGetPhysicalState
{
    public bool GetPhysicalState(string signal, string criteria)
    {
        return signal.Equals(criteria);
    }
}

// ILamp.cs
public interface ILamp
{
    void TurnOn();
    void TurnOff();
}

// ButtonAbstract.cs
public abstract class ButtonAbstract
{
    private readonly ILamp _lamp;
    private readonly IGetPhysicalState _physicalStateGetter;
    
    protected ButtonAbstract(ILamp lamp, IGetPhysicalState physicalStateGetter)
    {
        _lamp = lamp;
        _physicalStateGetter = physicalStateGetter;
    }

    public void Detect(string signal, string criteria)
    {
        var buttonOn = _physicalStateGetter.GetPhysicalState(signal, criteria);

        if (buttonOn)
        {
            _lamp.TurnOn();
        }
        else
        {
            _lamp.TurnOff();
        }
    }
}

// InvertedLamp.cs
public class InvertedLamp : ILamp
{
    public void TurnOn()
    {
        Console.WriteLine("Lamp On.");
    }
    
    public void TurnOff()
    {
        Console.WriteLine("Lamp Off.");
    }
}

// InvertedButton.cs
public class InvertedButton : ButtonAbstract
{
    public InvertedButton(ILamp lamp, IGetPhysicalState physicalStateGetter) : base(lamp, physicalStateGetter)
    {
    }
}

// LampService.cs
public class LampService
{
    private bool _isStopRequired = false;
    private readonly ButtonAbstract _button;
    public LampService(ButtonAbstract button)
    {
        _button = button;
    }

    public void Run(string criteria)
    {
        while (!_isStopRequired)
        {
            var input = Console.ReadLine();
            switch (input)
            {
                case "stop":
                    _isStopRequired = true;
                    break;
                case "on":
                case "off":
                    _button.Detect(input, criteria);
                    break;
                default:
                    Console.WriteLine(input);
                    break;
            }
        }
    }
}

// Program.cs
Console.WriteLine("Hello, World!");
new LampService(new InvertedButton(new InvertedLamp(), new PhysicalStateGetter())).Run("on");
Console.WriteLine("Bye.");

이 글을 쓰며 의존관계 역전 원칙의 두 번째에서 '세부사항’이 무엇인가 고민을 했다. 버튼은 상위 모듈, 램프는 하위 모듈이라 할 수 있는데, '세부사항’을 딱 골라낼 수 없었다. 나름 이렇게 정리했다.

추상화는 추상화에 의존해야 한다.

[참고 1] 로버트 마틴
http://cleancoder.com/products

[참고 2] 로버트 마틴이 쓴 The Dependency Inversion Principle

5 Likes

디자인 담론에 나타나는 "모듈"이라는 단어는 C#에서는 “프로젝트” 혹은 “패키지” 혹은 "어셈블리"를 가리킵니다.

아래 글은 링크2 의 3 페이지에서 발췌한 내용입니다.

Figure 1 is a “structure chart”1. It shows that there are three modules, or subprograms, in the application.

마틴이 저 책을 지은 시대적 상황, 혹은 저 책은 C++에 기반하고 있기에, "서브 프로그램"이라는 용어를 썼겠지만, 지금의 C#의 관점에서는 위에 언급한 용어들이 더 어울립니다.

그리고, details를 "세부사항"으로 번역하신 것 같은데, abstraction 의 반대말로 사용된 것 뿐입니다. C# 관점에서는 “구현 클래스” 혹은 "구체 클래스"를 가리킵니다.

번역의 오류가 있었던 이유는 두 개의 문맥을 가지는 abstraction 을 “추상” 한 단어로 번역했기 때문입니다.

사실, 거의 모든 문서와 책이 그런 문맥의 겹칩을 인지하지 못하는 경향이 있습니다.

OOP의 3대장 중에 하나인 abstraction 도 "추상"으로 번역하고, 구체적이지 않은 정의(interface, abstract)를 가리키는 abstraction 도 "추상"으로 번역해서 생긴 문제로, 후자를 “피상” 이라는 용어로 구분해 보면, 문맥의 겹침을 피할 수 있습니다.

클래스는 현실의 사물/개념을 추상적으로 모델링/샘플링하는데(추상화, abstraction), 이 모델을 구체적으로 정의하면 Details(구체 클래스)가 되는 것이고, (구체적이지 않게) 피상적으로 정의하면, Abstraction(피상 클래스 혹은 인터페이스)가 되는 것이죠.

아래 문장에서 해당 단어들을 위의 단어들로 대치하면 글의 의미가 훨씬 명확해지고, 또한 일상에서 우리가 쓰는 방식과 많이 다르지 않다는 것을 느낄 것입니다.

HIGH LEVEL 프로젝트 SHOULD NOT DEPEND UPON LOW LEVEL 프로젝트. BOTH 프로젝트 SHOULD DEPEND UPON 피상 클래스.

B. 피상 클래스 SHOULD NOT DEPEND UPON 구체 클래스. 구체 클래스 SHOULD DEPEND UPON 피상 클래스.

참고로, 프로젝트 단위의 참조에서 하위 프로젝트를 직접 참조하지 말라는 의미는 피상 클래스만 담은 제3의 프로젝트를 정의하고, 상위 프로젝트와 하위 프로젝트 모두 그 프로젝트를 참조하라는 의미로 받아 들일 수 있습니다.

예를 들어, 누겟 패키지 중에 ".Abstractions"라는 접미어가 붙은 것들이 있는데, 이들이 하는 역할이 그것입니다.

요즘 유행하는 클린 아키텍쳐에서는 코어 영역에 있는 Application 레이어에 모아 두라고 하는데, 이는 좀 더 세련된 관리 방식을 제시한 것으로 볼 수 있습니다.

6 Likes