시그니처 파일

시그니처 파일 (Signature Files)

시그니처 파일(Signature File)은 F# 프로그램 요소의 공개 시그니처(public signature) 정보를 담는 파일이에요. 여기서 "프로그램 요소"란 형식, 네임스페이스, 모듈 같은 것들을 말하죠. 이 파일을 이용하면 해당 요소들을 코드 밖에서 얼마나 접근할 수 있게 할지, 즉 접근성(accessibility)을 지정할 수 있어요. 라이브러리를 만들 때 공개 API 표면을 의도대로 통제하고 싶다면 이 개념을 꼭 알아야 합니다. 하나씩 차근차근 살펴볼게요.

출처: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/signature-files

본문

개요 (Remarks)

F# 코드 파일마다 시그니처 파일을 둘 수 있어요. 코드 파일과 이름은 같고, 확장자만 .fs 대신 .fsi를 쓰는 파일이죠. 명령줄을 직접 쓴다면 시그니처 파일을 컴파일 명령줄에 함께 넣을 수도 있어요. 코드 파일과 시그니처 파일을 구분하기 위해 코드 파일을 *구현 파일(implementation file)*이라고 부르기도 합니다. 프로젝트에서는 시그니처 파일이 연결된 코드 파일보다 앞에 와야 해요.

시그니처 파일은 대응하는 구현 파일에 들어 있는 네임스페이스, 모듈, 형식, 멤버를 서술해요. 그리고 이 정보를 통해 구현 파일 안의 코드 중 어떤 부분을 구현 파일 밖의 코드에서 접근할 수 있게 할지, 어떤 부분을 구현 파일 내부로만 남길지를 지정하게 됩니다. 시그니처 파일에 포함된 네임스페이스·모듈·형식은 반드시 구현 파일에 포함된 것들의 부분 집합이어야 해요. 이 문서 뒷부분에서 따로 언급하는 몇 가지 예외를 빼면, 시그니처 파일에 나열되지 않은 언어 요소는 구현 파일에 대해 private으로 취급돼요. 프로젝트나 명령줄에서 시그니처 파일을 찾을 수 없으면 기본 접근성이 적용됩니다.

기본 접근성에 대한 자세한 내용은 Access Control을 참고하세요.

시그니처 파일에서는 형식의 정의나 각 메서드·함수의 구현을 다시 적지 않아요. 대신 각 메서드와 함수의 시그니처를 쓰는데, 이 시그니처가 모듈이나 네임스페이스 조각이 구현하는 기능의 완전한 명세 역할을 합니다. 형식 시그니처의 문법은 인터페이스와 추상 클래스에서 추상 메서드를 선언할 때 쓰는 문법과 같아요. IntelliSense나 F# 인터프리터 fsi.exe가 올바르게 컴파일된 입력을 보여 줄 때도 이와 같은 형태를 쓰죠.

형식 시그니처만으로는 해당 형식이 sealed인지, 인터페이스 형식인지를 판단하기에 정보가 부족할 때가 있어요. 그럴 땐 형식의 성격을 컴파일러에 알려 주는 특성을 직접 붙여야 합니다. 이런 용도로 쓰는 특성은 아래 표와 같아요.

특성 (Attribute) 설명 (Description)
[<Sealed>] 추상 멤버가 없거나 확장하면 안 되는 형식에 사용해요.
[<Interface>] 인터페이스인 형식에 사용해요.

시그니처 파일과 구현 파일의 선언 사이에서 특성이 일치하지 않으면 컴파일러가 오류를 냅니다.

val 키워드로 값이나 함수 값의 시그니처를 만들고, type 키워드로 형식 시그니처를 도입해요.

--sig 컴파일러 옵션을 쓰면 시그니처 파일을 생성할 수 있어요. 보통 .fsi 파일을 직접 작성하지는 않습니다. 대신 컴파일러로 .fsi 파일을 생성하고, 프로젝트가 있다면 여기에 추가한 다음, 접근을 허용하고 싶지 않은 메서드와 함수를 제거하는 방식으로 편집해요.

형식 시그니처에는 몇 가지 규칙이 있어요.

  • 구현 파일의 형식 약어(type abbreviation)는 시그니처 파일에서 약어가 아닌 형식과 일치하면 안 됩니다.
  • 레코드와 구별된 공용체(discriminated union)는 필드와 생성자를 전부 노출하거나 아예 노출하지 않아야 하며, 시그니처 안의 순서가 구현 파일의 순서와 일치해야 해요. 클래스는 필드와 메서드 중 일부, 전부, 또는 아무것도 노출하지 않을 수 있어요.
  • 생성자를 가진 클래스와 구조체는 기본 클래스의 선언(inherits 선언)을 노출해야 합니다. 또한 생성자를 가진 클래스와 구조체는 모든 추상 메서드와 인터페이스 선언을 노출해야 해요.
  • 인터페이스 형식은 모든 메서드와 인터페이스를 드러내야 합니다.

값 시그니처의 규칙은 다음과 같아요.

  • 접근성 수식어(public, internal 등)와 시그니처의 inline, mutable 수식어는 구현과 일치해야 합니다.
  • 제네릭 형식 파라미터의 개수(암시적으로 유추되든 명시적으로 선언되든)가 일치해야 하고, 제네릭 형식 파라미터의 형식과 형식 제약도 일치해야 해요.
  • Literal 특성을 쓴다면 시그니처와 구현 양쪽에 모두 있어야 하며, 양쪽에서 같은 리터럴 값을 써야 합니다.
  • 시그니처와 구현의 파라미터 패턴(인자 개수, arity)이 일관되어야 해요.
  • 시그니처 파일의 파라미터 이름이 대응하는 구현 파일과 다르면 시그니처 파일의 이름이 대신 사용되는데, 디버깅이나 프로파일링할 때 문제를 일으킬 수 있어요. 이런 불일치를 통보받고 싶다면 프로젝트 파일이나 컴파일러 호출 시 경고 3218을 활성화하면 됩니다(Compiler Options--warnon을 참고하세요).

아래 코드 예시는 네임스페이스, 모듈, 함수 값, 형식 시그니처에 적절한 특성을 함께 갖춘 시그니처 파일을 보여 줘요. 대응하는 구현 파일도 함께 보여 줍니다.

// Module1.fsi

namespace Library1
  module Module1 =
    val function1 : int -> int
    type Type1 =
        new : unit -> Type1
        member method1 : unit -> unit
        member method2 : unit -> unit

    [<Sealed>]
    type Type2 =
        new : unit -> Type2
        member method1 : unit -> unit
        member method2 : unit -> unit

    [<Interface>]
    type InterfaceType1 =
        abstract member method1 : int -> int
        abstract member method2 : string -> unit

다음 코드는 위 시그니처 파일에 대응하는 구현 파일을 보여 줍니다.

namespace Library1

module Module1 =

    let function1 x = x + 1

    type Type1() =
        member type1.method1() =
            printfn "type1.method1"
        member type1.method2() =
            printfn "type1.method2"

    [<Sealed>]
    type Type2() =
        member type2.method1() =
            printfn "type2.method1"
        member type2.method2() =
            printfn "type2.method2"

    [<Interface>]
    type InterfaceType1 =
        abstract member method1 : int -> int
        abstract member method2 : string -> unit

더 알아보기 (Learn more)