F# 특성(Attributes)

F# 특성(Attributes)

F#의 특성(attributes)은 프로그램의 구성 요소에 메타데이터를 붙이는 .NET 기능이에요. 코드 자체의 동작을 바꾸는 대신, 해당 요소에 대한 정보를 담아 두고 컴파일러나 리플렉션(reflection)이 그 정보를 활용하게 할 수 있죠. 예를 들어 특정 함수를 더 이상 쓰지 말자는 표시(Obsolete)를 붙이거나, 네이티브 함수를 호출하는 방법(DllImport)을 알려 주는 데 씁니다. 이번 글에서는 특성을 선언하는 문법부터, 어디에 붙일 수 있는지, 그리고 대상(target)을 지정하는 방법까지 하나씩 살펴볼게요.

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

본문

구문(Syntax)

[<target:attribute-name(arguments)>]

설명(Remarks)

위 구문에서 target은 생략 가능해요. 붙이면 그 특성이 적용되는 프로그램 요소의 종류를 가리키고, 지정할 수 있는 값은 이 문서 뒤쪽에 있는 표에서 확인할 수 있어요.

attribute-name은 유효한 특성 타입의 이름(네임스페이스로 한정된 이름일 수도 있어요)을 가리켜요. 특성 타입 이름에 보통 붙는 Attribute 접미사는 붙여도 되고 빼도 되죠. 예를 들어 이 자리에서 ObsoleteAttribute 타입은 그냥 Obsolete라고 줄여 쓸 수 있어요.

arguments는 특성 타입의 생성자에 전달하는 인자예요. 매개변수가 없는 생성자라면 인자 목록과 괄호를 아예 생략할 수 있어요. 특성은 위치 인자(positional arguments)와 명명 인자(named arguments)를 모두 지원하는데, 위치 인자는 나타나는 순서 그대로 사용되는 인자예요. 특성에 공개(public) 프로퍼티가 있으면 명명 인자를 쓸 수 있고, 인자 목록에서 다음과 같은 문법으로 값을 지정해요.

property-name = property-value

이런 프로퍼티 초기화는 어떤 순서로 써도 되지만, 반드시 위치 인자 뒤에 와야 해요. 위치 인자와 프로퍼티 초기화를 함께 쓰는 특성의 예를 볼게요.

open System.Runtime.InteropServices

[<DllImport("kernel32", SetLastError=true)>]
extern bool CloseHandle(nativeint handle)

이 예시에서 특성은 DllImportAttribute이고, 여기서는 짧은 형태로 썼어요. 첫 번째 인자는 위치 매개변수이고 두 번째는 프로퍼티예요.

특성은 .NET의 프로그래밍 구성 요소로, 특성(attribute)이라는 객체를 타입이나 다른 프로그램 요소에 연결해 주는 기능이에요. 특성이 적용되는 프로그램 요소를 특성 대상(attribute target)이라고 부르고, 특성은 보통 자신의 대상에 대한 메타데이터를 담고 있어요. 여기서 메타데이터란 타입의 필드와 멤버를 제외한, 그 타입에 대한 모든 데이터를 뜻한다고 보면 돼요.

F#의 특성은 다음 프로그래밍 요소에 적용할 수 있어요. 함수, 메서드, 어셈블리, 모듈, 타입(클래스, 레코드, 구조체, 인터페이스, 대리자, 열거형, 구분된 공용 구조체 등), 생성자, 프로퍼티, 필드, 매개변수, 타입 매개변수, 반환 값이죠. 반면 클래스 안의 let 바인딩, 식(expression), 워크플로 식에는 특성을 붙일 수 없어요.

보통 특성 선언은 특성 대상의 선언 바로 앞에 둡니다. 여러 특성 선언을 함께 쓰는 것도 가능해요. 다음과 같이요.

[<Owner("Jason Carlson")>]
[<Company("Microsoft")>]
type SomeType1 =

특성은 런타임에 .NET 리플렉션을 이용해 조회할 수 있어요.

