# 멤버 초기화 구문이 좋은 이유 

 

. 생성자가 여러 개일 때도 멤버 변수 초기화를 누락하지 않도록 미연에 방지할 수 있다.

  (다만, 모든 생성자가 동일한 방법으로 멤버 변수를 초기화하는 경우에 한해서 사용해야 한다. )

 

. 가독성이 좋고, 유지보수도 용이하다. 

 

 

 

 

# 초기화 구문의 특성

 

. 컴파일러는 모든 생성자(생성자가 없는 경우에 생성하는 기본생성자 포함)의 시작 부분에 멤버 초기화 구문을 포함시키기 때문에 새로운 생성자를 추가하더라도 항상 멤버 초기화 구문이 포함된다.

 

. 또한, 생성하려는 타입이 클래스를 상속하고 있는 경우 베이스 클래스의 생성자가 호출되기 전에 멤버에 대한 초기화가 이루어진다.

 

. 멤버 변수의 초기화 순서는 변수의 선언 순서이다.

 

 

 

 

# 사용 방법

 

  변수를 선언하는 곳에 변수를 초기화 하도록 작성하기만하면 된다.

public class MyClass
{
    //컬렉션 선언과 동시에 초기화
    private List<string> labels = new List<string>();
}

 

 

 

 

# 주의해야할 경우

 

1. 객체를 0이나 null로 초기화하는 경우.

 

  기본 시스템 초기화 루틴은 코드를 실행하기 전에 모든 값을 0으로 설정한다.

그런데 객체를 0이나 null로 초기화 하게되면, C#컴파일러는 개의치 않고 코드를 생성하기 때문에 괜한 일을 추가적으로 하는 꼴이 된다.

public struct MyValType
{
    // 생략
}

MyValType myVal1; // 0으로 초기화
MyValtype myVal2 = new MyValType(); // 반복해서 0으로 초기화

  myVal2같은 경우 initobj라는 IL 명령을 사용하는데 박싱/언박싱 된 myVal2 변수 모두에 대해서 0으로 초기화 하는 과정이 수행된다.

이 과정으로 인해 약간의 추가 시간이 소요된다.

 

 

2. 동일한 객체를 반복해서 초기화하는 경우.

public class MyClass2
{
    private List<string> labels = new List<string>();

    MyClass2() {    }

    MyClass2(int size) 
    {
        labels = new List<string>(size);
    }
}

  위와 같이 List 객체를 생성하는 방식이 다양하게 혼재하는 경우 멤버 초기화 구문을 사용하지 않는 것이 좋다.

MyClass2를 생성할 때 컬렉션의 크기를 지정하게 되면 실제로 2개의 List<> 객체가 생성되며 그 중 하나는 즉각 가비지가 된다.

  멤버 초기화 구문은 생성자의 본문보다 앞서 수행되므로 생성자 본문에서 생성한 객체만 살아남는다.

 

 

3. 예외처리가 반드시 필요한 경우.

 

  멤버 초기화 구문은 try로 감쌀 수 없기 때문에 초기화 과정에서 예외가 발생하면 예외가 외부로 전파된다. 따라서 클래스 내부에서 복구를 시도할 수가 없다.

  예외처리가 필요하다면 반드시 생성자 내부에서 처리해야만 한다.

 

 

# 가비지 수집기

 

  가비지 수집기는 관리되는 메모리를 관장하며 네이티브 환경과는 다르게 메모리 누수, 댕글링 포인터, 초기화되지 않는 포인터, 여타의 메모리 관리 문제를 개발자들이 직접 다루지 않도록 자동화해줍니다.

 

  개발자가 직접 메모리를 관리하는 경우에 발생할 수 있는 해제 순서의 선후 관계에 대한 문제나 순환 참조의 문제, 여러 객체들 간의 복잡한 연관 관계의 문제를 가비지 수집기는 알아서 처리해줍니다.

 

  가비지 수집기는 COM처럼 개별 객체가 스스로 자신의 참조 여부나 횟수 등을 관리하도록 하지 않고, 응용프로그램의  최상위 객체로부터 개별 객체까지의 도달 가능 여부를 확인하도록 설계되어 있습니다. 응용프로그램 내의 최상위 객체로부터 참조 트리를 구성하여 도달 가능한 객체를 살아 있는 객체로 판단하고 도달 불가능한 객체를 가비지로 간주합니다.

 

 

 

 

