홈으로 돌아가기

C#의 Elvis 액세스 수정자 — 정적 액세스 제어

Elvis 액세스 수정자 — C#용 속성과 Roslyn 분석기 세트로 엄격히 지정된 타입에 대해 private 및 internal 멤버에 선언적으로 액세스를 허용합니다. 컴파일 타임에 검사되며 런타임 리플렉션이 필요 없고 CI/CD에 통합됩니다.

C#의 Elvis: 캡슐화 깨뜨리지 않는 안전한 friend
Advertisement 728x90

C#에서의 엘비스 접근 제한자: 캡슐화를 깨지 않고 private 멤버에 대한 통제된 접근을 구현하는 방법

엘비스 접근 제한자는 C#용 맞춤형 어트리뷰트 시스템이자 Roslyn 분석기로, 선언적으로 privateinternal 클래스 멤버에 대해 엄격히 제한된 '친근한' 타입만 접근할 수 있도록 합니다. C++의 friend와 달리, 엘비스는 새로운 신뢰받는 소비자를 추가할 때 대상 클래스를 수정할 필요가 없으며 컴파일 타임에 엄격한 검증을 제공합니다. 이 솔루션은 기본적으로 불변성을 원칙으로 하고 지역화된 변경 가능성을 중시하는 고부하, 멀티스레드 또는 프레임워크 기반 프로젝트에서 작업하는 중급/고급 개발자들을 겨냥하고 있습니다.

엘비스 작동 방식: 아키텍처와 주요 구성 요소

엘비스는 서로 연관된 세 가지 계층으로 이루어져 있습니다:

  • 컴파일 타임 어트리뷰트[Elvis], [OnlyType], [OnlyNs], [ExcludeType], [ExcludeNs]. 이들은 멤버(속성, 메서드, 필드)에 주석을 달아 어떤 타입이나 네임스페이스만 접근할 수 있는지를 지정합니다.
  • Roslyn 분석기 — IDE와 컴파일 시에 의미적 검사를 수행하며, 접근 컨텍스트('유도체')를 분석하고 이를 어트리뷰트 규칙과 비교하여 위반이 발생하면 진단 메시지를 출력합니다.
  • VSIX 확장 및 NuGet 패키지 — 개발 워크플로우에 통합되도록 보장하며, 어트리뷰트 자동 완성, 템플릿 빠른 적용, 그리고 결정 트리에서 허용된 관계 시각화를 제공합니다.

특히 중요한 점은 엘비스가 IL 코드를 수정하지 않고, 런타임 리플렉션을 사용하지 않으며, InternalsVisibleTo에도 의존하지 않는다는 것입니다. 모든 검증은 정적—즉, 컴파일 시—으로 이루어집니다. 이는 디버그 빌드에서도 금지된 접근이 빠져나가지 않도록 보장합니다.

Google AdInline article slot

사용 사례: 기본부터 고급 시나리오까지

기본적인 예를 들어보겠습니다: Me 클래스에는 private decimal Money가 있지만, 다른 클래스인 MyFriend에게는 해당 값의 안전한 변경을 허용하면서도 어셈블리 내 모든 코드에 접근 권한을 부여하지 않아야 합니다.

class Me
{
    [Elvis(typeof(MyFriend))]
    private decimal _money = 100;
    public decimal Money => _money;

    public void SetMoney(decimal value) => _money = value;
}

class MyFriend
{
    public void TakeHalf(Me me)
    {
        var half = me.Money / 2;
        me.SetMoney(me.Money - half); // ✅ 허용: MyFriend가 [Elvis]에 지정됨
        // me._money = 0; // ❌ 컴파일 타임 오류: private 필드에 직접 접근하는 것은 금지됨
    }
}

2026-03 및 2026-04 버전에서 추가된 새로운 기능은 다음과 같습니다:

  • 문자열 이름을 통한 타입 지정: [Elvis("MyNamespace.MyFriend")]—타입 참조가 불가능한 경우 어셈블리 간 연결에 유용합니다.
  • 정규 표현식: [Elvis(@"^MyNamespace\.Tests\.*$")]—네임스페이스 내 모든 테스트 클래스에 접근할 수 있게 합니다.
  • 제외 어트리뷰트: [ExcludeType(typeof(ObsoleteHelper))]—일반적인 [OnlyNs] 규칙에 포함되어도 특정 타입의 접근을 차단합니다.
  • 네임스페이스: [OnlyNs("MyCompany.Core")]—지정된 네임스페이스의 타입만 접근을 허용합니다.

