네임스페이스

네임스페이스 (Namespaces) — F#

여러분이 만든 코드가 커지면 이름이 충돌하거나, 어디에 뭐가 있는지 한눈에 안 들어오는 문제가 생기기 마련이에요. 이럴 때 코드를 '관련 기능별 묶음'으로 정리해 주는 게 바로 네임스페이스입니다. 이 문서에서는 F#에서 네임스페이스를 선언하는 문법부터, 파일·어셈블리 사이에 걸쳐 쓰는 방법, 그리고 이전부터 자주 헷갈리던 rec 재귀 네임스페이스까지 차근차근 살펴볼게요.

출처: Namespaces — F# Language Reference (Microsoft Learn)

본문

네임스페이스는 F# 프로그램 요소들을 하나의 이름으로 묶어서, 관련된 기능끼리 코드를 정리할 수 있게 해 줘요. F# 파일에서는 보통 네임스페이스가 최상위 요소로 자리잡습니다.

구문(Syntax)

namespace [rec] [parent-namespaces.]identifier

설명(Remarks)

네임스페이스 안에 코드를 넣고 싶다면, 파일의 첫 번째 선언이 네임스페이스 선언이어야 해요. 그러면 그 파일의 전체 내용이 그 네임스페이스에 속하게 됩니다. 파일 안쪽에 다른 네임스페이스 선언이 없다면 말이죠. 만약 뒤에 다시 네임스페이스가 등장한다면, 그 지점까지의 코드는 앞선 네임스페이스에 속한다고 보면 돼요.

네임스페이스는 값과 함수를 직접 담을 수는 없어요. 대신 값과 함수는 '모듈' 안에 두고, 그 모듈을 네임스페이스 안에 넣는 구조를 씁니다. 즉 네임스페이스는 타입과 모듈을 포함할 수 있어요.

XML 문서 주석은 네임스페이스 위에 달아도 되지만, 실제로는 무시됩니다. 컴파일러 지시문은 네임스페이스 위에 선언해도 괜찮고요.

네임스페이스는 namespace 키워드로 명시적으로 선언하거나, 모듈을 선언할 때 암시적으로 만들 수 있어요. 명시적으로 선언할 때는 namespace 키워드 뒤에 네임스페이스 이름을 붙입니다. 아래 예시는 Widgets 네임스페이스를 선언하고, 그 안에 타입 하나와 모듈 하나를 넣은 코드 파일이에요.

namespace Widgets

type MyWidget1 =
    member this.WidgetName = "Widget1"

module WidgetsModule =
    let widgetName = "Widget2"

파일의 전체 내용이 하나의 모듈에 담겨 있다면, module 키워드로 네임스페이스를 암시적으로 선언할 수도 있어요. 전체 모듈 이름(fully qualified name)에 새 네임스페이스 이름을 넣어주면 됩니다. 아래 예시는 Widgets 네임스페이스와, 함수 하나를 담은 WidgetsModule 모듈을 선언한 코드 파일이에요.

module Widgets.WidgetModule

let widgetFunction x y =
   printfn "%A %A" x y

아래 코드는 위 코드와 같은 뜻이지만, 모듈을 로컬 모듈 선언으로 만든 버전이에요. 이 경우에는 네임스페이스가 반드시 자기 줄에 따로 등장해야 합니다.

namespace Widgets

module WidgetModule =

    let widgetFunction x y =
        printfn "%A %A" x y

같은 파일 안에서 여러 네임스페이스에 걸쳐 모듈이 둘 이상 필요하다면, 로컬 모듈 선언을 써야 해요. 로컬 모듈 선언을 쓸 때는 모듈 선언에 정규화된 네임스페이스(qualified namespace)를 쓸 수 없습니다. 아래 코드는 네임스페이스 선언 하나에 로컬 모듈 선언 두 개를 둔 파일이에요. 이 경우 모듈은 네임스페이스에 직접 들어가고, 파일 이름과 같은 이름의 '암시적으로 만들어진 모듈'은 생기지 않아요. 파일에 있는 다른 코드(예: do 바인딩)는 네임스페이스에는 속하지만 내부 모듈에는 속하지 않기 때문에, 모듈 멤버인 widgetFunction을 쓸 때는 모듈 이름으로 한정(qualify)해 줘야 합니다.

namespace Widgets

