시그니처 파일
시그니처 파일 (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