C# Nullable 타입
소프트웨어 개발 세계에서 null 참조는 꽤나 골치아픈 문제로, 개발자들 사이에서 악명 높은 "NullReferenceException"으로 이어질 수 있다. 지속적으로 발전하고 있는 C# 언어는 이러한 문제를 해결하기 위해 안전성과 개발자의 생산성을 향상시키는 강력한 기능들을 도입하고 있다. C#의 최신 버전에서 주목할 만한 발전 중 하나는 nullability 기능의 도입 및 강화이다.
이번에는 C#에서 nullability의 중요성을 다루며, 코드 안전성과 명확성을 어떻게 향상시키는지 실용적인 예시와 함께 살펴본다.
Nullable을 활용해 Non-Nullable 타입을 Nullable로 만들기
C#에서 타입 시스템은 변수가 null 값을 가질 수 있는지를 구분하는 데 큰 역할을 한다. 값 타입들(예를 들어 int, double, bool 등)은 기본적으로 null을 받을 수 없는 non-nullable 타입이다. 하지만, 데이터베이스 작업이나 외부 API 호출 등에서 이러한 값 타입들이 null 값을 가질 필요가 있는 경우가 종종 있다. 이런 상황에서 C#의 Nullable<T> 구조체는 값을 가질 수도 있고 null일 수도 있는 값을 다루기 쉽게 해준다.
Nullable<T> 알아보기
Nullable<T>는 값 타입 T에 null을 할당할 수 있게 해주는 일반적인 구조체다. 이를 통해 값 타입에 '정의되지 않음'이나 '값 없음' 같은 추가 상태를 부여할 수 있다.
사용 방법은 다음과 같다.
Nullable<int> nullableInt = null;
int? anotherNullableInt = 5; // int?는 Nullable<int>의 축약형이다.
여기서 int?는 Nullable<int>를 더 간단하게 쓰는 방법이다. C#에서 ?를 붙이면 해당 타입을 Nullable<T>로 자동 변환한다.
Nullable 타입 변수가 null인지 아닌지를 확인하고, 값이 있으면 그 값을 사용하는 건 아주 간단하다.
다음 예제를 보자.
if (nullableInt.HasValue)
{
Console.WriteLine(nullableInt.Value); // nullableInt가 null이 아니면 여기서 값 사용
}
else
{
Console.WriteLine("값 없음");
}
Nullable 참조 타입
역사적으로 C#의 참조 타입은 null 값을 가질 수 있었고, 이는 의도하지 않은 null 참조를 역참조할 때 잠재적인 문제점으로 이어질 수 있었다. C# 8.0에서는 변화가 생겼는데. nullable 참조 타입이라는 기능이 도입되었고, 이를 통해 참조 타입이 nullable인지 아닌지를 명시적으로 표현할 수 있게 되었다. 이는 언어에 새로운 안전 수준을 더했다.
참조 타입의 null 의도 표현하기 (string vs string?)
간단한 string 타입을 고려해 보자. C# 8.0 이전에는 모든 문자열 변수에 null을 할당할 수 있었고, 컴파일러는 이에 대해 불만을 제기하지 않았다. 그렇기에 아래와 같이 의도하지 않게 null이 표현되는 상황이 발생할 수 있다.
string name = null; // Perfectly valid, but potentially risky
만약 nullable이 도입된다면 어떨까? 이 개념이 도입된다면 다음과 같은 컨벤션을 적용할 수 있게 된다.
1. 기존처럼 string 타입을 사용하는것은 이 참조 타입이 null을 허용하지 않는 참조변수임을 나타낸다.
2. string? 타입을 사용하는것은 이 참조 타입이 null이 될 수 있는 참조변수임을 나타낸다.
// null을 허용하지 않는 참조타입
string notNullable = "Hello, C#!";
// 명시적으로 nullable
string? nullable = null;
이 구분은 중요하다. 컴파일러와 동료 개발자에게 의도를 명확히 알릴 수 있으며, 컴파일러는 잠재적인 null 참조 예외를 방지하기 위해 이 정보를 사용하여 경고를 발생시킬 수 있기 때문이다.
코드 안전성에서 Nullability의 역할
nullability를 명시함으로써 C#은 여러 방면에서 코드 안전성을 향상시킨다.
1. 컴파일 시간 검사
컴파일러는 정적 분석을 수행하여 가능한 null 참조 역참조를 감지하고, 잠재적인 런타임 오류를 컴파일 시간 경고로 전환한다.
2. 코드 명확성
non-nullable 변수를 선언함으로써 null 값이 예상되지 않음을 명확한 계약으로 표현하며, 이는 코드의 가독성과 유지보수성을 향상시킨다.
3. 도구 개선
IDE는 nullability 주석을 사용하여 더 나은 코드 완성, 리팩토링 및 분석을 제공하며, 개발자의 생산성을 더욱 향상시킨다.
실용적인 예시들
Nullable 참조 타입 사용하기
string notNullable = "값이 있어야 한다.";
string? nullable = null;
// 컴파일러 경고: 가능성이 있는 null 참조 역참조
Console.WriteLine(nullable.Length);
Null-Conditional 연산자 (?.)
nullable 참조의 멤버에 안전하게 접근하기 위해 null conditional 연산자를 이용하면 편하다.
// 다음 코드를 보자.
var p = man;
if(p != null)
{
var result = man.Name;
}
else
{
var result = null;
}
// 위 코드를 다음과 같이 표현할 수 있다.
var result = man?.Name;
Null-Coalescing 연산자 (??)
null 병합 연산자(??)는 표현식이 null일 경우 기본값을 지정할 수 있는 간결한 방법을 제공한다. 이 연산자는 nullable 타입이나 초기화되지 않았을 수 있는 참조 타입을 다룰 때 특히 유용하다. 다음은 null 병합 연산자를 사용하는 방법을 보여주는 예시이다.
// Nullable 정수를 정의하고 null 값을 할당한다.
int? nullableInt = null;
// NullableInt가 null일 경우 기본값으로 10을 제공하기 위해 null 병합 연산자를 사용한다.
int result = nullableInt ?? 10;
Console.WriteLine(result); // 출력: 10
ps.
삼항연산자를 사용해도 null 병합 연산자와 비슷한 결과를 낼 수 있는데 왜 굳이 ?? 연산자를 만들었는지 의문이 들 수 있다. 이에 대한 답변은 특수화 vs 일반화의 차이이다. null 병합 연산자는 null 체크 후에 null인 경우 기본값을 지정하기 위한 목적으로 사용되는 개념인 반면, 삼항연산자는 boolean 조건에 맞게 좀 더 일반적인 상황을 위한 목적으로 사용되는 개념이다.
Null-Coalescing assignment 연산자 (??=)
null 병합 할당 연산자(??=)는 해당 변수가 현재 null인 경우에만 변수에 값을 할당하는 편리한 약어이다. 즉, 왼쪽 피연산자가 null인지 확인하고, null인 경우에만 오른쪽 피연산자의 값을 왼쪽 피연산자에 할당한다. 왼쪽 피연산자가 null이 아니면 연산자는 아무 작업도 수행하지 않는다.
Null 허용 변수가 있고 해당 변수에 값이 있는지 확인하려고 한다고 가정하자. 전통적으로 if 문을 사용하여 변수가 null인지 확인한 다음 null이면 값을 할당할 수 있다. ??= 연산자는 이 패턴을 단순화한다.
string? name = null;
// 전통적인 방법
if (name == null)
{
name = "Default Name";
}
// ??= operator 이용
name ??= "Default Name";
Null-Forgiving 연산자 (!)
null 허용 연산자(!)는 컴파일러의 null 분석을 재정의하는 데 사용된다. 이 연산자는 컴파일러에게 null 경고를 "용서"하도록 지시한다. 이는 기본적으로 개발자가 연산자 왼쪽의 식이 null이 아님을 보장한다는 의미이다. 이는 분석에도 불구하고 코드의 특정 지점에서 값이 null이 아닐 것이라고 확신한다고 컴파일러에 주장하는 방법이다.
전체 동작에 대해 알아보자. null 허용 참조 유형이 활성화되면(#nullable enable) C# 컴파일러는 잠재적인 null 참조 예외에 대해 경고하기 위해 정적 분석을 수행한다. 예를 들어 먼저 null을 확인하지 않고 null 허용 참조 유형을 역참조하려고 하면 컴파일러에서 경고가 표시된다.
그러나 프로그램의 논리로 인해 값이 null이 아니라는 것을 알지만 컴파일러가 이를 확인할 수 없는 경우가 있을 수 있다. 이러한 상황에서는 null 허용 연산자를 사용하여 컴파일러 경고를 무음으로 설정할 수 있다.
// Suppose this method might return null, but due to program logic, it won't here.
string? nullableString = GetAString();
// The compiler warns about possible null dereference.
Console.WriteLine(nullableString.Length);
// Using the null-forgiving operator to assert the value isn't null.
Console.WriteLine(nullableString!.Length); // No warning
이 예에서 nullableString은 해당 유형 주석에 따라 null일 수 있는 null 허용 문자열이다. 그러나 프로그램의 흐름에 따라 nullableString이 역참조될 때 null이 되지 않는다는 것을 알고 있다고 가정해 보자. nullableString! 이 표현식에 대한 null 허용 여부 경고를 무시하도록 컴파일러에 지시한다.
이 연산자를 사용하는 경우 다음과 같은 부분들이 고려되어야한다.
1. null 허용 연산자는 값이 null이 아니라고 확신하는 경우에만 드물게 사용해야 한다. 이를 과도하게 사용하면 코드에서 실제 null 허용 여부 문제가 숨겨 잠재적인 런타임 예외가 발생할 수 있다.
2. null 허용 연산자는 런타임에 영향을 미치지 않는다는 점을 이해하는 것이 중요하다. 이는 순전히 컴파일러의 이점을 위한 것이며 생성된 IL 코드에 영향을 주거나 런타임 검사를 추가하지 않는다.
3. 사실 null 허용 연산자는 임시방편이다. 이 연산자가 남용된다 싶으면, 코드 디자인을 다시 검토하는 것이 좋다.
요약하자면, Null 허용 연산자는 컴파일러에 Null 허용 여부를 주장하는 데 유용한 도구이지만 Null 허용 여부와 관련하여 코드의 안전성과 견고성을 유지하려면 신중하게 사용해야 한다.
결론
C#에서 nullable 기능의 도입과 지속적인 강화는 더 안전하고 예측 가능한 코드를 작성하는 방향으로의 중요한 발전이다. nullable 참조 타입을 활용함으로써 개발자는 강력한 도구 세트를 얻게 되어 의도를 명확히 표현하고, 런타임 오류를 줄이며, 코드 기반을 명확히 할 수 있다. 소프트웨어 개발의 복잡성을 계속해서 탐색함에 따라, 이러한 기능들은 C#의 안전성, 명확성 및 개발자 효율성에 대한 약속을 강조한다. 이러한 Convention을 채택함으로써 애플리케이션을 더 견고하게 만들 뿐만 아니라 코드의 공동 이해와 유지보수성을 향상시킬 수 있다.
ps.
이제 기존처럼 일반 참조 변수에 null을 넣는 것은 지양해야할 것이다. 그리고 언급한 null 관련 연산자들에 대해 완벽하게 숙지하지 않는다면 최신 C# 코드를 읽는데 어려움이 있을 것이다.
'프로그래밍 > C#' 카테고리의 다른 글
[C#] 불변성 처리 (0) | 2024.03.05 |
---|---|
[C#] 비동기 프로그래밍 (1) | 2024.03.04 |