확장 메서드는 C# 및 객체 지향 프로그래밍(OOP)의 기본적인 부분입니다. C#의 확장 메서드는 기존 유형, 즉 클래스, 인터페이스 또는 구조체를 수정하지 않고 “확장”할 수 있게 해줍니다.
이는 특히 제어할 수 없거나 변경할 수 없는 유형, 예를 들어 서드파티 라이브러리의 유형이나 string
, List<T>
와 같은 내장 .NET 유형에 새로운 기능을 추가하고 싶을 때 유용합니다.
이 기사에서는 클래스뿐만 아니라 서드파티 및 시스템 클래스에 확장 메서드를 추가하는 방법을 배우게 됩니다.
목차
DateTime 확장 메소드 만드는 방법
기존 DateTime 클래스와 함께 사용할 수 있는 메소드가 필요하다고 가정해봅시다. 아마도 주어진 DateTime 객체가 주말인지 또는 다른 날짜인지를 반환하는 메소드가 필요할 것입니다.
확장 메소드는 정적 클래스에 정의되어야 합니다. 왜냐하면 확장 메소드는 사실상 문법적 설탕이기 때문에 확장하는 유형의 인스턴스 메소드처럼 정적 메소드를 호출할 수 있게 해줍니다.
확장 메소드는 정적 클래스에 있어야 합니다.
-
객체 필요 없음: 확장 메서드를 사용하기 위해 객체를 생성할 필요가 없습니다. 메서드는 기존 유형(예:
string
)에 새로운 기능을 추가하므로 클래스의 인스턴스가 없어도 작동할 수 있습니다. -
조직화된 코드: 확장 메서드를 정적 클래스에 넣으면 깔끔하게 유지할 수 있습니다. 관련된 메서드를 그룹화할 수 있으며, 적절한 네임스페이스를 사용하여 코드를 쉽게 포함할 수 있습니다.
따라서 정적 클래스를 사용하면 기존 유형에 유용한 메서드를 추가할 수 있으며, 원래 코드를 변경하지 않고도 객체 없이 호출할 수 있습니다.
먼저, DateTimeExtensions
정적 클래스를 생성해 보겠습니다.
public static class DateTimeExtensions {
}
이것은 우리가 만들고자 하는 모든 DateTime
확장자를 포함할 것입니다.
public static bool IsWeekend(this DateTime date)
{
return date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
}
설명:
public static bool IsWeekend
: 이는 IsWeekend
라는 정적 메서드를 정의하며, bool
값을 반환합니다(참/거짓).
this DateTime date
: this
키워드는 메서드 인수로 사용될 때 이 메서드가 확장 메서드임을 나타냅니다. 이는 해당 메서드가 DateTime
클래스의 확장임을 의미합니다.
동일 유형 확장 메서드 체이닝 방법
확장 메서드가 다른 메서드와 체이닝되기 위해서는 일반적으로 확장하는 것과 동일한 유형(또는 호환되는 유형)을 반환해야 합니다. 이렇게 하면 이전 메서드의 결과에 대해 다른 메서드를 호출할 수 있습니다.
using System.Globalization;
public static string ToTitleCase(this string str)
{
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str.ToLower());
}
public static string TrimAndAppend(this string str, string toAppend)
{
return str.Trim() + toAppend;
}
위의 예에서 ToTitleCase
와 TrimAndAppend
메서드 모두 문자열 값을 반환하므로, 문자열을 제목 형식으로 변환한 후 모든 공백을 잘라내고 제공된 문자열을 추가하는 아래와 같이 확장 메서드를 체이닝할 수 있습니다.
첫 번째 매개변수가 확장 메서드가 적용된 문자열이므로(this
키워드로 설명된 바와 같이) TrimAndAppend
메서드에 두 번째 매개변수만 제공했다는 점에 유의하세요.
var title = "hello world "
.ToTitleCase()
.TrimAndAppend("!!");
//출력:
// 안녕하세요, 세계!!
확장 메서드가 다른 유형(원래 유형이 아니거나 호환되는 유형이 아닌)을 반환하면 체이닝할 수 없습니다. 예를 들어:
var date = new DateTime();
date.IsWeekend().AddDays(1);
덜 명백한 이유로, 이것은 작동하지 않습니다. 메서드를 체이닝할 때, 원래 변수에서 체이닝되는 것이 아니라 이전 메서드 호출의 반환 유형에서 체이닝됩니다.
여기에서 우리는 IsWeekend()
라는 날짜를 가지고 있으며, 이는 부울 값을 반환합니다. 그런 다음 부울 값에 대해 AddDays(1)
를 호출하려고 했지만, 이는 DateTime
확장으로 존재하지 않습니다. 코드 컴파일러는 빌드를 실패하고 이 사실을 알리는 오류를 발생시킵니다.
인스턴스를 반환하여 체인하기
일부 확장 메서드, 특히 구성(예: 의존성 주입)을 위한 메서드에서는 메서드 체이닝을 허용하기 위해 동일한 인스턴스를 반환합니다. 이를 통해 여러 호출에 걸쳐 원래 객체 또는 수정된 상태로 계속 작업할 수 있어 유창한 인터페이스를 가능하게 합니다.
자동차 목록의 예를 들어 봅시다.
public static List<T> RemoveDuplicates<T>(this List<T> list)
{
// 중복을 제거하고 목록을 업데이트하기 위해 Distinct 사용
list = list.Distinct().ToList();
// 메서드 체이닝을 허용하기 위해 수정된 목록 반환
return list;
}
public static List<T> AddRangeOfItems<T>(this List<T> list, IEnumerable<T> items)
{
// 목록에 항목 범위를 추가
list.AddRange(items);
// 메서드 체이닝을 허용하기 위해 수정된 목록 반환
return list;
}
이제 이러한 확장 메서드에서 목록을 반환했으므로 동일한 목록에서 추가 메서드를 체인할 수 있습니다. 예를 들어, RemoveDuplicates()
로 중복을 제거한 후, 동일한 목록에서 즉시 AddRangeOfItems()
를 호출할 수 있습니다.
그래서 우리는 다음과 같은 일을 할 수 있습니다:
var existingStock = new List<string> { "Ford", "Jaguar", "Ferrari", "Ford", "Renault" };
var availableBrands = existingStock
.RemoveDuplicates()
.AddRangeOfItems(new[] { "Lamborghini" }); // 새로운 재고 가능
Console.WriteLine("Brands Available Now: " + string.Join(", ", availableBrands));
// 출력: 현재 이용 가능한 브랜드: Ford, Jaguar, Ferrari, Renault, Lamborghini
우리는 자동차 브랜드 목록에서 중복을 제거하고 동일한 목록에 새로운 재고를 추가했습니다. 이것은 RemoveDuplicates
가 목록을 반환하기 때문에 우리가 AddRangeOfItems
와 연결할 수 있기 때문에 작동합니다.
RemoveDuplicates
가 목록 대신 void
를 반환한다면, 우리는 메소드를 연결할 수 없을 것입니다. 중복을 제거하지만 새로운 재고 추가와 같은 추가 작업은 동일한 표현식에서 불가능할 것입니다.
RemoveDuplicates
를 업데이트하여 전달된 목록 인수를 업데이트해야 합니다. 왜냐하면 Distinct()
는 새 목록을 반환하며 아래에 표시된 대로 반환되지 않기 때문입니다. 이것은 더 많은 설명이 필요한 것으로 생각됩니다.
public static void RemoveDuplicates<T>(this List<T> list)
{
// 고유 요소 가져오고 원래 목록 지우기
var distinctItems = list.Distinct().ToList();
list.Clear();
// 고유한 항목을 원래 목록에 다시 추가
list.AddRange(distinctItems);
}
왜 이러한 메소드를 클래스에 추가할 수 없을까요?
메소드가 클래스의 핵심 부분이 아닌 경우, 확장 메소드에 배치하는 것은 클래스를 중심으로 유지하고 유지할 수 있습니다.
관심사의 분리: 확장 메소드 사용은 코드를 더 깔끔하게 유지하고 복잡성을 줄이는 데 도움이 됩니다. 빈번하게 사용되지 않을 수도 있는 메소드를 클래스에 불필요하게 추가하는 것을 방지합니다.
외부 라이브러리 향상: 소스 코드를 수정할 수 없는 라이브러리나 프레임워크를 사용하는 경우, 확장 메소드를 사용하여 정의를 변경하지 않고 해당 유형에 기능을 추가할 수 있습니다.
파일 작업을 위해 System.IO
네임스페이스의 FileInfo
클래스를 사용한다고 가정해 보겠습니다. 파일 크기가 너무 큰지(예: 1GB 이상) 쉽게 확인할 수 있는 메서드를 추가하고 싶지만, FileInfo
클래스는 System.IO 네임스페이스에 속하므로 직접 수정할 수 없습니다.
확장 없이:
var fileInfo = new FileInfo("myFile.txt");
if (fileInfo.Length > 1024 * 1024 * 1024) // 파일 크기가 1GB보다 큽니다.
{
Console.WriteLine("The file is too large.");
}
else
{
Console.WriteLine("The file size is acceptable.");
}
확장 메서드 사용:
파일이 1GB보다 큰지 확인하는 확장 메서드를 추가하여 재사용성을 높일 수 있습니다.
public static class FileInfoExtensions
{
// 확장 메서드, 기본 파일 크기는 1GB(재정의 가능)
public static bool IsFileTooLarge(this FileInfo fileInfo, long sizeInBytes = 1024 * 1024 * 1024)
{
return fileInfo.Length > sizeInBytes;
}
}
이제 FileInfo
객체에서 직접 IsFileTooLarge
메서드를 사용할 수 있어 코드가 더 깔끔해집니다:
csharpCopy codevar fileInfo = new FileInfo("myFile.txt");
if (fileInfo.IsFileTooLarge())
{
Console.WriteLine("The file is too large.");
}
else
{
Console.WriteLine("The file size is acceptable.");
}
타사 라이브러리 및 패키지를 확장하면 코드의 호환성이 크게 향상될 수 있습니다.
더 나은 조직 및 가독성: 기능 또는 컨텍스트에 따라 확장 메서드를 정적 클래스에 조직할 수 있어 찾고 사용하기가 더 쉽습니다. 확장 메서드를 체이닝할 수 있도록 하면 확실히 향상됩니다.
확장을 사용할 때
-
유틸리티 메서드의 경우: 특정 유형에 유용하지만 해당 유형 자체에 직접 포함되지 않는 유틸리티 메서드가 있는 경우(예: 포맷팅, 검증).
-
내장 형식을 강화하는 경우: 내장 형식(예:
string
또는DateTime
)에 기능을 추가하고 싶지만 수정하지 않고 싶을 때. -
메소드를 선택적으로 유지하고 싶을 때: 사용자가 필수로 사용하도록 강요하지 않고 선택하여 사용할 수 있는 추가 메소드를 제공하고 싶을 때.
예시 시나리오
만약 Person
클래스가 있고, 사람의 이름을 멋지게 형식화하는 메소드를 추가하고 싶다면:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
// 정적 클래스에서의 확장 메소드
public static class PersonExtensions
{
public static string GetFullName(this Person person)
{
return $"{person.FirstName} {person.LastName}";
}
}
GetFullName
에 대한 확장 메소드를 사용하면 Person
클래스를 간단하게 유지하고 주요 책임에 집중할 수 있으면서도 유용한 기능을 제공할 수 있습니다.
확장 메소드를 사용하지 않아야 하는 경우
-
핵심 기능에 대해서: 메소드가 클래스의 핵심 동작에 중요한 경우, 확장이 아닌 클래스 자체의 일부여야 합니다.
-
강한 결합을 위한: 확장 메소드가 클래스의 비공식 상태에 대한 친밀한 지식이 필요하거나 내부 로직에 정기적으로 접근해야 하는 경우입니다.
-
공개 API를 위한: 공개 라이브러리나 API를 설계할 때, 사용자가 확장 메소드를 찾거나 생성하도록 강요하기보다는 필요한 메소드를 클래스에 직접 포함시키는 것이 종종 더 나은 선택입니다.
확장을 설계할 때 고려해야 할 사항들
확장 메소드는 많은 경우 강력하고 편리하지만, 사용하지 않는 것이 최선의 선택일 수 있는 특정 단점이나 상황이 있습니다:
숨겨진 동작/혼란
-
확장 메소드는 클래스 정의에 직접 나타나지 않기 때문에, 사용 가능한 확장에 익숙하지 않은 개발자들이 발견하기 더 어려울 수 있습니다.
-
개발자는 이러한 확장 메서드가 존재함을 알아야 하며, IntelliSense(예: Visual Studio, JetBrains Rider)와 같은 기능이 있는 IDE에서 작업 중이 아닌 경우에는 사용을 놓칠 수 있습니다. 이러한 IDE는 적절한 유형을 감지하면 다른 파일이나 네임스페이스에서 확장 메서드를 제안할 수 있습니다. 기능이 풍부한 IDE가 없는 경우, 개발자는 확장 메서드를 알고 있거나 해당 폴더를 찾아야 합니다.
비공개 멤버에 액세스할 수 없음
-
확장 메서드는 오직 공개(public) 또는 내부(internal) 멤버(메서드, 속성, 필드)에만 액세스할 수 있습니다.
-
확장 메서드는 클래스의 비공개(private) 또는 보호된(protected) 멤버에 액세스할 수 없습니다. 외부에서 클래스의 일부인 것처럼 작동하기 때문에 확장 메서드는 클래스 외부에서의 일반적인 메서드 호출과 유사합니다.
예시:
public class Car
{
private string engineNumber = "12345"; // 개인 필드
public string Brand { get; set; } = "Ford"; // 공용 속성
private void StartEngine() // 개인 메서드
{
Console.WriteLine("Engine started");
}
}
public static class CarExtensions
{
public static void DisplayBrand(this Car car)
{
Console.WriteLine($"Brand: {car.Brand}"); // 공용 '브랜드' 속성에 접근
}
public static void TryAccessPrivateField(this Car car)
{
// 비공개 'engineNumber'에 접근할 수 없음
// 컴파일 시간 오류가 발생합니다.
Console.WriteLine(car.engineNumber);
}
}
코드 중복 및 남용
-
일부 경우에는 확장 메서드가 코드 중복을 유발할 수 있습니다. 여러 프로젝트나 클래스가 유사한 확장 메서드를 필요로 하는 경우, 동일한 확장 메서드를 다른 위치에 작성하거나 복사하여 관리하고 코드를 일관되게 업데이트하는 것이 더 어려워질 수 있습니다.
이를 피하기 위해 코드를 효과적으로 구성하세요. 응용 프로그램 내에서 사용되는 디자인 패턴에 따라 기원 근처에 있는 확장을 모두 확장 폴더나 프로젝트에 보관하는 것을 권장합니다.
- 확장 기능 남용: 과도하게 사용하면 전역 공간을 사용하여 전역으로 설정할 필요가 없는 메서드로 인해 혼란을 줄 수 있습니다. 이는 타입 API의 오염을 일으킬 수 있으며, 클래스의 핵심이 무엇이고 확장을 통해 추가된 것이 무엇인지를 이해하기 어렵게 만들 수 있습니다.
일부 경우에는 확장 메서드를 통해 추가하는 대신 기능을 별도의 도우미 클래스나 서비스로 캡슐화하는 것이 더 나을 수 있습니다.
결론
확장 메서드는 기능을 깔끔하고 모듈식으로 추가하는 데 유용하지만, 혼란, 네임스페이스 충돌 및 비공개 멤버에 대한 접근 부족을 야기할 수도 있습니다.
본문 전반에 강조된 바와 같이, 확장 메서드에는 많은 용도가 있으며, 효과적으로 사용할 때 닷넷 프레임워크의 매우 좋은 기능이 될 수 있습니다. 적절한 경우에 사용되어야 하지만, 클래스 자체에 속하는 기능을 대체하는 용도로 사용해서는 안 됩니다.
Source:
https://www.freecodecamp.org/news/how-to-write-extension-methods-in-csharp/