아이템 1: 지역변수를 선언할 때는 var를 사용하는 것이 낫다

관련 지식 : LINQ

관련 지식 : IEnumerable VS IQueryable

지역변수의 타입을 암시적으로 선언하는 것이 좋은 이유는 C#언어가 익명 타입(anonymous type)을 지원하기 위해서 타입을 암시적으로 선언할 수 있는 손쉬운 방법을 제공하기 때문이다.

IEnumerable VS IQueryable

또한 일부 쿼리 구문의 경우 IEnummerable를 반환하는 경우도 있지만 IQueryable를 반환하기도 하는데, 정확한 반환 타입을 타입을 알지 못한 채 올바르지 않은 타입을 명시적으로 지정하게 되면 늑보다 실이 많다.

IQueryable 컬렉션을 IEnumerable로 강제 형변환하게 되면 IQueryProvider가 제공하는 장점을 모두 잃게 된다.

여기서 말하는 단점이란 IEnumerable VS IQueryable내용을 참고

코드를 읽을 때도 var를 사용하여 암시적으로 변수를 선언한 코드가 더 잘 읽힌다.

이것에 대해서 다르게 생각할 수 있지만 Dictionary<int, Queue<string>>과 같은 정확히 기술된 타입 자체보다 jobsQueueByRegion과 같이 타입을 유추할 수 있는 이름이 코드를 읽을 때 더욱 도움이 된다.

이러한 이유로 익명 타입을 사용하는 편이 낫다.

개발자 입장에선 변수의 타입과 같은 지엽적인 부분보다 변수의의미를 파악에 더 집중할 수 있다.

타입을 명시적으로 지정할 경우 타입 안정성이 향상될 것이라 생각하지만 이 또한 사실이 아니다.

앞서 살펴본 IQueryable, IEnumerable예와 같이 올바르게 타입을 지정하지 않으면 타입 안정성을 해치는 꼴이 될 수 있다.

지역변수에 대한 타입 추론 != 동적 타이핑

생각할 수 있다. 지역 변수를 var로만 사용하면 C#의 고유특성인 정적타이핑을 훼손하는 것 처럼 느껴진다.

동적 타이핑을 지원하는 언어들은 대부분 var조차 명시하지 않기 때문에 크게 다른 점이 없어보인다.

하지만 C#에서 특정 변수를 var로 선언하면 동적타이핑이 수행되는 것이 아니라 할당 연산자 오른쪽 타입을 확인하여 왼쪽 변수의 타입을 결정하게 된다.

컴파일러에게 변수의 타입을 명시적으로 알려주지 않아도 개발자를 대신하여 올바른 타입을 추론해 주는 것이다.

실제로 IDE에서 var키워드를 사용한 변수에 마우스를 올리면 동적 타이핑이 아닌 정적타이핑임을 알 수 있다.

정적타이핑 동적타이핑

간단하게 정적타이핑은 변수 데이터 타입을 직접 명시하여 컴퓨터의 일을 덜어준다.

코드의 안정성과 정교함은 올라가지만 그 만큼 복잡해진다.

동적 타이핑은 컴퓨터가 알아서 해석하여 데이터 타입을 정해준다.

그 만큼 로직을 명확하게 보여줄 수 있지만 실행속도의 저하가 일어난다.

타입을 명시하는 경우

물론 명시적으로 지정하면 잘못된 동작을 미연에 방지하는 효과가 있을 때도 있다.

하지만 대체로 var를 사용하여 컴파일러에게 적절한 타입을 위임하는 편이 더 나은 결과를 보여줄 때가 많다.

당연하게 var를 과도하게 사용하여 코드의 가독성을 해치고 내부적으로 이루어지는 자동 타입 변환 과정으로 인해 발견하기 어려운 버그를 만들 경우도 없지 않다.

가독성을 유발하는 몇가지 문제

  • 지역변수를 초기화할 때 var를 사용하는 경우는 비교적 문제를 일으킬 여지가 적다.
var foo = new MyType();

이 경우에는 쉽게 타입을 추론할 수 있다.

  • 팩토리 메서드를 사용하는 경우에도 어떤 타입으로 추론될지 어렵지 않게 유추할 수 있다.
var thing = AccountFactory.CreatSavingsAccount();

메서드의 이름만으로는 반환 타입을 짐작하기 어려운 경우도 있다.

var result = someObject.DoSomeWork(anotherParameter);

하지만..! 실제 코드 작성 시에는 이보다 훨씬 명확하게 메서드의 이름을 지을 것이며 반드시 그래야만 한다.

개선된 예이다.

var HighestSllingProduct = someObject.DoSomeWork(anotherParameter);

이 코드에는 타입과 관련된 정보가 없지만 HighestSllingProduct라는 변수 이름을 통해 Product타입임을 짐작할 수 있다.

물론 DoSomeWork를 어떻게 작성했는지에 따라 Product타입이 아닐 수 있다.

Product상속한 다른 타입일 수도 있고, Product타입이 구현한 인터페이스일 수도 있다.

하지만 어느 경우라도 DoSomeWork의 정의에 부합하도록 HighestSllingProduct의 타입을 추론한다.

var를 이용해 특정 메서드의 반환값을 저장할 변수를 선언할 경우

위 처럼 조금 어려울 수 있다.

코드를 읽는 동안 개발자는 변수 타입을 특정 타입으로 가정하고 읽기가 조금 어려울 수 있다.

코드를 읽는 동안 개발자는 변수의 타입을 특정 타입으로 가정하고 코드를 읽게 될 텐데 대부분 런타임의 타입으로 가정하는 경우가 많다.

