아이템 14: 초기화 코드가 중복되는 것을 최소화하라

생성자를 작성하다 보면 종종 동일한 작업을 되풀이할 때가 있어서 기존에 작성한 코드를 그대로 복사하곤 한다.

공통적으로 수행하는 코드를 private 메서드로 뽑아내어 생성자에서 호출하는 방법도 있지만 C#에서는 권장할 만한 방법이 아니다.

여러 생성자 내에서 동일한 코드를 반복적으로 사용해야 공용으로 사용할 수 있는 생성자를 작성하는 편이 낫다.

이렇게 하면 코드가 중복되는 것을 막고 더 효율적인 생성자를 작성할 수 있다.

C# 컴파일러는 공용 생성자를 이용하는 초기화 방식을 매우 특별한 문법으로 인식한다.

변수에 대한 중복 초기화 코드를 제거해줄 뿐 아니라 베이스 클래스의 생성자가 반복적으로 호출되는 것도 막아준다.

즉 객체 초기화를 위해 수행해야 하는 코드를 최적화해준다.

개발자 입장에선 초기화 과정 일부를 공용 생성자에 위임할 수 있으므로 작성하는 코드의 양을 최소화할 수 있다.

생성자를 이용하여 멤버 변수의 값을 초기화하는 경우라면 다른 생성자를 호출하여 초기화 과정의 일부를 위임할 수 있다.

public class MyClass
{
    // 데이터 컬렉션
    private List<ImportantData> coll;

    // 인스턴스 변수
    private string name;

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

    }

    public MyClass(int initialCount) : this(initialCount, stirng.Empty)
    {

    }
    
    public MyClass(int initialCount, string name)
    {
        coll =  (initialCount > 0) ? new List<ImportantData>(initialCount) : new List<ImportantData>();
        this.name = name;
    }
}

C# 4.0에 추가된 기본 매개변수 기능을 활용하면 생성자 내의 중복 코드를 더욱 줄일 수 있다.

public class MyClass
{
    // 데이터 컬렉션
    private List<ImportantData> coll;

    // 인스턴스 변수
    private string name;

    // new() 제약 조건을 만족시키기 위한 생성자
    public MyClass() : this(0, string.Empty)
    {

    }

    public MyClass(int initialCount = 0, string name = "")
    {
        coll =  (initialCount > 0) ? new List<ImportantData>(initialCount) : new List<ImportantData>();
        this.name = name;
    }
}

여러 개의 생성자를 작성하는 대신 기본값을 갖는 매개변수를 취하는 생성자를 작성할 때는 몇가지 트레이트 오프를 고려해야 한다.

우선 기본값을 갖는 매개변수는 사용자에게 더 많은 옵션을 제공한다.

앞의 예제에서는 두 매개변수에 대하여 기본값을 지정했으므로 사용자는 매개변수를 지정하지 않거나, 하나 혹은 둘 모두에 임의의 값을 지정할 수 있다.

이 기능이 없다면 4개의 생성자를 만들어야 했을 것이다. (필요로 하는 인수에 따라 경우의 수 증가)

기본값을 가진 매개변수를 활용하면 코드의 양을 최소화할 수 있다.

생성자의 모든 매개변수에 대해서 기본값을 정의한 경우 new MyClass()라고만 작성해도 유효한 코드가 된다.

하지만 어떤 경우에도 제한 없이 이런 구조를 사용하려면 앞의 예제와 같이 매개변수가 없는 생성자(즉 기본 생성자를 임의로 생성)작성해야 한다.

대부분의 기본값을 가지는 매개변수를 취하는 생성자가 사용될 것이므로 큰 문제는 없다.

그러나 new() 제약 조건을 명시한 제네릭 클래스와 함께 사용해야 할 경우에는 기본값을 가지는 매개변수를 취하는 생성자만으로는 이 제약 조건을 만족시킬 수 없으며 반드시 매개변수가 없는 생성자를 구현해야 한다.

당연한 이야기지만 모든 타입이 매개변수가 없는 생성자를 반드시 가져야 할 이유는 없으며 단지 new() 제약 조건을 만족시키려면 매개변수가 없는 생성자를 반드시 구현해야 한다는 것이다.

