시그니처 파일

시그니처 파일 (Signature Files)

F#에서는 코드 파일마다 그와 짝이 되는 시그니처 파일을 둘 수 있어요. 시그니처 파일은 코드 파일과 같은 이름을 가지되 확장자가 .fs 대신 .fsi인 파일이에요. 명령줄에서 직접 컴파일할 때는 시그니처 파일을 컴파일 명령줄에 함께 넣을 수도 있고요. 코드 파일과 시그니처 파일을 구분하기 위해, 코드 파일 쪽을 가끔 구현 파일(implementation file) 이라고 부르기도 해요. 프로젝트에서는 시그니처 파일이 대응하는 코드 파일보다 앞에 와야 합니다.

출처

본문

시그니처 파일은 대응하는 구현 파일에 들어 있는 네임스페이스·모듈·타입·멤버를 설명해요. 시그니처 파일에 있는 정보를 이용해, 구현 파일 안의 코드 중 어떤 부분을 바깥 코드에서 접근할 수 있게 할지, 어떤 부분을 구현 파일 내부로 한정할지를 정할 수 있어요.

시그니처 파일에 포함되는 네임스페이스·모듈·타입은 반드시 구현 파일에 포함된 것들의 부분집합이어야 해요. 이 문서에서 나중에 언급하는 몇 가지 예외를 빼면, 시그니처 파일에 나열되지 않은 언어 요소는 구현 파일에 private으로 간주돼요. 프로젝트나 명령줄에서 시그니처 파일을 찾지 못하면 기본 접근성(default accessibility)이 적용됩니다.

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

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

타입 시그니처만으로는 그 타입이 sealed인지, 인터페이스 타입인지 알 수 없는 경우가 있어요. 그럴 때는 컴파일러에 그 타입의 성격을 알려주는 특성(attribute) 을 붙여야 합니다. 이런 용도로 쓰는 특성은 다음 표와 같아요.

Attribute 설명
[<Sealed>] 추상 멤버가 없거나, 확장되면 안 되는 타입에 사용
[<Interface>] 인터페이스인 타입에 사용

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

값이나 함수 값의 시그니처를 만들 때는 키워드 val을 쓰고, 타입 시그니처는 키워드 type으로 도입해요.

시그니처 파일은 컴파일러 옵션 --sig로 생성할 수 있어요. 보통 .fsi 파일을 손으로 직접 쓰기보다는, 컴파일러로 .fsi 파일을 만들어 프로젝트에 추가하고, 접근 가능하게 하고 싶지 않은 메서드·함수를 지워나가며 편집하는 방식으로 작업합니다.

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

  • 구현 파일에 있는 타입 약어(type abbreviation)는 시그니처 파일에서 약어가 없는 타입과 일치하면 안 됩니다.
  • 레코드와 판별 유니온(discriminated union)은 필드와 생성자를 전부 노출하거나 전혀 노출하지 않아야 하고, 시그니처의 순서는 구현 파일의 순서와 일치해야 해요. 클래스는 필드와 메서드를 일부만, 전부, 또는 아예 노출하지 않는 형태를 선택할 수 있어요.
  • 생성자가 있는 클래스와 구조체는 기반 클래스(베이스 클래스)의 선언(inherits 선언)을 노출해야 해요. 또한 생성자가 있는 클래스·구조체는 추상 메서드와 인터페이스 선언을 모두 노출해야 합니다.
  • 인터페이스 타입은 모든 메서드와 인터페이스를 노출해야 합니다.

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

  • 접근성 한정자(public, internal 등)와 inline, mutable 한정자는 시그니처와 구현에서 일치해야 해요.
  • 제네릭 타입 매개변수의 개수(암시적으로 추론되었든 명시적으로 선언되었든)는 일치해야 하고, 제네릭 타입 매개변수의 타입과 타입 제약 조건도 일치해야 합니다.
  • Literal 특성을 쓰면 시그니처와 구현 양쪽 모두에 나타나야 하고, 양쪽에서 같은 리터럴 값을 사용해야 해요.
  • 시그니처와 구현의 매개변수 패턴(이를 항수(arity) 라고도 해요)은 일관돼야 합니다.
  • 시그니처 파일의 매개변수 이름이 대응하는 구현 파일과 다르면, 시그니처 파일 쪽 이름이 대신 사용돼요. 이 경우 디버깅이나 프로파일링에서 문제가 생길 수 있어요. 이런 불일치를 알림으로 받고 싶다면 프로젝트 파일이나 컴파일러 호출 시 경고 3218을 켜세요(--warnon 항목은 Compiler Options를 참고).

다음 코드 예시는 네임스페이스·모듈·함수 값·타입 시그니처가 적절한 특성과 함께 들어 있는 시그니처 파일을 보여줘요. 그리고 그에 대응하는 구현 파일도 함께 보여줍니다.

// 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

더 알아보기