use 바인딩

use 바인딩 (use Binding)

F#에는 리소스를 정리해 주는 use 키워드가 있어요. 파일을 열고 닫는 일처럼, 리소스를 제때 해제해야 하는 상황에서 아주 유용하죠. 여기서는 use 바인딩과 using 함수가 어떻게 리소스의 초기화와 해제를 제어하는지 살펴볼게요.

출처: Resource Management: The use Keyword - F#

본문

리소스(Resources)

resource라는 말은 여러 뜻으로 쓰여요. 어떤 때는 애플리케이션이 사용하는 데이터, 예를 들어 문자열이나 그래픽 같은 것을 가리키기도 하죠. 하지만 지금 다루는 맥락에서 리소스는 소프트웨어나 운영체제가 관리하는 리소스를 뜻해요. 그래픽 디바이스 컨텍스트, 파일 핸들, 네트워크·데이터베이스 연결, wait handle 같은 동시성 객체가 대표적이에요.

이런 리소스를 쓰는 애플리케이션은 보통 두 단계를 거쳐요. 운영체제나 리소스 제공자로부터 리소스를 얻고, 나중에는 다시 풀(pool)에 돌려줘서 다른 애플리케이션이 쓸 수 있게 하죠. 문제는 애플리케이션이 리소스를 제때 공용 풀에 돌려주지 않을 때 생겨요.

리소스 관리하기(Managing Resources)

효율적이고 책임감 있게 리소스를 관리하려면, 리소스를 빠르고 예측 가능한 순서로 해제해야 해요. .NET Framework는 이를 위해 System.IDisposable 인터페이스를 제공하죠. System.IDisposable를 구현하는 타입은 System.IDisposable.Dispose 메서드를 갖고 있고, 이 메서드가 리소스를 올바르게 해방해요. 잘 짜인 애플리케이션은 제한된 리소스를 들고 있는 객체가 더 이상 필요 없어지면 System.IDisposable.Dispose가 즉시 호출되도록 보장해요.

다행히 대부분의 .NET 언어가 이런 작업을 편하게 해 주고, F#도 예외가 아니에요. dispose 패턴을 지원하는 언어 구조가 두 가지 있는데, 바로 use 바인딩using 함수예요.

use 바인딩(use Binding)

use 키워드는 let 바인딩과 모양이 아주 비슷해요:

use value = expression

기능적으로는 let 바인딩과 같지만, 값이 스코프를 벗어날 때 그 값에 대해 Dispose를 호출한다는 점이 달라요. 컴파일러가 값에 null 검사를 넣어 주기 때문에, 값이 null이면 Dispose 호출은 시도되지 않아요.

다음 예시는 use 키워드를 써서 파일을 자동으로 닫는 방법을 보여줘요.

open System.IO

let writetofile filename obj =
   use file1 = File.CreateText(filename)
   file1.WriteLine("{0}", obj.ToString() )
   // file1.Dispose() is called implicitly here.

writetofile "abc.txt" "Humpty Dumpty sat on a wall."

use가 여러 개 있으면, 선언된 순서의 역순으로 해제돼요. 다시 말해 처음 선언한 use가 가장 나중에 해제된다는 뜻이에요.

참고: 계산 표현식(computation expression) 안에서도 use를 쓸 수 있는데, 이때는 맞춤형 버전의 use 식이 사용돼요. 자세한 내용은 Sequences, Async expressions, Task expressions, Computation Expressions 문서를 참고하세요.

using 함수(using Function)

using 함수는 다음과 같은 형태를 가져요:

using (expression1) function-or-lambda

using 식에서 expression1은 해제해야 할 객체를 만들어요. expression1의 결과(즉 해제할 객체)는 function-or-lambda의 인자 value가 되죠. 이때 function-or-lambdaexpression1이 만든 값의 타입과 맞는 인자를 하나만 받는 함수이거나, 그 타입의 인자를 받는 람다 식이에요. 함수 실행이 끝나면 런타임이 Dispose를 호출하고 리소스를 해방해요(값이 null이면 Dispose 호출을 시도하지 않아요).

다음 예시는 람다 식과 함께 using 식을 쓰는 모습이에요.

open System.IO

let writetofile2 filename obj =
    using (System.IO.File.CreateText(filename)) ( fun file1 ->
        file1.WriteLine("{0}", obj.ToString() )
    )

writetofile2 "abc2.txt" "The quick sly fox jumps over the lazy brown dog."

다음 예시는 함수와 함께 using 식을 쓰는 모습이에요.

let printToFile (file1 : System.IO.StreamWriter) =
    file1.WriteLine("Test output");

using (System.IO.File.CreateText("test.txt")) printToFile

인자가 이미 일부 적용된 함수도 쓸 수 있어요. 다음 코드가 그 예시인데, XYZ라는 문자열을 담은 파일을 만들어요.

let printToFile2 obj (file1 : System.IO.StreamWriter) =
    file1.WriteLine(obj.ToString())

using (System.IO.File.CreateText("test.txt")) (printToFile2 "XYZ")

using 함수와 use 바인딩은 사실상 같은 일을 하는 아주 유사한 방법이에요. 다만 using 키워드는 Dispose가 호출되는 시점을 더 세밀하게 제어할 수 있어요. using을 쓰면 함수나 람다 식이 끝나는 지점에서 Dispose가 호출되고, use 키워드를 쓰면 포함하는 코드 블록이 끝나는 지점에서 호출되죠. 일반적으로는 using 함수보다 use를 쓰는 걸 권장해요.

더 알아보기