
코드를 짜다 보면 "String에 이런 메서드 하나만 있으면 좋겠다" 싶은 순간이 자주 옵니다. 공통 함수 모듈에 IsFirstUpper(s) 같은 함수를 만들어 두고 불러 써도 되지만, 그러면 호출할 때마다 모듈 이름을 떠올려야 하고 인텔리센스에도 잘 안 보이죠. 확장 메서드(Extension Method)를 쓰면 s.IsFirstUpper() 처럼 원래 그 형식에 있던 메서드인 양 점(.)을 찍어서 부를 수 있습니다. String, List, DataTable처럼 소스를 고칠 수 없는 .NET 기본 형식에도 붙일 수 있다는 게 가장 큰 장점입니다.
선언할 때 지킬 규칙
만드는 법은 간단합니다. 몇 가지만 지키면 됩니다.
Module안에 선언해야 합니다. 클래스 안에서는 확장 메서드를 만들 수 없습니다.- 메서드 위에
<Extension()>특성을 붙입니다. 이 특성은System.Runtime.CompilerServices네임스페이스에 있어서 파일 위에Imports가 필요합니다. - 첫 번째 매개변수의 형식이 "어느 형식을 확장할지"를 정합니다. 호출할 때는 이 매개변수가 점 앞의 객체로 자동으로 채워지고, 실제로 넘기는 인수는 두 번째 매개변수부터입니다.
VB.NET은 C#과 달리 첫 매개변수를 ByRef 로 받는 것도 허용합니다. 다만 대부분은 ByVal 로 두고, 값 형식(구조체)의 원본을 직접 바꿔야 하는 특수한 경우에만 ByRef를 씁니다.
문자열 첫 글자가 대문자인지 확인하는 메서드를 String에 붙여 보겠습니다.
Imports System.Runtime.CompilerServices
Module StringExtensions
<Extension()>
Public Function IsFirstLetterUpperCase(ByVal s As String) As Boolean
If String.IsNullOrEmpty(s) Then
Return False
End If
Return Char.IsUpper(s(0))
End Function
End Module
빈 문자열이나 Nothing이면 False를 돌려주고, 그렇지 않으면 첫 글자 s(0) 을 Char.IsUpper 로 검사합니다. 쓰는 쪽에서는 이렇게 부릅니다.
Dim result As Boolean = "Hello".IsFirstLetterUpperCase()
Console.WriteLine(result) ' 출력: True
여기서 재미있는 점은 Nothing 인 문자열 변수에 대고 불러도 예외가 나지 않는다는 겁니다. 확장 메서드는 겉모습만 인스턴스 메서드일 뿐 실제로는 모듈의 정적(Shared) 함수 호출로 바뀌기 때문에, Nothing이 그대로 첫 인수로 넘어갑니다. 그래서 위 코드처럼 메서드 안에서 Nothing 검사를 빼먹으면 안 됩니다. 같은 이유로 확장 메서드는 상속받은 클래스에서 오버라이드할 수도 없습니다.
제네릭 List에 붙인 예
두 번째 예는 List(Of T) 에서 지정한 위치의 항목을 문자열로 꺼내되, 인덱스가 범위를 벗어나면 예외 대신 Nothing을 돌려주는 메서드입니다. 형식 매개변수 I 를 써서 어떤 형식의 List든 받을 수 있게 했습니다.
Imports System.Runtime.CompilerServices
Module Module1
<Extension()>
Public Function ToString(Of I)(L As List(Of I), stri As Integer) As String
If L.Count >= (stri + 1) Then
Return L.Item(stri).ToString
End If
Return Nothing
End Function
End Module
Dim U As New List(Of String)
U.AddRange({"가", "을", "하"})
MsgBox(U.ToString(1))
인덱스는 0부터 시작하니 U.ToString(1) 은 두 번째 항목인 "을"을 돌려줍니다. U.ToString(5) 처럼 범위를 넘기면 ArgumentOutOfRangeException 대신 Nothing이 나와서, 파싱한 결과 배열에서 칸 수가 들쭉날쭉할 때 쓰기 편합니다. 음수 인덱스는 검사하지 않으니 그런 값이 들어올 수 있으면 조건을 하나 더 넣어야 합니다.
이름이 겹치면 확장 메서드가 무시됩니다
위 예제에서 메서드 이름을 ToString 으로 지은 건 일부러 위험한 사례를 보여 주는 것이기도 합니다. 확장 메서드와 같은 이름, 같은 시그니처의 인스턴스 메서드가 이미 형식에 있으면 컴파일러는 항상 인스턴스 메서드를 먼저 고릅니다. List 에는 매개변수 없는 ToString() 만 있어서 정수 인수를 주면 확장 메서드 쪽으로 가지만, 인수 없이 U.ToString() 이라고 쓰면 원래 메서드가 불려 형식 이름만 나옵니다. 나중에 .NET 버전이 바뀌면서 같은 이름의 메서드가 기본 형식에 추가되면 내 확장 메서드가 조용히 무시될 수도 있고요. 그래서 실무에서는 ItemText, SafeItem 처럼 기본 메서드와 겹치지 않는 이름을 쓰는 편이 안전합니다.
확장 메서드는 해당 모듈이 있는 네임스페이스를 Imports 해야 보입니다. 다른 프로젝트에서 "분명 만들었는데 인텔리센스에 안 뜬다"면 대부분 이 Imports가 빠진 경우입니다. 그리고 편하다고 String이나 Object에 이것저것 붙이다 보면 인텔리센스 목록이 지저분해지고, 이 메서드가 원래 .NET 것인지 우리 프로젝트 것인지 헷갈리게 됩니다. 저는 여러 화면에서 반복되는 처리만 골라서 StringExtensions, DataTableExtensions 처럼 확장 대상별로 모듈을 나눠 관리합니다.