Null-conditional Operator

Null-conditional Operator

C#에서 null은 자주 발생하는 문제 중 하나이며, 이를 적절히 처리하지 않으면 NullReferenceException과 같은 예외가 발생할 수 있습니다. 이를 방지하고 안정적이며 가독성 높은 코드를 작성하기 위해 C#은 null-conditional operator (?.)와 null-forgiving operator (!)를 제공합니다. 두 연산자의 동작 원리, 활용법, 성능, 그리고 적절한 사용 시점을 설명합니다.

Null-conditional Operator (?.)

null-conditional operator는 nullable 참조 형식 또는 nullable 값 형식에서 속성, 메서드, 또는 인덱서를 안전하게 호출하기 위한 연산자입니다. 왼쪽 피연산자가 null이면 결과는 null이 되고, 그렇지 않으면 속성이나 메서드를 정상적으로 호출합니다.

int? length = myString?.Length;

myString이 null이면 length는 null이 됩니다. myString이 null이 아니면 myString.Length가 실행되어 결과가 length에 저장됩니다.

코드 예제

string? name = person?.Name; // person이 null이면 name은 null
int? length = myString?.Length; // myString이 null이면 length는 null
char? firstChar = myString?[0]; // myString이 null이면 firstChar는 null

장점

  • 코드 간결화: null 체크 코드를 제거함으로써 코드가 간결해집니다.
// 기존방식
string? name = person != null ? person.Name : null;
// null-conditional operator를 사용
string? name2 = person?.Name;
  • 안전성 : null인 경우 예외가 발생하지 않고 호출 실패 시 null을 반환하여 추가적인 예외 처리를 방지합니다.

단점

  • null 결과를 처리할 필요가 있습니다. null-conditional operator를 사용하면 결과가 null이 될 수 있으므로, 추가적인 null 처리 코드가 필요합니다.
  • null 체크는 조건 분기를 포함하므로 자주 호출되는 경우 성능 병목이 될 수 있습니다.

성능

null-conditional operator는 컴파일된 IL 코드에서 null 체크를 추가하는 분기를 생성합니다.

var length = myString?.Length;

위 코드는 다음 IL 코드로 변환됩니다.

IL_0000: ldloc.0          // myString 로드
IL_0001: brtrue.s IL_0004 // null 체크
IL_0003: ldc.i4.0         // null일 경우 결과는 0
IL_0004: callvirt         // Length 호출

null 체크 자체는 가벼운 연산이므로 대부분의 상황에서 성능 영향은 미미하지만, 루프나 성능 민감한 코드에서는 누적 비용이 발생할 수 있습니다. 이런 경우 미리 null 가능성을 제거하거나 null-forgiving operator를 사용할 수 있습니다.

null-conditional operator를 활용

체인 연산

?.를 체인chain 형태로 사용하면, 중첩된 객체에서 null 가능성을 안전하게 처리할 수 있습니다.

var result = person?.Address?.City?.Name;

위 코드는 다음과 같이 동작합니다:

  • person이 null이면 result는 null.
  • Address가 null이면 result는 null.
  • City가 null이면 result는 null.
  • 모든 객체가 null이 아니면 Name 값을 반환.

Null-coalescing Operator (??)와 결합

?.??를 결합하여 null일 때 기본값을 제공할 수 있습니다.

var cityName = person?.Address?.City?.Name ?? "Unknown City";
  • Name이 null이면 "Unknown City"를 반환합니다.

Delegate 호출에서 사용

?.는 delegate를 호출할 때 유용합니다. delegate가 null일 가능성이 있을 때 안전하게 호출할 수 있습니다.

public event EventHandler? MyEvent;
MyEvent?.Invoke(this, EventArgs.Empty); // MyEvent가 null인지 확인 후 호출
  • ?.를 사용하면 null 체크 없이 이벤트를 안전하게 호출할 수 있습니다.

인덱서와 결합

인덱서를 사용할 때도 null 안전성을 확보할 수 있습니다.

var value = myDictionary?["key"];
  • myDictionary가 null이면 value는 null.

Null-forgiving Operator (!)

null-forgiving operator는 nullable 참조 형식에서 컴파일러의 null 경고를 무시할 수 있도록 합니다. 이는 “이 값은 절대 null이 아니다"라는 개발자의 확신을 컴파일러에 전달합니다. 런타임에는 어떤 동작도 추가하지 않습니다.

public string GetName(Person? person)
{
    return person!.Name; // person이 null이 아님을 컴파일러에게 보장
}

코드 예제

string name = person!.Name; // 컴파일러 경고 제거
DoSomething(nullableObject!); // nullableObject는 null이 아님을 보장

장점

  • nullable 참조 형 경고를 제거하여 코드 가독성을 향상시킵니다.
  • 개발자가 null 가능성을 확신할 수 있는 경우 추가적인 조건문을 줄일 수 있습니다.

단점

  • 잘못된 사용 시 NullReferenceException이 발생할 수 있습니다.
  • 실제로 null 가능성이 있는 값을 잘못 판단하여 사용하면 문제가 감춰질 수 있습니다.

성능

null-forgiving operator는 런타임에 어떤 영향을 미치지 않습니다. 이는 컴파일러 지시문으로, 최종 IL 코드에는 !의 흔적이 남지 않습니다.

string name = person!.Name;

위 코드는 다음과 같은 IL로 변환됩니다.

IL_0000: ldloc.0          // person 로드
IL_0001: callvirt         // Name 호출
IL_0006: ret

!는 컴파일 타임에만 영향을 미치므로 성능 상의 비용이 없습니다.

Null-forgiving Operator 활용

제네릭 타입에서 경고 제거

제네릭 타입에서 컴파일러는 nullable 여부를 알 수 없기 때문에 경고를 발생시킬 수 있습니다. !를 사용해 이를 명시적으로 제거할 수 있습니다.

public void ProcessItem<T>(T item)
{
    Console.WriteLine(item!.ToString()); // item은 null이 아님을 보장
}
  • 제네릭 타입 T가 nullable로 추론되어 발생하는 경고를 제거합니다.

패턴 매칭과 결합

패턴 매칭에서 nullable 값을 명시적으로 보장할 수 있습니다.

if (person is not null)
{
    Console.WriteLine(person!.Name); // person이 null이 아님을 보장
}
  • person이 null이 아님이 확실한 경우, !를 사용해 컴파일러 경고를 제거합니다.