Software Develop/Design Pattern

Inversion Of Control Container, IUnityContainer

jaywapp 2022. 2. 24. 16:49

개요

Inversion Of Control Container(이하 IoC Container)를 활용하여 개발을 하면 굉장히 편리하다.

Container에서 객체의 생성과 의존성 등을 관리하기 때문에 정작 개발을 하는 당사자는 각 객체에 대한 신경을 쓰지 않아도 된다. 오늘은 IUnityContainer를 사용한 IoC Container의 사용법을 설명해보고자 한다.

 

기본적으로 IoC Container를 사용하는 구조는 아래와 같다. 

어떤 객체가 생성될 때 필요한 항목들을 Container에서 알맞게 꺼내쓰는 방식이다.

DIP (Dependency Inversion Principle)

IoC Container는 기본적으로 DIP(Dependency Inversion Principle)를 바탕으로 한다. 

 

1) 프로젝트를 구성하는 Module간 상위 Module이 하위 Module에 의존하면 안된다.

2) 모든 세부 사항은 추상적인 것에 의존해야하며 반대의 경우는 불허한다.

 

DIP는 위 두 가지 조건을 만족해야하는데, 이는 결국 Interface에 의존해야 함을 뜻한다. 즉, Interface를 활용하여 개체간 결합도를 낮추는 것이 목적이다.

 

DI (Dependency Injection)

먼저, 아래 코드를 보자.

Person Class는 Job 타입의 객체를 프로퍼티로 갖고 있다. Person의 Constructor에서 Job을 생성하여 대입하여 준다. 이러한 코드는 Person이 Job을 생성하여 Person과 Job의 의존성을 결정한다.

public class Job
{
    public string Name { get; set; }
}

public class Person
{
    public Job Job { get; set; }

    public Person()
    {
        Job = new Job();
    }
}

DI를 적용한 코드의 경우는 아래와 같다.

아래의 코드의 경우는 Person Constructor의 매개 변수로 Job 객체를 입력받고 이를 할당해주고 있다. 이때, Person과 Job간의 의존성은 외부에서 결정해준다.

public class Job
{
    public string Name { get; set; }
}

public class Person
{
    public Job Job { get; set; }

    public Person(Job job)
    {
        Job = job;
    }
}

위 두 가지 경우를 보더라도, Person을 생성함에 있어서 DI를 적용한 코드가 더 높은 유연성을 갖게 된다.

 

Container와 DI, DIP

Container를 통해 객체 생성 및 주기를 관리하게 되면 DI, DIP의 개념을 자동으로 적용할 수 있게 된다.

평소 Prism의 UnityContainer를 주로 사용하기 때문에 아래 예제도 Prism을 기반으로 작성되었다.

 

TEST 1 (DI)

 아래 코드를 보면 container에 미리 "Singer"라는 정보를 갖는 Job Instance를 정의하여 등록한다. 그리고 Person 객체를 Container를 통해 Resolve하면, "Singer"가 출력되는 것을 확인할 수 있다.

 이는 Container를 통해 Person을 생성할 때, Parameter로 Job 객체가 필요한데 Container가 등록된 Job Instance를 알아서 주입한다. 이에 따라 생성된 Person 객체의 Job 프로퍼티는 미리 등록해놓은 "Singer" Job Instance가 주입되게 된다.

var container = new UnityContainer();

// 1. Job 생성
var singer = new Job() { Name = "Singer", };

// 2. Job Instance를 container에 등록
container.RegisterInstance(singer);

// 3. container를 통해 Person Instance Resolve
var person = container.Resolve<Person>();

// 4. person의 Job 프로퍼티 확인
Console.WriteLine(person.Job.ToString());

// 대기를 위한 ReadLine 함수
Console.ReadLine();


############################################
Singer

TEST 2 (복수 객체 생성)

 아래 코드는 Job 객체를 Container에 하나만 등록하고 Container를 통해 여러 Person 객체를 만드는 코드이다. Instance 구분을 위해 HashCode를 함께 출력했다. 결과에 나타나듯이 Container를 통해 Person을 Resolve하게 되면 Resolve하게 될 때마다 Instance가 새로 생성이 되지만, 내부의 Job Instance는 모두 동일한 Instance를 참조하는 것을 알 수 있다. 이를 활용하여 Singleton Pattern을 설계할 수 있다.

