본문 바로가기

C# .Net

[EFC#] effective c# - 2. 리소스 관리


# .NET 리소스 관리의 이해 finalizer/IDisposable

1. 닷넷은 가비지 수집기가 있기 때문에 개발자가 메모리 해제/누수와 같은 문제를 직접적으로 처리할 필요가 없다.

2. 그러나 비관리 리소스의 경우 관리가 필요하며, 이를 위해 finalizer와 IDisposable 라는 두가지 메커니즘을 제공한다. 

3. finalizer의 경우 가비지로 간주된 이후에도 메모리 점유시간이 길어져(언제 해제될지도 알 수 없다) 둘 중 IDisposable이 더 권장된다

    - 가바지로 간주된 객체는 다음 가비지 수집세대에서 삭제되지만

    - finalizer를 갖고있는 객체는 가비지로 판단되더라도 finalizer를 호출하기 전까지 공간이 해제가 안된다.



# 할당구문보다 멤버 초기화 구문이 좋다.

- 생성자에서 초기화 코드를 누락하는 경우가 많으므로 변수를 선언하는 곳에서 초기화를 해주는 것이 좋다.

- 초기화를 하지 않으면 예외가 발생할 위험이 있기 때문이다.

- 다만 초기화가 필요없는 경우가 있는데, 다음과 같다.

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

    - 저수준에서 CPU 명령이 메모리블록을 0으로 설정하기때문.

    - 아래 코드는 동일한 0 초기화 구문이다.

     {
          MyValType myVal1;
          MyValType myVal2 = new MyValType();
      }

  2. 객체를 생성하는 방식이 다를경우 ex) list 객체의 size를 생성자마자 달리 설정하는 것



# 정적 클래스 멤버를 올바르게 초기화하라

- 정적생성자는 타입 내에 정의된 모든 변수/메서드/속성에 접근하기전에 최초로 접근하는 메서드이다.

- 복잡한 초기화가 이뤄지는 경우 그 대안으로 사용되기도 한다.

  - 가령 일반적인 멤버초기화는 예외발생 가능성이 있으면 잡기가 어려운데, 정적생성자의 catch 구문을 통해 복구를 시도할 수 있다.

- 때문에 private 메서드 사용등의 잘못된 초기화에 유의해야한다

  1. 만일 잘못된 초기화가 이뤄지면 CLR 단계에서 초기화 익셉션이 발생하며,

  2. 만약 예외가 발생할 가능성이 있는 정적생성자의 예외를 호출부에서 catch로 잡아버리면 해당 AppDomain이 unload하지 않는 한 해당 객체 생성이 불가능하다.



# 초기화가 중복되는 것을 최소화 하라

- 생성자를 초기화하는 구문을 두는 방법은 크게 두가지가 있다.

  1. 다양한 변수를 인자로 받는 생성자 오버로딩 + 초기화 할당 메서드 호출

  2. 기본값을 매개변수로 하는 생성자 작성 + this(params) 혹은 base(params) 호출 구문을 통해 상속

- 여기서 두번쨰 방법을 사용하는 것이 좋다.

  - 전자의 방식을 사용할 경우 생성자 초기화 후에 다시 재 할당이 이뤄지므로 가비지가 생성되며

  - readonly 예약어가 적용된 변수는 메서드에서 초기화가 불가능하다.

  - 또한 코드가 멤버가 많을수록 오버로드의 경우의 수가 많아져 코드가 장황해질 수 있다

- 참고) 초기화 우선순위

    1. 정적변수 저장공간 0으로 초기화 -> 변수 초기화

    2. 베이스 클래스의 정적 생성자 수행

    3. 정적 생성자 수행

    4. 인스턴스 변수 저장공간 0으로 초기화 -> 변수 초기화

    5. 베이스 클래스의 인스턴스 생성자 수행

    6. 인스턴스 생성자 수행



# 불필요한 객체를 만들지 말라

- 자주 사용되는 지역변수의 경우 멤버 변수로 선언하고 사용해야한다.

- string 객체의 경우 문자열이 추가될때마다 string 객체가 다시 생성되므로 StringBuilder를 사용할것을 권장한다.



# 생성자 내에서는 절대로 가상함수 호출을 하지 말라(더 고민해볼것)

- 런타임에서는 멤버변수가 초기화 된 다음에 생성자가 수행되는것이 전제되므로, 멤버변수를 사용하는 가상함수가 null 포인터 예외를 방지하기위해 일관성이 없어질 확률(즉 가상메서드가 구현된 메서드를 호출할 위험)이 있다.

- 이를 극복하려면 베이스 클래스에서 추상함수를 선언하고 파생클래스에서 오버라이드 하면된다. 그러나 



# 표준 Dispose패턴을 구현하라

- 파생클래스가 있는 클래스에서 IDispose 인터페이스를 상속 받으면 반드시 표준 Dispose패턴 구현이 필요하다.

- Dispose 패턴 구현시에 인터페이스 상속을 받는 클래스 멤버중 비관리 리소스가 있으면, 반드시 finalizer를 구현해야한다.

    1. 이용자가 비관리 리소스 정리를 하지 못하는 경우를 대비하기 위한 방어적 코딩이다.

    2. 하지만 멤버에 비관리리소스가 없다면 절대 사용하지 말아야한다.

- Dispose나 finalizer에서 다른 작업을 수행하면 GC도, Dispose나 finalizer도 닿지 못하는 객체가 생길 수 있다. 이미 정리된 리소스라고 판단되기 때문이다.