# 리소스 관리에 대한 이해의 필요성

 

1. 가비지 수집기가 관리해주지 않는 비관리 리소스는 개발자가 직접 관리해야 합니다. 

...더보기

관리 리소스 - CLR에 있는 자원들.

비관리 리소스 - 닷넷에 관리 자원이 아닌 자원. OS 자원.

(데이터베이스 연결, GDI+ 객체, COM 객체, 시스템 객체 등)

 

2. 이벤트 핸들러나 델리게이트 등도 잘못 사용하면 불필요하게 메모리에 오래 남게 됩니다.

 

때문에, 관리되는 리소스를 이해하고 올바른 해제 작업을 수행한다면 더욱 효과적인 가비지 수집이 가능해집니다.

 

 

 

 

# 비관리 리소스 해제

 

  비관리 리소스의 생명주기에 대해 개발자가 더 손쉽게 관리할 수 있도록 finalizer와 IDisposable 인터페이스라는 두 가지 메커니즘을 제공합니다. 하지만, finalizer는 단점이 많기 때문에 IDisposable 인터페이스를 통해 리소스 관리할 것을 더욱 추천합니다. 

 

- finalizer

  비관리 리소스에 대한 해제 작업이 반드시 수행되도록 도와주는 방어적인 매커니즘입니다.

finalizer() 메서드는 직접 오버라이드하여 사용할 수 없고, 소멸자를 만듬으로써 오버라이드 됩니다. 소멸자가 만들어 지면 메모리 해제 작업이 필요하다는 것을 명시하게 됩니다. 개발자가 직접 관리하다보면 메모리 해제 작업을 까먹는 경우도 발생할 수 있는데, 이러한 경우에도 최소한 소멸자에서 호출할 수 있도록 하고 있습니다.

 

 

 

 

# finalizer가 성능에 미치는 영향

 

1. finalizer를 포함하고 있는 객체를 사용하면 가비지 수집 과정이 길어져, 비교적 오랫동안 관리되는 힙에 남아 있게 됩니다.

 

  finalizer는 소멸자를 통해서 호출된다고 앞서 설명하였습니다.

아래와 같이 C++에서 대중적으로 사용되는 리소스 해제 구문이 C#에서는 동일한 방식으로 동작하지 않습니다.

class CriticalSection
{
    public CriticalSection()
        {
            EnterCriticalSection();
        }
        
        //finalizer 오버라이드
        ~CriticalSection()
        {
            ExitCriticalSection();
        }
}

가비지 수집기가 finalizer를 포함하는 객체를 가비지로 판단한 경우, 그 즉시 객체가 점유하고 있는 메모리 공간을 해제하지 못합니다. finalizer를 호출해야 하는데 가비지 수집을 수행하는 스레드를 통해 직접 finalizer를 호출할 수 없습니다.

때문에, 가비지 수집기는 이 객체에 대한 참조를 다른 큐에 삽입하여 나중에 finalizer가 호출될 수 있도록 사전 준비만을 수행합니다.

 

이 과정에서

- finalizer가 없는 객체는 메모리로부터 즉각 제거되지만,

- finalizer가 있는 객체는 이후 다시 가지비 수집 절차가 진행될 때 다른 큐에 삽입해 두었던 객체의 참조를 꺼내어 finalizer를 순차적으로 호출하여 메모리를 반납합니다.

 

이러한 이유로 finalizer를 포함한 객체들은 가비지 수집 과정이 더욱 길어집니다.

 

가비지 수집과정에 세대 개념에 따라 finalizer의 비용에 대해 생각해보면,

...더보기

[세대 개념]

0세대 : 가비지 수집 이후 임의의 생성된 객체.

