.Net 리소스 관리에 대한 이해

 

.Net의 리소스를 관리하려면 가비지 수집기(Garbage Collector, GC)의 동작 방식을 알아야 한다.

C#은 C++과 다르게 Heap 메모리에 적재시킨 인스턴스 영역을 해제 시킬 필요가 없다.

왜냐하면 가비지 수집기가 알아서 해제 시켜주기 때문이다.

C++을 먼저 접했던 나는 조금 꺼림찍한 내용이었다.

 

자 그럼 가비지 수집기의 동작방식을 알아보자.

 

가비지 수집기는 관리되는 메모리(managed memory)를 다룬다.

그래서 아래의 3가지와 같은 문제를 개발자는 부담을 줄일 수 있다.

1. 메모리 누수(leak)

2. 댕글링 포인터(dangling pointer)

3. 초기화되지 않는 포인터

 

그렇다면 개발자는 마음대로 객체를 할당하고 사용만 하면 되는 것인가??

반은 맞고 반은 틀리다.

 

이유를 들어보자

1. 가비지 수집기는 데이터베이스 연결, GDI+ 객체, COM 객체, 시스템 객체 등과 같은 비관리 리소스는 여전히 개발자가 관리해야 한다.

2. EventHandler나 delegate 등도 참조는 되어있지만 사용하지 않는 객체들이 불필요하게 메모리에 남을 수도 있다.

 

자 위와 같은 종류의 객체들은 어떻게 관리하고 사용해야 할까??

.Net framework는 IDispose 인터페이스를 제공하고 개발자는 표준 Dispose 패턴만 활용하면 문제를 해결할 수 있다.

IDispose패턴을 활용하는 방법은 ITEM17에 다시 포스팅하겠다..

 

자 다시 돌아와서 가비지 수집기는 어떤 원리로 메모리를 관리하는 것일까?

혹시 마크/콤팩트(Mark/Compact) 알고리즘이라고 들어봤는가?

알면 해당 포스팅은 그냥 넘기면 될 것 같다.

 

마크/콤팩트 알고리즘은 여러 객체 사이의 관계를 파악하고 사용하지 않는 객체는 자동으로 제거하는 알고리즘이다.

 

여러 객체사의 관계란?

Class A는 Class B를 상속한다.

Class A의 필드 변수에 Class C라는 인스턴스를 가지고 있다.

이런 부분을 관계라고 생각하면 될 것이다.

 

본인은 WPF에서 MVVM 패턴을 사용하므로 MainViewModel으로 예시를 들겠다

1. MainViewModel안에는 Test1Model, Test2Model을 가지고 있다.

Heap 메모리 상태

2. 사용자가 Test2Model에서 일련의 연산 수행시켜

  Class C라는 것을 할당하여 실행했다.

Class C를 할당한 Heap 메모리 상태

3. 실행이 끝난 후 Class C는 그대로 남아있다.

실행이 끝난 후 Heap 메모리 상태 Class C는 그대로 남아있다.

4. 가비지 수집기가 최상단 MainViewModel 객체의 관계를 탐색한다.

마크/콤팩트 알고리즘의 탐색 결과

5. Class C는 Test1Model에서 계속 사용하고 있는가?? -> No 

Class C를 제거한 Heap 메모리

6. Heap 메모리는 Compact 됐다. 

Compact 적용 Heap 메모리

그림이 많이 조잡하지만 이해해주길 바란다.

마크/콤팩트 알고리즘의 자세한 내용은 google에서 조금 더 탐색해보길 바란다...

 

가비지 수집기는 수집 과정을 최적화 하기 위해서 세대(generation)이라는 개념이 도입되어있다.

내용이 너무 많으므로 간단하게 설명하겠다.

세대는 가비지 수집기가 한번 수행할 때마다 증가하게 되어있다.

 

예를 들어 위의 상황을 들어보자

 

1. 프로그램 실행 시

MainViewModel : 0 세대

Test1Model : 0 세대

Test2Model : 0 세대

 

2. 사용자가 Test1ViewModel에서 연산을 시켜서 Class C를 추가했을 때

MainViewModel : 0 세대

Test1Model : 0 세대

Class C       : 0 세대

Test2Model : 0 세대

 

3. 가비지 수집 수행

MainViewModel : 1 세대

Test1Model : 1 세대

Class C       : 제거

Test2Model : 1 세대

 

4. 사용자가 Test1ViewModel에서 연산을 다시 실행

MainViewModel : 1 세대

Test1Model : 1 세대

Class C       : 0 세대

Test2Model : 1 세대

 

5. 가비지 수집 수행

MainViewModel : 2 세대

Test1Model : 2 세대

Class C       : 제거

Test2Model : 2 세대

 

이해되었는가?

가비지 수집기로부터 살아남으면 세대가 증가하는 것이다.