앞의 코드처럼 특성을 하나씩 여러 번 선언할 수도 있고, 개별 특성과 생성자를 세미콜론으로 구분해서 하나의 대괄호 안에 묶어 선언할 수도 있어요.

[<Owner("Darren Parker"); Company("Microsoft")>]
type SomeType2 =

자주 마주치는 특성으로는 Obsolete 특성, 보안 고려 사항을 위한 특성, COM 지원을 위한 특성, 코드 소유권과 관련된 특성, 타입을 직렬화할 수 있는지 나타내는 특성 등이 있어요. 다음 예시는 Obsolete 특성의 사용법을 보여줘요.

open System

[<Obsolete("Do not use. Use newFunction instead.")>]
let obsoleteFunction x y =
  x + y

let newFunction x y =
  x + 2 * y

// The use of the obsolete function produces a warning.
let result1 = obsoleteFunction 10 100
let result2 = newFunction 10 100

assemblymodule이라는 특성 대상에는 어셈블리의 최상위 do 바인딩에 특성을 적용해요. 특성 선언에 assembly 또는 module이라는 단어를 포함하면 돼요.

open System.Reflection
[<assembly:AssemblyVersionAttribute("1.0.0.0")>]
[<``module``:MyCustomModuleAttribute>]
do
   printfn "Executing..."

do 바인딩에 적용한 특성에서 특성 대상을 생략하면, F# 컴파일러가 그 특성에 어울리는 대상을 스스로 판단하려고 해요. 많은 특성 클래스는 System.AttributeUsageAttribute 타입의 특성을 갖고 있는데, 여기에 그 특성이 지원하는 가능한 대상에 대한 정보가 담겨 있어요. 이 System.AttributeUsageAttribute가 특성이 함수를 대상으로 지원한다고 표시하면, 그 특성은 프로그램의 진입점(main entry point)에 적용된다고 간주돼요. 반대로 어셈블리를 대상으로 지원한다고 표시하면 컴파일러는 그 특성을 어셈블리에 적용한다고 봐요. 대부분의 특성은 함수와 어셈블리 양쪽에 모두 적용되지는 않지만, 둘 다 적용되는 경우에는 프로그램의 main 함수에 적용되는 것으로 간주돼요. 특성 대상을 명시적으로 지정하면, 그 특성은 지정된 대상에 적용돼요.

사실 특성 대상을 명시적으로 지정할 필요가 흔하진 않지만, 특성에서 사용할 수 있는 target의 유효한 값과 사용 예시를 정리해 볼게요.

assembly

[<assembly: AssemblyVersion("1.0.0.0")>]

module

[<``module``: MyCustomAttributeThatWorksOnModules>]

method

[<MyCustomAttributeThatWorksOnMethods>]
let someFunction() = 42

class

[<MyCustomAttributeThatWorksOnClasses>]
type MyClass(myValue: int) =
    member _.MyValue = myValue

struct

[<MyCustomAttributeThatWorksOnStructs>]
[<Struct>]
type MyStruct(myValue: int) =
    member _.MyValue = myValue

interface

[<MyCustomAttributeThatWorksOnInterfaces>]
type MyInterface =
    abstract member Prop: string

enum

[<MyCustomAttributeThatWorksOnEnums>]
type Color =
    | Red = 0
    | Green = 1
    | Blue = 2

constructor

type MyClass(myValue: int) =
    member _.MyValue = myValue

    [<MyCustomAttributeThatWorksOnCtors>]
    new () = MyClass 42

return

let function1 x : [<return: MyCustomAttributeThatWorksOnReturns>] int = x + 1

field

[<DefaultValue>] val mutable x: int

property

[<Obsolete>] this.MyProperty = x

param

member this.MyMethod([<Out>] x : ref<int>) = x := 10

type

[<type: StructLayout(LayoutKind.Sequential)>]
type MyStruct =
  struct
    val x : byte
    val y : int
  end

더 알아보기 (Learn more)