Effective C#

 EffectiveC_.bmp

 

목차


  1. Chapter 01 – C# 언어 요소

    1. 1. 데이터 멤버 대신에 항상 프로퍼티를 사용하라.
    2. 2. const보다는 readonly가 좋다.
    3. 3. cast 보다는 is나 as가 좋다.
    4. 4. #if 대신 Conditional Attribute를 사용하라.
    5. 5. 항상 ToString()을 작성하라.
    6. 6. value 타입과 reference 타입을 구분하라.
    7. 7. immutable atomic value 타입이 더 좋다.
    8. 8. value 타입을 사용할 때 0이라는 값이 의미를 가질 수 있도록하라.
    9. 9. ReferenceEquals(), static Equals(), instance Equals(),operator==의 상호 연관성을 이해하라.
    10. 10. GetHashCode()의 함정에 유의하라.
    11. 11. foreach 루프가 더 좋다.
  2. Chapter 02 – 닷넷 리소스 관리
    1. 12. 할당문보다는 변수 초기화를 사용하는 편이 더 좋다.
    2. 13. static 클래스 멤버는 static 생성자를 사용하여 초기화하라.
    3. 14. 연쇄적인 생성자 호출을 이용하라.
    4. 15. 자원해제를 위해서 using과 try/finally를 이용하라.
    5. 16. Garbage를 최소화하라.
    6. 17. boxing과 unboxing을 최소화하라.
    7. 18. 표준 Dispose 패턴을 구현하라.
  3. Chapter 03 – 설계사항 구현
    1. 19. 상속보다는 interface를 정의하고 구현하는 것이 좋다.
    2. 20. interface의 구현과 virtual 메서드의 overriding을 구분하라.
    3. 21. delegate를 이용하여 콜백을 표현하라.
    4. 22. 이벤트를 이용하여 외부로 노출할 인터페이스를 정의하라.
    5. 23. 클래스 내부 객체에 대한 reference 반환을 피하라.
    6. 24. 명령적 프로그래밍보다 선언적 프로그래밍이 더 좋다.
    7. 25. serializable 타입이 더 좋다.
    8. 26. IComparable과 IComparer를 이용하여 순차관계를 구현하라.
    9. 27. ICloneable의 구현을 피하라.
    10. 28. 형변환 연산자의 구현을 피하라.
    11. 29. 기반 클래스의 변경이 영향을 줄 경우에만 new 한정자를 사용하라.
  4. Chapter 04 – 이진 컴포넌트 작성
    1. 30. CLS를 준수하는 어셈블리가 더 좋다.
    2. 31. 작고 단순한 메서드가 더 좋다.
    3. 32. 작고 응집도가 높은 어셈블리가 더 좋다.
    4. 33. 타입의 가시성을 제한하라.
    5. 34. 웹 API는 큰 단위로 작성하라.
  5. Chapter 05 – 프레임워크의 사용
    1. 35. 이벤트 핸들러보다 override를 사용하는 편이 낫다.
    2. 36. 닷넷 런타임의 진단기능을 활용하라.
    3. 37. 표준 환경설정 메커니즘을 이용하라.
    4. 38. 데이터 바인딩을 사용하라.
    5. 39. 닷넷의 유효성 검증 기능을 사용하라.
    6. 40. 적절한 collection 개체를 이용하라.
    7. 41. 새로운 구조체보다는 DateSet이 좋다.
    8. 42. reflection을 단순화하기 위해서 attribute를 사용하라.
    9. 43. reflection을 과도하게 사용하지 말라.
    10. 44. 애플리케이션에 특화된 예외 클래스를 완벽하게 작성하라.
  6. Chapter 06 – 기타
    1. 45. 견고한 예외 보증 기법이 더 좋다.
    2. 46. Interop를 최소화하라.
    3. 47. 안전한 코드가 더 좋다.
    4. 48. 활용할 수 있는 다양한 툴과 리소스에 대해서 알아두라.
    5. 49. C# 2.0의 주요 특징
    6. 50. ECMA 표준을 익혀라.

 

 


Chapter 01 – C# 언어 요소#

    1. 데이터 멤버 대신에 항상 프로퍼티를 사용하라.#

프로퍼티는 데이터 멤버처럼 접근가능하면서 메서드의 형태로 구현되는 C# 언어의 요소이다. 프로퍼티는 메서드의 이점을 가지면서도 마치 데이터에 직접 접근하는 것과 같은 방법을 제공한다. 프로퍼티를 이용하여 작성된 타입을 사용하는 것은 public 변수를 접근하는 것과 동일한 방법으로 구현된다. 하지만 프로퍼티를 구현하는 것은 메서드를 구현하는 것과 유사하며, 실제 데이터 값을 얻어내거나 바꿀 때에 행동방식을 정의할 수 있다.

만일 public 이나 protected 형태로 타입의 값을 노출해야 하는 경우라면 항상 프로퍼티를 사용하자. 다수의 값에 대해서 순서대로 접근을 하거나 디렉토리 형태의 자료 구조를 표현할 때는 인덱서를 사용하자. 모든 데이터 멤버는 예외없이 private로만 사용하는 것이 좋다. 이렇게 함으로써 데이터 바인딩 지원이 가능해지며 추후에 메서드의 변경이 발생했을 때 좀 더 쉽게 코드를 변경할 수 있다.


2. const보다는 readonly가 좋다.#

C#은 상수형으로 컴파일타임 상수와 런타임 상수 두 가지를 가지고 있다. 컴파일타임 상수는 런타임 상수에 비해 다소 빠르지만 유연성이 떨어진다. 컴파일타임 상수는 수행성능이 매우 중요하고, 상수의 값이 절대로 바뀌지 않는 경우에 한해서만 사용하는 것이 좋다.

런타임 상수는 readonly 키워드로 선언되고, 컴파일타임 상수는 const 키워드로 선언된다.

  1. // 컴파일 타임 상수

  2. public const int _Millennium = 2000;

  3. // 런타임 상수

  4. public static readonly int _ThisYear = 2009;

런타임 상수와 컴파일타임 상수의 근본적인 차이는 그 값들이 어떤 방식으로 평가되는가에 있다. 컴파일타임 상수를 이용하면 컴파일 시에 상수형 변수가 사용되는 위치가 실질적인 값으로 치환된다. 반면 런타임 상수는 수행시에 평가된다. readonly 키워드를 이용하여 선언된 상수를 사용하는 코드를 살펴보면 컴파일타임 상수의 사용 예와는 다르게 실질적인 값으로 대체되지 않고 상수에 대한 참조자가 위치하게 된다.

이러한 차이점으로 인해 각각의 상수형들은 서로 다른 한계를 가지고 있는데, 컴파일타임 상수는 단지 내장자료형( 정수형, 실수형 등 )이나 enum, string에 대해서만 사용될 수 있다. 이는 내장자료형만이 유일하게 변수의 초기화 단계에서 의미를 가지고 문자 자체로 표현되는 값( literal value )을 대체할 수 있기 때문이다.

이러한 제약 때문에 컴파일타임 상수는 숫자와 문자열에 한해서만 사용될 수 있다. 런타임 상수는 생성자가 호출이 완료된 이후에는 값을 변경할 수 없지만 생성자 내에서는 값이 할당될 수 있고, 컴파일타임 상수에 비해 비교적 유연하게 사용될 수 있다.

const 상수와 readonly 상수의 가장 중요한 차이점은 readonly 상수는 수행시에 그 값이 평가된다는 것이다. readonly 상수를 참조하는 코드를 IL 코드로 생성해 보면, 상수값 자체로 대체되는 것이 아니라 여전히 상수에 대한 참조자가 위치하게 된다는 것을 확인할 수 있다.

readonly 대신 const 를 썼을 때의 유일한 장점은 수행성능이다. 그렇지만 이를 통해 얻을 수 있는 수행성능의 개선효과가 작고, 무엇보다 유연성을 감소시키는 단점이 있다.

const는 컴파일 시에 값으로 평가되어야 하는 곳에서만 사용되어야 한다. Attribute의 인자나, enum형의 상수 값 선언 등에 주로 쓰일 수 있고, 드물긴 하겠지만 어셈블리의 지속적인 수정과 배포에도 항상 동일값을 유지해야 하는 경우에 쓰일 수 있다. 다른 대부분의 경우에 있어서 유연성을 확보할 수 있는 readonly 상수를 쓰는 것이 좋다.

 