그럼 Global 변수와 멤버 변수는?? 

자연히 세대가 높은 객체가 될 것이다.

 

자 그럼 세대가 큰 것과 작은 것의 차이는 무엇일까?

바로 가비지 수집기가 탐색/수집하는 대상 선정의 차이다.

 

1세대 객체는 가비지 수집 10번에 한번 탐색 대상

2세대 객체는 가비지 수집 100번에 한번 탐색 대상

이라는 뜻이다. 10번, 100번은 대략이다 자세한 내용은 스스로 찾아보길 바란다.

 

자 여기까지가 가비지 수집기의 간단한 작동 이론이다.

 

결론

1. 가비지 수집기 덕분에 .Net Framework 개발자들은 메모리 관리에 신경을 덜 써도 된다.

2. 가비지 수집기는 마크/콤팩트(Mark/Compact) 알고리즘, 세대(Generation) 최적화 기법을 사용하여 가비지 수집을 한다.

3. 데이터베이스 연결, GDI+ 객체, COM객체, 시스템 객체 등 과같은 비관리 리소스는 IDispose 패턴을 사용하여 메모리 해제 기능을 만든다.

4. eventhandler, delegate 등 참조는 되어있지만 사용하지 않는 객체들이 불필요하게 메모리에 남을 수도 있다.

 

 

 

'Program > Effective C#' 카테고리의 다른 글

Effective C# ITEM14  (0) 2020.10.28
Effective C# ITEM13  (0) 2020.10.28
Effective C# ITEM 12  (0) 2020.10.26
Effective C# ITEM10  (0) 2020.10.21
Effective C# ITEM9  (0) 2020.10.21

베이스 클래스가 업그레이드된 경우에만 new 한정자를 사용하라

 

비가상 메서드정적으로 바인딩된다. 이는 런타임에 파생 클래스에서 새롭게 정의하고 있는 메서드를 호출한다.

가상 메서드동적으로 바인딩된다. 이는 런타임에 객체 타입이 무엇이냐에 따라 파생 클래스에서 새롭게 정의하고 있는 메서드를 호출한다.

 

new 한정자는 비가상 메서드를 가상 메서드로 바꾸는 것이 아니라 명명 범위(naming scope) 내에 새로운 메서드를 추가하는 역할을 한다.

 

예시 소스부터 보자

 

public class MyClass
{
	public void MagicMethod(){...}
}

public class MyOtherClass : MyClass
{
	publie new void MagicMethod(){...}
}

 

 

object c = MakeObject();

MyClass cl = c as MyClass;
cl.MagicMethod();

MyOherClass cl2 = c as MyOtherClass;
cl2.MagicMethod();

 

두 메서드의 호출 결과는 각각 다르다. 이와 같은 코드를 보는 개발자들은 혼란스러울 뿐이다. 왜냐하면 동일한 객체를 이용하여 동일한 메서드를 호출했지만 결과는 다르기 때문이다. 내가 제일 중요시 여기는 협업하는 코딩에서는 쥐약인 형태이다. 

사실 이런 한정자는 C#에서 왜 제공하는지는 잘 모르지만 덕분에 개발의 자유도는 높아진 것 같다.

 

사용해야 하는 상황이 책에 나와있으니 예시를 올리겠다.

상황 : 헬스 쟁이 개발자는 Widget회사에서 제공하는 BaseWidget Class를 상속받아 휘황찬란한 Widget을 만들고 있었다. 뿐만 아니라 협업하는 개발자 A, B, C.... Z들도 쓰고 있었다.

 

public class MyWidget : BaseWidget
{
	public void NormalizeValues(){...}
}

 

그런데 어느 날 Widget회사에서 새로운 버전을 출시했다고 한다.

새로운 기능을 기대하고 있던 헬스 쟁이 개발자는 당장 구매하여 이전 BaseWidget Class를 갈아치웠다.

 

public class MyWidget : newBaseWidget
{
	public void NormalizeValues(){...}///빨간줄
}

 

큰일이다 자세히 보니 

 

public class newBaseWidget
{
	public void NormalizeValues(){...}
}

 

newBaseWidget에 같은 이름의 함수가 생겨난 것이다...

여기서 2가지 방법이 있다.

 

방법 1 (소요시간 약 일주일)

NormalizeValues 함수 명을 바꾼다.

그리고 NormalizeValues함수를 사용하는 모든 코드를 수정한다.

 

public class MyWidget : newBaseWidget
{
	public void NormalizeAllValues()
	{
		base.NormalizeValues();
	}
}

방법 2 (소요시간 약 10분)

new 한정자를 써서 탈출한다.

 

public class MyWidget : newBaseWidget
{
	public new void NormalizeValues()
	{
		base.NormalizeValues();
	}
}

 

 

자 문제는 해결했다. 하지만 장기적인 대안으로 봤을 때는 방법 1이 올바른 방법이다. 

