'.NET 4.0'에 해당되는 글 1건

  1. 2015.02.05 Lazy Initialization
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

작성 : 김수영(Microsoft Visual C# MVP)

        .NET N'Gene(http://www.dotnetngene.kr)

        훈스닷넷 SYSOP(http://www.hoons.kr)

버전 : 1.0

 

보통은 사용하려는 객체를 모두 초기화 하고 사용한다. 하지만 때에 따라서(메모리 효율 등 여러가지 이유로) 객체가 필요로 하는 시점까지 초기화를 지연하고 있다가 최초 사용하게 되는 시점에 객체를 초기화 하여 사용하기도 한다. 바로 이런 방식을 지연 로딩(Lazy loading : 명확한 의미 전달의 위해 이하 Lazy로 표기 ) 이라고 얘기한다. Lazy loading은 Deferred loading 이라고도 얘기하고 있다. LINQ 관련 문서를 보았다면 Deferred loading이라고 많이 보았을 것이다. Lazy loading과 반대로 사용하는 시점에 바로 초기하는 것을 즉시 로딩(Eager loading : 이하 Eager로 표기) 이라고 한다. Lazy loading, Lazy initialization, Lazy execution, Deferred loading, Deferred execution 다 비슷한 의미라고 보면 될 것이다.

지금 살펴볼 내용은 프로그램 개발시 Lazy와 Eager 중 어떤 방식으로 객체를 초기화 할 것인가에 대한 내용이 아니라 C# 에서의 Lazy loading에 대한 내용, 특히 C# 4.0에서 새롭게 지원하는 내용으로 보도록 하겠다.(Eager loading은 이미 너무나 익숙하므로 더 이상 자세한 언급은 하지 않는다.)

 

[Lazy - Eager]

두가지 패턴 모두 상황게 맞게 사용하면 된다.

 

먼저 Lazy loading은 무엇인가? 위에서 언급 하였듯이 초기화를 바로 하는 것이 아니라 최초 필요한 시점에 초기화가 이루어 지고, 이후에는 생성된 객체를 참조하게 된다.

 

Lazy loading(Initialization)은 보통 다음과 같은 특정을 지니고 있다.

  • 객체 생성시 new를 사용하면 바로 객체가 생성 되므로 팩토리(Factory) 패턴을 이용하여 객체를 생성한다. 그래야 사용을 위한 선언과 실제 객체가 생성되는 시점을 관리 할 수 있다.
  • 한번 생성된 객체는 이후에는 재사용 되므로 싱클톤(Singleton) 패턴을 띠고 있다.
  • 싱클톤 패턴의 유형을 따르므로 멀티스래드 프로그램 작성시에서는 동기화(Sync)에 신경을 써야 한다. 그렇치 않으면 의도 않은 결과를 가져 올 수 있다.
  • 객체 초기화는 첫 요청(혹은 사용되어지는)이 일어나는 시점에 이루어 진다.

 

Lazy에 대한 보다 깊은 개념은 다음 링크에서 참조해 보기 바란다.

 

그럼 .NET에서는 Lazy loading을 어떻게 사용할 수 있고, 어디서 찾아 볼 수 있는지 알아 보도록 하자.

 

1. LINQ

.NET Framework 3.5가 출시되면서 가장 큰 변화를 뽑으라면 바로 LINQ가 아닐까 한다. 바로 LINQ가 대표적인 Lazy Execution 방식이다.

다음 코드를 보자 LINQ 쿼리하는 부분이 존재하고 Select에서 외부 변수값으로 연산을 수행하고 있다.

 

List<int> number = new List<int>{ 1, 2, 3, 4, 5 };

 

int factor = 10;

 

var result = from x in number

               select x * factor;

 

//변수변경

factor = 20;

 

foreach (var item in result)

    Console.WriteLine(item);

 

연산되는 factor 변수의 값은 10일까? 20일까? LINQ쿼리의 실제적인 실행은 선안하는 시점이 아니라 루프문(foreach, while, for 등)이 실행되는 시점에 비로서 처리가 일어 나게 된다. 위 예제에서는 foreach문이 실행되지 직전에 factor 변수의 값이 20으로 변경이 되었으므로 값 20을 가지고 연산이 이루어 졌을 것이다. 그러므로 외부 변수에 영향을 받는 처리는 유의해서 작성해야 한다. 그렇치 않으면 예제처럼 의도하지 않은 결과를 가져 올 수 있다. Lazy에 대한 이해가 없이 작성되었다면 아미도 "쿼리작성 시점에 factor 값이 10으로 이미 처리되고 foreach에서는 단순 출력만 이루어 진다." 라고 착각을 할 수 있다.

 

[LINQ 처리결과]

 

2. Entity Framework 4

데이터 처리를 비즈니스 레벨로 추상화 하여 시스템 유연성을 확보하고 MS SQL 서버만 지원하는 LINQ to SQL과 다르게 이기종 DB(Oracle, DB2 등)를 지원하는 Microsoft의 ORM(Object Relation Mapping) 프레임워크라고 할 수 있다. 엔티티 프게임워크(Entity Framework, 이하 EF)는 .NET Framework 3.5 SP1에서 처음 등장하였으며, 이번 .NET Framework 4에서 많은 개선을 가지고 왔다. 그 중에서 가장 큰 변화가 POCO(Plain Old CLR Object) 클래스 지원과 엔티티의 Lazy loading 지원이다.

쇼핑몰에 회원이 있고, 그 회원의 주문내역이 있다고 가정해 보자. DB에서 아마도 회원테이블과 주문테이블로 정규화 되어 있을 것이다. 그러나 비즈니스 관점에서 데이터를 바라보면 회원을 객체를 생성할 때 (혹은 어디선가 가져올 때) 그와 연관된 주문내역도 같이 보고 싶을 것이다. EF4에서는 context.ContextOptions.DeferredLoadingEnabled 통해 Lazy loading 할지 Eager loading 할지를 선택 할 수 있다.(LINQ to SQL 에서는 DataLoadOptions 으로 처리 가능하다.) Include() 메소드를 통해 첫 쿼리 실행시 모든 SubSet 집합을 다져 오거나 Load() 메소드를 통해 부분적으로 로딩이 가능하다.

한가지 팁으로 MS SQL 쿼리 프로파일러 로 확인하면 각 시점에 어떻게 실행되는지 한눈에 파악 가능하다.

 

EF4에 좀더 알고 싶다면 부족하지만 필자의

를 참고 바란다.

 

[부분적 로딩]

var items = (from x in entity.authors

             where x.au_fname.StartsWith(fname)

             select x).Take(2);

 

Console.WriteLine("SubSet 탐색 – 명지적 로딩");

foreach (authors item in items)

{

    if (item.au_id == "213-46-8915")

    {

        if (!item.titleauthor.IsLoaded)

            item.titleauthor.Load();

 

        foreach (var titleAu in item.titleauthor)

        {

            if (titleAu != null)

                Console.WriteLine("{0} : {1} : {2}"

                    , titleAu.authors.au_fname, titleAu.au_id, titleAu.au_ord);

        }

    }

    else

    {

        foreach (var titleAu in item.titleauthor)

        {

            if (titleAu != null)

                Console.WriteLine("{0} : {1} : {2}"

                    , titleAu.authors.au_fname, titleAu.au_id, titleAu.au_ord);

        }

    }

}

 

[최초 모두 로딩]

var items2 = from x in entity.authors.Include("titleauthor")

             where x.au_fname.StartsWith(fname)

             select x;

 

Console.WriteLine("SubSet 탐색 - Eager Loading");

 

foreach (authors item in items2)

{

    titleauthor tempTitleAu = item.titleauthor.FirstOrDefault();

 

    if (tempTitleAu != null)

        Console.WriteLine(tempTitleAu.au_id);

}

 

Console.WriteLine("============== Trace ==============");

Console.WriteLine(((ObjectQuery)items2).ToTraceString()); 

 

3. Lazy Initialization

이제 제목에서 본 C# 4.0 에서의 Lazy Initialization을 살펴 보자. C# 4.0 & .NET Framework 4에서는 크게 2가지 형태의 Lazy Initialization을 지원하고 있다.

 

  • Lazy<T> (System)
  • LazyInitializer (System.Threading)

 

프레임워크에서 지원하는 인프라를 사용하면 쉽게 Lazy 패턴을 사용할 수 있으며, 특히 까다로운 멀티스래드 프로그램시 동기화 부분을 쉽게 처리 가능하다. LazyInitializer는 네임스페이스가 System.Threading 인 것만 봐도 무엇에 초점을 맞추고 있는지 알 수 있다.

 

3.1 Lazy Initialization 직접 구현

먼저 직접 Lazy Initialization 을 구현을 한번 보자.

사용할 기본 Foo 클래스를 정의해 보자.

 

[Lazy Initialization 할 클래스 정의]

public sealed class Foo

{

    string _Lazy;

 

    public Foo(string lazy)

    {

        this._Lazy = String.Format("{0}({1})", lazy

            , DateTime.Now.ToLongTimeString());

 

        Console.WriteLine("Init Foo -> instance : {0}"this._Lazy);

    }

 

    public void HelloWorld(string value)

    {

        Console.WriteLine("Foo({0}) -> HelloWorld : {1}{2}"

            , this._Lazy, value

            , Environment.NewLine);

    }

}

 

이제 Foo 클래스를 Lazy 방식으로 호출하려면 Foo 객체의 인스턴스를 호출해 주는 Factory가 필요하다. FooObject라는 프로퍼티로 Foo 인스턴스가 없으면 생성을 하고 기존에 생성된 것이 존재하면 있는 인스턴스를 반환하도록 하였다.

 

[Lazy Initialization – Thread Unsafe 방식]

public sealed class LazyLoadingPattern

{

    private Foo _foo;

 

    //Thread Unsafe

    public Foo FooObject

    {

        get

        {

            if (this._foo == null)

            {

                this._foo = new Foo("Lazy Pattern");

            }

 

            return this._foo;

        }

    }

}

 

멀티 스래드 환경에서 호출하여 보자. 2개의 스래드에서 각각 호출 하게 된다.

 

[멀티 스래드에서 호출]

LazyLoadingPattern lazyPatternObj = new LazyLoadingPattern();

 

ArrayList list01 = new ArrayList(2);

list01.Add(lazyPatternObj);

list01.Add("thread01");

Thread thread01 = new Thread(LazyPatternHelloWorld);

 

ArrayList list02 = new ArrayList(2);

list02.Add(lazyPatternObj);

list02.Add("thread02");

Thread thread02 = new Thread(LazyPatternHelloWorld);

 

thread01.Start(list01);

thread02.Start(list02);

 

FooObject에서 Foo 인스턴스 생성시 기존에 생성된 인스턴스를 반환하도록 되어 작성이 되었지만 아래 실행 결과를 보면 각 호출마다 서로 다른 인스턴스가 생성이 되었다. 위와 같은 방식은 멀티 스레드 환경에서 각 생성된 객체에 대한 동기화 문제를 해결 할 수 없다. 멀티스래드에서 동기화는 런타임시 프로그램의 신뢰성에 아주 중요한 문제이다. 아래 결과를 보면 예상했던 결과와는 전혀 다르다. 각각의 스래드가 인스턴스를 생성하고 있다.

 

[Lazy Initialization Thread Unsafe 결과 ]

 

위와 같은 동기화 문제를 해결하기 위해 C# lock 구문을 사용하여 쉽게 처리 가능하다. 이외에 .NET에서는 MutexSemaphore를 통해서도 동기화 처리가 가능하다. 아래 코드에서는 인스턴스 생성하는 부분을 lock 구분으로 감싸주어 동시에 스래드가 접근 하여 인스턴스 생성에 문제가 발생하는 부분을 해결 하였다.

 

[Lazy Initialization – Thread Safe 방식]

public sealed class LazyLoadingPattern

{

    private Foo _foo;

    //Thread Safe

    readonly object _lockObj = new object();

 

    public Foo FooObject

    {

        get

        {

            lock (this._lockObj)

            {

                if (this._foo == null)

                {

                    this._foo = new Foo("Lazy Pattern");

                }

 

                return this._foo;

            }

        }

    }

}

 

lock 구문 사용으로 Foo 인스턴스는 한번만 생성이 되는 것을 볼 수 있다.

 

[Lazy Initialization Thread Safe 결과 ]

 

3.2 Lazy<T>

C# 4.0 에서는 Lazy<T> 라는 새로운 클래스를 통해서 Lazy Initialization을 사용할 수 이으며, 특히 멀티스래드에 안전하게 사용가능하다. 내부적으로 Lazy<T> 클래스는 DCL(double-checked locking) 패턴을 사용한다. 우리가 직접 구현했던 코드에서 문제가 되었던 것은 객체의 인스턴트가 생성되어 있는지 아니면 null 상태인지를 판단하는 부분에서 문제가 발생하는 것이지 이미 인스턴스가 생성되어 있으면 나머지 부분은 큰 문제가 없다. 바로 이 부분을 해결해 주는 것이 DCL 이다. DCL의 핵심은 인스턴스 생성에 대한 처음에만 동기화를 수행하고 인스턴스 생성된 이후에는 확인하지 않는다.(이미 생성된 되었기 때문에 이후에는 전혀 문제가 없다.)

DCL에 대해서는

에서 자세히 볼 수 있다.

Lazy<T> 클래스 생성자에서 사용할 인스턴스 생성 구문을 작성하고 Value 프로퍼티를 통해 접근 가능하다.

 

[Lazy<T> 방식]

//Eager Init

Foo eagerInstance = new Foo("Eager Init");

Console.WriteLine("...01. Working...");

eagerInstance.HelloWorld("Eager");

 

//Lazy Init

Lazy<Foo> lazyInstance = new Lazy<Foo>(() => new Foo("Lazy<T> Init"));

Console.WriteLine("...02. Working...");

lazyInstance.Value.HelloWorld("Lazy 01");

 

결과를 보면 첫번에 Eager 방식은 즉시 인스턴스가 생성된다. 하지만 Lazy<T> 방식의 결과를 보면 인스턴스 생성되는 시점이 첫 호출 될 때인 것을 확인 할 수 있다. (Working… 이란 문자열이 출력되는 시점을 자세히 보라.)

 

[Lazy<T> 결과]

 

3.3 LazyInitializer.EnsureInitialized

특정 객체를 사용하는 것을 굳이 Lazy<T> 클래스의 인스턴스를 통해서 사용하고 싶지 않다면 C# 4.0에서는 또하나의LazyInitializer 정적(static) 클래스를 지원한다. 최초 인스턴스가 필요한 시점에 LazyInitializer. EnsureInitialized 메소드를 통해서 사용하고자 하는 객체의 인스턴스를 생성한다. EnsureInitialized 의 파라미터는 사용하고자 하는 객체의 인스턴스와 인스턴스 생성 팩토리 델리게이트로 되어 있다.

한가지 주의 할 것은 아래 코드에서 보는 것처럼 동일한 인스턴스를 참조하고 있다면(01번과 02번 처럼) 최초 인스턴스 생성 이후의 팩토리 델리게이트는 모두 무시 된다. 실행 결과를 보면 모두 인스턴스는 한번만 생성 되고 하나의 인스턴스를 공유하는 것을 볼 수 있다.

 

[LazyInitializer.EnsureInitialized 방식]

Foo instance = null;

 

//01

LazyInitializer.EnsureInitialized(ref instance

, () => new Foo("Lazy<T> Init 01"));

instance.HelloWorld("01");

 

Thread.Sleep(2000);

 

//02

LazyInitializer.EnsureInitialized(ref instance

, () => new Foo("Lazy<T> Init 02"));

instance.HelloWorld("02");

 

[LazyInitializer.EnsureInitialized 결과]

 

Lazy Initialization 에 대해서 C# 4.0의 새롭게 추가된 클래스 뿐만 아니라 여러 관점에서 살펴 보았다. 어차피 Lazy Initialization은 특정 기술에 종속적인 것이 아니라 디자인 패턴적인 요소이고 그 것을 .NET 에서는 개발자가 보다 쉽게 사용할 수 있다록 지원해 주고 있는 것이다. 필요할 때 적절히~~ 잘~~ 사용하면 된다.

C# 4.0과 .NET Framework 4 에서는 이외에 재미있는 것들이 참 많이 추가 되었다. 이후에 다른 재밌는 요소 더 살펴 보도록 하자.


출처 : 

http://blog.naver.com/PostView.nhn?blogId=dotnethelper&logNo=60104797637&categoryNo=0&parentCategoryNo=0&viewDate=¤tPage=2&postListTopCurrentPage=&isAfterWrite=true

Posted by 역시인생한방
,