Null Values(널 값)

Null Values(널 값)

이 문서에서는 F#에서 null 값이 어떻게 쓰이는지를 다루고 있어요. F#에서는 보통 null을 거의 쓰지 않지만, 다른 .NET 언어와 연동할 때는 반드시 만나게 되는 개념이죠. 함께 살펴볼게요.

출처: Microsoft Learn — F# Null Values 원문 갱신일: 2025-01-29

본문

F# 9 이전의 Null Values

F#에서는 값이나 변수에 null을 평범하게 쓰지 않아요. 하지만 몇 가지 특수한 상황에서 null이 비정상적인 값으로 등장하곤 합니다.

  • F#으로 정의한 타입이라면 해당 타입에 AllowNullLiteral 특성이 붙어 있지 않는 한, null을 일반적인 값으로 쓸 수 없어요.
  • 다른 .NET 언어로 정의한 타입이라면 null이 가능한 값이고, 그런 타입과 상호 운용할 때 F# 코드에서 null 값을 만날 수 있어요.

F#으로 정의하고 F#에서만 사용하는 타입이라면, F# 라이브러리만으로 null 값을 만드는 방법은 사실 Unchecked.defaultofArray.zeroCreate를 쓰는 것뿐이에요. 다만 그 타입을 다른 .NET 언어에서 사용하거나, .NET Framework처럼 F#으로 작성되지 않은 API와 함께 쓴다면 null 값이 생길 수 있답니다.

다른 .NET 언어에서 "null이 될 수 있는 참조 변수"를 쓸 자리에, F#에서는 option 타입을 활용할 수 있어요. null 대신 객체가 없을 때는 None, 객체가 있을 때는 Some(obj)를 사용하세요. 자세한 내용은 Options을 참고해요.

참고로 Some x에서 x가 우연히 null이라면 그 nulloption에 그대로 담을 수는 있어요. 그래서 값이 정말 null일 때는 반드시 None을 쓰는 게 중요합니다.

null 키워드는 F#에서도 유효한 키워드예요. .NET Framework API나 다른 .NET 언어로 작성된 API를 다룰 때는 이 키워드를 써야 합니다. null 값이 필요해지는 상황은 크게 두 가지예요.

  1. .NET API를 호출하면서 인자로 null을 넘길 때
  2. .NET 메서드 호출에서 반환값이나 출력 매개변수로 받은 값이 null인지 해석할 때

.NET 메서드에 null을 전달할 때는 호출 코드에서 null 키워드를 그냥 쓰면 돼요. 다음 예시를 볼게요.

open System

// Pass a null value to a .NET method.
let ParseDateTime (str: string) =
    let (success, res) =
        DateTime.TryParse(str, null, System.Globalization.DateTimeStyles.AssumeUniversal)

    if success then Some(res) else None

.NET 메서드에서 얻은 null 값을 해석할 때는 가능하면 패턴 매칭을 활용하세요. 아래 예시는 입력 스트림의 끝까지 읽었을 때 ReadLine이 돌려주는 null 값을 패턴 매칭으로 처리하는 코드예요.

// Open a file and create a stream reader.
let fileStream1 =
    try
        System.IO.File.OpenRead("TextFile1.txt")
    with :? System.IO.FileNotFoundException ->
        printfn "Error: TextFile1.txt not found."
        exit (1)

let streamReader = new System.IO.StreamReader(fileStream1)

// ProcessNextLine returns false when there is no more input;
// it returns true when there is more input.
let ProcessNextLine nextLine =
    match nextLine with
    | null -> false
    | inputString ->
        match ParseDateTime inputString with
        | Some(date) -> printfn "%s" (date.ToLocalTime().ToString())
        | None -> printfn "Failed to parse the input."

        true

// A null value returned from .NET method ReadLine when there is
// no more input.
while ProcessNextLine(streamReader.ReadLine()) do
    ()

