타입 시스템 마스터하기 모던 C#

책에서 새로 알게된 정보들을 나열합니다.

5개의 좋아요

P24~25

var result = Displacement(.523, 65, 4);

이처럼 하드 코딩된 값을 매직 넘버(magic number) 라 부른다. 의미나 목적에 관한 어떠한 설명도 존재하지 않기 때문이다. 이 매직 넘버들은 더 나은, 이름 있는 변수로 바꿔서 그 목적을 분명하게 할 수 있지만 이 메서드의 호출자는 여전히 인수들을 잘못된 순서로 제공할 수 있다.
호출자 코드의 순서 오류를 방지하는 일반적인 방법은 각 인수에 매개변수 이름을 지정하는 것이다.

var result = Displacement(elapsedTime: 4, speed: 65, angle: .523);


이 기법에서는 명확함을 확보할 책임을 메서드의 호출자(caller)에게 전가한다.


요약: C# 4.0의 명명된/선택적 인수 기능을 적극적으로 사용하자. 문법적으로 semantic을 좀 더 강조할 수 있다.

4개의 좋아요

P26

이는 강박적 기본 타입 사용(Primitive Obsession) 이 라는 나쁜 코드의 일종으로, int, double, string과 같이 해당 언어에 내장된 기본 타입에 과도하게 의존하는 코드를 말한다.

강박적 기본 타입 사용의 잘 알려진 해결책은 구분된 목적에 맞는 고유한 타입을 제공해 이들 사이에서 암묵적 변환이 일어나지 않도록 보장하는 것이다.

public struct Angle
{
    public double Size { get; set; }
}

public struct Speed
{
    public double Amount { get; set; }
}

요약: 너무 기본 타입에만 의존하지말고 간단한 타입도 새로 정의해서 쓰자.

7개의 좋아요

P27


Speed와 Angle 타입은 그들의 값을 캡슐화 하지 않는다. 이들의 값은 공개적으로 변형 할 수 있는(mutable) Size와 Amount 속성을 노출하기 때문이다.

var result = Displacement(angle: new Angle { Size = .523 },
                          speed: new Speed { Amount = 65 },
                          elapsedTime: seconds);

공개적인 속성을 사용해서 값을 설정하는 방법은 불필요하게 장황하다.

public struct Speed
{
    public Speed(double amount) => Amount = amount;

    public double Amount { get; set; }
}

public struct Angle
{
    public Angle(double size) => Size = size;

    public double Size { get; set; }
}

var result = Displacement(new Angle(.523), new Speed(65), seconds);

Displacement 메서드를 호출할 때 Angle과 Speed 인스턴스를 생성하는데, 이때 이들의 순서는 더이상 모호하지 않으며, 인수에 이름을 붙이는 것이 더이상 중요하지 않다.
이 생성자들은 표현식 바디 문법(expression body syntax) 를 사용한다. 이 문법은 메서드의 경우 C# v6.0, 생성자의 경우 C# v7.0 이후에 사용할 수 있다.


요약: 공개 속성이라도 생성자를 통해 ‘의미있게’ 값을 초기화하자. 의미있게에 포인트를 둔다면 이것이 깨달음이 있는 방법일 것이고 모로가나 서울로만 가면 된다고 하면 중요하지 않을 것이다.

링크:

5개의 좋아요

P28

현재 값들은 변경될 수 있다(mutable).

Angle의 Size 속성에 대한 set 접근자(accessor)를 제거해 읽기 전용으로 만들어 이 불변성을 달성할 수 있다.

public readonly struct Speed
{
    public Speed(double amount) => Amount = amount;

    public double Amount { get; }
}

Speed 인스턴스가 변경되지 않도록 하기 위해 읽기 전용으로 만들었다.

타입을 불변으로 설계함으로써 코드 인스펙션(code inspection) 과정에서 코드를 쉽게 이해하도록 할 수 있다.

이는 멀티스레드 프로그램(multithreaded program)에서 특히 중요하다.


요약: value type에 대해서 readonly struct를 적용했고 setter를 제거하여 읽기 전용으로 만들었다. 오로지 생성자를 통해서만 값을 초기화 할 수 있다.
불변성에 대한 중요성을 느끼고 있는 개발자라면 도움이 될 것이다.

2개의 좋아요

P29

우리가 작성한 타입에 대한 생성자를 사용해 유효하지 않은 인수를 체크하고 사용자가 합법적이지 않은 값을 전달했을 때 예외를 발생시킬 수 있다.

public Speed(double amount)
{
    if (amount < 0)
        throw new ArgumentOutOfRangeException(
            paramName: nameof(amount),
            message: "Speed must be positive");
    Amount = amount;
}

Speed의 생성자는 제공된 값을 검증하고 생성자는 Speed에 값을 제공할 수 있는 유일한 수단이므로, 존재 가능한 Speed 값이 생성됐음을 보장할 수 있다.

사용자 정의 타입을 만들어서 얻을 수 있는 이익이 이것이다. 타입 안에 그 책임을 캡슐화함으로써 해당 타입을 사용하는 메서드에 전가되는 책임을 없앨 수 있다. 중복된 코드를 최소화하는 것은 여전히 명확하고, 사용하기 쉽고, 유지보수하기 덜 어려운 코드를 만드는 좋은 방법이다.

Speed의 생성자는 클래스 불변량(class invariant, 해당 타입의 모든 인스턴스가 수명 동안 유지해야 하는 조건)을 만든다.


요약 생성자에서 유효한 값을 검증하자.

링크:

3개의 좋아요

P30

Speed 타입 안에 검증 로직을 캡슐화했으므로 이 타입에 의존하는 알고리즘에 대해 독립적으로 클래스 불변량을 테스트할 수 있다.