초기화 과정의 컴파일 타임 상수

앞 코드의 생성자를 보면 name 매개변수의 기본값을 string.Empty가 아니라 ““로 지정했다.

여기서 string.Empty를 사용할 수 없는 이유는 이 값이 컴파일 타임 상수가 아니라 string 클래스에서 정의하고 있는 정적 속성이기 때문이다.

매개벼수의 기본값으로는 컴파일타임 상수만을 지정할 수 있다.

기본값을 갖는 매개변수를 취하는 생성자의 단점

여러 개의 생성자를 만드는 대신 기본값을 갖는 매개변수를 취하는 생성자를 만들게 되면 이 타입을 사용하는 코드와의 결합도가 높아지는 단점도 있다.

기본값을 갖는 매개변수를 사용하면 형식 매개변수의 이름과 매개변수의 기본값이 모두 공개 인터페이스의 일부가 된다.

이로 인해 매개변수의 이름을 변경하면 이 타입을 사용하는 모든 코드를 다시 컴파일해야 한다.

머지않아 코드를 변경할 수도 있는 상황이라면 기본값을 갖는 매개변수를 사용하는 것보다 기존 방식과 같이 여러 생성자를 오버로딩하는 편이 나을 수도 있다.

이 경우 새로운 생성자를 추가하거나 기존 동작 방식을 변경하더라도 클라이언트 코드를 다시 컴파일할 필요가 없기 때문이다.

내부 컴파일러 동작 과정

생성자를 정의할 때 기본값을 갖는 매개변수를 사용하는 방법은 널리 사용되는 방식이지만 일부 API는 리플렉션을 통해 객체를 생성하기 때문에 매개변수가 없는 생성자가 반드시 필요한 경우가 있다.

사용자 측면에선 둘 다 비슷해 보이지만 컴파일러는 둘을 완전히 동일한 것으로 간주하지 않는다.

따라서 어떤 경우에는 매개변수가 없는 생성자를 작성해야 할 수도 있다.

이 경우 중복을 피하기 위해 처음 언급했던 유틸리티 메서드를 만들 수 있지만 앞 예제와 같이 생성자 체인 기법을 이용하는 것이 낫다.

생성자 체인 기법이란 임의의 생성자가 동일 클래스 내에 정의된 다른 생성자를 호출하는 방식을 말한다.

public class MyClass
{
    private List<ImportantData> coll;
    private string name;

    public MyClass()
    {
        commonConstructor(0, "");
    }

    public MyClass(int initialCount)
    {
        commonConstructor(initialCount, "");
    }

    public MyClass(int initialCount, string name)
    {
        commonConstructor(initialCount, name);
    }

    private void commonConstructor(int initialCount, string name)
    {
        coll =  (initialCount > 0) ? new List<ImportantData>(initialCount) : new List<ImportantData>();
        this.name = name;
    }
}

이 코드는 첫 번째 예와 비슷해 보이지만 훨씬 비효율적인 코드를 생성한다.

실제로 컴파일러는 이 코드를 컴파일할 때 사용자가 작성하지 않은 코드를 추가한다.

먼저, 모든 인스턴스 변수에 대한 초기화 코드가 추가된다.

다음으로 베이스 클래스의 생성자를 호출하는 코드가 추가되고, 마지막으로 사용자가 작성한 공유 유틸리티 함수를 호출하는 코드가 추가된다.

컴파일러는 해당 과정에서 중복을 제거하지 못한다.

읽기 전용 상수에 대한 초기화

public class MyClass
{
    private List<ImportantData> coll;
    private int counter;
    private readonly string name;

    public MyClass()
    {
        commonConstructor(0, "");
    }

    public MyClass(int initialCount)
    {
        commonConstructor(initialCount, "");
    }

    public MyClass(int initialCount, string name)
    {
        commonConstructor(initialCount, name);
    }

    private void commonConstructor(int initialCount, string name)
    {
        coll =  (initialCount > 0) ? new List<ImportantData>(initialCount) : new List<ImportantData>();

        // !!! 오류 생성자 외부에서 readonly 필드를 초기화할 수 없다.
        this.name = name;
    }
}