module WidgetModule1 =
   let widgetFunction x y =
      printfn "Module1 %A %A" x y
module WidgetModule2 =
   let widgetFunction x y =
      printfn "Module2 %A %A" x y

module useWidgets =

  do
     WidgetModule1.widgetFunction 10 20
     WidgetModule2.widgetFunction 5 6

이 예시의 출력은 다음과 같아요.

Module1 10 20
Module2 5 6

더 자세한 내용은 Modules 문서를 참고하세요.

중첩 네임스페이스(Nested Namespaces)

중첩 네임스페이스를 만들 때는 반드시 정규화된 이름을 써야 해요. 그렇지 않으면 새 '최상위' 네임스페이스가 만들어져 버리거든요. 네임스페이스 선언에서는 들여쓰기가 무시됩니다.

아래는 중첩 네임스페이스를 선언하는 예시예요.

namespace Outer

    // Full name: Outer.MyClass
    type MyClass() =
       member this.X(x) = x + 1

// Fully qualify any nested namespaces.
namespace Outer.Inner

    // Full name: Outer.Inner.MyClass
    type MyClass() =
       member this.Prop1 = "X"

파일과 어셈블리의 네임스페이스

네임스페이스는 한 프로젝트나 컴파일 단위의 여러 파일에 걸쳐 있을 수 있어요. 하나의 파일에 담긴 네임스페이스의 일부를 네임스페이스 조각(namespace fragment) 이라고 부릅니다. 네임스페이스는 여러 어셈블리에 걸쳐 있을 수도 있어요. 예를 들어 System 네임스페이스는 .NET Framework 전체를 포함하는데, 이게 수많은 어셈블리에 걸쳐 있고 또 많은 중첩 네임스페이스를 담고 있죠.

전역 네임스페이스(Global Namespace)

미리 정의된 global 네임스페이스는 이름을 .NET 최상위 네임스페이스에 둘 때 사용해요.

namespace global

type SomeType() =
    member this.SomeMember = 0

global은 .NET 최상위 네임스페이스를 가리킬 때도 쓸 수 있습니다. 예를 들어 다른 네임스페이스와의 이름 충돌을 해결할 때요.

global.System.Console.WriteLine("Hello World!")

재귀 네임스페이스(Recursive namespaces)

네임스페이스는 재귀적(recursive) 으로도 선언할 수 있어요. 이렇게 하면 그 안에 있는 모든 코드가 서로를 상호 참조할 수 있게 됩니다. namespace rec로 선언하면 돼요. 타입과 모듈 사이에서 서로를 참조하는 코드를 쓸 수 없어서 겪던 불편함을, namespace rec가 상당 부분 덜어 줍니다. 예시를 볼게요.

namespace rec MutualReferences

type Orientation = Up | Down
type PeelState = Peeled | Unpeeled

// This exception depends on the type below.
exception DontSqueezeTheBananaException of Banana

type Banana(orientation: Orientation) =
    member val IsPeeled = false with get, set
    member val Orientation = orientation with get, set
    member val Sides: PeelState list = [Unpeeled; Unpeeled; Unpeeled; Unpeeled] with get, set

    member self.Peel() = BananaHelpers.peel self // Note the dependency on the BananaHelpers module.
    member self.SqueezeJuiceOut() = raise (DontSqueezeTheBananaException self) // This member depends on the exception above.

module BananaHelpers =
    let peel (banana: Banana) =
        let flip (banana: Banana) =
            match banana.Orientation with
            | Up ->
                banana.Orientation  banana

        // Update the peel state for all sides of the banana.
        let peelSides (banana: Banana) =
            banana.Sides
            |> List.map (function
                         | Unpeeled -> Peeled
                         | Peeled -> Peeled)

        // Apply the flipping and peeling logic based on the orientation.
        match banana.Orientation with
        | Up ->   banana |> flip |> peelSides
        | Down -> banana |> peelSides

눈여겨볼 점은, 예외 DontSqueezeTheBananaException과 클래스 Banana가 서로를 참조하고 있다는 거예요. 또 모듈 BananaHelpers와 클래스 Banana도 서로를 참조하죠. 이런 표현은 MutualReferences 네임스페이스에서 rec 키워드를 빼 버리면 F#에서 쓸 수 없게 됩니다.

이 기능은 최상위 모듈(Top-level Modules)에서도 쓸 수 있어요.

더 알아보기