[Test]
public void Speed_cannot_be_negative()
{
    Assert.That(
        () => new Speed(-1),
        Throws.TypeOf<ArgumentOutOfRangeException>());
}

검증 코드는 Speed 타입 안에 캡슐화돼 있으므로 테스트는 한 번으로 충분하다.

도메인 개념을 위해 타입을 사용하면 여러 장점을 얻을 수 있다. 우리가 작성한 타입을 사용한 코드는 매우 명확하다.


요약: 도메인 개념을 위해 타입을 사용하면, 캡슐화 및 테스트가 간단해진다.

2개의 좋아요

P34

우리가 작성한 타입에 대한 암묵적 변환을 정의하겠다.

public readonly struct Speed
{
    //-- 생략 --
    public static implicit operator double(Speed speed) => speed. Amount;
}


이제 Math.Cos, Math.Sin 메서드를 호출할 때 명시적으로 angle.Size를 사용할 필요가 없으며, speed.Amount 속성을 얻지 않고도 speed와 elaspedTime.TotalSeconds를 곱할 수 있다.


요약: 코드를 간결하게 만들기 위해 암시적 변환을 정의할 경우 코드 가시성이 좋아진다.
그런데 암시적인 변환이니 만큼, 보이지 않는 부분에 대해 이해하지 못하고 있다면 지식수준이 다른 팀원들에 대해 협업이 오히려 어려워 질 것이므로, 잘 공유하고 사용해야할 것 같다.

링크:

3개의 좋아요

P35

암묵적 변환은 Speed와 Angle에 대한 인터페이스를 약화시킨다. double이 사용될 수 있는 모든 위치에서 사용될 수 있도록 하는 것은 적절하지 않은 표현식에서도 사용될 수 있다는 의미이다.

var angle = new Angle(.523);
var speed = new Speed(65);

var unknown = speed / angle;


또한 암묵적 변환은 일반적으로 코드에서 드러나지 않는다. Math.Cos 메서드에 Angle 값대신 Speed 값을 전달했다 하더라도 계산 결과로부터 에러를 찾아내기 어렵다.


요약: 앞서 얘기한 암시적 변환에 대한 문제점이다. 기술은 언제나 트레이드오프로서 좋기만 한 측면은 없다. 편리할수록 기저에 깔린 암시적인 이해를 반드시 동반해야 한다.

4개의 좋아요

암묵적 변환의 경우, 변수에 대입할 때 var 키워드를 이용해서 암묵적 변환의 원본 타입을 받으면 작동하지 않는다는 특성 (무효화되는 성질)이 있습니다. 암묵적 변환이 쓰이기를 의도한 경우 오히려 간편하자고 쓰는 var 키워드에 의해 의도가 깨져서 코드가 역으로 복잡해지는 아이러니도 발생하는 것 같습니다.

그래서 개인적으로는 암묵적 변환은 지양하는 쪽이 좋고, 대신 명시적 변환만 제공하거나 extension 메서드를 이용해서 ToXYZ() 형식의 확장 메서드를 제공하는 편이 더 좋은게 아닌가 하는 생각을 가지고 있습니다. 암묵적 변환으로 인해서 빚어지거나 초래될 불편, 혼란이 결코 가벼운 것은 아니라 여기기 때문에 특히 그렇게 생각하게 되었습니다.

4개의 좋아요

P35

Speed에 대한 커스텀 타입을 도입하면서 깨달은 장점 중 하나는 Speed 생성자 안에 검증 로직을 캡슐화 할 수 있다는 것이었다.
Speed에서 double로의 암묵적 변환을 허가한다는 것은 double 결과값을 사용하는 코드가 Speed에 허용된 범위 제약을 빠져나갈 수 있다는 것을 의미한다.

var verySlow = new Speed(10);
var reduceBy = new Speed(30);

var outofRange = verySlow - reduceBy;


이 범위를 벗어나는 값을 해결하기 위해서는 Speed와 Angle이 수행할 수 있는 연산을 제한해 합리적인 결과값으로 한정해야 한다. 사실 이것이 Speed에 클래스 불변량을 도입해서 Speed의 인스턴스가 항상 유효한 값을 갖도록 보장한 이유 그 자체이다. 또한 상식을 벗어나는 연산을 금지하고, 컴파일러가 이런 상황에서 무언가 잘못됐음을 우리에게 알려주길 원한다.

우리가 만든 타입을 산술 표현식에서 쉽게 사용할 수 있도록 만들어서 원하는 목적을 여전히 달성할 수 있지만, 이 타입들이 수행할 수 있는 연산의 종류가 무엇인지 제어해야 한다. 앞으로 Speed와 Angle의 자연스러운 사용을 훼손하거나 타입의 캡슐화를 손상시키지 않으면서 특정한 연산이 가능하도록 만들 것이다.


요약: 코딩할 때는 의미가 중요하기 때문에 간단한 타입도 의미를 부여하여 새로 정의해서 타입을 생성하라고 위에서 배웠다.
하지만 새로 생성된 타입은 의미적으로 필요하다고 생각하여 ‘이런 것이 있으면 좋겠다’ 하고 생성한 것이기 때문에 의미와는 별개로 예측 가능한 부분들이 빠질 수 있다.
그래서 위의 코드처럼 속력은 방향이 없는 스칼라 값이므로 속력에서 음수는 존재할 수 없음에도 double이 아닌 새로 만든 타입이라 음수가 발생할 여지가 생긴다. 그래서 도메인에서 최소 단위가 되는 원시타입 하나만을 감싼 타입들에 대한 단위 테스트가 필요한 것 같다.
테스트에 대한 초보들은 단위테스트와 통합테스트를 혼동하는 경향이 있다.

3개의 좋아요