// container 생성
var container = new UnityContainer();

// 1. Job 생성
var singer = new Job() { Name = "Singer", };

// 2. Job Instance를 container에 등록
container.RegisterInstance(singer);

var persons = new List<Person>();

for(int i = 0; i<10; i ++)
{
    // 3. container를 통해 Person Instance Resolve
    var person = container.Resolve<Person>();
    persons.Add(person);
}

for (int i = 0; i < 10; i++)
{
    // 4. person의 Job 프로퍼티 및 Instance 정보 확인
    var person = persons[i];
    Console.WriteLine($"[{i}] {person} (person instance : {person.GetHashCode()}, job instance : {person.Job.GetHashCode()})");
}

// 대기를 위한 ReadLine 함수
Console.ReadLine();


###########################################
[0] Singer (person instance : 22687807, job instance : 2863675)
[1] Singer (person instance : 25773083, job instance : 2863675)
[2] Singer (person instance : 30631159, job instance : 2863675)
[3] Singer (person instance : 7244975, job instance : 2863675)
[4] Singer (person instance : 65204782, job instance : 2863675)
[5] Singer (person instance : 49972132, job instance : 2863675)
[6] Singer (person instance : 47096010, job instance : 2863675)
[7] Singer (person instance : 21210914, job instance : 2863675)
[8] Singer (person instance : 56680499, job instance : 2863675)
[9] Singer (person instance : 40362448, job instance : 2863675)

TEST3

 다음은 Container에 미리 Job Instance를 등록하지 않았을 경우에 대한 처리이다. 확인을 용이하게 하기 위해 ShuffleJob과 ShufflePerson을 아래와 같이 정의했다.

public class ShuffleJob : Job
{
    public ShuffleJob()
    {
        var random = new Random();
        var value = random.Next();

        switch (value % 3)
        {
            case 0:
                Name = "Single";
                break;
            case 1:
                Name = "Doctor";
                break;
            case 2:
                Name = "Software Developer";
                break;
            default:
                break;
        }
    }
}

public class ShufflePerson : Person
{
    public ShufflePerson(ShuffleJob job) : base(job)
    {
    }
}

위와 같이 정의된 ShuffleJob과 ShufflePerson에 대해서 Container에 Instance를 정의하지 않고 Resolve를 수행하면 아래와 같이 나타난다.

 

 Container의 Resolve 함수를 호출하면 ShufflePerson의 Constructor가 호출된다. 이때 Constructor의 매개변수인 ShuffleJob에 대한 Instance를 Container에서 찾게 되는데 등록된 ShuffleJob Instance가 존재하지 않으므로, Instance를 생성하기 위한 ShuffleJob의 Constructor가 호출된다. 이처럼 등록되지 않은 Constructor의 매개변수는 Container에서 생성하여 주입하게 된다.  또한 아래 코드에서는 ShufflePerson이 여러 번 생성되는데 그 때마다 ShuffleJob도 여러 번 생성된다.

// container 생성
var container = new UnityContainer();
var persons = new List<ShufflePerson>();

for (int i = 0; i < 10; i++)
{
    // container를 통해 Person Instance Resolve
    var person = container.Resolve<ShufflePerson>();
    persons.Add(person);
}

for (int i = 0; i < 10; i++)
{
    // person의 Job 프로퍼티 및 Instance 정보 확인
    var person = persons[i];
    Console.WriteLine($"[{i}] {person} (person instance : {person.GetHashCode()}, job instance : {person.Job.GetHashCode()})");
}

// 대기를 위한 ReadLine 함수
Console.ReadLine();


#############################
[0] Doctor (person instance : 32347029, job instance : 22687807)
[1] Doctor (person instance : 2863675, job instance : 25773083)
[2] Doctor (person instance : 30631159, job instance : 7244975)
[3] Doctor (person instance : 65204782, job instance : 49972132)
[4] Doctor (person instance : 47096010, job instance : 21210914)
[5] Doctor (person instance : 56680499, job instance : 40362448)
[6] Doctor (person instance : 27717712, job instance : 48132822)
[7] Doctor (person instance : 30542218, job instance : 6444509)
[8] Doctor (person instance : 58000584, job instance : 52243212)
[9] Doctor (person instance : 426867, job instance : 3841804)

TEST4 (Type Register)