3. cast 보다는 is as가 좋다.#

      특정 타입에서 다른 타입으로의 강제적인 형변환은 가능한 한 피하는 편이 좋다. 하지만 런타임에 타입을 변경하는 일이 반드시 필요할 때가 종종 있다. 타입을 변경하는 데 있어 우리는 두 가지의 방법을 선택할 수 있는데, as 연산자를 쓰거나 C 언어에서 사용해왔던 전통적인 cast 연산자를 사용할 수 있다. 형변환의 가능성 여부는 is 연산자 수행하고, 형변환 수행을 위해서는 as 연산자를 쓴다면 좀 더 방어적으로 프로그램을 작성할 수 있다.

      가능한 한 모든 곳에서 전통적인 C 언어에서 사용해왔던 cast 방식보다는 as 연산자를 쓰는 것이 좋은데, 이는 as 연산자가 소위 말하는 C 언어의 blinding casting보다 좀 더 안정적이기 때문이다. 또한 as와 is 연산자는 사용자가 정의한 형변환 연산자의 존재를 고려하지 않기 때문에 런타임의 수행성능 효율도 좋다. as 와 is 연산자는 런타임에 정확한 타입의 변환이 가능할 때에만 형변환을 수행한다. 또한 형변환을 수행하기 위해서 새로운 객체를 만들거나 하지 않는다.

  1. object o = Factory.GetObject();

  2. Mytype t = o as Mytype;

  3. if( t != null )

  4. {   // MyType형의 t 객체 사용 }

  5. else

  6. {   // 오류 보고 }

 

 

 

4. #if 대신 Conditional Attribute를 사용하라.#

      #if / #endif 블록은 단일의 소스를 이용하여 디버그 버전과 릴리즈 버전에서 서로 다르게 동작하는 실행파일을 생성하거나, 서로 다른 플랫폼에서 수행가능한 실행파일을 생성하기 위해서 자주 쓰여 왔다. 그러나 #if / #endif 블록은 혼돈스럽고 이해하기 어려우며 디버깅을 수행하기도 매우 힘든 코드를 만드는 부작용이 있다. 컴퓨터 언어 설계자들은 이러한 부작용을 경감시키기 위해 노력해 왔고, C#에서는 Conditional이라는 Attribute가 포함되었다. 이 Attribute는 특정 메서드가 환경설정 값에 따라 호출될 수 있는지의 여부를 결정하게 되며, 비교적 #if / #endif를 사용하는 것에 비해 깔끔한 코드를 유지할 수 있도록 도와 준다. 컴파일러는 컴파일시에 Conditional Attribute의 값을 조사하고, 주어진 값을 검토하여 적절한 코드를 만들어낸다. Conditional Attribute는 메서드에 적용 가능한 Attribute이기 때문에 각 메서드 별로 구분하여 Attribute를 부여할 수 있다. 만일 특정 조건하에서 서로 다른 결과를 만들어내는 코드를 작성하기 위해서 #if / #endif를 사용하고 있었다면 앞으로는 Conditional Attribute를 사용하기 바란다.

  1. private void CheckState()

  2. {

  3.      // 이전 방법

  4. #if DEBUG

  5.      Trace.WriteLine("Entering CheckState for Person");

  6.      ...

  7. #endif

  8. }

     

      위의 예제와 같이 #if / #endif를 사용하게 되면 Release 모드로 빌드를 수행시켰을 때 CheckState()는 아무런 내용도 담지 않은 메서드로 컴파일된다. 하지만 CheckState()를 호출하는 코드는 컴파일 모드와 상관없이 모든 빌드에 포함된다. 따라서 Release 모드에서는 아무런 동작도 하지 않는 메서드를 호출하는 코드를 포함하게 된다. 이러한 메서드 호출은 비록 적지만 메서드 호출 비용과 JIT 작업에 따른 비용이 수반된다.

      C#에서는 다른 대안이 있다. Conditional Attribute가 바로 그것인데, 이 Attribute를 쓰면 특정 환경변수 값이 정의되어 있거나 특정 값을 가질 때에만 컴파일되는 코드를 독립된 메서드 내에 격리하기가 좀 더 쉬워진다.

  1. [Conditional("DEBUG")]

  2. private void CheckState()

  3. { ... }

      Conditional Attribute는 C# 컴파일러에게 이 메서드는 컴파일러가 DEBUG라는 환경변수 값이 정의되어 있을 때만 호출될 수 있다고 알려준다. Conditional Attribute는 CheckState() 메서드의 컴파일과 코드생성에는 영향을 미치지 않는다. 대신 CheckState() 메서드를 호출하는 측의 코드생성에 영향을 준다. 만일 DEBUG라는 환경변수값이 정의되어 있다면, 컴파일러는 다음과 같은 코드를 생성할 것이다.

  1. public string LastName

  2. {

  3.      get

  4.      {     CheckState();   return _lastName;    }

  5.      set

  6.      {     CheckState();   _lastName = value;   CheckState();   }

  7. }

     하지만 만일 DEBUG 환경변수가 정의되어 있지 않으면 다음과 같은 코드가 만들어진다.

  1. public string LastName

  2. {

         get

         {     return _lastName;    }

         set

         {     _lastName = value;   }

    }

      DEBUG 환경변수의 정의여부와 상관없이 CheckState() 메서드는 컴파일되고 어셈블리 내에 위치하게 된다. 이것은 자칫 비효율적으로 보일 수도 있다. 하지만 이렇게 함으로써 우리가 잃는 것은 겨우 디스크 공간 조금이다. CheckState()가 호출되지 않는다면 이 메서드는 메모리상에 로드되지도 않을 것이고 JIT가 수행되지도 않는다. 어셈블리 내에 존재하기는 하지만 사용되지는 않는다. 이러한 전략은 유연성을 증대시키며 수행성능에 미치는 영향을 최소화한다.

Conditional Attribute는 한 개 이상의 환경변수와 함께 쓰일 수 있다. 다수의 Conditional Attribute를 쓰게 되면 각각의 조건들이 'or 조건'으로 결합된다.

      Conditional Attribute는 단지 메서드에 대해서만 지정이 가능하다. 추가적인 제한사항으로는 Conditional Attribute를 지정하는 메서드는 반드시 void 형태리턴 타입을 가져야 한다는 점과 메서드 내의 일부 문장에 대해서만 Conditional Attribute를 지정할 수는 없다는 점이다.

      Conditional Attribute는 #if / #endif를 사용하는 것보다 좀 더 효율적인 IL 코드를 생성한다. 메서드 단위로만 이러한 Attribute를 지정할 수 있기 때문에, 조건적으로 생성되는 코드들을 일반 코드들과 격리시키고, 보다 구조적으로 프로그램을 만들 수 있도록 한다. Conditional Attribute는 컴파일러에게 #if / #endif를 사용했을 때 자칫 범하기 쉬운 오류들을 미연에 방지할 수 있도록 도와주며, #if / #endif보다 좀 더 깔끔하게 그러한 루틴들을 격리시킬 수 있다.

 

 

5. 항상 ToString()을 작성하라.#

       System.Object.ToString()은 닷넷 환경에서 가장 빈번하게 쓰이는 메서드 중의 하나이다. 사용자들을 위해 앞으로 작성할 모든 타입에 대해서는 적절한 ToString() 메서드를 반드시 제공하는 것이 좋다. 이렇게 타입을 사람이 읽을 수 있는 문자열 형태로 표현할 수 있게 되면, 사용자의 프로그램 형태가 윈도우 폼, 웹 폼이든, 또는 콘솔 출력을 수행하는 형태인가와 상관없이 좀 더 쉽게 객체의 상태를 출력할 수 있게 된다. 또한 이러한 문자열 표현 방식은 디버깅을 수행할 때도 많은 도움이 되는 것이 사실이다. 모든 타입들에 대해서 ToString() 메서드를 적절히 overriding하자. 만일 구조가 매우 복잡한 타입을 만들고 있다면, IFormattable.ToString()을 이용하여 좀 더 정교한 출력 결과를 만들어낼 수도 있다.

      IFormattable.ToString()을 어떻게 구현해도 상관없지만 일반적으로 IFormattable interface를 구현할 때 지켜야 할 몇 가지 규칙이 있다. 첫째로, 일반적인 출력형태를 위해 "G" format 문자열을 지원해야 한다. 둘째로, format 문자열로 비어있는 문자열 ""와 null이 전달될 경우를 고려해야 한다. 마지막 규칙은 format 문자열로 "G","",null을 전달했을 때 반환되는 문자열은 Object.ToString() 메서드가 반화하는 문자열과 동일해야 한다. 닷넷 프레임워크 클래스 라이브러리는 일반적인 출력문자열을 얻기 위해서 IFormattable interface를 구현한 타입에 대해서는 Object.ToString() 대신 IFormattable.ToString()을 호출한다. 이 경우 닷넷 프레임워크 클래스 라이브러리는 보통의 경우 format 문자열로 null을 전달하며 몇몇의 경우에 한해 "G"를 전달하는 경우도 있다. 만일 우리가 이렇게 제한된 몇몇 format 문자를 지원하지 않는다면 닷넷 프레임워크 클래스 라이브러리에 내재되어 있는 자동 문자열 전환 규칙을 깨버리게 되므로 주의가 필요하다.

      언젠가는 사용자들이 우리가 개발한 타입에 대한 내용을 확인하려 할 것이고, 이때 문자열을 통해 확인할 수 있도록 하는 것이 가장 간단한 방법이다. 모든 타입에 대해서 ToString()을 overriding하자.

 

 

6. value 타입과 reference 타입을 구분하라.#

      두 가지 타입 중 어떤 것이 더 좋다는 식으로 말하기는 매우 어렵다. 따라서 우리가 정의하는 타입이 어떤 식으로 동작하길 바라는가에 맞추어 적절한 선택을 해야만 한다. value 타입은 다형적이지 않기 때문에 우리의 애플리케이션이 직접 다루는 값을 저장하기에 알맞다. 반면 reference 타입은 다형적일 수 있으며, 애플리케이션의 동작을 정의하는 데 주로 쓰인다. 새롭게 작성하는 타입이 어떤 식으로 동작해야 하는지를 충분히 고려해서 타입의 형태를 결정지어야 한다. 구조체는 값을 저장하기에 알맞고 클래스는 행동을 정의하는 데 알맞다.

      C#에서는 struct나 class 키워드를 이용하면 value 타입과 reference 타입 어떤 형태로도 타입을 정의할 수 있다. value 타입은 작고 단순한 타입에 사용되는 것이 좋고, reference 타입은 클래스간 상속이 필요한 경우 사용되는 것이 좋다.

      value 타입보다는 reference 타입을 더 많이 사용할 것이다. 만일 다음 질문에 모두 '예'라고 답변할 수 있는 경우에만 value 타입을 사용하는 편이 좋다.

      1. 타입이 단지 데이터를 저장하기 위해서만 사용되는가?

      2. 모든 public interface가 단지 내부적인 값을 획득하거나 수정하기 위해서만 사용되고 있는가?

      3. 타입은 앞으로 상속될 가능성이 전혀 없다고 확신할 수 있는가?

      4. 타입은 앞으로 다형적으로 다루어져야 할 필요가 없다고 확실할 수 있는가?

      value 타입을 단순히 저수준의 자료 저장소로만 생각하자. 행동을 정의해야 한다면 reference 타입을 사용하자. value 타입이 특정 객체 내에 자료저장을 위해서 사용될 경우 이 값이 객체 외부로 전달되더라도 복사본이 전달되기 때문에 좀 더 안정적이다. 또한 스택 메모리를 사용하기 때문에 메모리 사용의 효율이 좀 더 증대되고 애플리케이션의 로직을 생성하기 위한 기본적인 표준화 기법을 사용할 수 있다. 하지만 value 타입의 추후 동작방식에 대해서 조금이라도 의구심이 든다면 reference 타입을 사용하자.

 

7. immutable atomic value 타입이 더 좋다.#

      immutable 타입은 생성된 이후에는 마치 상수와 같이 변경될 수 없는 타입이다. 만일 우리가 immutable 타입의 생성자에서 인자의 유효성을 검증한다면 객체는 생성 시점으로부터 파괴시까지 항상 유효한 상태에 있다고 할 수 있다. 왜냐하면 이러한 객체는 한번 생성되고 나면 객체의 내부상태를 유효하지 않도록 변경할 수 없기 때문이다. 객체의 상태변경이 원천적으로 금지되어 있기 때문에 사용자가 유효하지 않은 값으로 객체의 상태를 변경하는 등의 오류를 막기 위해서 필요했던 다양한 오류확인 루틴이 추가적으로 필요하지 않다. immutable 타입은 또한 본질적으로 멀티쓰레드에 대해서 안전하게 동작한다. 다수의 쓰레드가 동시에 접근하더라도 항상 동일한 내용에 접근하게 된다.

      모든 타입을 immutable 타입으로 구성하는 것은 좋지 않다. 만일 그렇게 하면 애플리케이션의 상태를 변경하기 위해서 항상 새로운 상태를 가진 객체를 만들어야 한다. 그렇기 때문에 immutable 타입은 단지 atomic value 타입에 대해서만 분리하여 적용하는 것이 좋다.

      immutable 타입은 코드를 좀 더 간단하고 유지하기 쉽게 만든다. 타입내에 아무런 의미없이 프로퍼티에 대한 get/set accessor를 만들지 말자. 타입을 생성하는 초기에는 항상 타입이 immutable 타입이 될 수 있을지에 대해서 고려해 보아야 한다.

 

8. value 타입을 사용할 때 0이라는 값이 의미를 가질 수 있도록하라.#

      기본적으로 닷넷 시스템은 value 타입으로 인스턴스를 만들 때 '0'으로 값을 초기화한다. 우리는 이러한 초기화를 막을 방법이 없다. 따라서 가능하다면 '0'이라는 값을 의미있는 유효한 값으로 활용하는 것이 좋다. 하지만 예외적으로 enum과 같은 자료형에서는 '0'을 의미있는 값으로 활용하는 것이 좋지 않을 수 있다.

      enum과 같은 자료형에서는 의미있는 값으로 '0'을 사용하지 말자.

  1.   public enum Planet

  2.   {

  3.  // 명시적으로 값 할당 지정하지 않으면 기본 시작 값은 0

  4.  Mercury = 1,

  5.  Venus = 2,

  6.  Earth =3,

  7.  ...

  8. }

  9. public struct Observationdata

  10. {

  11. Planet _whichPlanet;   // 어떤 별을 찾고 있는가?

  12. Double _magnitude;      // 별의 등급

  13.  }

  14.  ObservationData d = new ObservationData();

      개발자는 ObservationData 객체를 만들기 위해 위와 같은 코드를 쓸 수 있는데, 이 경우 _whichPlanet과 _magnitude는 모두 '0'의 값을 가지게 된다. _whichPlanet은 Planet이라는 enum 타입이므로 '0'이라는 값이 적절하지 않지만 _magnitude의 '0'은 충분히 유효한 값이라 할 수 있다. 이러한 일관되지 않은 값의 초기화를 방지하기 위해서는 Planet 타입에 '0'이라는 값의 의미를 부여화되 '다른 어떤 유효한 값도 가지고 있지 않음'의 의미로 활용하자.

  1. public enum Planet

  2. {

  3. None = 0;

  4. Mercury = 1,

  5. Venus = 2,

  6. ...

  7. }

      닷넷 시스템은 value 타입의 인스턴스를 항상 '0'으로 초기화한다. 사용자가 value 타입을 활용함에 있어 이러한 초기화 자체를 명시적으로 막을 수 있는 방법은 없다. 따라서 가능한 한 '0'이라는 값을 유효한 범주로 포함시키고, '다른 어떤 유효한 값도 가지지 않음을 의미'를 유지할 수 있도록 하자.

 

9. ReferenceEquals(), static Equals(), instance Equals(),operator==의 상호 연관성을 이해하라.#

      새로운 타입을 만드는 경우(클래스 혹은 구조체로) 동일성의 의미를 정확히 규정하는 것은 매우 중요하다. C#은 두 개의 객체가 '동일한가'를 확인하기 위해서 서로 다른 4가지의 메소드를 제공한다.

  1. public static bool ReferenceEquals( object left, object right )

  2. pubilc static bool Equals( object left, object right )

  3. public virtual bool Equals( object right )

  4. public static bool operator==( Myclass left, Myclass right )

      개발자들은 얼마든지 위의 4개의 메소드 각각에 대해서 자신만의 메소드를 재정의할 수 있다. 하지만 동일성의 의미를 규정하기 위해서 4개의 메소드를 모두 재작성해야 하는 것은 아니다. 사실 위의 4개의 메소드들 중 앞쪽 2개의 static 메소드는 절대 재작성하면 안 된다.

      Object.ReferenceEquals()의 메소드 구현부는 두 개의 변수가 담고 있는 객체의 참조자를 비교하는 역할을 이미 정확히 구현하고 있기 때문에 절대로 Object.ReferenceEquals()를 재정의해서도 안 되고 할 필요도 없다. static Equals()는 Object.ReferenceEquals()와 마찬가지로 런타임에 형식을 알 수 없는 두 개의 객체들의 동일성 여부를 확인하는 역할을 정확히 구현하고 있기 때문에 절대로 재정의 해서는 안 된다.

      Reference 타입의 인스턴스형 Object.Equals()의 기본동작은 Object.ReferenceEquals()와 완전히 동일하다. 그런데 value 타입의 경우에는 그 동작방식이 다른데, 그 이유는 모든 value 타입의 공통 상위 타입인 System.ValueType이 Object 타입의 인스턴스형 Equals()의 기본 동작을 재정의하고 있기 때문이다. System.ValueType의 인스턴스형 Equals()의 구현부는 주어진 두 변수의 타입이 같고, 그 내용이 일치할 경우에 한해서만 true를 반환하도록 구현되어 있다. 불행히도 이러한 구현부는 그다지 효율적으로 작성되지 못했다. 그 이유는 System.ValueType을 모든 value 타입이 상속하고 있고, Ssytem.ValueType을 상속하는 모든 value 타입에 대해서 비교 연산이 제대로 동작되도록 하기 위해서 reflection을 이용하였기 때문이다. reflection을 자주 사용하게 되면 프로그램의 수행성능에 매우 좋지 않은 영향을 미치게 된다. value 타입을 만들 때는 항상 인스턴스형 Equals()를 재정의하고, Reference 타입을 만들 때는 System.Object에 정의되어 있는 Equals()의 동작방식을 따르고 싶지 않을 경우에 한해서만 재정의를 수행하자는 것이다. Equals() 메소드를 재정의할 때는 항상 이러한 규칙을 따르도록 하자. 사실 Equals() 메소드를 재정의하려면 추가적으로 GetHashCode()라는 메소드도 같이 구현해야 한다.

      만일 value 타입을 정의하고 있는 경우라면 operator==()을 반드시 재정의해야 한다. 그 이유는 value 타입에 대해서 인스턴스형 Equals()메소드를 재정희해야 하는 이유와 마찬가지로 기본적인 구현부가 성능상의 문제를 야기할 수 있기 때문이다. 인스턴스형 Equals() 메소드를 재정의할 경우 항상 operator==()까지 재정의해야 하는 것은 아니다. 정확하게 말하면 value 타입의 Equals() 메소드를 재정의하는 경우에만 operator==()을 재정의하는 것이 좋다. 그리고 Reference 타입의 경우에는 operator==()를 재정의해야 하는 경우는 거의 없다. 실제로 닷넷 프레임워크의 클래스들은 reference 타입에 대해서는 operator==() 메소드가 값이 아닌 단순 객체 참조자 비교만을 수행하기를 기대한다.

 

 

10. GetHashCode()의 함정에 유의하라.#

      단 한가지 경우를 제외하고 GetHashCode() 메소드는 재정의하지 않는 것이 좋다. 단 한가지의 경우란 해시테이블이나 Dictionary와 같이 해시 코드를 기반으로 하는 collection에 Key로 활용할 타입을 정의하는 경우다. 이러한 collection들은 해시 코드를 획득하기 위해서 GetHashCode() 메소드를 이용하게 되는데, 이 메소드의 기본 구현 내용은 많은 문제를 내포하고 있다. 그나마 Reference 타입에 대해서는 비효율적이긴 하지만 동작은 한다. value 타입에 대해서는 immutable 타입에 대해서만 정상적으로 동작한다. 물론 이때에도 매우 비효율적으로 동작한다.

GetHashCode()를 재정의하기 위해서는 최소한 아래의 3가지 규칙을 준수해야 한다.

       만일 두 개의 객체가 동일하다면(operator==에 의해서 정의된 동일 여부) 두 객체는 동일한 해시 코드를 생성해야 한다. 하지만 해시 코드를 이용하여 특정 컨테이너 내에서 다수의 객체들을 찾아내는 데 사용할 수 없다.

       특정 객체 a에 대해서 a.GetHashCode()의 반환값은 객체의 인스턴스가 생성된 이후에는 변하지 않아야 한다. 어떠한 시점에 a 객체의 GetHashCode()를 호출하더라도 반환되는 값은 항상 동일해야 한다. 해시 기반 컨테이너 내에서는 객체를 찾을 때 해시 코드를 이용하여 저장 공간을 검색하기 때문에 이 값이 변경되면 객체가 저장된 올바른 저장공간을 찾지 못할 수 있다.

       해시 함수는 모든 입력 값에 대해서 integer의 표현범위 내에서 골고루 잘 분산되어야 한다. 이러한 특성이 해시기반 컨테이너의 수행성능에 영향을 미친다.

     

      좀 더 나은 해시 코드를 생성하는 코드를 작성하려면 새롭게 작성하는 타입에 몇 가지 제약을 가해야만 한다. 세 가지 규칙 각각에 대해서 다시 한번 그 내용을 검토해 보면서 GetHashCode()를 어떻게 구현해야 할지에 대해서 살펴보자.

      첫 번째 규칙은 두 개의 객체가 같다면(operator==에 의해서 정의된) 동일한 해시 코드를 반환해야 한다는 것이다. 동일여부를 확인하기 위해서 사용했던 다양한 프로퍼티나 데이트들 모두는 해시 코드를 생성할 때에도 사용되어야만 한다. 누락된 프로퍼티나 데이터가 있을 경우 종종 세 번째 규칙을 위반하게 된다. 해시 코드 생성과 동일성 비교에 있어서 언제나 같은 요소들을 기반으로 하는 것이 좋다.

      두 번째 규칙은 객체 생성 이후 GetHashCode()는 항상 동일한 값을 반환해야 한다는 규칙이다. 두 번째 규칙을 지키기 위한 유일한 방법은 타입 내의 불변의 값을 기반으로 해시 코드를 생성하는 해시 함수를 구성하는 것이다. 이러한 방식은 System.Object의 GetHashCode() 메소드내에서 볼 수 있는데, 이 메소드의 기본구현은 객체 생성시에 생성되는 불변의 고유한 값을 반환하도록 구성되어 있다. System.ValueType의 기본 구현이 이런 규칙을 지키도록 하려면 첫 번째 필드의 값이 변경되지 말아야 한다. 사실 두 번째 규칙을 준수하기 위해서는 value 타입을 immutable 타입으로 구성하는 것보다 더 좋은 방법은 없다. 해시 기반 컨테이너에 key로 사용할 value 타입을 구성하고자 한다면 반드시 immutable 타입으로 구현하여야 한다. 해시 코드를 생성하는 데 사용할 모든 멤버들은 반드시 immutable 타입이어야 한다.

      세 번째 규칙은 GetHashCode() 메소드가 모든 입력값에 대해서 integer의 표현범위 내에서 골고루 잘 분산되어야 한다는 규칙이다. 이 규칙은 타입을 어떻게 구현하느냐에 달려있다. 가장 일반적이면서 비교적 양호한 알고리즘은 우리가 구현할 타입내의 모든 필드의 해시 코드를 이용하여 XOR 연산을 수행하는 것이다. 물론 이 과정에서 immutable 타입이 아닌 필드들은 연산에서 배제되어야 한다.

 

 

11. foreach 루프가 더 좋다.#

C#의 foreach문은 do, while, for를 이용한 루프보다 좀 더 다양하게 쓰일 수 있으며 어떠한 collection에 대해서도 최고의 순회 코드를 만들어 낼 수 있다. 이 구문은 닷넷 프레임워크 내의 collection interface와 매우 밀접하게 연관되어 있으며 C#의 컴파일러는 서로 다른 collection타입을 사용할 경우라도 항상 최적의 코드를 생성한다. collection에 대한 순회를 수행할 때는 다른 루프 구문을 사용하지 말고 항상 foreach를 사용하자. 다음의 세 개의 예제를 확인해 보자.

  1. int[] foo = new int[100];

  2.  

    // 루프 1

  3. foreach( int i in foo )

  4. Console.WriteLine( i.ToString() );

  5.  

  6. // 루프 2

  7. for( int index = 0 ; index < foo.Length ; index++ )

  8. Console.WriteLine( foo[index].ToString() );

  9.  

  10. // 루프 3

  11. int len = foo.Length;

  12. for( int index = 0 ; index < len ; index++ )

  13. Console.WriteLine( foo[index].ToString() );

최소한 닷넷 프레임워크 1.1 이상에 포함된 모든 C# 컴파일러에 대해서는 루프 1번이 가장 짧은 코드를 가지고 있고 가장 빠른 속도를 보인다. 대부분의 C/C++ 개발자들은 루프 3번이 가장 효율적일 것이라고 생각하겠지만, 실제로는 위의 예제 중 가장 좋지 않은 방법이다. Length 프로퍼티 루프 바깥쪽으로 빼버리면 JIT 컴파일러가 루프 내부에서 배열의 범위를 확인하는 코드를 제거할 수 있는 기회를 잃어버리게 된다.

C# 코드는 안전하게 관리되는 환경하에서 수행된다. 배열의 인덱스를 포함하여 모든 메모리 위치 정보는 사전에 검사되어야 한다. 이 때문에 루프 3은 다음과 같은 구조로 컴파일 된다.

  1. // 루프 3, 컴파일러에 의해 생성된 코드

    int len = foo.Length;

    for( int index = 0 ; index < len ; index++ )

    {

    if( index < foo.Length )

    Console.WriteLine( foo[index].ToString() );

    else

    throw new IndexOutOfRangeException();

    }

JIT C# 컴파일러는 Length 프로퍼티를 루프 바깥쪽으로 이동시킨 이런 코드들을 좋아하지 않을 뿐더러, 더 느린 코드를 만들어내게 된다. CLR은 변수에 의해서 점유되지 않은 메모리 공간을 우리가 직접적으로 사용하지 못하도록 한다. 이를 위해 배열의 각 요소에 접근하기 이전에 실제적인 배열의 범위를 확인(len 변수를 확인하는 것이 아니라)하는 위와 같은 코드를 추가한다. 따라서 루프의 조건검사와 함께 중복적인 범위검사를 수행하게 된다. 루프 1번과 루프 2번이 더 빠른 이유는 C# 컴파일러와 JIT 컴파일러가 최소한 해당 루프 내에서는 배열의 실제 범위를 넘어서는 접근이 이루어질 수 없음을 보장하기 때문이다. 이 경우 배열의 범위를 재확인하는 코드는 생략된다. 하지만 3번의 경우처럼 루프변수가 배열의 길이와 다른 경우라면 각각의 순회시마다 범위검사가 일어난다.

어떤 형태로 루프를 구성하는 것이 가장 효율적인가에 대해서 고민하지 말자. foreach를 사용하면 컴파일러는 항상 최적의 코드를 만들어낸다.

foreach 구문은 배열의 최소 및 최대 범위를 어떻게 확인해야 하는지 알고 있기 때문에 우리는 더 이상 이에 대해서 신경 쓸 필요가 없다. 또한 직접 for 루프를 쓰는 것에 비해서 좀 더 빠르게 코드를 만들 수 있을 뿐만 아니라 사람들이 배열의 범위를 어떻게 변경하여 사용하는지에 대해서도 고려할 필요가 없다.

foreach 구문은 언어적인 관점에서 다른 이점도 가져다 준다. foreach의 루프 변수는 항상 읽기전용이므로 collection 내의 객체를 변경할 수 없다. 또한 항상 올바른 타입으로 형변환을 수행한다. 만일 collection이 적절하지 않은 타입의 객체를 포함하고 있었다면, 순회과정에서 예외를 유발시킬 것이다.

foreach 구문은 다차원 배열에 대해서도 유사한 이점을 가져다 준다. 체스판을 구현하려 한다고 해보자. 아마도 다음과 같은 코드를 작성할 수 있다.

  1. private Square[,] _theBoard = new Square[8,8];

  2.  

    // 프로그램의 다른 부분

  3. for(int i = 0 ; i < _theBoard.GetLength(0) ; i++ )

  4. for( int j = 0 ; j < _theBoard.GetLength(1) ; j++ )

  5. _theBoard[i,j].PaintSquare();

  6. // 위 코드 대신 좀 더 간단히 코드를 쓸 수 있다.

  7. foreach( Square sq in _theBoard )

  8. sq.PaintSquare();

foreach 구문은 어떠한 차원의 배열에 대해서도 좀 더 나은 코드를 만들어낸다. 만일 우리가 미래에 3차원 체스판을 만들어야 한다 할지라도 foreach 루프는 수정없이도 여전히 잘 동작한다. 다차원 배열에서 각 차원이 서로 다른 범위를 가진다 하더라도 foreach 루프는 여전히 잘 동작한다.

만일 우리가 새로운 collection을 만들 때 닷넷 프레임워크 환경에서 collection이 준수해야 하는 일반적인 규칙을 따르고 있다면, 그러한 collection에 대해서도 foreach 구문을 사용할 수 있다. foreach 구문을 특정 타입과 함께 사용할 때에는 해당 타입이 collection으로 다루어져야 함을 명시해야 한다. IEnumerable interface의 GetEnumerator()가 바로 collection 타입을 만들기 위한 것이며, GetEnumerator()가 반환하는 IEnumerator interface가 바로 foreach 구문과 함께 쓰여지기 위해서 반드시 필요한 interface이다. 



Chapter 02 – 닷넷 리소스 관리#


12. 할당문보다는 변수 초기화를 사용하는 편이 더 좋다.#

C# 에서는 멤버변수의 선언시에 초기화를 병행하는 것이 매우 자연스럽게 이루어질 수 있다.

  1.  public class MyClass

  2.  {

  3.  // collection을 선언하고, 초기화 한다.

  4.  private ArrayList _coll = new ArrayList();

    }

      MyClass에 몇 개의 생성자가 있는지와 상관없이 _coll은 적절한 시점에 초기화될 수 있다. 컴파일러는 모든 생성자의 시작위치에 인스턴스 변수에 대한 초기화를 수행하는 코드를 추가한다. 새로운 생성자를 추가하더라도 _coll의 초기화는 항상 보장된다.

      선언시 지정한 초기화 루틴은 생성자의 본문보다 앞서 수행된다. 변수의 초기화는 초기화되지 않은 객체의 사용과 같은 부적절한 동작을 피할 수 있는 가장 간단한 방법이기도 하다. 하지만 변수의 초기화가 항상 완벽하지만은 않다. 다음의 3가지의 경우 초기화 구문을 사용하지 않는 편이 더 낫다.

      첫째로 변수를 null이나 0으로 설정하는 경우다. 이러한 동작은 시스템의 매우 낮은 레벨에서 일련의 메모리 블록에 대해서 한번에 이루어진다. C# 컴파일러는 모든 메모리를 0로 바꾸는 코드를 추가해야 하는 의무가 있으므로 추가적으로 0이나 null로 변수를 초기화하는 것은 의미가 없다.

      두 번째로 동일한 객체에 대해서 여러 번의 초기화를 수행하는 경우에 비효율성을 초래하게 된다. 모든 생성자에서 동일한 형태로 변수값을 할당하는 경우에만 초기화 루틴을 사용하는 것이 좋다. 아래의 MyClass 예제는 어떠한 생성자가 호출되었느냐에 따라 서로 다른 방식으로 ArrayList 객체를 초기화하게 된다.

  1. public class MyClass

  2. {

  3. // collection을 선언하고, 초기화 한다.

  4. private ArrayList _coll = new ArrayList();

  5.  

    MyClass() { }

  6.  

  7. MyClass(int size)

  8. {

  9. _coll = new ArrayList(size);

  10. }

  11. }

      MyClass 객체를 생성할 때 collection의 크기를 인자로 전달하게 되면, 두 개의 ArrayList 객체가 생성된다. 선언시 초기화는 모든 생성자보다 앞서 수행되기 때문에 최초 생성된 ArrayList 객체는 바로 garbage가 되어버린다. 다음으로 생성자 내에서 ArrayList 객체를 다시 생성하게 된다. 컴파일러는 다음과 같은 코드를 내부적으로 생성할 것이다.

  1. public class MyClass

  2. {

  3. // collection을 선언하고, 초기화 한다.

  4. private ArrayList _coll = new ArrayList();

  5. MyClass()

  6. {

  7. _coll = new ArrayList();

  8. }

  9. MyClass(int size)

  10. {

  11. _coll = new ArrayList();

  12. _coll = new ArrayList(size);

  13. }

  14. }

      초기화를 사용하지 않고 생성자에서 할당을 수해하는 것이 좋은 마지막 경우는 예외처리를 해야 하는 경우다. 초기화를 수행하면 try 블록을 사용하여 예외를 잡아낼 수 없기 때문에 발생된 예외는 객체 외부로 전파된다. 이렇게 되면 객체 내부에서 예외 처리루틴을 이용하여 복원을 시도할 방법이 없어지게 된다.

      선언시에 변수의 초기화는 어떤 생성자가 호출될지의 여부와 상관없이 반드시 초기화를 수행할 수 있는 가장 간단한 방법이다. 이러한 초기화 방법은 어떠한 생성자보다 먼저 수행된다. 모든 생성자에서 멤버변수에 대한 할당이 동일하게 이루어진다면 선언시에 변수를 초기화하는 방법을 사용하자. 이렇게 하면 코드를 읽기도 쉽고 관리하기도 편하다.

 

13. static 클래스 멤버는 static 생성자를 사용하여 초기화하라.#

      C#에서 static 멤버변수에 대한 초기화를 수행하는 방법에는 선언시 초기화를 사용하는 방법과 static 생성자를 사용하는 방법이 있다. static 생성자는 접근할 클래스의 어떤 메서드, 변수, 프로퍼티보다 먼저 수행되는 특별한 생성자이다. static 변수를 인스턴스형 생성자 내에서 초기화한다던가 초기화만을 위한 특별한 private 메서드를 이용하거나 혹은 특수한 초기화 구문 등은 가능한 사용하지 않는 것이 좋다.

      static 생자를 사용하는 방법과 함께 선언시 초기화 방법을 사용하는 것도 좋은 대안이 될 수 있다. 만일 static 멤버변수에 대한 단순한 초기화를 수행하려 한다면 선언시 초기화 방법을 사용하자. 하지만 좀 더 복잡한 구현부가 필요한 초기화를 수행하려 한다면 static 생성자를 사용하자.

      선언시 초기화 구문은 static 생성자보다 먼저 수행된다. 물론 상위 클래스의 static 생성자보다도 먼저 수행된다.

      CLR은 애플리케이션 공간에 타입을 최초로 로드하는 시점에 static 생성자를 자동으로 호출한다. static 생성자는 타입별로 하나만 존재할 수 있다. static 생성자에서 외부로 전이되도록 허용해 둔다면 CLR은 프로그램을 종료할 것이다. 따라서 static 멤버의 초기화시에 예외가 발생할 가능성이 있다면, 선언시 초기화대신 static 생성자 구문을 사용하여 예외가 외부로 전이되지 않도록 다룰 수 있다. 선언시 초기화 방법을 사용하면 try/catch 문을 이용하여 예외를 잡아둘 방법이 없다.

 

 

14. 연쇄적인 생성자 호출을 이용하라.#

     C# 컴파일러는 생성자에서 또다른 생성자를 호출하는 구조를 특수한 문법의 일부로 인지하고, 변수에 대한 초기화나 기반 클래스의 생성자 호출 등의 중복 코드를 제거한다. 이런 방법을 통해 객체의 초기화를 위해 필요한 최소한의 코드만이 수행될 수 있도록 최적화를 수행한다. 뿐만 아니라 우리가 코딩해야 하는 코드의 양도 최소화할 수 있다.

      다음의 구현 예를 보자.

  1. public class MyClass

  2. {

  3. private ArrayList _coll;

  4. private string _name;

  5.  

    public MyClass() : this(0,"") { }

  6. public MyClass(int initialCount) : this(initialCount, "") { }

  7. public MyClass(int initialCount, string name)

  8. {

  9. _coll = (initialCount > 0 ) ? new ArrayList(initialCount) : new ArrayList();

  10. _name = name;

  11. }

  12. }

      C++와 같은 언어에서는 위의 예제와 같이 구현하지 않더라도, 기본 인자값을 가진 메서드를 정의하여 손쉽게 해결할 수 있는 방법이 있지만 C#은 기본 인자값을 가진 메서드를 언어차원에서 문법적으로 제공하지 않기 때문에, 서로 다른 인자의 개수를 가진 생성자를 각각 정의해 주어야만 한다. 하지만 그렇다 하더라도 아래의 코드와 같이 중복코드를 포함하는 독립된 메서드를 정의하여 생성자 내에서 호출하도록 코드를 구현하는 것은 좋지 않은 방법이다. 이 경우 일부 비효율성이 발생할 소지가 있으므로 가능한 생성자를 연쇄적으로 호출하는 구조로 코드를 작성하는 것이 좋다.

  1. public class MyClass

  2. {

  3. private ArrayList _coll;

  4. private string _name;

  5.  

    public MyClass()

  6. {

  7. commonConstructor(0,"");

  8. }

  9. public MyClass(int initialCount)

  10. {

  11. commonConstructor(initialCount, "");

  12. }

  13. public MyClass(int initialCount, string name)

  14. {

  15. commonConstructor(initialCount, name);

  16. }

  17. private void commonConstructor(int initialCount, string name)

  18. {

  19. _coll = (initialCount > 0 ) ? new ArrayList(initailCount) : new ArrayList();

  20. _name = name;

  21. }

      위 예제는 이전 예제와 완전히 동일하게 동작하지만 조금 더 비효율적이다. 컴파일러는 컴파일시에 생성자에 몇몇 추가적인 기능을 수행하는 코드를 추가하게 된다. 이러한 코드의 예로는 선언시 초기화 구문을 사용한 변수에 대한 초기화 루틴이나 상위 클래스의 생성자를 호출하는 루틴 등이 있을 수 있다. 만일 위 예제처럼 특별한 메서드를 정의하게 되면, 컴파일러는 컴파일러가 추가하는 코드들이 중복되는 것을 제거할 방법을 잃게 된다. 위의 예제에 대하여 컴파일러가 생성하는 IL 코드는 다음의 코드처럼 컴파일된다.

  1. // 이 코드는 IL이 생성한 코드를 설명하기 위한 내용이므로 컴파일 되지는 않는다.

  2. public class MyClass

  3. {

  4. private ArrayList _coll;

  5. private string _name;

  6.  

    public MyClass()

  7. {

  8. // 객체 인스턴스 초기화는 여기에 배치된다.

  9. object();   // 이해를 돕기 위한 코드이다. 컴파일 되지 않는다.

  10. commonConstructor(0,"");

  11. }

  12. public MyClass(int initialCount)

  13. {

  14. object();

  15. commonConstructor(initailCount, "");

  16. }

  17. public MyClass(int initialCount, string name)

  18. {

  19. object();

  20. commonConstructor(initialCount, name);

  21. }

  22. private void commonConstructor(int initialCount, string name)

  23. {

  24. _coll = (initialCount > 0 ) ? new ArrayList(initialCount) : new ArrayList();

  25. _name = name;

  26. }

  27. }

      만일 연쇄적인 생성자 호출방식을 사용하는 첫 번째 예제를 컴파일하게 되면 컴파일러는 기반 클래스의 생성자를 호출하는 코드를 중복적으로 생성하지 않으며, 변수에 대한 초기화 코드도 생성하지 않는다. 연쇄 생성자 호출방식을 사용하면 가장 먼저 호출된 생성자에서만 기반 클래스의 생성자를 호출한다. 생성자 연쇄 호출방식을 사용할 때는 this()을 이용한다. 기반 클래스의 생성자를 호출하려면 base() 구문을 이용하면 된다. 하지만 두 가지를 동시에 사용할 수는 없다.

      다음에 타입의 인스턴스 과정에 수반되는 내부 동작들을 순서대로 나열하였다.

          1. static 변수를 0으로 설정한다.

          2. static 변수에 대하여 선언시 초기화 루틴을 수행한다.

          3. 기반 클래스의 static 생성자를 수행한다.

          4. static 생성자를 호출한다.

          5. 인스턴스 변수를 0으로 설정한다.

          6. 인스턴스 변수에 대하여 선언시 초기화 루틴을 수행한다.

          7. 기반 클래스의 인스턴스 생성자를 수행한다.

          8. 인스턴스 생성자를 호출한다.

      static 변수의 초기화와 관련된 단계는 단일 타입에 대해서 반복 수행될 필요가 없으므로, 동일 타입의 다른 인스턴스가 추가로 생성되는 경우에는 위의 5번 항목부터 재수행된다. 또한 6번과 7번 단계는 연쇄 생성자 호출방식을 사용하는 경우, 컴파일러가 중복을 제거하여 최적화를 수행한다.

     C# 컴파일러는 객체가 생성될 때 객체의 모든 요소들이 초기화될 것임을 보장한다. 그러므로 아무런 추가 동작 없이도 최소한 객체가 점유하고 있는 메모리 공간은 모두 0으로 초기화될 것이고, 이는 static 멤버와 인스턴스 멤버를 구분하지 않는다. 이렇게 자세히 초기화 과정을 알아보는 진짜 이유는 객체를 구성하는 각 멤버들을 우리가 원하는 방법으로 유일하게 한 번씩만 초기화를 수행하기 위해서이다. 이렇게 함으로써 중복적으로 초기화가 일어나는 것을 방지할 수 있다.

 

 

15. 자원해제를 위해서 using try/finally를 이용하라.#

      Unmanaged 자원들을 사용하는 타입들은 IDisposable interface의 Dispose() 메서드를 호출하여 명시적으로 자원을 해제해 주어야 한다. 닷넷 환경에서의 이러한 책임은 타입 자체나 시스템의 책임이 아니라 타입을 사용하는 사용자의 책임이다. 그러므로 Dispose() 메서드를 구현해둔 타입을 사용할 때에는 항상 자원의 해제를 위해서 Dispose()가 호출될 수 있도록 주의 해야 한다. Dispose()가 항상 호출될 수 있도록 코드를 작성하는 가장 쉬운 방법은 using 문장과 try/finally 블록을 사용하는 것이다.

      C# 컴파일러는 using 구문을 컴파일할 때 내부적으로 try/finally 블록을 추가하여 Dispose()가 반드시 호출될 수 있도록 코드를 생성해 준다. IDisposable interface를 구현한 객체의 Dispose() 메서드가 호출하도록 하는 가정 간단한 방법은 using 구문을 사용하는 방법이다. 다음 예제의 using문을 사용한 코드는 try/finally로 구현한 코드와 동일한 IL 코드를 생성하게 된다.

  1. SqlConnection myConnection = null;

  2.  

  3. // using 사용 예

  4. using ( myConnection = new SqlConnection(connString) )

  5. {

  6. myConnection.Open();

    }

  7.  

  8. // try/finally 블록의 사용 예

  9. try

  10. {

  11. myConnection = new SqlConnection(connString);

  12. }

  13. finally

  14. {

  15. myConnection.Dispose();

  16. }

만일 IDisposable interface를 구현하지 않은 타입을 using 구문과 함께 사용하면 C# 컴파일러는 컴파일 오류를 발생시킨다.

      IDisposable interface를 구현한 객체를 단지 한 개만을 쓰고 있는 경우라면 using 구문으로 둘러싸는 정도로도 충분하다. 이제 좀 더 복잡한 경우를 살펴보자. 첫 번째 예제에서는 myConnection과 mySqlCommand 두 개의 객체에 대해서 Dispose() 메서드가 호출되어야 한다. 두 개의 객체에 대해서 각기 using 구문을 사용하면, 각각 try/finally 블록을 독립적으로 생성하게 된다.

      IDisposable interface를 구현한 다수의 객체를 사용하는 경우 using 구문을 사용하는 것은 보기 흉한 코드를 만들어낸다. 이럴 때는 try/finally 구문을 우리가 직접 구현하는 것이 좀 더 나을 때가 있다.

  1. public void ExecuteCommand(string connString, string commandString)

  2. {

  3. SqlConnection myConnection = null;

  4. SqlCommand mySqlCommand = null;

  5.  

  6. try

  7. {

  8. myConnection = new SqlConnection(connString);

  9. mySqlCommand = new SqlCommand(commandString, myConnection);

  10. myConnection.Open();

  11. mySqlCommand.ExecuteNonQuery();

  12. }

  13. finally

  14. {

  15. if ( mySqlCommand != null )

  16. mySqlCommand.Dispose();

  17. if ( myConnection != null )

  18. myConnection.Dispose();

  19. }

  20. }

      지금까지 알아본 바와 같이 두 가지의 경우를 명백하게 구분해서 적절한 코드를 작성해야 한다. 만일 하나의 객체만을 사용하고 있는 경우라면 단순히 using 키워드를 사용하는 것이 자원의 해제를 자동화하는 가장 좋은 방법이다. 만일 다수의 객체에 대해서 Dispose() 메서드가 호출되어야 하는 경우라면, using 문장을 반복 배치하거나 try/finally 블록을 직접 구현하는 것이 좋다.

      IDisposable interface를 구현한 객체들 사이에는 자원을 해제하는 과정에서 미묘한 차이점을 보이는 경우가 있다. 몇몇 타입은 자원해제를 위해 Dispose() 메서드와 함께 Close() 메서드도 같이 제공한다. SqlConnection 타입이 이러한 타입 중의 하나이다. Dispose() 메서드는 자원의 해제 이상의 일을 담당할 수 있다. 실제로 SqlConnection 객체의 Dispose()를 호출하게 되면, 객체는 Garbage Collector에게 이 객체는 더 이상 종료를 위해서 finalizer를 호출할 필요가 없다는 것을 알려주기 위해서 GC.SuppressFinalize()를 호출한다. 하지만 Close() 메서드는 그러한 역할을 수행하지 않는다. 결국 객체는 finalization이 필요하지 않음에도 finalization queue에 남게 된다. 만일 둘 중 하나를 선택해야 하는 상황이라면, Close()보다 Dispose()를 호출하는 것이 더 좋다.

      Dispose()는 메모리로부터 객체를 제거하지 않는다. 이것은 단지 unmanaged 자원을 해제하는 것을 도와주는 시기를 제공해 줄 뿐이다. 그렇기 때문에 만일 다른 부분에서 참조되고 있는 경우라면, Dispose()를 호출했을 때 문제가 발생할 수 있다. 따라서 프로그램 내의 다른 부분에서 참조되고 있는 객체의 경우는 절대로 Dispose()를 호출해서는 안 된다.

 

 

16. Garbage를 최소화하라.#

      Garbage Collector는 우리를 대신하여 메모리 관리를 훌륭하게 수행해 준다. 매우 효율적인 방법으로 더 이상 사용되지 않는 객체를 삭제하지만 그 어떠한 방법을 쓴다 해도 힙을 기반으로 하는 객체를 생성하고 파괴하는 동작은 하지 않는 것에 비한다면 여전히 많은 프로세서 시간을 소비한다. 이번에는 필요하지 않은 객체를 생성함으로써 발생할 수 있는 애플리케이션의 심각한 성능문제와 생성객체의 최소화 방법에 대해 소개하려고 한다.

      Garbage Collector를 너무 혹사시키지 말자. 다음의 몇 가지 방법을 이용하게 되면 Garbage Collector가 수행해야 하는 일의 양을 최소화할 수 있다. 지역변수를 포함하여 모든 reference 타입 객체는 힙 공간에 할당된다. 지역변수는 메소드를 빠져나오는 순간 즉시 garbage가 된다.

      reference 타입의 지역변수를 포함하고 있는 메서드가 반복적으로 자주 호출된다면 지역변수를 멤버변수로 변경하는 것이 좋다. 하지만 빈번하게 호출되는 메서드 내의 지역변수이거나, 반복적으로 호출되는 메서드 내의 지역변수에 대해서만 멤버변수로 변경을 수행해야 한다. 절대로 모든 지역변수를 멤버변수로 변경하지 마라.

      매우 일반적으로 사용되는 reference 타입 객체는 static 멤버변수로 변경하는 것이 좋다.

  1. private static Brush _blackBrush;

  2. public static Brush Black

  3. {

  4. get

  5. {

  6. if( _blackBrush == null )

  7. _blackBrush = new SolidBrush( Color.Black );

  8. return _blackBrush;

  9. }

  10. }

      위 코드를 보면 검정브러시가 처음으로 요청될 때 비로소 검정브러시가 생성됨을 알 수 있다. Brushes 객체는 단지 하나의 검정브러시에 대한 reference만을 유지하고 있으면서 사용자의 요청시마다 동일한 Brush 객체를 반환한다. 결국 하나의 검정브러시만을 생성하였고, 이 객체를 계속해서 재활용하게 된다. 또한 사용하지 않는 브러시는 결코 생성되지 않는다. 닷넷 프레임워크는 이러한 방식을 사용하여 생성해야 할 객체의 수를 최소화하면서도 사용자의 요구에 즉각적으로 응답할 수 있도록 구현되어 있다.

      프로그램이 필요한 객체의 생성을 최소화하기 위한 마지막 방법은 immutable 타입을 생성하기 위해서 builder 클래스를 제공하는 것이다. System.String 객체는 immutable 타입이므로 객체가 생성된 이후에는 그 내용을 변경할 수 없다. 따라서 string 객체가 포함하는 문자열의 내용을 수정하려고 시도하면 실제로는 새로운 객체가 생성되고 이전 객체는 garbage화 된다. StringBuilder는 immutable string 객체를 생성하기 위해서 문자열을 생성하거나 덧붙이고, 수정할 수 있는 편의를 제공하도록 구현된 mutable string builder 객체이다. 복잡한 string을 생성하기 위해서는 StringBuilder 객체를 쓰는 것이 좋다. immutalble 타입을 고려하고 있는 경우라면, 해당 타입의 객체를 생성하기 위하여 builder 클래스를 제공해야 할지의 여부를 추가적으로 고려해 보아야 한다. 이러한 builder 클래스는 사용자가 여러 단계를 거쳐 immutable 객체를 생성 할 수 있도록 도와준다.

      Garbage Collector는 애플리케이션이 사용하는 메모리를 효율적으로 관리해주는 역할을 수행한다. 하지만 힙에 객체를 생성하고 삭제하는 과정은 여전히 시간을 필요로 한다. 필요하지 않은 객체를 과도하게 생성하는 것을 피하라. 함수 내에 reference 타입의 지역변수를 반복적으로 생성하지 않도록 하기 위해서 멤버변수로의 변경을 고려하고, 공통으로 사용하는 객체에 대해서는 static 변수로 선언하자. 마지막으로 immutable 타입을 구성하고 있는 경우라면 mutalble builder 클래스를 제공할 것인지를 고려해 보자.

 

 

17. boxing unboxing을 최소화하라.#

      value 타입은 주로 값의 저장장소로 쓰이며 다형적이지 못한 타입이다. 반면에 닷넷 프레임워크는 모든 타입의 최상위 타입을 reference 타입인 System.Object로 정의하고 있다. 이 두 가지는 서로 양립될 수 없는 것처럼 보인다. 하지만 닷넷 프레임워크는 boxing과 unboxing이라는 방법을 통해 둘 사이의 차이점을 극복하고 있다. boxing이란 value 타입 객체를 타입이 구체적으로 정의되지 않은 reference 타입 객체 내부로 포함하여, reference 타입이 요구되는 경우에도 value 타입을 쓸 수 있도록 해주는 메커니즘을 말한다. unboxing 이란 boxing 되어 있는 value 타입 객체의 복사본을 획득하는 과정을 말한다. boxing과 unboxing은 value 타입을 System.Object 타입이 필요한 시점에 사용하기 위해서 반드시 필요하다. 하지만 boxing과 unboxing 동작은 항상 복사 과정을 수반하기 때문에, 수행성능에 좋지 않은 영향을 미친다.

      컴파일러는 System.Ojbect와 같이 reference 타입이 요구되는 곳에 value 타입을 사용한 경우나 특정 interface를 통하여 value 타입에 접근하려 할 때 boxing과 unboxing 코드를 생성한다. 이 과정에서 컴파일러는 어떠한 경고도 발생시키지 않는다. 실제로 다음과 같은 간단한 문장의 경우에도 여러 번의 boxing 과정을 유발한다.

  1. Console.WriteLine(" A few numbers: {0}, {1}, {2}", 25, 32, 50);

      Console.WriteLine()은 System.Object의 배열을 인자로 받는다. 정수들은 모두 value 타입이기 때문에 value 타입을 인자로 전달하기 위해서는 reference 타입으로 boxing을 수행해야만 한다. 왜냐하면 세 개의 정수값을 System.Object 타입으로 boxing을 수행하는 것이 이 메서드를 수행하는 유일한 방법이기 때문이다. 또 호출된 WriteLine() 메서드 내부에서는 새로이 생성된 reference 객체의 내부값에 접근하기 위해서 ToString() 메서드를 호출한다. 어떤 의미에서는 다음과 같은 동작을 수행하는 것과 같다.

  1. int i = 25;

  2. object o = i;   // box

  3. Console.WriteLine(o);

WriteLine() 내부에서는 다음과 같은 코드가 수행될 것이다.

  1. object o;

  2. int i = (int) o;   // unbox

  3. string output = i.ToString();

      위와 같은 코드를 우리가 직접 쓰지는 않겠지만 컴파일러는 value 타입을 System.Object 타입으로 변경하기 위해서 자동적으로 위와 같은 코드를 생성한다. 컴파일러는 value 타입을 System.Object 타입으로 변경해야 할 필요가 있는 모든 곳에 boxing과 unboxing 코드를 추가해 줄 것이다. 하지만 이에 따른 불이익은 우리의 몫이다.

      value 타입을 System.Object 타입으로 변경되도록 하는 또 다른 대표적인 예가 닷넷 프레임워크 1.x 의 collection과 value 타입을 같이 운용하는 경우이다. 이 버전의 collection은 System.Object의 reference 만을 저장할 수 있도록 작성되었다. 따라서 collection에 value 타입 객체를 저장할 때는 항상 boxing이 일어나고, collection에서 객체를 꺼내올 때에는 항상 unboxing, 즉 value 타입 객체에 대한 복사가 일어난다. boxing된 객체로부터 값을 얻어올 때에는 항상 복사가 일어난다.

      이러한 한계점들은 C# 2.0의 generics에 의해서 해결되었다. generic interface와 generic collection들은 앞서 알아본 다양한 문제점과 단점들을 해결할 수 있다.

 

 

18. 표준 Dispose 패턴을 구현하라.#

닷넷 프레임워크는 비 메모리 자원을 제거하기 위하여 표준화된 패턴을 사용하고 있다. 표준화된 패턴이란 IDisposable Interface를 구현하여 unmanaged 자원을 해제하고, 개발자들이 Dispose() 메서드의 명시적인 호출을 잊었을 경우를 대비하기 위해서 finalizer에도 적절한 해제 코드를 배치해 두는 것을 말한다. finalizer를 구현할 경우에는 Garbage Collector가 객체를 제거하는 데 있어 수행성능에 나쁜 영향을 주지만, 그럼에도 이러한 방식이 unmanaged 자원을 다루는 가장 유효한 방법이다.

만일 unmanaged 자원을 가지고 있는 클래스들이 상속관계에 있다면 최상위에 있는 클래스는 반드시 자원 해제를 위해서 IDisposable interface를 구현해야 하고 방어적인 코드를 위해 finalizer도 동시에 구현해야만 한다. Dispose()와 finalizer는 자원의 해제를 위해서 특정 virtual 메서드에 자원해제를 요청하도록 구현하는 것이 좋다. 하위 클래스들은 자신만의 자원관리가 필요할 경우 상위 클래스의 virtual 메서드를 override하여 자기자신이 소유하고 있는 자원에 대한 정리를 수행하고, 다시 상위 클래스의 virtual 메서드를 호출하도록 구성해야 한다.

만일 새로이 작성한 타입이 unmanaged 자원을 사용하고 있다면 반드시 finalizer를 작성해야 한다. 왜냐하면 타입의 사용자가 Dispose()를 항상 명시적으로 호출하여 unmanaged 자원을 해제할 것이라고 기대할 수 없기 때문이다.

 

Garbage Collector가 수행되면 그 즉시 finalizer를 가지고 있지 않은 garbage 객체를 메모리로부터 제거하지만, finalizer를 가지고 있는 객체는 여전히 메모리 상에 남게 된다. 이러한 객체들은 finalization 큐에 포함되어 있는 노드에 의해서 참조되고 있는데, Garbage Collector는 독립된 스레드를 이용하여 finalization 큐 내의 노드가 가리키고 있는 객체의 finalizer를 호출한다. finalizer 스레드가 동작을 완료한 이후에나 비로소 객체들은 메모리로부터 제거될 수 있다.

 

IDisposable.Dispose() 메서드를 구현할 때에는 반드시 다음의 4가지 작업을 수행해야 한다.

    1. unmanaged 자원의 해제

    2. managed 자원의 해제(참조되지 않은 이벤트를 포함해서)

    3. 객체가 dispose 되었음을 표시하는 상태 플래그 값 설정. Dispose()가 호출된 이후에 사용자가 객체를 사용할 경우를 대비하여 모든 public 메서드에는 상태 플래그 값이 설정되어 있는 경우에 ObjectDisposed 예외를 발생시키도록 작성되어야 한다.

    4. finalization 동작이 수행되지 못하도록 함. GC.SuppressFinalize(this)를 호출하면 finalization 동작이 일어나지 못하도록 할 수 있다.

IDisposable을 구현하면 사용자가 원하는 시기에 객체가 사용하고 있는 자원을 해제할 수 있고 동시에 unmanaged 자원을 해제하는 표준적인 방법을 제공해 줄 수 있게 된다. 또한 객체의 finalization 수행에 따른 성능 손실도 보전할 수 있다.

 

하지만 여기에는 여전히 구조적인 문제가 있다. 첫째로, IDisposable interface와 finalizer를 구현한 클래스를 상속한 하위 클래스가 있을 경우, 하위 클래스는 자기자신이 소유하고 있는 자원뿐만 아니라 상위 클래스가 소유하고 있던 자원도 해제해야 한다. 둘째로, Dispose()와 finalize는 거의 유사한 내용을 중복적으로 담고 있다. 이러한 중복은 제거될 필요가 있다. 마지막으로, interface에서 정의한 메서드를 overriding하면(이 경우 IDisposable.Dispose()를 말한다) 우리가 기대하는 대로 동작하지 않는다. 이러한 몇 가지 이유로 인해 표준적인 Dispose 패턴에는 제 3의 메서드가 필요하다. 이 메서드는 protected virtual helper 메서드 형태로 구성되는데, finalizer와 Dispose()에 공통으로 나타나는 코드를 포함하고, 상속 관계에 있는 하위 클래스가 적절한 시점에 자원을 해제할 수 있도록 도와준다. 이러한 virtual 메서드는 하위 클래스가 자원해제를 위해 Dispose()와 finalization이 수행되는 시점에 상위 클래스의 자원해제를 수행할 수 있도록 한다.

  1. protected virtual void Dispose( bool isDisposing );

이 overload 메서드는 필요할 때 finalizer와 Dispose()에 의해서 호출되고, virtual로 선언되어 있기 때문에 모든 하위 클래스에 의해서 적절히 overriding될 수 있다.

 

다음에 이러한 Dispose 패턴을 구현하는 간단한 예제가 있다. MyResourceHog 클래스는 IDisposable interface, finalize와 제 3의 virtual Dispose(bool) 메서드까지 모두 구현하였다.

  1. public class MyResouceHog : IDisposable

    {

    // 이미 dispose 되었는지의 여부를 저장하기 위한 플래그

    private bool _alreadyDisposed = false;

  2.  

    // finalizer

    // vitual Dispose 메서드를 호출한다.

    ~MyResouceHog()

    {

    Dispose(false);

    }

  3.  

    // IDisposable interface의 구현

    // virtual Dispose 메서드를 호출하고

    // Finalization이 수행되지 않도록 한다.

    public void Dispose()

    {

    Dispose(true);

    GC.SuppressFinalize(true);

    }

  4.  

    // virtual Dispose 메서드

    protected virtual void Dispose(bool isDisposing)

    {

    // 여러 번 dispose를 수행하지 않도록 한다.

    if( _alreadyDisposed )

    return;

  5.  

    if( isDisposing )

    {

    // 해야할 일 : managed 리소스를 해제한다.

    }

  6.  

    // 해야할 일 : unmanaged 리소스를 해제한다.

    // disposed 플래그를 설정한다.

    _alreadyDisposed = true;

    }

    }

만일 하위 클래스에서 추가적인 정리작업을 수행할 필요가 있다면 다음과 같이 protected Dispose(bool) 메서드를 override한다.

  1. public class DeriveResourceHog : MyResouceHog

  2. {

  3. // 자신만의 dispose 플래그를 가지고 있다.

  4. private bool _disposed = false;

  5.  

    protected override void Dispose(bool isDisposing)

  6. {

  7. // 여러번 dispose를 수행하지 않도록 한다.

  8. if( _disposed )

  9. return;

  10. if( isDisposing )

  11. {

  12. // 해야할 일 : managed 리소스를 해제한다.

  13. }

  14. // 해야할 일 : unmanaged 리소스를 해제한다.

  15.  

    // 기반 class가 자신의 리소스를 해제할 수 있도록 한다.

  16. // 기반 class는

  17. // GC.SuppressFinalize()를 호출할 책임이 있다.

  18. base.Dispose(isDisposing);

  19.  

    // 하위 class의 dispose 플래그를 설정한다.

  20. _disposed = true;

  21. }

  22. }

상위 클래스와 하위 클래스 각각에 dispose의 수행상태를 저장하기 위한 플래그가 있는 것은 순전히 보호차원에서 포함시킨 것이다. 플래그를 이중화함으로써 객체의 소멸과정에서 혹시나 발생할지 모르는 오류로부터 보호하고, 객체를 이루고 있는 다수의 타입(상, 하위 클래스 등)중 자신이 정의한 부분이 명백하게 삭제되었음을 밝히기 위함이다.

 

Dispose()와 finalizer는 반드시 방어적으로 구현해야 한다. 객체의 dispose 과정은 어떤 순서로 일어날지 예측할 수 없다. 이러한 코드를 작성할 때 우리는 자원의 해제 이외에는 어떠한 다른 동작도 수행해서는 안 된다. Dispose()나 finalizer 내에서는 절대 다른 처리 루틴을 포함시키지 말자. 만일 Dispose()나 finalizer에서 다른 처리 루틴을 수행하게 되면, 객체의 생명주기와 관련된 아주 복잡한 문제에 직면할 수 있다. 객체는 우리가 생성하는 시점에 만들어지지만 소멸시점은 Garbage Collector가 결정한다. 객체의 finalizer는 객체의 마지막 임종 직전에 호출된다. 따라서 finalizer는 unmanaged 자원의 해제를 제외한 어떤 일도 수행해서는 안 된다. 만일 finalizer에서 억지로 해당 객체에 접근이 가능하도록 만들게 되면 우리는 다시 객체에 접근할 수 있게 되지만, 많은 문제점들을 유발시킨다. 첫째로 객체는 이미 finalize되었다고 판단되기 때문에 Garbage Collector는 해당 객체에 대해서 더 이상 finalizer를 호출할 필요가 없다고 판단하게 된다. 따라서 실제로 객체가 제거되어야 하는 시점이 되어서도 finalizer는 호출되지 않는다. 둘째로 몇몇 자원은 더 이상 가용되지 않을 수 있다. finalization 큐의 특정 노드가 가리키는 객체가 포함하고 있는 또 다른 객체들은 접근이 가능하기 때문에 메모리 상에서 삭제되지는 않는다. 하지만 이미 dispose 과정을 거쳤을 수도 있기 때문에 객체들은 더 이상 사용 가능하지 않을 것이다.

 

Managed 환경에서는 대부분의 경우 finalizer를 구현할 필요가 없다. 하지만 특정 타입이 unmanaged 자원을 저장하고 있거나 IDisposable을 구현한 객체를 멤버로 가지고 있는 경우에는 finalizer를 구현하자. 

 


Chapter 03 – 설계사항 구현#


19. 상속보다는 interface를 정의하고 구현하는 것이 좋다.#

      상속은 'is-a'로 interface는 'behaves like'로 표현될 수 있다. 'is a'는 '하위 클래스는 상위 클래스이다'로 'behaves like'는 '구현 클래스는 interface처럼 동작한다'라고 할 수 있다. interface는 기능 명세인 동시에 계약이다. 따라서 interface 내에 선언될 수 있는 메서드, 프로퍼티, 인덱서, 이벤트는 interface를 구현하는 클래스에 의해 빠짐없이 구현되어야 한다.

      기반 클래스와 interface 중에 하나를 선택하는 것은 어떻게 추상화를 제공하는 것이 가장 좋은가 하는 문제로 귀결된다. interface를 특정 타입이 반드시 구현해야 하는 계약과 같은 형태로 활용하려면 interface를 확정짓고 변경하지 말아야 한다. 하지만 기반 클래스는 언제든 필요시에 변경되고 확장될 수 있다. 이렇게 확장된 부분은 모든 하위 클래스에서도 활용할 수 있는 기능의 일부가 된다.

 

      interface는 상속관계가 없는 서로 다른 타입에 의해서 구현될 수 있다. 이러한 interface의 특징은 항상 동일한 기반 클래스를 상속받아서 overriding 메서드를 구현하는 것에 비해 좀 더 유연하다고 할 수 있다. 이 점은 닷넷 프레임워크가 단일 상속만을 허용하기 때문에 더욱 중요하다.

      우리가 작성하는 타입이 클래스형을 반환형으로 가지게 되면 반환하는 클래스의 모든 메서드가 외부로 노출된다. 하지만 interface를 사용하면 우리가 노출하고자 하는 메서드나 프로퍼티를 제한할 수 있으며, interface의 세부적인 내부구현은 언제든지 다른 부분과 연관성없이 독립적으로 수정될 수 있다.

 

      interface는 서로 상속의 연관성이 없는 여러 타입에서 독립적으로 구현될 수 있다고 말했다. 직원, 고객, 대리점 등을 관리하는 애플리케이션을 개발한다고 가정해 보자. 각각의 요소들은 상호간에 어떠한 상속 관계도 가지지 않는다고 하자. 그럼에도 각각의 요소들은 각기 이름을 저장할 수 있는 프로퍼티를 가지고 있고, 이러한 이름들은 윈도우 컨트롤에 출력되어야 한다고 하자.

  1. public class Employee

  2. {

  3. public string Name

  4. {

  5. get

  6. {

  7. return string.Format("{0}, {1}", _last, _first);

  8. }

  9. }

  10. // 세부 구현내용 생략

  11. }

  12. public class Customer

  13. {

  14. public string Name

  15. {

  16. get

  17. {

  18. return _customerName;

  19. }

  20. }

  21. // 세부 구현내용 생략

  22. }

  23. public class Vendor

  24. {

  25. public string Name

  26. {  

  27. get

  28. {

  29. return _vendorName;

  30. }

  31. }

  32. // 세부 구현내용 생략

  33. }

      위 예제의 Employee, Customer, Vendor 클래스는 동일 클래스를 상속받고 있지 않지만 동일한 프로퍼티를 노출하고 있다(위 예에서는 Name 프로퍼티만을 가지고 있지만 동일한 형태로 Address나 Contact Phone Number 등도 가지고 있다고 가정하자). 이 경우 우리는 이러한 공통 요소를 interface로 분리해낼 수 있다.

  1. public interface IContactInfo

  2. {

  3. string Name { get; }

  4. PhoneNumber PrimaryContact { get; }

  5. PhoneNumber Fax { get; }

  6. Address PrimaryAddress { get; }

  7. }

  8. public class Employee : IContactInfo

  9. {

  10. // 구현부 생략

  11. }

이렇게 새로운 interface를 구현하면 상관관계가 없는 모든 클래스들에 대해 공통처리 루틴을 작성하기가 쉬워진다.

  1. public void PrintMailingLabel( IContactInfo ic )

  2. {

  3. // 구현부 생략

  4. }

공통의 동작을 IContactInfo interface로 분리했기 때문에 위의 메서드는 IContactInfo interface를 구현한 Customer, Employee, Vendor형 객체 모두를 인자로 받아 동작을 수행 할 수 있다.

 

      interface를 구현하게 되면 때때로 구조체의 unboxing에 따르는 불이익을 피할 수 있다. 구조체를 boxing하게 되면, 구조체가 구현했던 모든 interface들은 box에 의해서 다시 한번 재구현된다. 만일 interface를 통하여 box내의 구조체에 접근하게 되면, unboxing을 수행할 필요가 없다. Link와 Link의 설명을 담기 위한 다음의 예제를 살펴보자.

  1. public struct URLInfo : IComparable

  2. {

  3. private string URL;

  4. private string description;

  5.  

    public int CompareTo( object o )

  6. {

  7. if( o is URLInfo )

  8. {

  9. URLInfo other = (URLInfo) o;

  10. return CompareTo(other);

  11. }

  12. else

  13. throw new ArgumentException( "Compare object is not URLInfo");

  14. }

  15.  

    public int CompareTo( URLInfo other )

  16. {

  17. return URL.CompareTo( other.URL );

  18. }

  19. }

      URLInfo가 IComparable interface를 구현하고 있기 때문에 우리는 URLInfo 객체들에 대한 정렬된 리스트를 만들 수 있다. 하지만 Sort() 메서드를 호출하는 경우에는 object의 ComapreTo() interface를 통해서 구조체 내부의 값에 접근하기 때문에 unboxing을 수행하지 않아도 된다. 물론 여전히 인자값으로 전달되는 객체에 대해서는 boxing을 수행해야 하겠지만, 값의 비교를 위해 IComparable.CompareTo() 메서드를 호출하는 과정에서 비교의 대상이 되는 왼쪽값은 unboxing을 수행할 필요가 없다.

 

      기반 클래스들은 상속관계가 있는 타입들 사이에 공통적인 동작을 정의하고 구현하는 데 사용된다. interface는 상관관계가 없는 타입들간의 공통적인 기능성을 표현하는 데 주로 사용된다. 클래스는 타입을 정의하는 데 쓰고, interface는 타입들이 어떠한 기능을 제공해야 하는지를 나타내기 위해서 사용된다.

 

 

20. interface의 구현과 virtual 메서드의 overriding을 구분하라.#

      interface의 구현은 virtual function에 대한 overriding과는 매우 다르게 동작한다. interface 내에 선언된 멤버는 virtual이 아니다. 따라서 상위 클래스에서 이미 특정 interface를 구현해 두었다면 하위 클래스는 해당 interface 멤버를 overriding할 수 없다. 하위 클래스에서도 동일 interface 멤버에 대해서 새로운 구현부를 제공할 수 있으나 이 경우 상위 클래스의 구현부를 대체하게 된다. 이것은 virtual 메서드에 대한 overriding과는 전혀 다른 개념이다.

      이러한 차이점을 설명하기 위해 간단한 interface 선언과 구현부를 다음처럼 구성하였다.

  1. interface IMsg

  2. {

  3. void Message();

  4. }

  5.  

  6. public class MyClass : IMsg

  7. {

  8. public void Message()

  9. {

  10. Console.WriteLine("MyClass");

  11. }

  12. }

  13.  

  14. public class MyDerivedClass : MyClass

  15. {

  16. public new void Message()

  17. {

  18. Console.WriteLine("MyDerivedClass");

  19. }

  20. }

      상위 클래스의 Message() 메서드를 대체해야 하기 때문에 new 키워드를 반드시 사용해야 함에 주목하자. MyClass의 Message() 메서드는 virtual 메서드가 아니므로 하위 클래스에서는 Message() 메서드를 overriding 할 수 없다. MyDerivedClass의 Message() 메서드는 MyClass의 Message()를 완전히 대체하는 새로운 메서드를 제공하는 것이다. 하지만 여전히 MyClass의 Message() 메서드는 IMsg interface를 이용하여 접근할 수는 있다.

  1. MyDerivedClass d = new MyDerivedClass();

  2. d.Message();   // "MyDerivedClass" 출력

  3. IMsg m = d as IMsg;

  4. m.Message();   // "MyClass" 출력

 

      상위 클래스에서 이미 구현한 interface 요소의 동작 방식을 하위 클래스에서 변경해야 할 때 크게 2가지 방법이 있을 수 있다. 첫째로 만일 우리가 상위 클래스의 내용을 수정할 수 없는 경우라면 하위 클래스에서 interface를 다시 구현할 수 있다.

  1. public class MyDerivedClass : MyClass, IMsg

  2. {

  3. public new void Message()

  4. {

  5. Console.WriteLine("MyDerivedClass");

  6. }

  7. }

하위 클래스를 선언할 때에 IMsg interface를 한번 더 써 주게 되면 IMsg interface를 통해서 Message() 메서드를 호출했을 때 하위 클래스에서 작성한 Message() 메서드가 호출된다.

  1. MyDerivedClass d = new MyDerivedClass();

  2. d.Message();   // "MyDerivedClass" 출력

  3. IMsg m = d as IMsg;

  4. m.Message();   // "MyDerivedClass" 출력

      MyDerivedClass의 Message()를 구현할 때, 여전히 new 키워드를 써 주어야 한다. 이러한 사실이 가끔은 문제의 원인이 될 경우도 있다. 왜냐하면 상위 클래스에서 정의한 Message() 메서드는 상위 클래스의 reference를 통하여 여전히 호출 가능하기 때문이다.

  1. MyDerivedClass d = new MyDerivedClass();

  2. d.Message();   // "MyDerivedClass" 출력

  3. IMsg m = d as IMsg;

  4. m.Message();   // "MyDerivedClass" 출력

  5. MyClass b = d;

  6. b.Message();   // "MyClass" 출력

 

이러한 문제를 해결하기 위한 유일한 방법은 기반 클래스에서 구현한 interface 메서드를 virtual로 선언하는 것이다.

  1. public class MyClass : IMsg

  2. {

  3. public virtual void Message()

  4. {

  5. Console.WriteLine("MyClass");

  6. }

  7. }

  8.  

    public class MyDerivedClass : MyClass

  9. {

  10. public override void Message()

  11. {

  12. Console.WriteLine("MyDerivedClass");

  13. }

  14. }

      MyDerivedClass를 포함하여 MyClass를 상속하는 모든 클래스들은 그 자신의 Message() 메서드를 overriding할 수 있다. 이렇게 되면 MyDerivedClass의 reference 또는 IMsg나 MyClass reference를 사용하는 것과 상관없이 항상 새로 구현한 하위 클래스의 Message() 메서드가 호출될 것이다.

      만일 메서드를 virtual로 선언하는 것이 썩 내키지 않는다면 MyClass를 조금 다른 방법으로 변경할 수도 있다.

  1. public abstract class MyClass : IMsg

  2. {

  3. public abstract void Message();

  4. }

      위와 같이 interface 내에서 선언한 메서드를 실제로 구현하지 않고 abstract로 선언할 수 있다. 메서드를 abstract로 선언하였기 때문에 MyClass를 상속하는 모든 타입들은 반드시 Message()를 구현해야 한다. IMsg interface는 MyClass 선언의 일부가 되고, 하위 클래스에 메서드를 구현할 책임을 전가하게 된다.

 

      interface를 구현하는 것은 virtual function을 선언하고 구현하는 것에 비해 몇 가지 다른 선택사항이 있다. interface를 더 이상 추가적으로 구현하지 못하도록 seal 형태로 구현할 수도 있고, virtual 형태로 구현하거나 또는 abstract 형태로 선언할 수도 있다.

 

 

21. delegate를 이용하여 콜백을 표현하라.#

      delegate 타입은 안정적인 콜백 메서드를 정의할 수 있게 해준다. 서로 다른 클래스들 사이에 상호 통신을 수행하는 데 있어 클래스간 결합도를 낮추려면 interface를 사용하는 것보다 delegate를 사용하는 것이 더 좋다. delegate는 메서드에 대한 참조를 가진 객체이다. delegate를 사용하면 통신하고자 하는 대상이 몇 개인가과 상관없이 런타임시 구성을 수행할 수 있다.

      Multicast delegate는 delegate 객체에 다수의 메서드들을 추가하여 한번의 delegate 호출로 다수의 메서드를 호출하는 것을 말한다. Multicast delegate를 사용할 경우에는 2가지의 주의 사항이 있다. 하나는 메서드 내에서 예외가 발생했을 때 안정적이지 못하다는 것이고, 다른 하나는 마지막에 호출된 메서드의 변환결과만 반환될 수 있다는 것이다.

      Multicast delegate는 내부적으로 저장된 메서드들을 연속적으로 호출하기 때문에 예외를 잡아내지 못한다. 만일 메서드들을 호출하는 과정에서 예외가 발생되면, 더 이상 다른 메서드들을 호출되지 못하고 메서드 호출은 끝나버린다.

      값을 반환할 때에도 유사한 문제가 발생할 수 있다. delegate를 선언할 때에는 반환형을 void 이외의 형으로도 지정할 수 있는데, 이것은 사용자의 호출 중단을 감지할 목적으로만 사용되어야 한다.

  1. public void LengthyOperation( ContinueProcessing pred )

  2. {

  3. foreach( ComplicatedClass cl in _container )

  4. {

  5. cl.DoLengthyOperation();

  6. // 중단 요청을 확인

  7. if( false == pred() )

  8. return;

  9. }

  10. }

위의 코드는 single delegate에서는 정상적으로 동작하지만, multicast delegate에서는 문제가 발생한다.

  1. ContinueProcessing cp = new ContinueProcessing( CheckWithUser );

  2. cp += new ContinueProcessing( CheckWithSystem );

  3. cp.LengthyOperation( cp );

delegate를 호출하고 반환된 값은 연속적인 메서드의 호출 중에 가장 마지막에 호출된 메서드의 반환값이다. 따라서 앞선 CheckWithUser()의 반환값은 무시된다.

이러한 두 가지 문제는 delegate에 추가된 다수의 메서드들을 호출하도록 하는 코드를 직접 작성함으로써 해결이 가능하다.

  1. public void LengthyOperation( ContinueProcessing pred )

  2. {

  3. bool bContinue = true;

  4. foreach ( ComplicatedClass cl in _container )

  5. {

  6. cl.DoLengthyOperation();

  7. foreach( ContinueProcessing pr in pred.GetInvocationList() )

  8. bContinue &= pr();

  9. if( false == bContinue )

  10. return;

  11. }

  12. }

이 경우 delegate에 추가된 각각의 메서드 호출이 true를 반환할 경우에만 계속해서 다음의 메서드들을 호출하고 예외 처리 루틴을 추가할 수도 있다.

      delegate는 런타임시 콜백을 구성하는 최고의 방법이다. 콜백으로 호출되어야 하는 메서드들의 리스트를 런타임시 구성할 수 있다. 또한 다수의 메서드가 호출되도록 구성할 수도 있다. 닷넷 환경에서 콜백을 구성할 때에는 반드시 delegate를 사용하자.

 

22. 이벤트를 이용하여 외부로 노출할 인터페이스를 정의하라.#

      event 타입은 안정적인 이벤트 핸들러를 구현할 수 있도록 이벤트 핸들러의 원형을 제공할 목적으로 포함된 내장 delegate이다. 우리가 만드는 타입이 다수의 다른 타입들과 통신을 해야 할 때 이벤트를 유용하게 활용할 수 있다.

      간단한 예를 살펴보자. 애플리케이션이 전달하는 모든 메시지에 대하여 분배 기능을 수행하는 log 클래스를 만든다고 하자. 애플리케이션으로 전달되는 모든 메시지는 먼저 log 클래스로 전달되고, 이렇게 전달된 메시지는 다시 적절한 곳으로 분배될 수 있도록 구성되어야 한다. 메시지를 실제로 수신해야 하는 Listener 들은 콘솔, 데이터베이스, 시스템 로그나 다른 메커니즘을 통하여 전달된 메시지를 처리할 수 있다. log 클래스는 다음의 예와 같이 message가 도착할 때마다 특정 이벤트를 발생시키도록 작성될 수 있다.

  1. public class LoggerEventArgs : EventArgs

  2. {

  3. public readonly string Message;

  4. public readonly int Priority;

  5.  

    public LoggerEventArgs( int p, string m )

  6. {

  7. Priority = p;

  8. Message = m;

  9. }

  10. }

  11.  

  12. // 이벤트 핸들러의 형태를 정의함

  13. public delegate void AddMessageEventHandler( object sender, LoggerEventArgs msg );

  14.  

    public class Logger

  15. {

  16. static Logger()

  17. {

  18. _theOnly = new Logger();

  19. }

  20.  

    private Logger()

  21. {

  22. }

  23.  

    private static Logger _theOnly = null;

  24.  

    public static Logger Singleton

  25. {

  26. get

  27. {

  28. return _theOnly;

  29. }

  30. }

  31.  

    // 이벤트 정의

  32. public event AddMessageEventHandler Log;

  33.  

    public void AddMsg( int priority, string msg )

  34. {

  35. // 아래 구조는 본문에서 설명될 것이다.

  36. AddMessageEventHandler l = Log;

  37. if( l != null )

  38. l( null, new LoggerEventArgs(prioriry, msg) );

  39. }

  40. }

      AddMsg() 메서드는 이벤트를 발생시키는 적절한 구조를 보여준다. Log 이벤트 핸들러를 임시변수에 할당하는 문장은 멀티스레드 프로그램에서의 스레드간 race condition을 방지하기 위한 매우 중요한 보호 수단이다. Log 이벤트 핸들러를 임시로 할당하는 문장이 없다면, if 문장이 특정 이벤트 핸들러가 null임을 확인하는 동안, 다른 스레드가 이벤트 핸들러를 수행할 수 있다. reference를 복사해 두면 결코 이런 일이 일어나지 않는다.

      LogEventArgs는 이벤트의 우선순위와 메시지를 가질 수 있도록 정의하였다. delegate는 이벤트 핸들러의 원형을 정의하고 있다. Logger 클래스 내부에서는 이벤트 핸들러를 위한 event 객체를 선언하고 있다. 컴파일러는 public event 필드가 정의되어 있으면, 이벤트 필드를 위한 add와 remove accessor를 자동으로 추가한다. 생성되는 IL 코드는 다음의 소스 코드와 유사한 구조가 된다.

  1. private AddMessageEventHandler _Log;

  2. public event AddMessageEventHandler Log

  3. {

  4. add

  5. {

  6. _Log = _Log + value;

  7. }

  8. remove

  9. {

  10. _Log = _Log - value;

  11. }

  12. }

      event를 public으로 선언하는 것이 좀 더 간결하고, 읽고 유지하기도 쉬울 뿐더러 좀 더 정확하다. 우리가 만드는 클래스가 이벤트를 포함해야 한다면 컴파일러가 add와 remove를 자동으로 생성할 수 있도록 public 으로 선언하는 것이 좋다. 또한 필요하다면 컴파일러가 생성하는 기본 accessor의 내용을 변경할 수 있다.

      이벤트는 이벤트를 수신할 listener에 대해서는 알 필요가 없다. 다음의 예제는 자동적으로 표준 에러 콘솔에 모든 message를 전달하도록 구성되었다.

  1. public class ConsoleLogger

  2. {

  3. private static Logger logger = Logger.Singleton;

  4.  

    static ConsoleLogger()

  5. {

  6. logger.Log += new AddMessageEventHandler(Logger_Log);

  7. }

  8.  

    private static void Logger_Log(object sender, LoggerEventArgs msg)

  9. {

  10. Console.Error.WriteLine("{0} : \t {1}", msg.Priority.ToString(), msg.Message);

  11. }

  12. }

      이벤트가 발생하면 이벤트를 수신하는 listener가 몇 개이든 상관없이 모든 listener에게 발생된 이벤트를 전달하게 된다. Logger 클래스는 어떤 listener가 event를 받게 될지에 대해서 전혀 알지 못해도 상관없다.

      Logger 클래스는 단지 하나의 event 객체만을 가지고 있지만 윈도우 컨트롤과 같은 많은 클래스들은 많은 수의 이벤트들을 가지고 있기 때문에, 모든 이벤트를 각기 다른 필드로 구성하는 것은 적절하지 않다. 몇몇의 경우는 정의되어 있는 이벤트 중에서 아주 소수만이 실제로 애플리케이션에서 사용된다. 이런 상황이라면 event 객체를 런타임시 생성하도록 설계를 변경하는 것이 매우 좋은 대안이 될 수 있다.

      C# 에서 이벤트를 사용하게 되면 이벤트 송신자와 수신자 사이의 결합도를 낮추는 효과를 가져온다. 따라서 이벤트 송신자를 개발할 때에는 수신자의 구성을 전혀 고려하지 않아도 된다. 이벤트는 다수의 클래스에게 특정 정보를 손쉽게 전달할 수 있는 표준화된 방법이기도 하다.

 

 

23. 클래스 내부 객체에 대한 reference 반환을 피하라.#

      사용자가 임의로 객체의 내부 정보에 접근하고 값을 수정하는 것을 바라지 않는다면, 클래스에 대한 특정 interface를 구성하고 구성된 interface를 통해서만 내부 정보에 접근할 수 있도록 하는 것이 좋다. 실제로 내부자료에 대하여 우리가 의도하지 않은 변경이 일어나는 것을 막는 데는 4가지 정도의 훌륭한 전략이 있다. value 타입, immutable타입, interface 그리고 wrapper를 활용하는 방법이다.

      value 타입은 프로퍼티를 통해서 사용자가 값에 접근하게 되면 항상 복사가 일어나게 된다. 따라서 복사된 값을 아무리 변경한다 하더라도 내부값에는 아무런 영향을 주지 않는다.

      System.String과 같은 immutable 타입도 의도하지 않는 변경에 안정적이다. string에서와 같이 사용자는 immutable 타입 객체의 내부값을 변경할 수 없으므로 immutable 타입은 얼마든지 노출되어도 무방하다.

      3번째 전략은 사용자가 사용할 수 있는 interface를 정의하고, 내부자료에 interface를 통해서만 접근하도록 제한하는 방식이다. 이러한 전략은 DataSet과 IListSource interface의 예에서 발견할 수 있다.

      System.DataSet 클래스는 마지막 4번째 전략인 wrapper를 활용하는 방법도 동시에 사용하고 있다. DataViewManager 객체는 DataSet 객체에 접근할 수 있는 DataView 객체를 생성할 수 있다.

  1. public class MyBusinessObject

  2. {

  3. // private 데이터에 대한 읽기 프로퍼티만을 구현

  4. private DataSet _ds;

  5. public DataView this[string tableName]

  6. {

  7. get

  8. {

  9. return _ds.DefaultViewManager.CreateDataView(_ds.Tables[tableName]);

  10. }

  11. }

  12. }

  13. // 내부 DataSet 데이터에 접근

  14. MyBusinessObject bizObj = new MyBusinessObject();

  15. DataView list = bizObj["customers"];

  16. foreach(DataRowView r in list)

  17. Console.WriteLine(r["name"]);

      위 예제를 보면 DataViewManager 객체(_ds.DefaultViewManager)는 DataSet 내의 테이블들에 접근할 수 있는 DataView 객체를 생성한다. 이렇게 생성된 DataView 객체를 통해서는 테이블에 아이템을 추가, 삭제하거나 변경을 수행할 수 있지만 테이블 자체를 변경하거나 테이블의 column을 변경하는 등의 작업은 수행할 수 없다.

 

      특정 타입을 구현할 때 reference 타입 객체를 public 필드로 선언하여 외부에서의 직접 접근을 허용한다면, 사용자는 우리가 구현하는 타입에서 정의한 메서드나 프로퍼티를 이용하여 간접적으로 내부자료에 접근하려 하지 않고 직접 내부 데이터를 변경하려고 시도할 것이다. 이 경우 매우 발견하기 어려운 오류를 유발할 가능성이 있다. reference 타입을 private으로 선언하고 프로퍼티나 특정 메서드의 반환값으로만 사용한다 하더라도 사용자는 반환된 객체 내의 public필드를 무제한으로 사용할 수 있다. 따라서 private으로 선언된 reference 타입 객체를 노출할 경우에는 interface를 사용하거나 wrapper class를 생성하여 접근권한을 제한할 필요가 있다. 만일 사용자가 내부자료에 변경을 가하는 것을 허용해야 하는 경우라면 내부자료의 변경이 유효한 변경인지를 검토하기 위해서 Observer pattern을 구현하는 것이 좋다.

 

 

24. 명령적 프로그래밍보다 선언적 프로그래밍이 더 좋다.#

      선언적 프로그래밍을 이용하면 명령적 프로그래밍에 비해 좀 더 쉽고 간결하게 프로그램의 동작 방식을 표현할 수 있다. 선언적 프로그램이란 명령행을 작성하여 프로그램을 작성하는 것이 아니라 프로그램의 동작방식을 선언을 통해 구현하는 것을 말한다. 제한적이긴 하지만 C# 언어에서도 attribute를 이용하여 선언적 프로그램을 수행할 수 있다. attribute를 사용하면 보다 짧은 시간에 보다 오류가 적은 코드를 개발할 수 있다.

      만일 이미 정의되어 있는 attribute들이 우리의 의도에 정확히 부합하지 않을 경우라면 전용의 attribute를 정의하고 reflection을 활용하여 선언적 프로그램을 만들 수 있다. 다음에 자료형의 기본 정렬 순서를 정의할 수 있도록 전용 attribute를 만들고 이러한 attribute를 활용하는 예제가 있다.

  1. [DefaultSort("Name")]

  2. public class Customer

  3. {

  4. private string _name;

  5. private decimal _balance;

  6. public string Name

  7. {

  8. get { return _name; }

  9. set { _name = value; }

  10. }

  11. public decimal Balance

  12. {

  13. get { return _balance; }

  14. }

  15. public decimal AccountValue

  16. {

  17. get { return calculateValueOfAccount(); }

  18. }

  19. }

      DefaultSort attribute로 Name 프로퍼티를 지정하였다. 이는 Customer 객체를 포함하는 collection은 기본적으로 Name 프로퍼티를 이용하여 정렬을 수행하라는 의미를 함축하고 있다. DefaultSort attribute는 닷넷 프레임워크가 기본적으로 제공하는 attribute가 아니므로, 다음과 같이 DefaultSortAttribute 클래스를 구현해 주어야 한다.

  1. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]

  2. public class DefaultSortAttribute : System.Attribute

  3. {

  4. private string _name;

  5. public string Name

  6. {

  7. get { return _name; }

  8. set { _name = value; }

  9. }

  10. public DefaultSortAttribute(string name)

  11. {

  12. _name = name;

  13. }

  14. }

      또한, DefaultSort attriubte를 사용하고 있는 타입의 객체들에 대한 collection을 주어진 attribute를 이용하여 정령하도록 하는 코드도 작성해야 한다. attribute에 지정된 프로퍼티를 찾고 프로퍼티의 값을 비교하기 위해서는 reflection을 사용할 것이다.

      다음으로 IComparer를 구현해야 한다. IComparer는 두 개의 객체를 비교할 수 있는 Compare()메서드를 선언하고 있다. 또한 비교의 대상이 되는 타입들은 각각 IComparable interface를 구현하고 있어야 한다.

      선언적 프로그래밍 방식을 사용하면 선언을 통하여 우리의 의도를 표현할 수 있기 때문에 반복적인 코드를 더 이상 작성하지 않아도 되는 장점이 있다. 그리고 선언적 프로그래밍을 사용하게 되면 타입을 구성한 이후에 선언부만을 변경함으로써 그 타입의 행동방식을 임의로 변경할 수 있는 장점이 있다. 다시 말하면 타입이 가지고 있는 알고리즘을 직접 변경하지 않고도, 선언한 내용에 기반하여 동작방식을 변경할 수 있다는 것이다.

 

 

25. serializable 타입이 더 좋다.#

      닷넷 serialization은 객체 내의 멤버변수를 출력 스트림으로 내보낼 수 있다. 또한 닷넷 serialization은 포함하는 모든 객체들에 대해서 객체 그래프를 구헌한다. 설사 순환참조가 있는 경우라도 serialize와 deserialize 메서드를 통해 정확하게 객체를 저장하고 복원할 수 있다. 또한 닷넷 프레임워크의 serialization 구현부는 객체가 deserialize될 때 객체간의 복잡한 참조관계를 재구성한다. 마지막 주요특징은 Serializeble attribute가 지정된 모든 객체는 바이너리와 SOAP 형태로 serialization될 수 있다는 것이다. 하지만 어떤 경우라도 serialization이 동작하려면 반드시 객체 그래프 내의 모든 객체가 serialize 가능한 타입이어야만 한다. 따라서 모든 타입을 serialize 가능 형태로 작성하는 것은 매우 중요하다. 만일 객체 그래프 내의 하나라도 serialize가 불가능한 타입이 있다면, 사용자가 손쉽게 serialization 기능을 구현하지 못할 것이다. 이 경우 serialization을 위한 복잡한 코드를 개발자가 직접 작성해야만 한다.

      닷넷 프레임워크는 객체의 serialization을 위한 쉽고 표준화된 방법을 제공한다. 만일 객체가 영속성을 가져야 한다면 이러한 표준화된 방법을 이용하는 것이 좋다. 만일 우리가 작성하는 타입이 serialization을 지원하지 않는다면, 이 타입을 사용하는 클래스들은 serialization을 지원하지 않을 수도 있다. 가능한 다른 사용자들이 쉽게 serialization을 구현할 수 있도록 모든 타입을 serialization이 가능하도록 만들고, 기본적으로 제공하는 기능을 이용하되 기본기능이 충분하지 않을 경우에는 ISerializable을 구현하자.

 

 

26. IComparable IComparer를 이용하여 순차관계를 구현하라.#

      IComparable 은 객체간의 기본적인 순차관계를 정의할 목적으로 사용된다. IComparer는 관계연산( <, >, <=, >= )의 의미를 별도로 정의하거나, 기존의 관계연산이 타입별로 다른 의미를 가질 수 있도록 하며, interface 구현에 따른 런타임시 수행성능의 비효율성을 극복하기 위해서 사용될 수 있다.

      IComparable interface는 CompareTo()라는 하나의 메서드만을 정의하고 있다. 이 메서드는 현재 객체가 비교할 객체보다 작을 경우 0보다 작은 값을, 같은 경우 0을, 클 경우 0보다 큰 값을 반환하도록 구현해야 한다. IComparable.CompareTo()는 System.Object형 객체 하나를 인자로 받기 때문에, 구현시에는 반드시 런타임시 타입 체크를 수행해야 하며, 비교시마다 인자로 전달된 객체를 적절한 형으로 형변환을 수행해야 한다.

      IComparable.CompareTo()는 System.Object형 인자를 취하므로, 각 타입에 부합하는 CompareTo() 메서드를 overload 하는 것이 좋다. IComparer는 기본적인 순차관계와는 별도로 다른 형태의 순차관계를 추가로 제공하거나, 순차관계를 제공하지 않는 타입에 대하여 정렬을 수행해야 할 경우 사용될 수 있다.

 

 

27. ICloneable의 구현을 피하라.#

      ICloneable interface를 구현하여 복사 기능을 제공할 수 있도록 타입을 작성하는 것은 매우 좋은 아이디어임에는 분명하다. 하지만 ICloneable interface를 구현하게 되면 모든 하위 클래스에 영향을 주게 되므로 복사 기능이 반드시 필요로 하지 않은 타입에 대해서는 ICloneable interface를 구현하지 않는 것이 좋다.

      내장 자료형만을 포함하고 있는 value 타입의 경우에는 ICloneable에 대한 구현부를 작성할 필요가 없다. 단지 할당연산자만으로도 ICloneable.Clone()을 호출하는 것보다 더 효율적으로 값을 복사할 수 있기 때문이다. 또한 Clone() 메서드는 object 타입을 반환하기 때문에 boxing이 이루어져야 하며, Clone()를 호출한 호출자는 값을 획득하기 위해서 캐스팅을 수행해야 한다. 이 경우 할당연산을 위해서 Clone() 메서드를 정의할 필요도 없고, 정의해서도 안된다.

      Reference 타입에서는 shallow와 함께 deep copy를 지원하기 위해서 ICloneable interface를 구현하는 것이 좋은 경우가 간혹 있다. 하지만 ICloneable interface를 구현할 때는 매우 신중해야 한다. 왜냐하면 ICloneable interface를 구현한 타입을 상속하는 경우에는 하위 클래스들도 반드시 ICloneable을 구현해야 하기 때문이다.

      ICloneable은 유용한 interface임에는 분명하다. 하지만 반드시 지켜야만 하는 규칙들이 있다. value 타입에 대해서는 ICloneable interface를 추가해서는 안 되며, 상속관계에 있는 타입들에 대해서는 가장 하위의 클래스에서만 필요에 따라 ICloneable interface를 구현할 때, 상위 클래스의 멤버를 복사할 수 있도록 protected 복사 생성자를 제공하는 것이 좋다. 이러한 경우를 제외하고는 ICloneable를 사용하지 말자.

 

 

28. 형변환 연산자의 구현을 피하라.#

      우리가 개발하는 타입 내에서 형변환 연산자를 자체적으로 정의하게 되면, 컴파일러는 특정 타입을 다른 타입으로 변환할 때 이러한 연산자를 사용하게 된다. 하지만 형변환 연산을 통해 변환된 타입이 기존의 타입을 완벽하게 대체하지 못하는 경우 종종 잠재적인 오류를 유발하는 경우가 있다. 변환된 타입에 대하여 상태정보를 변경하는 것이 기존 타입의 상태정보를 정확하게 변경하지 못하거나, 형변환 연산자가 임시 객체를 반환하는 경우 임시객체를 이용하여 수행된 상태정보의 변경이 기존 타입의 상태정보를 전혀 변경하지 못한 채 Garbage Collector에 의해서 제거되어 버리는 등의 부작용을 초래할 수 있다. 마지막으로 형변환 연산은 런타임시에 객체의 타입을 기준으로 하는 것이 아니라 컴파일타임의 타입을 근간으로 하고 있다. 이 때문에 형변환 연산자를 사용하기 위해서 반복적으로 형변환을 수행해야 할 수도 있다. 이러한 코드는 유지보수하기가 매우 힘들다.

 

 

29. 기반 클래스의 변경이 영향을 줄 경우에만 new 한정자를 사용하라.#

new 한정자는 상위 클래스로부터 상속받은 메서드 중 virtual이 아닌 메서드를 재정의하기 위해서 사용된다. new 한정자는 virtual이 아닌 메서드를 virtual로 만든다는 것이 아니라 클래스 내에 다른 메서드 하나를 추가하는 역할을 수행한다고 보는 것이 타당하다.

virtual이 아닌 메서드는 정적으로 결합된다. 소스의 어떤 곳에서도 항상 같은 메서드가 호출되게 된다. 프로그램 수행시에 하위 클래스에서 동일 메서드의 다른 구현부를 찾거나 하지 않는다. 하지만 virtual 메서드는 이와 반대로 동적으로 결합되기 때문에 프로그램 수행시에 객체의 런타임 타입에 근거하여 적절한 메서드를 호출한다.

virtual이 아닌 메서드를 재정의하기 위해서 new 한정자를 사용하지 않는 것이 좋다고 했지만 그렇다고 모든 상위 클래스의 메서드를 virtual로 선언하는 것은 더 더욱 피해야 할 일이다.

유일하게 new 한정자를 사용해도 괜찮은 경우가 있다. 만일 우리가 개발한 클래스가 특정 이름의 메서드를 정의하고 있는데, 상속관계에 있는 상위 클래스에 추가된 새로운 메서드의 이름이 우리가 개발한 메서드와 중복되는 경우라면 new 한정자를 사용하는 것을 고려해 볼 수 있다.

new 한정자는 주의깊게 사용해야 한다. 만일 new 한정자를 남용하게 되면 우리가 만든 객체의 메서드가 예상치 않은 결과를 유발할 수 있다. 상위 클래스의 변경으로 인한 이름 충돌을 극복할 목적으로만 사용하는 것이 가장 좋으며, 이러한 경우라도 장기적인 관점에서 이름을 바꾸는 것이 더 적절한지에 대해서 심사숙고해야 한다. 중요한 것은 new 한정자를 남용하지 말라는 것이다.



Chapter 04 – 이진 컴포넌트 작성#


30. CLS를 준수하는 어셈블리가 더 좋다.#

      우리가 개발하는 어셈블리가 서로 다른 닷넷 언어를 이용할 때에도 완전히 사용될 수 있도록 하기 위해서는 Common Language Specification(CLS)을 준수하도록 어셈블리를 개발해야 한다.

      CLS는 상호운용에 있어 최소한의 공통분모를 규정한 것이다. CLS 규격은 모든 닷넷 언어가 반드시 지원해야만 하는 공통 기능을 규정하고 있다. CLS를 준수하는 컴포넌트를 만들기 위해서는 public 및 protected 인터페이스가 CLS를 준수하도록 제한해야 한다. 그래야만 모든 닷넷 언어들이 이러한 컴포넌트를 활용할 수 있다.

      CLS를 준수하는 어셈블리를 개발할 때에는 두 가지 규칙을 반드시 따라야 한다. 첫 번째 규칙은 public이나 protected 멤버의 인자형과 반환형은 반드시 CLS 규격을 준수해야 한다는 것이다. 두 번째 규칙은 CLS를 준수하지 않은 public이나 protected 멤버는 CLS를 준수하는 별칭을 가져야 한다는 것이다.

      첫 번째 규칙은 비교적 따르기 쉽다. 컴파일러에게 어셈블리가 이러한 규칙을 준수하고 있는지의 여부를 확인하기 위해서 CLSCompliant attribute를 지정할 수 있다.

  1. [assembly: CLSCompliant(true)]

      컴파일러는 이와같이 attribute가 지정된 어셈블리에 대해서는 CLS를 준수하는지의 여부를 컴파일시에 확인한다. 만일 public 또는 protected 메서드나 프로퍼티가 CLS를 준수하지 않는다면 컴파일 오류를 발생시킨다. 이러한 방식은 CLS를 준수하는 어셈블리를 작성하는 가장 쉽고 좋은 방법이다.

      두 번째 규칙을 준수할 지의 여부는 전적으로 개발자 자신에게 달려있다. 이 규칙은 public과 protected 형태로 노출된 모든 동작을 언어 중립적으로 정의할 지를 결정하는 것이다. 다형성을 사용하여 CLS를 준수하지 않는 타입을 사용할 수 있도록 교묘히 타입을 구성하는 것은 가능한 피하는 것이 좋다.

      단순히 타입의 선언이 CLS 규격을 준수하는 것만으로는 충분하지 않다. 왜냐하면 컴파일러의 수행성능을 위해서 어셈블리 내의 모든 타입에 대해서 CLS 규격준수를 확인하지 않기 때문이다. 컴파일러는 컴파일하고자 하는 어셈블리의 CLSCompliant attriubte가 true로 설정되어 있는 경우에만 각 타입의 CLS 준수여부를 확인한다. 만일 CLSCompliant 가 true로 설정되어 있지 않은 경우라면, 컴파일러는 모든 타입의 인터페이스가 CLS를 준수하고 있다고 하더라도 타입이 CLS 규격을 준수하지 않는다고 가정하게 된다.

 

 

31. 작고 단순한 메서드가 더 좋다.#

      닷넷 런타임은 JIT 컴파일러에게 C# 컴파일러가 만들어낸 IL 코드를 machine 코드로 바꾸도록 한다. 이러한 작업은 프로그램이 수행되고 있는 동안 지속적으로 일어난다. 닷넷 런타임은 애플리케이션이 시작되기 이전에 모든 IL 코드를 미리 JITing을 수행하지 않고, 메서드 단위로 JITing을 수행한다. 이렇게 함으로써 애플리케이션의 최초 기동시간을 적절한 수준으로 유지할 수 있다. 어떤 메서드든지 JITing이 수행되기 이전에는 호출될 수 없다. 따라서 부가적인 수행을 담당하는 루틴들을 분리하여 메서드를 세분화하는 것이 메서드 호출의 수를 줄이는 방법보다 훨씬 더 효과적이다. 따라서 크기가 작은 메서드가 큰 메서드에 비해 더 좋은 성능을 보인다.

      작고 단순한 메서드는 JIT 컴파일러가 enregistration을 좀 더 쉽게 할 수 있도록 도와준다. enregistration이란 지역변수를 저장하기 위해서 스택을 사용하지 않고 CPU 내의 레지스터에 위치시키는 것을 말한다. 지역변수 개수를 적게 사용할수록 JIT 컴파일러가 enregistration을 수행하는 코드를 작성할 확률이 높아진다.

      JIT 컴파일러는 또한 메서드를 inlining 할지의 여부도 결정한다. Inlining이란 메서드의 호출 대신 메서드 내의 코드를 메서드 호출부에 직접 삽입하는 것을 말한다. 메서드의 호출은 일반적으로 레지스터의 상태저장, 메서드 수행을 위한 프롤로그, 에필로그 코드의 수행, 반환값의 저장과 같은 코드를 포함하게 된다. 또 필요에 따라서는 메서드에 전달해야 하는 인자를 스택에 담는 동작도  수행해야 한다.

      메서드의 크기가 작다면 JIT 컴파일러에 의해 최적화될 더 많은 기회를 가지게 되며, inlining이 수행될 가능성도 더 많아진다. 추가적으로 단순한 제어흐름을 가진 메서드도 최적화될 기회가 그렇지 않은 메서드에 비해 많은 편이다. 메서드 내의 복잡한 분기를 가지지 않는 메서드들은 지역 변수를 레지스터상에 할당하는 최적화가 적용될 가능성도 높다. 코드를 간결하게 구성해야 함은 물론이고 어떻게 작성하는 것이 좀 더 효율적인지에 대해서도 고려하면서 코드를 작성해야 한다.

 

 

32. 작고 응집도가 높은 어셈블리가 더 좋다.#

      여러 개의 작은 어셈블리로 기능을 분리하여 이진 컴포넌트를 구성하면 사용하기도 편리하며 업데이트도 용이하다. 이번 아이템에서는 특히 응집도에 대해서 강조하고자 하는데, 응집도란 단일의 컴포넌트가 그 고유의 기능만을 잘 처리할 수 있도록 잘 결합되어 있는가의 정도를 말한다. 응집도가 높은 컴포넌트는 하나의 문장으로 컴포넌트의 역할을 잘 표현할 수 있다.

      얼마나 많은 코드나 클래스들을 단일의 어셈블리에 포함시키는 것이 좋을까? 어떤 기능을 하는 코드들을 묶어서 단일의 어셈블리로 포함하는 것이 좋을까? 사실 이 점은 애플리케이션의 명세에 달려있기 때문에 한 마디로 답을 하기에는 어려움이 있다. 하지만 비교적 많은 경우에 적용될 만한 다음과 같은 몇 가지 방법이 있을 수 있다. 먼저 public 클래스들을 검토하여 유사한 클래스들을 단일의 기반 클래스를 사용하도록 구성하여 동일 어셈블리상에 포함시킨다. 그리고 어셈블리 내의 public 클래스가 수행되는 과정에서 필요한 유틸리티 성격의 클래스들을 선별하여 함께 어셈블리에 포함시킨다. public 클래스들의 public 인터페이스와 연관되어 있는 모든 타입은 가능한 동일 어셈블리에 포함시킨다. 마지막 단계로 애플리케이션 전반에 걸쳐 광범위 하게 사용되는 클래스들을 찾아내어 애플리케이션에 대한 유틸리티 성격의 어셈블리를 새로 구성한다.

 

 

33. 타입의 가시성을 제한하라.#

      모든 사람이 모든 것을 볼 필요는 없다. 새롭게 제작되는 모든 타입이 반드시 public일 필요는 없다. 새로운 타입을 개발할 때에는 타입을 사용하는 데 필요한 최소한의 내용만을 공개하도록 구성하는 것이 좋다.

      타입의 가시성을 제한하면 할수록 이후에 타입의 내용이 변경되더라도 시스템 전체를 변경해야 할 가능성이 줄어든다. 타입의 최소한만을 노출하면 타입이 변경되어도 외부에서 접근 가능한 부분이 적기 때문에 시스템 전체를 변경해야 할 가능성은 당연히 줄어들 수밖에 없다.

 

34.  API는 큰 단위로 작성하라.#

웹 서비스를 사용하든 아니면 닷넷 리모팅을 사용하든 상관없이 떨어져 있는 다른 컴퓨터로 객체를 전달하는 것은 매우 비싼 동작임을 기억해야 한다. 웹 서비스의 인터페이스를 구성할 때에는 문서기반의 방식을 사용하거나, 객체의 집합을 이용하여 한번에 많은 정보를 교환하는 방식을 사용하는 것이 좋다. 즉 서버로 접속하기 이전에 필요한 정보를 모두 수집한 후에 한번에 서버로 정보를 보내고, 서버 또한 클라이언트가 필요한 정보를 수집해서 한번에  응답을 주는 것과 같은 방식으로 작성하는 것이다. 서버는 클라이언트로부터 필요한 모든 정보를 한번에 수신하였기 때문에 응답을 보낼 때도 정보를 한번에 모아서 보낼 수 있다.



Chapter 05 – 프레임워크의 사용#


35. 이벤트 핸들러보다 override를 사용하는 편이 낫다.#

상위 클래스를 상속한 하위 클래스에서 이벤트를 처리할 경우에는 virtual 메서드를 override하여 사용하고, 객체 상호간에 연관성이 전혀 없는 경우는 이벤트 핸들러를 이용하는 것이 좋다.

Mouse Down 이벤트에 대해서 반응하는 윈도우 애플리케이션을 작성하는 경우라면 다음과 같이 Form 클래스 내부에서 OnMouseDown() 메서드를 overriding하는 방법을 사용할 수 있다.

  1. public class MyForm : Form

  2. {

  3. // 세부 구현 내용 생략

  4. protected override void OnMouseDown(MouseEventArgs e)

  5. {

  6. try

  7. {

  8. HandleMouseDown(e);

  9. }

  10. catch ( Exveption e1 )

  11. {

  12. // 예외 처리 루틴 생략

  13. }

  14. // 메시지 처리를 위한 다른 이벤트 핸들러가 수행될 수 있도록 항상 상위 클래스가 구현해 둔

  15. // 이벤트 핸들러를 호출하도록 하며 사용자들은 그렇게 될 것을 기대한다.

  16. base.OnMouseDown(e);

  17. }

  18. }

또는 다음과 같이 이벤트 핸들러를 결합할 수도 있다.

  1. public class MyForm : Form

  2. {

  3. // 세부 구현 내용 생략

  4. public MyForm()

  5. {

  6. this.MouseDown += new MouseEventHandler(this.MouseDownHandler);

  7. }

  8. void MouseDownHandler( object sender, MouseEventArgs e )

  9. {

  10. try

  11. {

  12. HandleMouseDown(e);

  13. }

  14. catch ( Exception e1 )

  15. {  

  16. // 예외 처리 루틴 생략

  17. }

  18. }

  19. }

      위의 두 가지 방법 중 첫 번째 방법이 좀 더 좋아 보인다. 두 번째 방법과 같이 이벤트 핸들러를 사용하는 경우에는 이벤트 핸들러 내부에서 예외를 발생하는 경우 이벤트 핸들러 chain상에 존재하는 다른 이벤트 핸들러는 호출되지 않을 수 있다. protected virtual 메서드를 overriding하는 방법의 경우에는 항상 우리가 작성한 메서드가 먼저 호출된다. 이벤트 핸들러를 사용하는 경우에는 우리가 작성한 이벤트 핸들러가 항상 호출되리라 보장할 수 없다. 잘못 구성된 이벤트 핸들러가 예외를 발생시킬 수도 있기 때문이다. 하지만 하위 클래스에서 메서드를 overriding한다면 이벤트 발생시에 항상 우리가 작성한 메서드가 호출될 것을 보장할 수 있다.

      또한 overriding을 사용하는 것이 이벤트 핸들러를 사용하는 것에 비해 성능이 좀 더 좋다. 이벤트 핸들링 메커니즘은 특정 이벤트 발생시에 이벤트 핸들러가 결합되었는지의 여부를 확인해야 하기 때문에 override된 메서드를 호출하는 것이 비하면 더욱 많은 수행시간을 필요로 하게 된다.

      virtual 메서드를 overriding하는 것은 폼을 유지, 관리하기 위해서 단일의 메서드만을 수정하면 되는 반면, 이벤트 핸들러 메커니즘을 사용하게 되면 이벤트 핸들러 메서드와 이벤트 핸들러를 결합하는 두 개의 메서드를 수정해야 한다. 두 개의 메서드 모두가 잘못될 가능성이 있으므로 하나의 메서드가 더 단순하고 유지보수가 편하다고 할 수 있다.

 

 

36. 닷넷 런타임의 진단기능을 활용하라.#

      분석정보를 생성하도록 코드를 작성하면 사용자의 컴퓨터에서 문제가 아주 드물게 발생하는 경우라 하더라도 그 원인을 진단하는데 많은 도움이 된다.

      System.Diagnostics.Debug, System.Diagnostics.Trace, System.Diagnostics.EventLog 클래스는 수행중인 프로그램으로부터 분석정보를 생성하는 데 필요한 대부분의 기능을 제공한다. 앞의 2개의 클래스들은 거의 유사한 기능을 가지고 있는데 차이점이라면 Trace 클래스는 TRACE라는 전처리 심볼에 의해서 제어되며, Debug 클래스는 DEBUG 전처리 심볼에 의해서 결정된다는 것 정도이다. Visual Studio를 이용하여 프로젝트를 생성하는 경우라면, TRACE 심볼은 릴리즈와 디버그 빌드 양쪽에 모두 정의되어 있다. 반면 DEBUG 심볼은 디버그 빌드에만 정의되어 있다. 따라서 릴리즈 빌드에서의 분석결과는 Trace 클래스를 통해서 출력하도록 작성되어야 한다. EventLog 클래스는 프로그램이 시스템 이벤트 로그로 분석정보를 출력할 수 있도록 한다.

 

 

37. 표준 환경설정 메커니즘을 이용하라.#

      닷넷 프레임워크는 사용자의 설정정보, 애플리케이션 설치시의 정보, 하드웨어의 설정정보나 그 외의 많은 정보들을 저장하는 데 사용할 수 있도록 표준화된 특수한 위치들을 가져오는 기능을 가지고 있다. 이러한 위치들은 사용자가 컴퓨터에 대하여 제한된 권한만을 가지고, 애플리케이션을 수행하더라도 정상적으로 수행이 가능하다.

      XML로 구성된 애플리케이션 환경설정 파일은 애플리케이션 수행에 있어 주요한 설정정보들을 읽기전용의 형태로 포함하고 있는데, 이러한 정보들은 닷넷 프레임워크 클래스 라이브러이에 의해서 자체적으로 파싱(parsing)된다. 환경설정 파일 내의 <appSettings> 부분은 웹 애플리케이션이나 데스크탑 애플리케이션 양쪽 모두에 영향을 미치는 요소로서, 애플리케이션이 수행될 때 런타임이 읽어 들이며, 특히 <appSettings> 이하에 지정된 모든 값은 애플리케이션내의 Collection 객체에 key와 value 형태로 구성된다. <appSettings> 이하에는 애플리케이션 수행에 필요한 다양한 설정 정보들을 임의로 추가할 수 있으며, 애플리케이션의 동작 방식을 이러한 정보를 기반으로 변경할 수 있다. 물론 설정 파일의 내용을 변경하여 애플리케이션의 동작방식을 변경할 수도 있다.

 

 

38. 데이터 바인딩을 사용하라.#

     닷넷 프레임워크가 제공하는 데이터 바인딩은 특정 객체의 프로퍼티와 컨트롤의 프로퍼티를 연결할 수 있다.

  1. textBoxName.DataBindings.Add("Text", myDataValue, "Name");

      이 예제는 textBoxName 컨트롤의 'Text' 프로퍼티를 myDataValue 객체의 'Name' 프로퍼티와 연결하는 코드이다. 내부적으로는 BindingManager와 CurrencyManager라는 두 개의 객체가 컨트롤과 데이터 소스 간에 값을 전달하는 역할을 담당한다. 이러한 데이터 바인딩을 활용하면 좀 더 효율적인 코드를 쉽게 작성할 수 있다.

      바인딩을 이용하면 다음과 같은 이점이 있다. 첫째로 데이터 바인딩을 사용하면 좀 더 단순한 코드를 작성할 수 있다. 둘째로 데이터 바인딩은 화면상에 출력되는 요소 이외에도 적용 할 수 있다. 셋째로 윈도우 폼에서 데이터 바인딩을 사용하면 단일의 값과 연결된 여러 개의 컨트롤들 사이에 동기화를 유지할 수 있다.

      하나의 데이터 소스와 다수의 컨트롤들을 연결할 때에는 항상 동일한 데이터 소스 이름으로 데이터 바인딩을 구성해야 한다. 만일 각 컨트롤별로 출력해야 할 프로퍼티가 서로 다르다면 DataMember 인자를 이용하면 된다. 다음의 코드를 보자.

  1. // 나쁜 예 : 서로 다른 2개의 바인딩 관리자를 사용했다.

  2. textBox1.DataBindings.Add("Text", src.Results, "Profit");

  3. textBox1.DataBindings.Add("ForeColor", src, "ProfitForegroundColor");

      위와 같은 코드는 2개의 독립된 Binding manager를 만들게 된다. 하나는 src 객체 자체를 위한 것이고, 다른 하나는 src.Results를 위한 것이다. 각각의 데이터 소스는 서로 다른 Binding manager에 의해서 관린된다. 만약 단일의 데이터 소스가 변경되었을 때 Binding manager가 변경된 모든 프로퍼티를 갱신하길 원한다면, 데이터 소스를 반드시 일치시켜 주어야 한다.

      거의 모든 윈도우 컨트롤이나 웹 컨트롤의 프로퍼티에 대해서 데이터 바인딩을 수행하는 것이 가능하다. 컨트롤에 출력되어야 하는 값, 폰트, 읽기 전용의 상태나 컨트롤 위치정보까지도 데이터 바인딩에 포함될 수 있다. 개발하고 있는 클래스나 구조체가 사용자의 요청에 따라 화면상에 출력되어야 하는 정보를 포함하고 있다면, 컨트롤의 상태를 갱신하기 위해서 항상 데이터 바인딩을 사용할 것을 권한다.

 

 

39. 닷넷의 유효성 검증 기능을 사용하라.#

      닷넷 프레임워크는 사용자가 입력한 데이터에 대한 유효성 검증을 위해 많은 기능들을 제공해 주고 있다. 하지만 완벽한 검증을 위해서는 여전히 추가적인 노력이 필요하다.

      RegularExpressionValidator를 사용하면 정규표현식을 이용할 수 있다. 정규표현식을 통한 비교 결과가 일치하면 데이터 유효한 것으로 치리한다. 정규표현식은 매우 강력하여 매우 다양한 상황에서 적절한 검증 구문을 만들어 낼 수 있다. 아래 표에는 유효성 검증을 위해서 비교적 많이 사용되는 정규표현식 구성요소만을 나열해 보았다. 

요소 의미
 [a-z]  소문자 한글자를 표현. 대괄호내에 글자집합 중 임의의 한 개를 의미
 \d  임의의 숫자 한 자리
 ^,$  ^는 문장의 시작, $는 문장의 마지막을 의미
 \w  문장표기를 위한 글자. [A-Za-z0-9]의 단축 표현
 (?NamedGroup\d{4,16})

 이 예제는 2가지의 공통적인 요소를 보여주고 있음. ?NamedGroup은 새로운 참조를 정의하는 데 사용됨

 {4,16}은 앞서의 요소가 4번 이상 16번 이하로 반복되어야 함을 의미하며, 이 예제에서는 4자리 이상, 16자리 이하의 숫자를 의미함

 즉, 4자리 이상 16자리 이하의 숫자가 발견, 이러한 숫자를 참조하는 NamedGroup을 정의하여, 추후에 NamedGroup 이름으로 참조할 수 있도록 함

 (a/b/c)  a이거나 b이거나 c인 경우
 (?(NamedGroup)a|b)  이는 C#의 3항 연산자와 유사하게 NamedGroup으로 정의된 값이 존재하면 a, 그렇지 않은 경우 b와 일치

      이러한 정규표현식을 사용하면 사용자가 입력한 데이터에 대한 충분한 검증을 수행할 수 있다. 그럼에도 정규표현식조차도 충분하지 않다고 느낀다면, CustomerValidator 클래스를 상속받아서 자신만의 유효성 검증 클래스를 만들 수도 있다. 하지만 이러한 작업은 상당한 분량의 작업이 필요하기 때문에 반드시 필요한 경우가 아니라면 피하는 것이 좋다.

 

 

40. 적절한 collection 개체를 이용하라.#

      닷넷 프레임워크는 특수한 형태의 다양한 collection들을 가지고 있고 이러한 collection들은 공통적으로 ICollection interface를 구현하고 있다. ICollection에 대한 문서를 찾아보면, 이 interface를 구현한 모든 클래스들의 리스트를 볼 수 있다. 즉시 활용할 수 있는 20개 이상의 collection들이 이미 구현되어 있다.

      collection을 이용하는 프로그램은 가능한 interface를 기반으로 프로그램을 개발하는 것이 좋다. 이렇게 함으로써 최초 프로그램 개발시에 우리가 세웠던 여러 가지 가정들이 잘못되어 기존에 사용했던 collection을 다른 것으로 바꾸어야 하는 상황이 오더라도 쉽게 변경이 가능하다.

      닷넷 프레임워크에서 제공하는 collection은 크게 배열, 배열과 유사한 collection, 해시 기반 컨테이너 세 가지로 구분할 수 있다.

      어떤 collection을 사용하는 것이 좋은가를 결정하기 위해서는 collection에 대하여 어떤 동작을 주로 수행하는가와 애플리케이션의 목표라 할 수 있는 공간과 속도 문제와도 매우 밀접하게 연관되어 있다. 하지만 많은 경우 배열을 이용하는 것이 효과적이다. C# 언어에 추가된 다차원 배열은 다차원의 자료구조를 표현하는 데 있어 최적의 구조이며 수행성능도 빠르다. collection에 새로운 아이템을 추가, 삭제하는 작업이 빈번한 경우라면 닷넷 프레임워크가 제공하는 다른 collection들을 고려해 보아야 한다. 마지막으로 collection을 포함하는 새로운 클래스를 작성하는 경우에는 인덱서와 IEnumerable interface를 같이 구현하는 것이 좋다.

 

 

41. 새로운 구조체보다는 DateSet이 좋다.#

      DataSet은 두 가지 이유로 인해 좋지 앟은 평가를 받고 있다. 첫째로 serialize된 DataSet은 닷넷 이외의 코드와는 상호운용이 불가능하다. 둘째로 DataSet은 매우 포괄적인 클래스이기 때문에 DataSet을 잘못 사용하면 닷넷 프레임워크의 타입 안정성을 망가트릴 수도 있다. 하지만 DataSet은 최신의 시스템들을 구축하기 위해 필요한 많은 부분을 해결할 수 있을 만큼 매우 강력한 클래스이다.

      DataSet은 관계형 데이터베이스로부터 획득된 데이터의 오프라인 저장소 역할을 수행하도록 설계되었다. 데이터베이스의 구조와 유사한 행과 열로 구성된 데이터를 저장하기 위한 DataTable이 DataSet에 포함될 수 있다는 것은 이미 잘 알려진 사실이다. 또한 DataSet과 DataSet이 포함하고 있는 객체들은 모두 데이터 바인딩을 지원한다는 사실도 알고 있을 것이다. DataTable 사이에 관계 설정을 어떻게 하는지에 대해서나 DataSet내에 유효한 데이터만을 유지하기 위하여 어떻게 제약조건을 설정하는지에 대해서도 이미 잘 알고 있으리라 생각한다.

      하지만 DataSet은 이러한 역할 이상의 일을 수행할 수 있다. DataSet은 내부데이터의 변경이력을 DiffGram이라고 알려진 부분에 저장해 두었다가 AcceptChanges()나 RejectChanges()메서드를 호출하여 트랜잭션 관리를 수행할 수도 있다. 여러 개의 DataSet은 동일 저장소를 사용하도록 병합될 수도 있다. 또한 데이터의 일부만을 획득하는 view의 기능을 수행할 수도 있다. 이러한 view는 여러 개의 테이블이 연관되는 경우에도 잘 동작한다.

      그럼에도 불구하고 일부 개발자들은 DataSet을 사용하기 보다는 자료 저장소로 새로운 구조체를 개발하는 것을 선호한다. DataSet은 다양한 자료구조를 수용할 수 있도록 작성되었기 때문에 수행성능에 있어서는 일부 불이익이 있을 수 있으며, 타입 안정적인 컨테이너도 아니다. 실제로 DataSet 내의 DataTable은 dictionary colection으로 작성되어 있고, 테이블내의 각 열들도 dictionary를 이용하도록 작성되었으며, System.Object의 참조형으로 자료를 저장한다. 따라서 DataSet으로부터 아이템을 획득하기 위해서는 다음과 같이 코드를 작성해야 한다.

  1. int val = (int)MyDataSet.Tabbles["table1"].Row[0]["total"];

      타입 안정적인 C# 언어라는 관점에서 본다면, 이러한 코드는 매우 적절하지 않은 구조이다. 만일 table1이나 total과 같은 문자를 잘못 입력하기라도 한다면, 프로그램 수행시에 오류를 유발할 것이며, 값을 획득할  때에는 항상 형변환을 수행해야만 한다. 만일 DataSet 내의 값에 접근하는 코드가 반복적으로 사용되는 경우라면, 타입 안정적으로 접근할 수 있는 다른 대안이 있었으면 하는 바램을 가지게 될 것이며, 다음과 같이 DataSet을 이용할 수 있기를 바랄 것이다.

  1. int val = MyDataSet.table1.Row[0].total;

      이 코드는 typed DataSet을 작성한 경우에 완벽하게 동작하는 코드이다. typed DataSet을 구성하기 위해서 자동으로 생성된 코드는 타입 안정적이지 않은 DataSet을 감싸서, 타입 안정적으로 값에 접근할 수 있도록 해 준다.

      DataSet을 사용하지 않고 자신만의 구조체를 만들어 쓰고자 하는 경우 DataSet의 많은 기능을 포기하게 된다. 수행성능이 매우 중요한 부분에 사용되는 collection이거나 매우 가벼운 포맷을 정의하는 경우가 아니라면 DataSet을 사용하라. 그 중에서도 Typed DataSet을 사용하라. DataSet은 엄청난 시간을 절약해 줄 것이다.

 

 

42. reflection을 단순화하기 위해서 attribute를 사용하라.#

      attribute는 런타임시 프로그램이 어떻게 수행되도록 할 것인가에 대한 개발자의 의도를 담고 있다고 볼 수 있다. 언어요소에 attribute를 지정함으로써 찾아내고자 하는 요소를 좀 더 쉽게 찾아낼 수 있다. 이러한 attribute가 없다면 타입의 이름을 정의하는 특별한 규칙을 만들어 두고, 그러한 규칙을 따르도록 타입의 이름을 붙여야만 런타임시 쉽게 활용될 것이다. 하지만 이러한 규칙은 기억하기도 힘들고, 개발시에 잘못 입력하는 등의 실수를 범할 수 있기 때문에 오류를 유발할 가능성이 매우 높다. attribute를 이용하여 개발자의 의도를 표현하게 되면, 특정 규칙에 맞도록 이름을 정해야만 하는 것과 같이 개발자가 짊어질 책임을 컴파일러에게 이전하는 효과가 있다.

      Reflection을 이용하면 동적으로 코드를 생성하는 것이 가능하다. 설계단계나 구현단계에서 타입, 메서드, 프로퍼티들에 대해서 사용자 정의 attribute를 고려한다면 런타임시에 오류를 발생할 가능성을 현저히 낮출 수 있다.

 

 

43. reflection을 과도하게 사용하지 말라.#

      reflection은 매우 강력한 기능이고 이를 이용하면 좀 더 동적인 소프트웨어를 개발하는 것이 가능하다. 하지만 유연성이 증가하면 복잡성도 같이 증가하며, 복잡성이 증가되면 문제를 유발할 가능성 또한 높아진다. 또한 reflection을 사용하면 C#의 타입 안정성이라는 특징은 사라진다. 간단히 말하면 reflection을 사용하게 되면 동적인 프로그램을 쉽게 만들 수 있지만 동시에 프로그램이 제대로 동작하지 않을 가능성도 많아진다.

      reflection은 특정 객체에 대해서 interface를 통해서 동작을 정확히 표현할 수 없는 경우에 한해서만 제한적으로 사용하는 것이 좋다. 만일 interface를 활용함과 동시에 attribute를 지정한 factory 메서드를 이용하면 reflection을 기반으로 구현해야 했던 거의 대부분의 기능들을 더욱 단순하게 구현할 수 있다.

      reflection은 느린 바인딩을 수행하는 강력한 메커니즘이다. 닷넷 프레임워크는 WinForm 기반이나 WebForm 기반의 모든 컨트롤들에 대해서 데이터 바인딩을 지원하기 위해서 reflection을 사용한다. 하지만 많은 경우에 있어 reflection을 사용하기보다는 클래스 factory, delegate, interface등을 활용하는 것이 유지보수가 쉬운 시스템을 구성하는 데 있어 많은 이점을 가져다 준다.

 

 

44. 애플리케이션에 특화된 예외 클래스를 완벽하게 작성하라.#
  1. try

    {

    Foo();

    Bar();

    }

    catch ( MyFirstApplicationException e1 )

    {

    FixProblem(e1);

    }

    catch ( AnotherApplicationException e2 )

    {

    ReportErrorAndContinue(e2);

    }

    catch ( Exception e )

    {

    ReportGenericError(e);

     }

    finally

    {

    CleanupResource();

    }

C#에서는 위와 같이 각각의 예외 별로 catch 구문을 반복적으로 배치하여 각각의 예외 발생시에 서로 다른 동작을 수행하도록 catch 구문을 작성할 수 있다. 다양한 예외 클래스들을 미리 작성해두면 개발자가 각 상황에 맞추어 서로 다른 동작을 수행하도록 catch 구문을 작성하는 것이 좀 더 수월해진다. 복구 가능한 다양한 오류 상황들을 검토하고 각각의 오류 상황별로 적절한 예외 클래스를 작성하자. 특정 오류가 발생했을 때 서로 다른 방법으로 오류상황에 대한 복구를 시도해야 한다면 각각에 대해서 새로운 예외 클래스를 만드는 것이 좋다.

새로운 예외 클래스를 만들 때는 반드시 준수해야 하는 몇 가지 규칙이 있다. 먼저 모든 사용자 정의 예외 클래스는 System.Exception이 아니라 System.ApplicationException 클래스를 상속받도록 작성되어야 한다. catch 구문 내에서 어떤 오류상황에서 예외가 발생했는지를 효과적으로 식별하기 위해서는 각각의 오류상황에 대하여 다양한 예외 클래스들을 가능한 미리 작성해 두는 것이 좋다.

모든 사용자 정의 예외 클래스의 기반 클래스인 ApplicationException 클래스는 4개의 서로 다른 형태의 생성자를 가지고 있다.

  1. // 기본 생성자

  2. public ApplicationException();

  3. // 오류 내용으로 객체 생성

  4. public ApplicationException(string message);

  5. // 오류 내용과 내부 예외객체를 이용하여 객체 생성

  6. public ApplicationException(string message, Exception innerException);

  7. // 스트림을 통한 객체 생성

  8. public ApplicationException(SerializationInfo info, StreamingContext context);

각각의 생성자들은 서로 다른 상황에서 각기 호출될 수 있기 때문에 새로운 예외 클래스를 만들때에는 4개의 생성자 모두를 재작성해야 한다. 또한 각각의 생성자들은 상위 클래스의 생성자를 재사용하도록 작성되어야 한다.

예외 발생시점에 좀 더 자세한 추가정보를 제공할 수 있다면 우리 자신에게나 또는 우리가 개발한 라이브러리의 사용자에게까지도 문제 상황을 정확히 분석하고 오류를 고치는 데 많은 도움이 될 것이다.



Chapter 06 – 기타#


45. 견고한 예외 보증 기법이 더 좋다.#

      데이브 에브러험은 기본 예외 보증, 강력한 예외 보증, 완벽한 예외 보증 등 세 가지의 예외 보증 기법을 정의하였다. 기본 보증이란 어떠한 자원도 부족해지지 않으며, 모든 객체는 설사 예외가 발생하더라도 유효한 상태를 유지하는 것을 말한다. 강력한 보증은 기본 보증에 더하여 만약 예외가 발생하면 프로그램의 상태가 변경되지 않도록 하는 것을 말한다. 완벽한 예외 보증은 어떤 경우라도 작업과정에 실패하지 않으며, 이 과정에서 호출되는 메서드들은 절대로 예외를 유발시키지 않는 것을 말한다. 이 중 강력한 예외 보증 기법은 예외 발생시에 어떻게 복구를 수행할 것인가 하는 복잡한 문제와 예외 처리 루틴의 단순화 사이의 적절한 타협점이라 할 수 있다.

      기본 보증은 닷넷과 C#에서의 기본적인 예외 보증 기법이다. 닷넷 환경에서 예외발생시에 자원의 누수가 생기는 유일한 경우는 IDisposable을 직접적으로 구현하고 있는 자원을 사용하는 과정이다.

      강력한 보증은 만일 어떤 작업이 수행 중에 예외가 발생한다 하더라도 프로그램의 상태는 변경되지 않는 것을 보증하는 기법이다. 이는 특정 작업이 완벽하게 끝나는 경우에만 프로그램의 상태가 바뀌는 것을 보증한다. 이러한 강력한 보증 기법의 장점은 예외가 발생한 이후에도 매우 쉽게 작업을 계속해 나갈 수 있다는 것이다. 특정 동작을 수행하는 중에 예외가 발생하면, 마치 예외를 일으키지 않은 것처럼 이전 상태를 유지하도록 만드는 것이다. 강력한 예외 보증을 구현하려면 애플리케이션이 자신의 상태를 변경하는 과정에서 다음과 같은 규칙을 따라야 한다.

         1. 값이 변경되어야 하는 데이터에 대한 복사본을 미리 만들어 둔다.

         2. 복사본에 대해서 변경을 수행한다. 변경작업의 일부는 예외를 유발시킬 수 있다.

         3. 실제 데이터를 복사본으로 대체한다. 이러한 대체 동작은 절대로 예외를 유발시켜서는 안 된다.

      reference 타입을 복사본으로 대체하는 메커니즘을 사용할 때에는 특별히 주의를 기울여야 한다. 이러한 동작은 잠재적 버그를 내포할 가능성이 있다.

      마지막으로 가장 엄격한 완벽한 예외 보증에 대해서 알아보자. 어떤 메서드가 호출되고 반활될 때까지 어떠한 예외도 발생하지 않는 경우라면 이 메서드는 완벽한 예외 보증을 준수하고 있다고 할 수 있다. 하지만 프로그램 전반에 걸쳐 모든 메서드들이 이러한 규칙을 준수하도록 개발하는 것은 그다지 실용적이지 않다. 하지만 Finalizer와 Dispose(), delegate target 메서드 등과 같은 메서드들은 반드시 완벽한 예외 보증 규칙을 준수해야 하며, 어떠한 경우라도 예외를 발생시켜서는 안 된다.

 

 

46. Interop를 최소화하라.#

      Interop가 해결하고 있는 문제는 그저 이전의 코드를 사용할 수 있는 방법이 있다는 수준 정도이다. 제어 흐림이 native 코드와 managed 코드 사이를 오가는 경우는 marshalling을 수행하여 적절한 자료형으로 변경하도록 한다는 점이 Interop가 해주는 거의 유일한 작업이다. 게다가 Interop를 사용하려면 메서드의 형태를 개발자가 직접 선언해 주어야만 하는 경우도 생긴다. 마지막으로 CLR은 Interop가 연관되는 코드에 대해서는 어떠한 최적화도 수행하지 못한다.

      managed 코드와 unmanaged 코드를 오가는 경우에 발생할 수 있는 성능상의 비용과 비효율성에 대해서 알아야 할 필요가 있다. 첫 번째로는 managed 힙과 unmanaged 힙 사이에 데이터를 상호 변환하는 marshalling에 필요한 비용이다. 두 번째로는 managed 코드 혹은 unmanaged 코드를 호출하기 위해 필요한 부가적인 코드의 수행문제이다. 이 두 가지는 수행성능상의 불이익과 관련되어 있다. 마지막으로 managed와 unmanaged 코드가 섞여 있는 복잡한 환경에서 작성해야 하는 많은 코드들을 개발자가 직접 작성해야 한다는 것이다. 따라서 이러한 복잡한 시스템의 경우 비용을 어떻게 최소화할 것인가를 염두에 두고 설계를 진행해야만 한다.

      이제 interop와 관련된 성능상의 비용과 그러한 비용을 어떻게 최소화할지에 대해서 알아보자. Marshalling은 가장 큰 성능상의 손실을 가져다 주는 요소다. 만일 unmanaged 코드를 재상용해야 하는 경우에는 새로운 API를 추가해서라도 기능을 세분화하도록 API를 구성해서 필요없는 marshalling이 제거되도록 해야 한다.

      COM 객체를 감싸는 새로운 wrapper를 만드는 경우에는 managed 코드와 unmanaged 코드 사이에 전달되는 데이터의 타입을 변경함으로써 성능상의 이익을 얻을 수 있다. managed 코드와 unmanaged 코드에서 내부구조가 완전히 동일한 자료형을 blittable 타입이라고 한다. 구조가 동일하므로 marshalling 단계에서 내부적인 값의 형태를 고려할 필요없이 단순 복사만 수행하면 되므로 빠르다.

      데이터 타입을 blittalble 타입만으로 제한하는 것이 불가능하다면, InAttribute와 OutAttribute를 사용하여, 데이터의 복사 횟수를 최소화할 수 있다.

      마지막으로 어떻게 데이터를 marshalling할 것인가를 선택함으로써 수행성능을 끌어올릴 수 있다.

      지금부터는 어떻게 managed 코드가 unmanaged 코드를 호출하도록 하겠는가에 대해 알아보자.

      만일 이미 개발된 COM 객체를 사용하는 경우라면 COM interop를 사용하자. 기존의 C++로 개발된 코드를 재사용해야 하는 경우라면 /clr 컴파일 옵션과 managed C++의 확장기능을 사용하여 기존 코드를 활용하는 라이브러리를 만들어서 C#에 활용하는 것이 최상의 방법이다.

 

 

47. 안전한 코드가 더 좋다.#

      특정 어셈블리가 FullTrust 신뢰수준을 가지지 않는다면, CLR이 이러한 어셈블리를 수행할 때 일부 동작이 제한도리 수 있다. 이것을 code access security(CAS)라고 한다. 또 다른 보안관련 사항으로는 CLR이 role-based security를 사용한다는 것이다. role-based security란 프로그램을 수행하는 사용자의 권한에 따라 프로그램의 일부 동작이 제한될 수 있음을 의미한다.

      닷넷의 managed 환경은 일정부분 안정성을 보장한다고 볼 수 있다. 닷넷 프레임워크 라이브러리는 설치시에 FullTrush 신뢰수준을 가지도록 구성된다. 그럼에도 CLR은 IL을 수행할 때 메모리를 직접 접근하는 것과 같이 잠재적인 위험을 내포하고 잇는 코드가 수행될 수 없도록 지속적인 감시를 수행한다.

      안전한 어셈블리 내에서는 unmanaged 메모리 영역에 대한 접근이 원천적으로 차단된다. 사실 안전한 어셈블리가 되기 위해서는 managed 메모리 형역 혹은 unmanaged 메모리 영역 어느 쪽을 가리키는 포인터도 사용되지 않아야 한다. 컴파일시에 /unsafe 옵션을 주지 않고 컴파일을 수행하게 되면, 생성된 어셈블리는 모두 안전한 어셈블리이다. CLR은 이러한 어셈블리에 대해서 지속적인 감시를 수행한다. /unsafe 옵션을 사용하게 되면 포인터를 활용할 수 있지만 이 경우 CLR은 코드의 보안위배 사항에 대한 감시를 수행하지 않게 된다.

      드문 경우이긴 하지만 반드시 unsafe 코드를 사용해야만 하는 경우가 간혹 있다. 가장 일반적인 사례가 수행성능이 매우 중요한 프로그램을 작성하는 경우이다. 아무래도 포인터를 통해 메모리에 직접 접근하는 것이 안전한 참조자를 이용하는 것에 훨씬 빠르게 동작한다.

      닷넷의 보안모델은 프로그램 수행시에 지속적으로 권한정보를 확인한다. 따라서 수행할 프로그램이 적절한 권한을 가지고 있는지에 대해서 항상 주의해야 하며, 필요한 최소한의 권한만을 가지도록 하는 것이 좋다. 프로그램이 보호된 자원들을 덜 사용하면 할수록 보안과 관련된 예외가 발생할 가능성이 적은 것은 너무나 당연하다. 가능한 보호된 자원들을 덜 사용하고, 다른 대안을 모색해보자. 특정코드를 수행하기 위해서 반드시 높은 권한이 필요한 경우라면 그러한 코드들은 독립된 어셈블리로 격리하자.

 

 

48. 활용할 수 있는 다양한 툴과 리소스에 대해서 알아두라.#

 NUnit

이 툴은 www.nunit.org를 통해서 얻을 수 있다. NUnit은 자동화된 단위테스트를 수행하는 도구로써 JUnit과 기능적으로 거의 동일하다.

 FXCop

FXCop은 어셈블리내의 IL를 분석하여 분석대상 어셈블리가 미리 정해둔 여러 가지 규칙이나 구조 등을 준수하고 있는지를 확인하여 그 결과를 보고하는 툴이다.

 IldAsm

이 툴은 닷넷 프레임워크 SDK에 포함되어 있다. 이 툴을 이용하면 우리가 개발한 어셈블리의 IL 코드뿐 아니라 닷넷 프레임워크 내의 어셈블리까지도 그 내용을 확인해 볼 수 있다.

 http://msdn.microsoft.com/vcsharp

C#팀의 MSDN에 있는 공식사이트.

 http://www.asp.net

ASP.NET팀의 공식 사이트. 주요업무가 웹에 관련된 내용이라면 방문해 볼만한 사이트

 http://windowsclient.net

윈도우 폼팀의공식 사이트. 주요업무가 윈도우 폼과 관련되어 있다면 방문.

 http://msdn.microsoft.com/ko-kr/practices

MS Patterns & Practices. 이 사이트를 통해 프로그램 개발시에 공통적으로 사용되는 pattern에 대한 정보를 확인할 수 있고, 최고의 예제를 접할 수 있다.

 http://blogs.msdn.com/csharpfaq/

C# 팀의 FAQ 페이지.

 shared source CLI

닷넷 프레임워크와 C# 컴파일러의 핵심적인 내용을 포함하고 있다. C# 언어와 닷넷 프레임워크에 대한 좀 더 심도있는 내용을 이해하기 위해서 코드를 읽어볼 수도 있다.

 

 

49. C# 2.0의 주요 특징#

      C# 2.0에는 generics, iterator, anonymous 메서드, partial 타입의 네 가지 주요기능이 추가되었다. 이러한 기능 전부는 C# 개발자의 생산성을 높이는 데 초점을 맞추고 있다.

      generics는 '인자를 통한 다형성'을 제공한다. 이것은 하나의 소스를 통해서 유사한 일련의 클래스들을 만들어 내는 고급기술이다. 컴파일러는 generics에 인자로 전달되는 타입에 따라 서로 다른 형태의 객체를 생성할 수 있도록 한다. generics를 적용할 수 있는 또 다른 사례로는 닷넷 프레임워크의 collection들을 예로 들 수 있다. 인자로 지정된 타입의 객체들만을 저장 할 수 있도록 구성된 collection 클래스들을 System.Collectiions.Generics namespace에 포함하고 있다. C# 설계자는 C++에서 template을 사용할 경우에 종종 발생하는 수행파일의 크기문제를 피할 수 있도록 C#을 설계하였다. 공간절약을 위해 JIT 컴파일러는 모든 reference 타입을 위해 인자의 타입과 상관없는 하나의 코드만을 생성한다. 즉 value 타입은 인자로 지정된 타입별로 서로 다른 타입을 생성하고 Reference 타입의 경우 인자와 상관없이 System.Object형 reference 타입을 저장하는 코드를 공유한다. C# 컴파일러는 이렇듯 코드를 공유함에도 불구하고 적절하지 않은 reference 타입의 사용에 대해서는 오류를 보고할 수 있도록 변경되었다.

      generics를 수용하기 위해서 CLR과 MSIL에도 일부 변화가 있었다. Generic 클래스를 컴파일 하면 MSIL은 각 인자로 전달될 타입을 위한 공간을 따로 마련해 둔다. 다음의 두 가지 메서드를 살펴보라.

  1. .method public AddHead(!0 t) {

  2. }

  3. .method public !0 Head() {

  4. }

!0은 인자로 전달될 타입을 위한 공간으로 generic 클래스의 인스턴스가 생성되는 시점에 다음과 같이 대체된다.

  1. .method public AddHead(System.Int32 t) {

  2. }

  3. .method public System.Int32 Head() {

  4. }

      이처럼 generics를 지원하기 위해서 C# 컴파일러와 JIT 컴파일러는 서로 다른 각각의 역할을 담당하고 있다. C# 컴파일러는 인자로 전달될 타입을 위한 공간을 마련하고 JIT 컴파일러는 이러한 공간을 인스턴스 생성시에 특정 타입으로 대체한다. 인자로 모든 reference 타입을 수용할 수 있도록 System.Object를 주든지 또는 적절한 value 타입을 주든지와 상관없이, generic 클래스의 인스턴스들은 타입 정보를 포함하고 있기 때문에 C# 컴파일러는 인스턴스의 타입안정성을 보장할 수 있다.

     

      Iterator는 enumerator pattern을 좀 더 적은 코딩으로 구현할 수 있도록 하기 위해서 추가된 문법사항이다. C# 2.0은 yield라는 키워드를 추가하여 좀 더 간결하게 코드를 구성할 수 있도록 변경되었다.

  1. public class List

  2. {

  3. public IEnumerator GetEnumerator()

  4. {

  5. int i = 0;

  6. while( i < theList.Length )

  7. yield return theList[i++];

  8. }

  9. // 세부 구현 내용 생략

  10. }

      내부적으로 살펴보면 컴파일러는 이전의 방법처럼 IEnumerator interface를 구현한 클래스를 생성하며 필요한 메서드를 적절히 구현한다. 하지만 중요한 것은 이러한 작업을 우리가 직접 하는 것이 아니라 컴파일러가 내부적으로 해준다는 것이다.

 

      마지막으로 살펴볼 주요 기능은 partial 타입이다. partial 타입이란 여러 개의 파일에서 하나의 C# 클래스를 나누어 구현할 수 있도록 해주는 기능을 말한다. Microsoft의 IDE와 자동화된 코드생성기에서 이 기능을 많이 활용한다. 가능한 IDE가 자동으로 생성해주는 partial 타입의 경우를 제외하고는 partial 타입을 직접 사용하지 않는 편이 좋다.

 

 

50. ECMA 표준을 익혀라.#

ECMA 표준문서는 C# 언어가 어떻게 동작하는가를 기술한 공식문서이다.