네임스페이스
네임스페이스 (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)에서도 쓸 수 있어요.