하지만 현재의 C# 컴파일러는 런타임에 객체가 어떤 타입이 될지를 추론할 만큼 훌룡하지 않아서 단순히 컴파일타임 타입과 메서드의 원형을 기반으로 변수 타입을 추론할 뿐이다.

따라서 앞의 예제의 경우에도 런타임에 HighestSllingProduct의 타입이 Product인지는 사실 고려 대상조차 되지 못한다.

타입을 명시적으로 기술한 코드를 읽는 경우에는 눈으로 타입을 직접 확인할 수 있지만, var를 사용한 경우에는 어떤 타입으로 추론될지를 직접 눈으로 확인할 수는 없다.

가끔은 개발자가 짐작한 타입과 컴파일러가 실제로 추론한 타입이 달라서 문제가 되는 경우도 있다.

이로인해 발생한 버그는 수정이 쉽지 않다.

내중 숫자 타입과 var를 함께 사용한 경우

내장된 숫자 타입들 간에는 다양한 변환 연산이 자동으로 수행된다.

float, double로의 변환과 같이 확대 변환은 항상 안전하게 수행된다.

반면 long에서 int로의 변환과 같이 축소 변환은 정밀도에 손실이 발생한다.

숫자를 저장할 변수의 타입을 명시적으로 선언하면 변환 과정에서 발생할 수 있는 위험성을 컴파일러가 사전에 경고한다.

var f = GetMagicNumber();
var total = 100 * f / 6;
Console.WriteLine($"Declared Type: {total.GetType().Name}, value: {total}");

여기서 total은 무슨 타입일까..??

정확한 타입은 GetMagicNumber()의 반환 타입에 의해서 결정된다.

이러한 차이는 컴파일러가 f변수의 타입을 추론하는 방식으로부터 기인한다.

컴파일러는 GetMagicNumber() 메서드의 반환 타입으로 f의 타입을 결정한다.

total 계산 시에 사용된 상수는 모두 리터럴이므로 컴파일러가 이 상수들을 f와 동일한 타입으로 변환한 후 계산하게 되는데 이런 이유로 결괏값에 차이가 생기는 것이다.

이는 언어 차원의 문제가 아니며 C# 컴파일러 입장에서는 사용자의 요청을 정확하게 수행했다고 볼 수 있다.

사용자가 애초에 var사용해 컴파일러에게 타입 추론을 위임한 경우 컴파일러는 할당문의 오른쪽의 내용을 기반으로 타입을 결정하기 때문이다.

이런 이유로 내장된 숫자 타입과 var를 사용할 때는 항상 주의해야 한다.

따라서 내장된 숫자 타입에는 안전하게 명시된 타입을 적어주는 것이 안전하다.

실제 발생하는 문제들

public IEnumerable<string> FindCustomersStartingWith1(string start)
{
    IEnumerable<string> q =
        from c in db.Cusomers
        select c.ContactName;
    
    var q2 = q.Where(s => s.StartsWith(start));
    return q2;
}

이 코드는 심각한 성능 문제를 유발한다.

결과로 받아올 변수 q를 IEnumerable형식으로 선언했기 때문이다.

데이터베이스에서 쿼리가 수행되는 경우 LINQ쿼리는 실제로 IQueryable타입을 반환하지만, 개발자가 q를 IEnumerable으로 선언해버렸기 때문에 IQueryable과 관련된 장점을 모두 잃게 된다.

이런 문제가 발생하는 이유는 IQueryable가 실제로 IEnumerable를 상속하다 보니 컴파일러 입장에서는 문제가 없다고 판단하기 때문이다.

IEnumerable VS IQueryable관련 내용에서도 나오지만 IQueryable를 사용하면 원격지에 데이터가 있는 데이터베이스를 이용하는 경우, 여러 단계에 걸쳐 수행되는 다수의 LINQ 쿼리 표현식들을 단일 SQL쿼리로 합한 후 단번에 수행할 수 있다.

하지만 이 경우에는 두 번째 LINQ쿼리의 대상이 IQueryable가 아니라 IEnumerable이므로 첫 번째 쿼리가 즉각 수행되고 원격지로부터 필터링되지 않은 연락처 이름이 모두 로컬 컴퓨터로 반환된다.

이 문제는 첫 번째 q의 형식을 var바꾸어주면 간단하게 해결된다.

컴파일러가 컴파일타임에 변수의 타입을 결정하도록 하는 것

컴파일러가 컴파일타임에 변수의 타입을 결정하도록 하는 것이 개발자가 코드를 읽을 때 방해가 되지 않을지 잘 판단해야 한다.

코드를 읽을 때 지역변수의 타입을 명확히 유추할 수 없고 모호함을 불러일으킬 가능성이 있다면 차라리 타입을 명시적으로 선언하는 것이 좋다.

하지만 제목과 마찬가지로 대부분의 경우 변수의 이름을 통해서 그 역할을 명확하게 드러내도록 코드를 작성하는 것이 훨씬 낫다.

정리

코드를 읽을 때 타입을 명시적으로 드러내야 하는 경우가 아니라면 var를 사용하는 것이 좋다.

이번 아이템을 통해서 살펴본 내용은 항상 그렇게 해야 한다는 것이 아니라 더 나을 수 있다 정도로 이해하면 좋겠다.

다만 내장 숫자 타입을 선언할 때는 명시적으로 타입을 선언하는 편이 낫다.

그 외에는 모두 var를 사용해보자..!

명시적으로 타입을 나타내기 위해서 장황하게 타입명을 사용한다 하더라도 타입 안정성이 향상되거나 가독성을 개선하지 못한다.

게다가 올바르지 않게 타입을 명시하게 되면 컴파일러의 도움을 얻더 충분히 피해갈 수 있는 문제를 강제로 일으키는 꼴이 되기도 한다.

댓글남기기