간혹 누군가가 newBaseWidget이라는 Class를 다루면 조금씩 꼬이기 시작하기 때문이다.

 

당장 내가 3달 안에 퇴사를 한다면 방법 2가 좋을 수도 있다.

하지만 뼈를 묻는다면 무조건 방법 1이 좋다!!

'Program > Effective C#' 카테고리의 다른 글

Effective C# ITEM14  (0) 2020.10.28
Effective C# ITEM13  (0) 2020.10.28
Effective C# ITEM 12  (0) 2020.10.26
Effective C# ITEM11  (0) 2020.10.25
Effective C# ITEM9  (0) 2020.10.21

박싱과 언박싱을 최소화하라

 

.NetFramework 환경에서는 모든 타입을 참조타입인 System.Object로 정의한다.

위와 같은 정의방식으로 우리는 서로 다른 타입을 이어준다.

 

박싱    : 생성한 인스턴스를 상위 클래스나 인터페이스에 포함시키는 작업

언박싱 : 박싱으로 인해 상위 클래스 내부에서 포함되어있는 인스턴스의 복사본을 가져오는 작업

 

int i = 25;
object o = i;      //박싱
int z = o as int; //언박싱

 

정말 쉬운 예제이다. 

 

나같은 초보 개발자들은 코딩을 하다보면 해당 상황과 비슷한 상황에 직면하면

구현이 최우선인지라 신경쓰지 않고 코딩부터 한다.

 

최근에는 같은 기능을 하는 Class 2개에서 메서드 1개씩만 각각 가져와서 사용하는 상황이 있었다.

 

interface iMethod
{

  void Function();

}


class FunctionalClass1 : iMethod
{

  public void Function(){...}

}

class FunctionalClass2 : i Method

{

  public void Function(){...}

}

class HandlerClass
{

  public List<iMethod> Methods
  {
    get
    {

       return new List<iMethod>(){new FunctionClass1(), new FunctionClass2};

    }

  }
}

 

일단 잘됐다! 

하지만 위와같이 사용하면 FunctionClass1~2를 iMethod라는 인터페이스로 박싱을 시켜버려

성능을 하향시킨다...

그러면 어떻게 해야 할까?

지금 떠오르는 방법은 Strategy pattern이다!!

 

interface iMethod
{
  void Function();
}

class MethodBehavior1 : iMethod
{
   public void Function(){...}
}

class MethodBehavior2 : iMethod
{
   public void Function(){...}
}

class FunctionalClass1
{
  private iMethod _method = new MethodBehavior1();
  
  public iMethod Method
  {
     get
     {
     	return _method;
     }
  }
}

class FunctionalClass2
{
 private iMethod _method = new MethodBehavior2();
  
  public iMethod Method
  {
     get
     {
     	return _method;
     }
  }
  
  public FunctionalClass2(iMethod method)
  {
     this._method = method;
  } 
}

class HandlerClass
{
  FunctionalClass1 _functionclass1 = new FunctionalClass1();
  FunctionalClass2 _functionclass2 = new FunctionalClass2();
  
  public List<iMethod> Methods
  {
    get
    {

       return new List<iMethod>(){_functionclass1.Method, _functionclass2.Method};
    }

  }
}

 

막상 구현해보니 BehaviorClass는 결국 인터페이스로 박싱을 한다...

하지만 이전 HandlerClass에서 Methods를 Get을 여러번하면 여러번 박싱 했지만 

이제는 한번만 한다. 별로 만족스럽지 못한 결과다...

 

Effective C#에서 보여준 예제로 마지막을 깔끔하게 정리해야겠다..

 

int nFirstNumber = 1;
int nSecondNumber = 2;


//박싱을 무시한 예시
Console.WriteLine($@"Number1 {nFirstNumber}, Number2 {nSecondNumber}")

//위의 동작은
//int i = 1;
//object o = i;
//string output = o.toString();
//와 같은 원리로 동작한다고한다..


//박싱을 최소화한 예시
Console.WriteLine($@"Number1 {nFirstNumber.toString()}, Number2 {nSecondNumber.toString()}")
//위의 동작은 object로 변하는 박싱동작을 피하고 바로 string으로 넘겨준다고한다.
//string output = nFirstNumber.toString();

 

결론

System.Object 타입이나 인터페이스 타입으로 변경하는 코드는 가능한 작성하지 말아야한다.

하지만 코드의 유지보수성과 코웍이 가능한 코드도 중요하다!

CLR 만세!

'Program > Effective C#' 카테고리의 다른 글

Effective C# ITEM14  (0) 2020.10.28
Effective C# ITEM13  (0) 2020.10.28
Effective C# ITEM 12  (0) 2020.10.26
Effective C# ITEM11  (0) 2020.10.25
Effective C# ITEM10  (0) 2020.10.21

+ Recent posts