메서드 체이닝은 호출 결과로 자기 자신 또는 동일한 타입을 반환하여 호출을 연속적으로 이어가는 패턴입니다. 가독성이 높고, 구성(config) API를 간결하게 만들며, 빌더(Builder)나 Fluent Interface에 자연스럽게 적용됩니다.
1. 언제 메서드 체이닝을 쓰나요?
- 옵션이 많은 객체를 구성할 때 빌더 패턴과 함께 사용합니다.
- LINQ처럼 읽기 쉬운 파이프라인 스타일 API가 필요할 때 유용합니다.
- 설정은 체인으로, 실행은 마지막 단일 메서드(예: Build, Execute, Send)로 분리하는 것이 좋습니다.
2. 기본 구현 패턴 (Mutable Builder)
가장 단순한 방식은 내부 상태를 변경하고 this를 반환하는 것입니다. 다음 예시는 URL을 조합하는 간단한 빌더입니다.
using System;
using System.Collections.Generic;
using System.Linq;
public sealed class UrlBuilder
{
private string _scheme = "http";
private string _host = string.Empty;
private readonly List<string> _segments = new();
private readonly Dictionary<string, string> _query = new();
public UrlBuilder UseHttps()
{
_scheme = "https";
return this;
}
public UrlBuilder Host(string host)
{
if (string.IsNullOrWhiteSpace(host)) throw new ArgumentException(nameof(host));
_host = host.Trim();
return this;
}
public UrlBuilder AddPath(string segment)
{
if (string.IsNullOrWhiteSpace(segment)) throw new ArgumentException(nameof(segment));
_segments.Add(segment.Trim('/'));
return this;
}
public UrlBuilder AddQuery(string key, string value)
{
if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException(nameof(key));
_query[key] = value ?? string.Empty;
return this;
}
public string Build()
{
var path = _segments.Count > 0 ? string.Join('/', _segments) : string.Empty;
var query = _query.Count > 0
? "?" + string.Join('&', _query.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}"))
: string.Empty;
var slash = string.IsNullOrEmpty(path) ? string.Empty : "/";
return $"{_scheme}://{_host}{slash}{path}{query}";
}
}
// 사용 예
var url = new UrlBuilder()
.UseHttps()
.Host("api.example.com")
.AddPath("v1")
.AddPath("users")
.AddQuery("page", "1")
.AddQuery("size", "50")
.Build();
Console.WriteLine(url); // https://api.example.com/v1/users?page=1&size=50
장점은 단순성과 성능입니다. 단점은 내부 상태가 변하므로 스레드 안전성이 낮고, 같은 인스턴스를 여러 곳에서 공유하면 위험하다는 점입니다.
3. 불변(Immutable) 스타일
불변 방식은 각 체인 호출마다 새 인스턴스를 반환합니다. 테스트와 스레드 안정성이 좋아지고, 중간 상태를 안전하게 재사용할 수 있습니다. 다만 객체 생성 비용이 증가합니다.
using System;
using System.Collections.Generic;
using System.Linq;
public sealed class ImmutableUrl
{
private readonly string _scheme;
private readonly string _host;
private readonly IReadOnlyList<string> _segments;
private readonly IReadOnlyDictionary<string, string> _query;
private ImmutableUrl(string scheme, string host, IReadOnlyList<string> segments, IReadOnlyDictionary<string, string> query)
{
_scheme = scheme; _host = host; _segments = segments; _query = query;
}
public static ImmutableUrl Create() => new("http", string.Empty, Array.Empty<string>(), new Dictionary<string, string>());
public ImmutableUrl UseHttps() => new("https", _host, _segments, _query);
public ImmutableUrl Host(string host)
{
if (string.IsNullOrWhiteSpace(host)) throw new ArgumentException(nameof(host));
return new(_scheme, host.Trim(), _segments, _query);
}
public ImmutableUrl AddPath(string segment)
{
if (string.IsNullOrWhiteSpace(segment)) throw new ArgumentException(nameof(segment));
var list = _segments.ToList();
list.Add(segment.Trim('/'));
return new(_scheme, _host, list, _query);
}
public ImmutableUrl AddQuery(string key, string value)
{
if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException(nameof(key));
var dict = new Dictionary<string, string>(_query) { [key] = value ?? string.Empty };
return new(_scheme, _host, _segments, dict);
}
public string Build()
{
var path = _segments.Count > 0 ? string.Join('/', _segments) : string.Empty;
var query = _query.Count > 0
? "?" + string.Join('&', _query.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}"))
: string.Empty;
var slash = string.IsNullOrEmpty(path) ? string.Empty : "/";
return $"{_scheme}://{_host}{slash}{path}{query}";
}
}
// 사용 예
var url2 = ImmutableUrl.Create()
.UseHttps()
.Host("api.example.com")
.AddPath("v1")
.AddPath("users")
.AddQuery("page", "1")
.Build();
선택 기준: 성능과 단순성이 우선이면 가변, 안전성과 테스트 편의가 우선이면 불변을 권장합니다.
4. 제네릭 Self 타입으로 상속 친화적 체인
상속 계층에서 체인 메서드가 정확한 파생 타입을 반환하도록 CRTP 패턴을 사용할 수 있습니다.
public abstract class Fluent<TSelf> where TSelf : Fluent<TSelf>
{
protected TSelf This => (TSelf)this;
}
public sealed class HttpRequestBuilder : Fluent<HttpRequestBuilder>
{
private readonly Dictionary<string, string> _headers = new();
public HttpRequestBuilder WithHeader(string name, string value)
{
_headers[name] = value;
return This;
}
public HttpRequestBuilder WithBearer(string token)
{
_headers["Authorization"] = $"Bearer {token}";
return This;
}
}
이 패턴은 파생 클래스에서 체인 호출 시 반환 타입이 흐트러지지 않도록 도와줍니다.
5. 확장 메서드로 LINQ 스타일 체이닝
원본을 바꾸지 않고 새로운 값을 반환하는 순수 함수형 체인도 좋습니다. 조건부 필터 같은 유틸리티를 확장 메서드로 제공하면 읽기 좋습니다.
using System;
using System.Collections.Generic;
using System.Linq;
public static class EnumerableExtensions
{
public static IEnumerable<T> WhereIf<T>(this IEnumerable<T> source, bool condition, Func<T, bool> predicate)
{
if (source is null) throw new ArgumentNullException(nameof(source));
return condition ? source.Where(predicate) : source;
}
}
// 사용 예
var names = new[] { "Alice", "Bob", "Charlie" };
bool onlyLong = true;
var q = names
.WhereIf(onlyLong, n => n.Length >= 5)
.Select(n => n.ToUpperInvariant());
확장 메서드는 기존 타입을 건드리지 않고 체이닝 기능을 추가할 수 있어 재사용성이 좋습니다.
6. 비동기(Async) 체이닝 설계
구성은 동기 체인으로, 실행은 마지막에 한 번 비동기 호출로 분리하는 것이 깔끔합니다.
using System;
using System.Threading;
using System.Threading.Tasks;
public sealed class Mailer
{
private string _to = string.Empty;
private string _subject = string.Empty;
private string _body = string.Empty;
public Mailer To(string address) { _to = address; return this; }
public Mailer Subject(string subject) { _subject = subject; return this; }
public Mailer Body(string body) { _body = body; return this; }
public async Task SendAsync(CancellationToken ct = default)
{
// 실제 전송 로직 대체
await Task.Delay(10, ct);
Console.WriteLine($"Send to {_to} - {_subject}");
}
}
// 사용 예
await new Mailer()
.To("dev@example.com")
.Subject("Hello")
.Body("Hi there")
.SendAsync();
중간 체인에서 Task<TSelf>를 반환하면 호출이 복잡해지므로, 가능하면 마지막만 비동기 메서드로 두는 것을 권장합니다.
7. 예외 처리와 유효성 검사
- 체인 메서드는 입력 검증을 빠르게 수행하고, 실패 시 즉시 예외를 던집니다.
- 검증 통과 시 this 또는 새 인스턴스를 반환하여 다음 호출로 이어집니다.
- 상태를 변경하기 전에 검증을 수행해 불완전 상태를 방지합니다.
8. 테스트 가능하고 안전한 체인 설계 팁
- 체인 메서드는 가능한 한 부수효과가 없도록 하고, 최종 동작은 Terminal 메서드(Build/Execute/Send)에서만 수행합니다.
- 불변 스타일은 중간 스냅샷 비교가 쉬워 단위 테스트에 유리합니다.
- 메서드 이름을 동사(With/Set/Add/Use 등)로 통일해 의도를 명확히 합니다.
- null을 반환하지 말고, 유효하지 않은 입력은 예외 또는 기본값 처리로 방어합니다.
9. 마무리
메서드 체이닝은 C#에서 읽기 쉬운 API를 구현하는 강력한 도구입니다. 단순한 빌더는 가변 스타일로, 공유와 테스트가 중요한 경우에는 불변 스타일로, 상속이 섞이는 경우에는 제네릭 Self 패턴을 도입하면 안정적으로 구현할 수 있습니다. 구성은 체인으로, 실행은 한 번에 하는 원칙을 지키면 유지보수성과 성능을 모두 챙길 수 있습니다.
'C#' 카테고리의 다른 글
| C# 인터페이스의 명시적 구현 활용과 주의사항 (0) | 2026.06.05 |
|---|---|
| C# 암호화된 설정값을 안전하게 로드하기 (0) | 2026.06.05 |
| C# PriorityQueue<T>를 이용한 우선순위 작업 처리 (0) | 2026.06.04 |
| C# Reflection.Emit으로 런타임 코드 생성하기 (0) | 2026.06.04 |
| C# 이벤트 필터링으로 불필요한 이벤트 호출 방지 (0) | 2026.06.02 |