Container를 사용하면 Singleton Pattern을 구현하기 용이하다. 이미 TEST2에서 Singleton Pattern을 경험했다. 다만, Instance를 직접 등록하지 않고, Type만 등록시켜서 사용가능하다. 예시는 다음과 같다. 

// container 생성
var container = new UnityContainer();

// 2. Job Type을 container에 등록
container.RegisterType<Job, ShuffleJob>();

var persons = new List<Person>();

for (int i = 0; i < 10; i++)
{
    // 3. container를 통해 Person Instance Resolve
    var person = container.Resolve<Person>();
    persons.Add(person);
}

for (int i = 0; i < 10; i++)
{
    // 4. person의 Job 프로퍼티 및 Instance 정보 확인
    var person = persons[i];
    Console.WriteLine($"[{i}] {person} (person instance : {person.GetHashCode()}, job instance : {person.Job.GetHashCode()})");
}

// 대기를 위한 ReadLine 함수
Console.ReadLine();


############################
[0] Software Developer (person instance : 2863675, job instance : 25773083)
[1] Software Developer (person instance : 30631159, job instance : 7244975)
[2] Singer (person instance : 65204782, job instance : 49972132)
[3] Singer (person instance : 47096010, job instance : 21210914)
[4] Singer (person instance : 56680499, job instance : 40362448)
[5] Singer (person instance : 27717712, job instance : 48132822)
[6] Singer (person instance : 30542218, job instance : 6444509)
[7] Singer (person instance : 58000584, job instance : 52243212)
[8] Singer (person instance : 426867, job instance : 3841804)
[9] Singer (person instance : 34576242, job instance : 42750725)

 위 코드에서 RegisterType<T>() 대신 RegisterType<TParent, TChild>()를 사용한 것을 확인할 수 있는데, 이는 Person의 Constructor의 매개변수는 ShuffleJob이 아니라 Job 유형이므로 Job 유형의 인스턴스가 필요하다. 하지만 Job을 직접 등록하면 기본 생성자에서 Name 프로퍼티를 주입하지 않기 때문에 RegisterType<TParent, TChild>()를 사용했다. 이 함수는 Container에서 Job 객체를 등록할 때, ShuffleJob을 등록한다라는 뜻이다.

TEST5 

TEST4의 결과를 보면 Job Instance가 모두 다른 것을 확인할 수 있다. 이는 Type을 Container에 등록하여 호출시마다 이를 생성하기 때문이다. 그렇다면, Type을 등록하되 Singleton Pattern을 사용할 수 있을까? 정답은 있다. ContainerControlledLifetimeManager를 사용한다면 Type을 등록하더라도 Singleton처럼 동일한 Instance를 사용할 수 있다. 아래 코드를 확인해보자. 

// container 생성
var container = new UnityContainer();

// 2. Job Type을 container에 등록
container.RegisterType<Job, ShuffleJob>(new ContainerControlledLifetimeManager());

var persons = new List<Person>();

for (int i = 0; i < 10; i++)
{
    // 3. container를 통해 Person Instance Resolve
    var person = container.Resolve<Person>();
    persons.Add(person);
}

for (int i = 0; i < 10; i++)
{
    // 4. person의 Job 프로퍼티 및 Instance 정보 확인
    var person = persons[i];
    Console.WriteLine($"[{i}] {person} (person instance : {person.GetHashCode()}, job instance : {person.Job.GetHashCode()})");
}

// 대기를 위한 ReadLine 함수
Console.ReadLine();

#####################################
[0] Software Developer (person instance : 2863675, job instance : 25773083)
[1] Software Developer (person instance : 30631159, job instance : 25773083)
[2] Software Developer (person instance : 7244975, job instance : 25773083)
[3] Software Developer (person instance : 65204782, job instance : 25773083)
[4] Software Developer (person instance : 49972132, job instance : 25773083)
[5] Software Developer (person instance : 47096010, job instance : 25773083)
[6] Software Developer (person instance : 21210914, job instance : 25773083)
[7] Software Developer (person instance : 56680499, job instance : 25773083)
[8] Software Developer (person instance : 40362448, job instance : 25773083)
[9] Software Developer (person instance : 27717712, job instance : 25773083)

코드

 

GitHub - jaywapp/Tutorials: Tutorial Codes

Tutorial Codes. Contribute to jaywapp/Tutorials development by creating an account on GitHub.

github.com