1세대 : 다시 가비지 수집 절차가 수행되고 이후에도 살아있는 0세대 객체가 1세대 객체가 된다.

2세대 : 두 번 혹은 그 이상의 가비지 수집 절차가 수행됐음에도 여전히 사용되고 있는 객체.

 

[ 가비지 수집 과정 ]

가비지 수집 절차가 시작되면

1. 우선 0세대 객체에 대해서만 가비지 수집을 수행합니다.

2. 대략 10번에 한 번 꼴로 추가적으로 1세대 객체에 대해서 가비지 수집을 수행합니다.

3. 그리고 약 100번에 한번 꼴2세대 객체를 포함한 모든 세대의 객체를 대상으로 가비지 수집을 수행합니다.

 

  finalizer를 가진 객체는 즉각 제거되지 못하므로 1세대 객체가 되고, 이 경우 9번의 가비지 수집 절차가 추가적으로 수행된 이후에 비로소 메모리에서 제거될 가능성이 있습니다. 만약 이 과정에서도 정리되지 못하고 2세대 객체가 되면 약 100번의 가비지 수집 절차가 추가로 수행된 후 삭제될 것입니다.

 

 

2. finalizer 메서드를 호출하는 스레드가 finalzier 스레드이기 때문에 특정 쓰레드에 선호도(affinity)를 갖는 객체일 경우 문제를 발생할 수 있다.

 

  예를 들어, finalizer 메서드 내에서 잠금을 해제하려는 시도는 매우 위험합니다. 잠금을 잠근 스레드는 어플리케이션의 쓰레드이지만 잠금을 해제하려는 쓰레드는 finalizer 쓰레드이기 때문입니다. 

 

 

3. finalizer 메서드가 호출되는 순서를 보장할 수 없습니다. 

 

아래의 코드는 ObjectDisposedException 예외를 유발합니다. 

class DangerousType
{
    private StreamWriter _stream;
    public DangerousType()
    {
        _stream = new StreamWriter("Test.txt");
    }
    ~DangerousType()
    {
        _stream.Close();    // 오류가 발생할 수도 있다!
    }
    public void DoSomething()
    {
        //...... 생략 ......
    }
}

DangerousType 객체는 멤버로 StreamWriter 객체를 참조하고 있고, StreamWriter 클래스 또한 finalzier 메서드를 정의하고 있다고 가정한다. 그러면 DangerousType 객체가 가비지 컬렉션의 대상이 되는 경우 두 객체가 모두 finalizer 큐에 삽입될 것입니다. 그리고 finalizer 쓰레드가 StreamWriter 객체에 대해 먼저 Finalizer 메서드를 호출한다면 StreamWriter 객체는 메모리를 반환하고, 그 이후 DangerousType의 Finalizer 메서드를 호출하면 이미 닫히 파일에 대해 또 다시 Close() 메서드가 호출되기 때문에 예외가 발생하게 됩니다.

 

 

리소스를 해제하는 가장 좋은 방법으로는, finalizer 사용을 지양하고 IDisposable 인터페이스와 표준 Dispose 패턴을 활용하는 것입니다.

 

 

해당 메모리 사용에 관한 내용은 이후 Item 12, Item 13, Item 14, Item 15, Item 16, Item 17에서 더 다루고 있습니다.

 

 

참  고 :  http://www.simpleisbest.net/post/2011/05/12/Finalizer-Usage-Pattern.aspx

베이스 클래스에서 virtual로 선언하지 않은 멤버를 재정의하려는 경우 new 한정자를 사용 할 수 있습니다. 

하지만 사용할 수 있다고 해서 new 한정자는 비가상 메서드를 가상 메서드로 만드는 것이 아니라 클래스이 명명 범위 내에 새로운 메서드를 추가하는 역활을 수행합니다.

 

# override와 new 한정자의 차이

ovrerride로 정의된 가상 메서드를 사용하는 경우는 다음과 같은 결과를 가집니다.

public class BaseClass
{
    public virtual void Func()
    {
        Console.WriteLine("MyClass");
    }
}
public class DerivedClass : BaseClass
{
    public override void Func()
    {
        Console.WriteLine("DerivedClass");
    }
}

