NGMsoftware

NGMsoftware
로그인 회원가입
  • 매뉴얼
  • 학습
  • 매뉴얼

    학습


    C# 19. 의존성 주입 디자인 패턴. (The Dependency Injection Design Pattern)

    페이지 정보

    본문

    안녕하세요. 소심비형입니다. 이전 시간에 추상화와 인터페이스에 대해 알아봤는데요. 속성을 진행하기에 앞서 인터페이스를 이용한 의존성 주입에 대해 알아보겠습니다. 보통 의존성 주입은 DI(Dependency Injection)라고 표현합니다. 의존성 주입 디자인 패턴을 사용하는 이유는 1:1 의존 관계를 느슨하게 결합되도록 구현하는 것입니다. 여기에서 말하는 느슨하게 결합(Loose Coupling)된다는 의미는 소스코드 내부가 아닌 외부에서 설정한다는 걸 말합니다. 반대로 강력한 결합(Strong Coupling or Tight Coupling)은 모듈간의 결합도가 높아서 분리하기 쉽지 않고, 코드의 재사용성이 저하되며 단위 기능의 테스트를 어렵게 합니다. 위와 같은 문제를 해결하고자 각 모듈간의 의존성을 줄이는 방식의 설계 패턴인 DI를 사용하고 있습니다. 아래는 C#에서 의존성 주입 패턴을 구현하는 방법을 설명하고 있습니다.

     의존성 주입

     설명

     Constructor Injection

     생성자 주입은 객체가 생성되는 시점에 지정된 파라메터를 전달해야 합니다.

     Property Injection

     Setter Injection과 동일한 의미이며, 속성을 통해 파라메터를  주입합니다.

     Method Injection

     메소드를 통해 파라메터를 주입합니다.

     

     

    결국 위의 3가지는 모두 동일한 작업입니다. 단지, 시점의 차이이며 의존성 주입을 어떤 방식으로 처리하는게 더 좋을지는 상황에 따라 다를 수 있습니다. 하지만, 가급적이면 설계된 클래스의 목적에 맞게 사용하는게 좋겠죠? 그리고, 꼭 하나만 사용해야 하는 것은 아닙니다. 아래 예제는 Strong CouplingLoose Coupling에 대해 보여주고 있습니다. 먼저 Strong Coupling에 대한 코드입니다.

    Program.cs

    namespace ConsoleApplication1
    {
        public class MSSqlProvider { }
     
        public class DatabaseConnector
        {
            public MSSqlProvider Connector { get; set; }
        }
     
        internal class Program
        {
            private static void Main(string[] args)
            {
            }
        }
    }

     

     

    위 코드에 대해 잠깐 설명하자면, MSSqlProvider 클래스와 DatabaseConnector 클래스는 1:1 관계로 강력하게 관계를 맺고 있습니다. 만약, 여기에서 OracleProvider를 서비스하고 싶다면 어떻게 해야 할까요? 아마도 아래와 같이 코드를 변경해야 할겁니다.

    Program.cs

    namespace ConsoleApplication1
    {
        public class MSSqlProvider { }
        public class OracleProvider { }
        public class OledbProvider { }
     
        public class DatabaseConnector
        {
            public MSSqlProvider MSSqlConnector { get; set; }
            public OracleProvider OracleConnector { get; set; }
            public OledbProvider OledbConnector { get; set; }
        }
     
        internal class Program
        {
            private static void Main(string[] args)
            {
            }
        }
    }

     

     

    위와 같은 코드의 문제점은 서비스가 늘어날 때마다 거기에 종속되는 클래스들도 같이 변경되어야 한다는 것입니다. 보통은 서비스를 제공하는 입장에서 개체를 다른 개체에 포함시키는게 아니라 인터페이스 형식으로 정의만 해놓고 서비스를 사용하는 개체가 설정한다는 개념입니다. 말이 좀 어렵게 느껴질지도 모르겠습니다. 우선, 아래와 같이 의존성 주입 디자인 패턴을 적용해 보면 쉽게 이해될지도 모릅니다. 아래는 Loose Coupling에 대해 보여주고 있습니다.

    Program.cs

    namespace ConsoleApplication1
    {
        public interface IDatabaseProvider { }
        public class MSSqlProvider : IDatabaseProvider { }
     
        public class DatabaseConnector
        {
            public IDatabaseProvider Connector { get; set; }
        }
     
        internal class Program
        {
            private static void Main(string[] args)
            {
            }
        }
    }

     

     

    위와 같이 인터페이스를 사용하면 언제든지 MSSqlProvider가 변경되거나 다른 공급자(Provider)가 추가되더라도 서비스 클래스(DatabaseConnector) 입장에서는 변경 없이 사용할 수 있게 됩니다. 이제 다른 데이타베이스 공급자를 추가합니다.

    Program.cs

    namespace ConsoleApplication1
    {
        public interface IDatabaseProvider { }
        public class MSSqlProvider : IDatabaseProvider { }
        public class OracleProvider : IDatabaseProvider { }
        public class OledbProvider : IDatabaseProvider { }
     
        public class DatabaseConnector
        {
            public IDatabaseProvider Connector { get; set; }
        }
     
        internal class Program
        {
            private static void Main(string[] args)
            {
            }
        }
    }
    

     

     

    DI의 Diagram을 그려보면 아래 그림처럼 표현됩니다.

    ScbICOw.png

     

     

    외부에서 커넥터를 설정하여 원하는 서비스를 받을 수 있도록 구성하였습니다. 그렇다면 실제 이 코드를 사용하는 방법에 대해 알아보겠습니다.

     

    1. Constructor Injection

    가장 일반적인 방법이기도 하고, 생성자 오버로딩을 통해 좀 더 다양하게 구현할 수 있습니다. 아래 코드와 같이 생성자에서 외부 설정을 받아 처리하는 패턴입니다.

    Program.cs

    using System;
    namespace ConsoleApplication1
    {
        public interface IDatabaseProvider
        {
            void Open(); void Close();
        }
     
        public class MSSqlProvider : IDatabaseProvider
        {
            public void Close()
            {
                Console.WriteLine("MSSQL 데이타베이스에 연결이 닫혔습니다.");
            }
     
            public void Open()
            {
                Console.WriteLine("MSSQL 데이타베이스에 연결이 열렸습니다");
            }
        }
     
        public class OracleProvider : IDatabaseProvider
        {
            public void Close()
            {
                Console.WriteLine("Oracle 데이타베이스에 연결이 닫혔습니다.");
            }
     
            public void Open()
            {
                Console.WriteLine("Oracle 데이타베이스에 연결이 열렸습니다");
            }
        }
     
        public class OledbProvider : IDatabaseProvider
        {
            public void Close()
            {
                Console.WriteLine("Oledb 데이타베이스에 연결이 닫혔습니다.");
            }
     
            public void Open()
            {
                Console.WriteLine("Oledb 데이타베이스에 연결이 열렸습니다");
            }
        }
     
        public class DatabaseConnector
        {
            public IDatabaseProvider Connector { get; set; }
     
            public void Open()
            {
                Connector.Open();
            }
     
            public void Close()
            {
                Connector.Close();
            }
     
            public DatabaseConnector(IDatabaseProvider provider)
            {
                Connector = provider;
            }
        }
     
        internal class Program
        {
            private static void Main(string[] args)
            {
                var databaseConnector = new DatabaseConnector(new MSSqlProvider());
                databaseConnector.Open();
     
                // do somthing or query...            
     
                databaseConnector.Close();        
            }
        }
    }

     

     

    2. Property Injection

    생성자에서 처리할 수 없는 상황이거나 또는 속성만 사용해야 한다면 아래와 같이 처리할 수 있습니다. 크게 문제가 되지 않는다면 둘다 사용해도 상관없습니다.

    Program.cs

    using System;
     
    namespace ConsoleApplication1
    {
        public interface IDatabaseProvider
        {
            void Open(); void Close();
        }
        public class MSSqlProvider : IDatabaseProvider
        {
            public void Close()
            {
                Console.WriteLine("MSSQL 데이타베이스에 연결이 닫혔습니다.");
            }
     
            public void Open()
            {
                Console.WriteLine("MSSQL 데이타베이스에 연결이 열렸습니다");
            }
        }
     
        public class OracleProvider : IDatabaseProvider
        {
            public void Close()
            {
                Console.WriteLine("Oracle 데이타베이스에 연결이 닫혔습니다.");
            }
     
            public void Open()
            {
                Console.WriteLine("Oracle 데이타베이스에 연결이 열렸습니다");
            }
        }
     
        public class OledbProvider : IDatabaseProvider
        {
            public void Close()
            {
                Console.WriteLine("Oledb 데이타베이스에 연결이 닫혔습니다.");
            }
     
            public void Open()
            {
                Console.WriteLine("Oledb 데이타베이스에 연결이 열렸습니다");
            }
        }
     
        /// <summary>    
        /// 데이타베이스에 연결하기 위한 공급자를 제공합니다. 기본값은 MSSQL입니다.    
        /// </summary>    
        public class DatabaseConnector
        {
            public IDatabaseProvider Connector { get; set; }
            public void Open() { Connector.Open(); }
            public void Close() { Connector.Close(); }
            public DatabaseConnector() : this(new MSSqlProvider()) { }
            public DatabaseConnector(IDatabaseProvider provider)
            {
                Connector = provider;
            }
        }
     
        internal class Program
        {
            private static void Main(string[] args)
            {
                var databaseConnector = new DatabaseConnector();
                databaseConnector.Connector = new OracleProvider();
                databaseConnector.Open();
     
                // do somthing or query...            
     
                databaseConnector.Close();
                Console.ReadKey();        
            }
        }
    }
    

     

     

    83라인과 같이 속성으로 처리해도 됩니다. 하지만, 이렇게 처리할 경우 정확한 스펙을 모른다면 예상하지 못한 에러가 발생될 수 있죠. 그렇기 때문에 클래스에 주석을 달아서 이 사실을 알려주는게 좋습니다. 또한, 67~70라인처럼 기본 생성자에서 초기값을 설정하는 것도 좋은 방법입니다. 이외에도 Open, Close에서 방어코드를 작성해도 됩니다.

     

    3. Method Injection

    서비스 개체를 초기화 할 수 있도록 메서드를 제공할수도 있습니다.

    Program.cs

    using System;
     
    namespace ConsoleApplication1
    {
        public interface IDatabaseProvider
        {
            void Open(); void Close();
        }
     
        public class MSSqlProvider : IDatabaseProvider
        {
            public void Close()
            {
                Console.WriteLine("MSSQL 데이타베이스에 연결이 닫혔습니다.");
            }
     
            public void Open()
            {
                Console.WriteLine("MSSQL 데이타베이스에 연결이 열렸습니다");
            }
        }
     
        public class OracleProvider : IDatabaseProvider
        {
            public void Close()
            {
                Console.WriteLine("Oracle 데이타베이스에 연결이 닫혔습니다.");
            }
     
            public void Open()
            {
                Console.WriteLine("Oracle 데이타베이스에 연결이 열렸습니다");
            }
        }
     
        public class OledbProvider : IDatabaseProvider
        {
            public void Close()
            {
                Console.WriteLine("Oledb 데이타베이스에 연결이 닫혔습니다.");
            }
     
            public void Open()
            {
                Console.WriteLine("Oledb 데이타베이스에 연결이 열렸습니다");
            }
        }
     
        /// <summary>    
        /// 데이타베이스에 연결하기 위한 공급자를 제공합니다. 기본값은 MSSQL입니다.    
        /// </summary>    
        public class DatabaseConnector
        {
            public IDatabaseProvider Connector { get; set; }
            public void Open() { Connector.Open(); }
            public void Close() { Connector.Close(); }
            public void Init(IDatabaseProvider provider) { Connector = provider; }
            public DatabaseConnector() : this(new MSSqlProvider()) { }
            public DatabaseConnector(IDatabaseProvider provider)
            {
                Connector = provider;
            }
        }
     
        internal class Program
        {
            private static void Main(string[] args)
            {
                var databaseConnector = new DatabaseConnector();
                databaseConnector.Init(new OledbProvider());
                databaseConnector.Open();
     
                // do somthing or query...            
     
                databaseConnector.Close();
                Console.ReadKey();
            }
        }
    }
    

     

     

    67~80라인처럼 초기화 메서드를 통해 처리해도 됩니다. 88라인처럼 사용하죠. 이 코드도 실행 해보면 동일한 결과를 얻을 수 있습니다. 저를 포함해서 대부분이 그렇겠지만, 책이나 인터넷에 나와 있는 강좌들을 보면 그때는 이해가 갑니다. 하지만, 막상 사용하려다보면 어떻게 적용해야 할지 막막할때가 많이 있죠. 대부분이 이런 문제에 봉착하는 이유는 크게 2가지 입니다. 하나는 요구 사항의 분석 단계에서 얼마만큼의 서비스가 추가될지 모른다는 것입니다. 또 하나는 과연, 이 응용 프로그램이 확장성을 가질지에 대한 의문이죠.

     

    예를들어 사용자가 생각하기에 회원과 비회원 고객만 필요하다고 말할지도 모릅니다. 하지만, 나중에 가서 고객이 생각해보니 준회원과 우수회원이 더 필요하다고 말할지도 모릅니다. 이렇듯 고객과의 업무협의 단계에서 어느정도 미래에 벌어질 일에 대한 예측(?)도 필요하고, 다른 사례나 BM을 통해 사전 분석도 필요한 부분입니다.

     

    의존성은 그리 좋은게 아닙니다-_-; 골고루 관계를 맺는게 중요하죠^^;

    enyyUVy.jpg

     

     

    한참 전에 Java에서 스프링 프레임워크가 대세일 때 주목받은 개념들이 있는데 DI, AOP, SoC가 대표적인 개념들입니다. DI의 경우 외부의 설정 파일을 통해 서비스를 호출하기 위한 용도로 이용되었기 때문에 대부분의 예제가 Service와 Begin, End 그리고 Start, DoWork등등과 같은 방식으로 되어 있습니다. 또, 실제 서비스를 만들때도 그랬구요. 뭐 이런것들이 중요한 부분은 아니므로 의존성 주입 디자인 패턴을 이해하고 잘 활용할 수 있도록 시야를 넓히는게 좋을거 같습니다.

     

    다음 시간에...

    • 네이버 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 카카오스토리 공유하기
    추천0 비추천0

    댓글목록

    등록된 댓글이 없습니다.