중첩 클래스는 특정 타입과 강하게 결합된 보조 타입을 외부에 노출하지 않고 캡슐화할 때 유용합니다. 코드 가독성을 높이고, 구현 디테일을 안전하게 숨기며, 의도를 분명히 표현할 수 있습니다.
1. 핵심 개념 요약
중첩 클래스는 클래스 내부에 선언된 클래스입니다. 외부에 공개할 필요가 없는 협력 타입을 한곳에 모아 관리할 수 있습니다. 중첩 클래스는 바깥 클래스의 private 멤버에도 접근할 수 있습니다. 반대로 바깥 클래스는 중첩 클래스의 private 멤버에 직접 접근할 수 없습니다.
2. 구현 디테일 숨기기: Tree 내부 Node 캡슐화
트리의 노드 구조를 외부에 노출하지 않고, 공용 API만 제공하는 방법입니다. 테스트와 유지보수에 유리합니다.
using System.Collections;
using System.Collections.Generic;
public sealed class IntBinarySearchTree : IEnumerable<int>
{
private Node? _root;
public void Add(int value)
{
if (_root is null) { _root = new Node(value); return; }
_root.Insert(value);
}
public bool Contains(int value) => Node.Find(_root, value) is not null;
private sealed class Node
{
public int Value { get; }
public Node? Left;
public Node? Right;
public Node(int value) => Value = value;
public void Insert(int value)
{
if (value < Value)
{
if (Left is null) Left = new Node(value); else Left.Insert(value);
}
else if (value > Value)
{
if (Right is null) Right = new Node(value); else Right.Insert(value);
}
}
public static Node? Find(Node? node, int v)
{
while (node is not null)
{
if (v == node.Value) return node;
node = v < node.Value ? node.Left : node.Right;
}
return null;
}
public static IEnumerable<int> InOrder(Node? node)
{
if (node is null) yield break;
foreach (var n in InOrder(node.Left)) yield return n;
yield return node.Value;
foreach (var n in InOrder(node.Right)) yield return n;
}
}
public IEnumerator<int> GetEnumerator() => Node.InOrder(_root).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
// 사용 예시
var tree = new IntBinarySearchTree();
tree.Add(5); tree.Add(2); tree.Add(8);
bool has2 = tree.Contains(2); // true
foreach (var v in tree) { /* 2,5,8 */ }
3. 불변 객체의 Builder 중첩
불변(immutable) 설정 객체에 Builder를 중첩해 안전하고 유창하게 생성합니다. 유효성 검사도 Builder 내부에서 처리합니다.
using System;
public sealed class MailOptions
{
public string SmtpHost { get; }
public int Port { get; }
public bool UseSsl { get; }
private MailOptions(string smtpHost, int port, bool useSsl)
{
SmtpHost = smtpHost;
Port = port;
UseSsl = useSsl;
}
public static Builder CreateBuilder() => new Builder();
public sealed class Builder
{
private string _smtpHost = "localhost";
private int _port = 25;
private bool _useSsl;
public Builder WithHost(string host) { _smtpHost = host; return this; }
public Builder WithPort(int port) { _port = port; return this; }
public Builder EnableSsl(bool enable = true) { _useSsl = enable; return this; }
public MailOptions Build()
{
if (string.IsNullOrWhiteSpace(_smtpHost)) throw new ArgumentException("SmtpHost");
if (_port < 1 || _port > 65535) throw new ArgumentOutOfRangeException(nameof(_port));
return new MailOptions(_smtpHost, _port, _useSsl);
}
}
}
// 사용 예시
var options = MailOptions.CreateBuilder()
.WithHost("smtp.example.com")
.WithPort(587)
.EnableSsl()
.Build();
4. 비교자/전략 캡슐화: 외부에 공개할 필요 없는 IComparer
특정 타입의 정렬 전략을 중첩 클래스로 숨기면, 외부 API는 단순한 정적 프로퍼티만 노출하면 됩니다. 중첩 클래스는 바깥 클래스의 private 필드에 접근할 수 있습니다.
using System;
using System.Collections.Generic;
public sealed class Person
{
private readonly string _firstName;
private readonly string _lastName;
public Person(string firstName, string lastName)
{
_firstName = firstName;
_lastName = lastName;
}
public static IComparer<Person> LastThenFirstComparer { get; } = new LastFirstComparer();
private sealed class LastFirstComparer : IComparer<Person>
{
public int Compare(Person? x, Person? y)
{
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;
int c = string.Compare(x._lastName, y._lastName, StringComparison.OrdinalIgnoreCase);
if (c != 0) return c;
return string.Compare(x._firstName, y._firstName, StringComparison.OrdinalIgnoreCase);
}
}
}
// 사용 예시
var list = new List<Person>
{
new Person("Amy", "Zed"),
new Person("Bob", "Anderson"),
new Person("Alex", "Anderson")
};
list.Sort(Person.LastThenFirstComparer);
5. 접근 제한자와 선언 규칙 정리
중첩 클래스는 private, protected, internal, protected internal, private protected, public 등 모든 접근 제한자를 사용할 수 있습니다. 명시하지 않으면 기본은 private입니다.
public class Outer
{
public class PublicInner { }
internal class InternalInner { }
protected class ProtectedInner { }
protected internal class ProtectedInternalInner { }
private protected class PrivateProtectedInner { }
private class PrivateInner { }
}
참고 사항입니다:
- 중첩 클래스는 바깥 클래스의 private 멤버에 접근 가능합니다.
- static 중첩 클래스는 바깥 인스턴스를 암묵적으로 캡처하지 않습니다. 필요 시 인스턴스를 명시적으로 전달합니다.
- 공개(public) 중첩 클래스를 외부에서 사용할 때는 Outer.Inner 형태로 접근합니다.
6. 언제 쓰면 좋은가
- 구현 세부 타입(Node, Enumerator, Comparer 등)을 숨기고 싶은 경우
- 불변 객체의 Builder처럼 생성 로직을 한곳에 모으고 싶은 경우
- 특정 클래스와 강하게 결합된 전략/정책 객체를 범위 한정하고 싶은 경우
7. 마무리
중첩 클래스는 캡슐화를 강화하고 API 표면을 단순하게 유지하는 실용적인 도구입니다. 외부 노출이 불필요한 타입은 과감히 내부로 숨기고, 공개해야 할 것은 최소한만 노출하는 것이 유지보수에 유리합니다.
'C#' 카테고리의 다른 글
| C# global using 지시문 활용 (0) | 2026.04.23 |
|---|---|
| C# 커스텀 예외(Custom Exception) 설계 (0) | 2026.04.23 |
| C# Dispose 패턴과 종속성 해제 (0) | 2026.04.22 |
| C# 고정 크기 버퍼(fixed size buffer) 사용 (0) | 2026.04.22 |
| C# BenchmarkDotNet으로 성능 측정하기 (0) | 2026.04.22 |