F# 타입의 null 값은 다른 방법으로도 만들어질 수 있어요. 예를 들어 Array.zeroCreateUnchecked.defaultof를 호출하거든요. 그런 코드를 쓸 때는 null 값이 밖으로 샐 수 없도록 잘 캡슐화해 두는 게 좋아요.

F# 전용 라이브러리라면 모든 함수에서 null을 검사할 필요는 없어요. 하지만 다른 .NET 언어와 연동하기 위한 라이브러리를 만든다면, C#이나 Visual Basic에서 그렇듯 null 입력 매개변수를 검사해서 ArgumentNullException을 던져야 할 수 있어요.

임의의 값이 null인지 확인할 때는 다음과 같은 코드를 쓰면 됩니다.

match box value with
| null -> printf "The value is null."
| _ -> printf "The value is not null."

F# 9부터의 Null Values

F# 9에서는 null을 값으로 가질 수 있는 참조 타입을 다루기 위한 기능이 언어에 추가되었어요. 이 기능은 기본적으로 꺼져 있기 때문에, 켜려면 프로젝트 파일에 다음 속성을 넣어야 합니다.

<Nullable>enable</Nullable>

이 속성은 F# 컴파일러에 --checknulls+ 플래그를 전달하고, 빌드에 NULLABLE 전처리기 지시문을 설정해 줘요. 자세한 내용은 컴파일러 옵션Nullable 전처리기 지시문을 참고하세요.

null 허용을 명시적으로 선택하려면 타입 선언 뒤에 다음과 같은 새 문법을 붙이면 됩니다.

type | null

여기서 막대 기호 |는 논리적 OR의 의미를 가져요. 즉 밑바탕이 되는 타입과 nullable 참조라는 두 서로소 타입 집합을 합집합으로 묶는 거예요. 이 기호는 F#의 discriminated union에서 여러 케이스를 선언할 때 쓰는 것과 동일한 문법 기호입니다. type AB = A | B가 "A 또는 B"라는 뜻을 담는 것과 같은 원리예요.

| null 어노테이션은 참조 타입을 평범하게 쓸 수 있는 모든 자리에서 사용할 수 있어요.

  • union 타입, 레코드 타입, 커스텀 타입의 필드
  • 기존 타입의 타입 별칭
  • 제네릭 타입의 타입 적용
  • let 바인딩, 매개변수, 반환 타입에 대한 명시적 타입 어노테이션
  • 멤버, 프로퍼티, 필드 같은 객체 지향 프로그래밍 구성 요소의 타입 어노테이션
type AB = A | B
type AbNull = AB | null

type RecordField = { X: string | null }
type TupleField = string * string | null

type NestedGenerics = { Z : List<List<string | null> | null> | null }

다만 | 기호는 F#에서 다른 용도로도 쓰이기 때문에, 문법적으로 모호해지는 상황이 생길 수 있어요. 그럴 때는 null 어노테이션을 붙인 타입을 괄호로 감싸면 됩니다.

// Unexpected symbol '|' (directly before 'null') in member definition
type DUField = N of string | null

같은 타입을 괄호 ( )로 감싸면 문제가 해결돼요.

type DUField = N of (string | null)

패턴 매칭에서 |는 서로 다른 패턴 매칭 절을 구분하는 데 쓰여요.

match x with
| ?: string | null -> ...

사실 이 코드는 먼저 string 타입 검사를 수행한 뒤, null을 처리하는 별도의 절을 두는 것과 같은 의미예요.

match x with
| ?: string 
| null -> ...

중요 — 추가된 null 관련 기능들은 상호 운용을 위해 만들어진 것이에요. F# 타입을 모델링할 때 누락된 정보를 표현하려고 | null을 쓰는 것은 F#답지 않은(비관용적) 방식이에요. 그런 목적에는 위에서 설명한 것처럼 Options을 사용하세요. null 관련 규칙을 더 읽어보려면 스타일 가이드를 확인해 주세요.

더 알아보기