// override를 사용한 가상 메서드

BaseClass cl = new BaseClass();
BaseClass cl2 = new DerivedClass();

cl.Func();  // MyClass 출력
cl2.Func(); // DerivedClass 출력

new 한정자를 이용하는 경우 다음과 같은 결과를 가집니다

public class BaseClass
{
    public void Func()
    {
        Console.WriteLine("MyClass");
    }
}
public class DerivedClass : BaseClass
{
    new public void Func()
    {
        Console.WriteLine("DerivedClass");
    }
}

// new 한정자를 사용한 비가상 메서드

BaseClass cl = new BaseClass();
BaseClass cl2 = new DerivedClass();

cl.Func();  // MyClass 출력
cl2.Func(); // DerivedClass 출력

DerivedClass dcl = cl2 as DerivedClass;

dcl.Func(); // DerivedClass 출력

 

비가상 메서드는 정적으로 바인딩되므로 런타임에서 파생 클래스에서 새롭게 정의하고 있는 메서드가 있는지 찾지 않습니다

반면, 가상 메서드는 동적으로 바인딩 되므로 런타임에 객체의 타입이 무엇이냐에 따라 그에 부합하는 메서드를 호출합니다.

 

가상메서드와 비가상 메서드를 재정의하는 new 한정자의 기능을 제대로 파악하고 사용해야 하며, new 한정자를 사용하지 않기 위해 모든 메서드를 가상메서드로 변경하는 것은 좋지 않습니다.

 

설계자가 특정 메서드를 가상으로 선언한다는 것은 파생 클래스에서 이 가상 메서드의 구현부를 변경할 것임을 예상하고 있으며, 파생 클래스에서 가상 메서드의 동작 방식을 변경할지라도 아무런 문제 없이 수행될 것임을 보장하는 것입니다. 따라서 어떤 메서드나 속성이 무엇인지를 우선 생각해보고 반드시 다형성이 필요한 경우에만 가상 메서드를 사용해야 합니다.

 

# new 한정자를 활용하는 경우

new 한정자를 활용해도 좋은 경우는 베이스 클래스에서 이미 사용하고 있는 메서드를 재정의하여 완전히 새로운 베이스 클래스를 만들어야 하는 경우 정도입니다.

이미 널리 사용되고 있는 메서드가 있어서 이를 사용하는 코드를 일일이 찾아서 수정하기 어렵거나, 외부 어셈블리에서 이 메서드를 사용하고 있어서 코드를 수정할 수 없는 경우라면 new 한정자를 사용해 볼 만합니다.

public class MyWidget : BaseWidget
{
    public void NormalizeValues()
    {
        // 세부 내용
    }
}

예를 들어, BaseWidget이라는 클래스를 상속하여 다음과 같이 MyWidget을 정의한 경우가 있고, 많은 사람들이 MyWidget 클래스를 사용 중인 상황에서 BaseWidget에 새로운 버전이 출시되었습니다.

이 때, BaseWidget에 NormalizeValues()라는 새로운 메서드가 추가된다면 빌드가 실패되는 결과가 발생할 것입니다.

 

이 문제를 해결하기 위해서 두가지 방법이 있다. MyWidget의 메서드 이름을 변경하거나, new 한정자를 사용하는 방법이 있습니다.

 

앞에서와 같은 상황에서 new 한정자를 이용하면 기존 메서드명을 그대로 사용할 수 있습니다. 하지만, new 한정자를 사용할 때는 각별히 주의해야 합니다.

코드를 모두 수정할 수 있다면 장기적으로 봤을 때는 메서드의 이름을 변경하는 것이 좋습니다. 왜냐하면, BaseWidget의 NormalizeValues()메서드와 같은 이름으로 다르게 동작하는 메서드 존재로 혼동이 올 수도 있기 때문입니다.

여러 상황을 고려하여 신중하게 new 한정자를 사용하는 것을 권합니다.

 

참 고 : Effective C#  

+ Recent posts