본문 바로가기

C#

C# 중첩 클래스(Nested Class) 활용 사례

중첩 클래스는 특정 타입과 강하게 결합된 보조 타입을 외부에 노출하지 않고 캡슐화할 때 유용합니다. 코드 가독성을 높이고, 구현 디테일을 안전하게 숨기며, 의도를 분명히 표현할 수 있습니다.

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 표면을 단순하게 유지하는 실용적인 도구입니다. 외부 노출이 불필요한 타입은 과감히 내부로 숨기고, 공개해야 할 것은 최소한만 노출하는 것이 유지보수에 유리합니다.