조합론과 계층 구조에서의 동작

엘비스는 복합 규칙을 지원합니다. 하나의 멤버에 여러 어트리뷰트가 적용될 경우 기본적으로 AND 논리가 사용되지만, 예외가 우선시됩니다:

Google AdInline article slot
  • 만약 [OnlyNs("A")][ExcludeNs("A.Sub")]가 동시에 지정되면, A.는 접근 가능하지만 A.Sub.는 접근 불가입니다.
  • 상속 관계에서는: 기초 클래스가 [Elvis(typeof(BaseFriend))]로 표시되고 파생 클래스가 [Elvis(typeof(DerivedFriend))]로 표시되면, 두 타입 모두 기초 클래스의 멤버에 접근할 수 있습니다. 그러나 파생 클래스는 자신의 private 멤버에 대한 권리를 상속받지 않습니다—별도로 주석을 달아야 합니다.
  • 오버로드된 메서드의 경우, 각 서명마다 어트리뷰트가 개별적으로 적용됩니다.

특히 주목할 점은 엘비스가 protected 멤버에는 영향을 미치지 않는다는 것입니다—이는 여전히 일반적인 상속 규칙에 따라 접근 가능합니다. 어트리뷰트는 오직 privateinternal 멤버에만 작용합니다.

무엇이 중요한가

  • 엘비스는 DI나 인터페이스를 대체하는 것이 아니라, 인터페이스 사용이 불편한 곳에서 캡슐화를 추가로 통제하는 레이어입니다(예: DTO, 설정 객체, 내부 프레임워크 메커니즘 작업 시).
  • 모든 규칙은 정적으로 검사되므로 런타임 오버헤드가 전혀 없고 AOT 컴파일 및 트리밍과도 완벽하게 호환됩니다.
  • 정규 표현식과 문자열 기반 타입 이름 지원 덕분에 컴파일 타임에 타입이 존재하지 않는 시나리오(플러그인 아키텍처, 코드 생성)에도 적용 가능합니다.
  • 제외 어트리뷰트([Exclude*])는 '예외가 있는 화이트리스트'를 만드는 데 유용하며, 대규모 코드베이스를 마이그레이션할 때 매우 중요합니다.
  • 엘비스는 SOLID를 위반하지 않습니다: DIP를 약화시키는 것이 아니라 이를 보완하며, 인공적인 래퍼 인터페이스를 만들지 않고도 컴포넌트 간 계약을 공식화할 수 있게 합니다.

실용적인 한계와 권장 사항

엘비스가 모든 접근 문제를 해결하는 것은 아닙니다. 다음 경우에 사용해야 합니다:

  • private 상태에 대한 정확하고 예측 가능한 접근이 필요하지만, 이를 public/internal API를 통해 노출시키고 싶지 않을 때.
  • 아키텍처가 책임 영역의 엄격한 분리(예: 프레임워크 핵심과 그 확장)를 요구할 때.
  • 클래스들이 논리적으로 밀접하게 결합되어 있지만 컴파일 관점에서는 독립적이어야 하는 패턴을 사용할 때(예: BuilderBuilderResult).

다음 경우에는 엘비스 사용을 피해야 합니다:

Google AdInline article slot
  • 설계가 부실한 의존성을 리팩토링해야 할 때—private 필드 하나를 위해 friend 클래스 15개가 필요한 경우, 해당 로직은 아마도 별도의 서비스에 포함되어야 합니다.
  • CI/CD 파이프라인이 없어 분석기가 빌드 과정에 포함되지 않을 때—IDE 외부에서는 규칙이 무시됩니다.
  • public 멤버의 경우—어트리뷰트는 이미 접근이 열려 있으므로 이를 무시합니다.

기존 프로젝트에 도입할 때는 DTO 클래스와 내부 팩토리의 private 필드부터 시작하세요. 이는 불변성을 위반할 위험이 가장 높고 리팩토링 비용도 가장 낮은 부분입니다.

— Editorial Team

Advertisement 728x90

다음 읽기