컴파일러는 this.name을 읽기 전용으로 만든다. 이 경우 생성자 외부에서는 이 변수의 값을 변경할 수 없다.

이 경우 C#의 생성자 초기화 구문이 상당히 비효율적인 대안이 될 수 있다.

단순한 클래스를 제외한다면 대부분의 클래스는 하나 이상의 생성자를 가지곤 한다.

하지만 이 경우에도 모든 생성자가 객체 초기화라는 본질적인 역할을 수행해야 하므로 유사한 코드가 반복적으로 나타날 수밖에 없다.

C#의 생성자 초기화 구문을 사용하면 공통 코드를 공용 생성자로 옮기고 다른 생성자에서는 이를 호출하도록 코드를 구성할 수 있다.

생성자를 작성할 때 기본값을 가지는 매개변수를 사용하는 방법과 다수의 생성자를 오버로딩하는 방법은 각기 적합한 용도가 있다.

일반적으로 여러 개의 생성자를 오버로딩하기보다 기본값을 가지는 매개변수를 사용하여 생성자를 작성하는 것이 좋다.

이 경우 사용자 입장에서는 생성자의 모든 매개변수에 원하는 값을 전달하거나 매개변수 중 일부에만 임의의 값을 전달할 수고 있다.

매개변수의 기본값은 합리적으로 지정돼야 하고 예외를 유발해서는 안 된다.

매개변수의 기본값을 변경하는 것이 기술적으로 큰 변화인 경우라도 이를 사용하는 사용자에게는 어떠한 영향도 미쳐서는 안된다.

사용자는 이전의 기본값을 계속 사용할 수 있어야 하며 이 경우에도 올바르게 작동해야 한다.

객체 초기화 정리

이번 Item이 객체 초기화를 다루는 마지막 Item이라 특정 타입의 인스턴스가 생성되는 순서를 다시 한번 정리한다.

C#개발자라면 인스턴스가 생성되는 순서뿐만 아니라 객체의 기본 초기화 과정의 순서도 알아야 한다.

그리고 인스턴스가 생성되는 동안 모든 멤버 변수들은 가능한 한 한 번만 초기화하도록 해야 한다.

이를 위해 초기화를 가능한 한 이른 시점에 수행하는 방식을 적용해보는 것도 좋다.

다음은 특정 타입의 첫 번째 인스턴스를 생성할 때 수행되는 과정을 단계별로 나열했다.

  1. 정적 변수의 저장 공간을 0으로 초기화
  2. 정적 변수에 대한 초기화 구문 실행
  3. 베이스 클래스의 정적 생성자 수행
  4. 정적 생성자 수행
  5. 인스턴스 변수의 저장 공간을 0으로 초기화
  6. 인스턴스 변수에 대한 초기화 구문 실행
  7. 적절한 베이스 클래스의 인스턴스 생성자 수행
  8. 인스턴스 생성자 수행

클래스 자체에 대한 초기화 작업은 단 한 번만 이뤄지기 때문에 동일한 타입으로 추가 인스턴스를 생성하면 앞의 단계 중 5단계에서부터 수행된다.

또한 컴파일러가 생성자 내에 중복된 멤버 초기화 코드를 생성하지 않도록 6단계와 7단계는 최적화되어 있다.

C#은 객체가 생성될 때 어떤 식으로든 모든 객체가 초기화될 것임을 보장한다.

인스턴스를 생성하면 인스턴스 내의 모든 멤버들이(정적 멤버, 인스턴스 멤버를 가리지 않고) 최소한 0으로 초기화된다.

생성자를 작성할 때 유념할 부분은 멤버들을 원하는 값으로 초기화할 때 가능한 한 한 번만 초기화가 이뤄지도록 해야 한다는 점이다.

이를 위해서 단순한 리소스의 경우 멤버 초기화 구문을 이용하고 복잡한 초기화 과정이 필요한 경우에만 생성자를 사용하는 것도 좋은 방법이다.

또한 코드의 중복을 피하기 위해서 공통적인 초기화 작업을 수행하는 공용 생성자를 작성하고 이 생성자를 이용하는 방식을 사용해야 한다.

댓글남기기