Quick Reference
외부 계약은 public, 어셈블리 내부 구현은 internal, 타입 내부 상태는 private부터 시작합니다. 상속 확장이 실제 계약일 때만 protected를 열고, protected internal은 같은 어셈블리 또는 모든 파생 타입, private protected는 같은 어셈블리의 파생 타입만 허용한다는 차이를 먼저 확인합니다.
public class Player
{
public string Name { get; private set; } // 읽기 공개, 쓰기 내부
private int _score; // 클래스 내부만
protected int Level; // 파생 클래스까지
internal string Region { get; set; } // 같은 어셈블리
}문법
어떤 접근 제한자를 먼저 고르면 되나
| 상황 | 먼저 떠올릴 것 |
|---|---|
| 외부 API로 열어야 함 | public |
| 클래스 내부 구현 | private |
| 파생 클래스에만 열어야 함 | protected |
| 같은 어셈블리 내부만 | internal |
| 어셈블리 내부 또는 파생 클래스 | protected internal |
6가지 접근 제한자와 경계
| 제한자 | 같은 클래스 | 파생 클래스 (동일 어셈블리) | 파생 클래스 (외부 어셈블리) | 동일 어셈블리 | 외부 어셈블리 |
|---|---|---|---|---|---|
public | ✅ | ✅ | ✅ | ✅ | ✅ |
protected internal | ✅ | ✅ | ✅ | ✅ | ❌ |
protected | ✅ | ✅ | ✅ | ❌ | ❌ |
internal | ✅ | ✅ | ❌ | ✅ | ❌ |
private protected | ✅ | ✅ | ❌ | ❌ | ❌ |
private | ✅ | ❌ | ❌ | ❌ | ❌ |
기본 접근 제한자
명시하지 않으면 컴파일러가 기본값을 적용합니다.
class Foo { } // internal (네임스페이스 수준 타입)
class Bar
{
int x; // private (멤버)
void M() { } // private (메서드)
}네임스페이스 수준 타입은 public 또는 internal을 쓸 수 있고, C# 11+에서는 현재 소스 파일에만 보이는 file도 쓸 수 있습니다. 중첩 타입과 멤버의 기본값은 private입니다. public 또는 protected 멤버의 시그니처에는 그보다 접근성이 좁은 타입을 노출할 수 없습니다.
internal — 어셈블리 경계
같은 어셈블리(.dll 또는 .exe) 안에서만 접근 가능합니다. 라이브러리 내부 구현을 외부에 숨기면서 라이브러리 안에서는 자유롭게 쓸 수 있는 패턴에 자주 씁니다.
InternalsVisibleTo 어트리뷰트를 쓰면 특정 테스트 어셈블리에만 internal을 열어 줄 수 있습니다.
// AssemblyInfo.cs
[assembly: InternalsVisibleTo("MyLibrary.Tests")]protected internal vs private protected
public class Base
{
protected internal void A() { } // 동일 어셈블리 전체 OR 파생 클래스
private protected void B() { } // 동일 어셈블리의 파생 클래스만
}protected internal은 "어셈블리 내부이거나 OR 파생 클래스"라서 범위가 넓습니다. private protected는 둘 다 충족해야 해서 더 좁습니다.
외부 어셈블리의 파생 클래스가 protected 계열 멤버에 접근할 때는 기반 타입 변수로 우회할 수 없습니다. receiver의 정적 타입이 현재 파생 타입 또는 더 파생된 타입이어야 합니다.
public class Base
{
protected int Value;
}
public class Derived : Base
{
public void CopyFrom(Derived other) => Value = other.Value;
// public void CopyFromBase(Base other) => Value = other.Value;
// 다른 어셈블리에서는 Base receiver를 통한 protected 접근이 허용되지 않는다.
}// ❌ 테스트 편의를 이유로 무조건 public으로 열어 두기
public class OrderCache
{
public void ResetCoreState() { }
}
// ✅ 외부 계약이 아니라면 더 좁은 범위부터 시작
public class OrderCache
{
internal void ResetCoreState() { }
}접근 범위 요약
| 제한자 | 한 줄 설명 |
|---|---|
public | 제한 없음 |
private | 선언된 타입 안에서만 |
protected | 선언 타입 + 모든 파생 타입 |
internal | 같은 어셈블리 안에서 |
protected internal | 같은 어셈블리 OR 파생 타입 |
private protected | 같은 어셈블리의 파생 타입만 |
주의할 점
처음부터 모두 public으로 열어두면 나중에 구조를 바꿀 때 외부 의존성이 걸려 있어 수정이 어려워집니다. 필요한 것만 열고, 나머지는 닫는 것이 기본 원칙입니다.
private set이나 init은 프로퍼티 접근자에 별도의 제한자를 붙이는 패턴으로, 읽기는 공개하고 쓰기만 제한할 때 씁니다. 이 경우 accessor는 프로퍼티나 indexer 자체보다 더 좁은 접근성만 가질 수 있습니다. protected는 테스트 편의용 공개 수단이 아니라 파생 타입에 지켜야 할 확장 계약이 있을 때만 씁니다.
참고 링크
2 sources