접근 한정자는 어떤 코드가 타입이나 멤버를 쓸 수 있는지 정합니다. 클래스는 이를 통해 다른 코드가 의존하는 부분(공개 표면)과 마음대로 바꿔도 되는 부분(내부)을 나눕니다. C#에는 여섯 가지 한정자가 있고, 아무것도 쓰지 않았을 때의 기본값이 있습니다.
여섯 가지 한정자
| 한정자 | 포함하는 타입 | 파생 타입, 같은 어셈블리 | 다른 코드, 같은 어셈블리 | 파생 타입, 다른 어셈블리 | 다른 코드, 다른 어셈블리 |
|---|---|---|---|---|---|
public | 예 | 예 | 예 | 예 | 예 |
protected internal | 예 | 예 | 예 | 예 | 아니요 |
protected | 예 | 예 | 아니요 | 예 | 아니요 |
internal | 예 | 예 | 예 | 아니요 | 아니요 |
private protected | 예 | 예 | 아니요 | 아니요 | 아니요 |
private | 예 | 아니요 | 아니요 | 아니요 | 아니요 |
어셈블리는 컴파일된 프로젝트 하나, 즉 그 프로젝트가 만드는 .dll이나 .exe입니다. "같은 어셈블리"는 "같은 프로젝트"라는 뜻이며, 그래서 internal이 "내 코드에는 보이고 내 라이브러리 사용자에게는 숨겨짐"으로 동작합니다.
private과 public: 캡슐화
private은 멤버의 기본값이며 대부분의 필드에 맞는 선택입니다. 클래스는 호출하는 쪽에 필요한 것을 public 메서드와 속성으로 노출하고, 그 메서드들이 객체의 데이터를 일관되게 유지합니다.
출력:
False
True
Priya: 180
balance가 private이므로 이를 바꾸는 방법은 Deposit과 TryWithdraw뿐이고, 둘 다 규칙을 강제합니다. 프로그램의 다른 어떤 코드도, 어떻게 작성되었든 음수 잔액을 만들 수 없습니다. 이것이 캡슐화가 주는 것입니다. 규칙이 한 곳에 있습니다.
private은 객체 단위가 아니라 타입 단위입니다. Account의 메서드는 다른 Account 인스턴스의 other.balance를 읽을 수 있으며, Equals와 비교 메서드는 보통 이렇게 작성합니다.
protected: 파생 클래스에 보임
protected 멤버는 바깥 세상에는 숨겨지지만, 선언한 클래스를 상속하는 클래스에서는 쓸 수 있습니다.
출력:
Sam: 4000.00
Rita: 6400.00
사람들을 놀라게 하는 규칙이 하나 있습니다. Manager 안에서 this나 다른 Manager의 baseSalary는 쓸 수 있지만, 임의의 Employee의 것은 쓸 수 없습니다:
class Manager : Employee
{
public decimal Compare(Employee other)
{
return baseSalary - other.baseSalary;
// error CS1540: Cannot access protected member 'Employee.baseSalary' via a qualifier
// of type 'Employee'; the qualifier must be of type 'Manager' (or derived from it)
}
}
이유는 이렇습니다. other는 Employee에서 파생된 Contractor일 수도 있고, protected는 계층 구조에서 자기 가지에 대한 접근을 허락하지 형제 가지에 대한 접근을 허락하지 않습니다.
protected 필드는 모든 파생 클래스를 그 필드에 영원히 묶습니다. 많은 코드베이스는 protected 속성이나 메서드를 선호하거나, 필드는 private으로 두고 파생 클래스에는 필요한 것만 줍니다.
internal: 어셈블리 안에서 보임
internal 멤버와 타입은 같은 프로젝트의 모든 코드가 쓸 수 있고, 그 밖의 어떤 것도 쓸 수 없습니다. 라이브러리는 공개 클래스가 의존하는 헬퍼에 이를 씁니다:
// In the MyShop.Pricing library project
public class PriceCalculator
{
public decimal Total(Cart cart) => TaxRules.Apply(cart.Subtotal);
}
internal static class TaxRules // callers of the library cannot see this class
{
internal static decimal Apply(decimal amount) => amount * 1.2m;
}
최상위 타입은 한정자를 쓰지 않으면 internal입니다. 그래서 클래스 라이브러리에서 public으로 표시하는 것을 잊은 클래스는 그것을 참조하는 프로젝트에서 "보이지 않습니다".
단위 테스트 프로젝트는 별도의 어셈블리이므로 internal 코드도 볼 수 없습니다. 표준 해결책은 라이브러리에 특성을 두는 것입니다:
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("MyShop.Pricing.Tests")]
protected internal과 private protected
이 둘은 앞의 한정자들을 서로 반대 방식으로 조합하며, 이름이 헷갈리기 쉽습니다:
- **
protected internal**은 protected 또는 internal입니다. 같은 어셈블리의 모든 코드와 다른 어셈블리의 파생 클래스입니다. 더 넓은 쪽입니다. private protected(C# 7.2)는 protected 이고 internal입니다. 같은 어셈블리에 있는 파생 클래스만입니다. 확장 지점을 라이브러리 안에 두어야 하는 기본 클래스를 위한 더 좁은 쪽입니다.
public class ReportBase
{
protected internal string Title; // same assembly, or subclasses anywhere
private protected int RowLimit = 500; // subclasses in this assembly only (C# 7.2)
}
아무것도 쓰지 않았을 때의 기본값
| 위치 | 기본값 | 허용되는 것 |
|---|---|---|
| 네임스페이스에 선언한 타입 | internal | public, internal, file(C# 11) |
| 클래스의 멤버 | private | 여섯 가지 모두 |
| 구조체의 멤버 | private | public, internal, private(구조체는 상속할 수 없음) |
| 중첩 타입 | private | 여섯 가지 모두 |
| 인터페이스 멤버 | public | C# 8부터 명시적 한정자 허용 |
| 열거형 멤버 | public | 없음 |
명시하는 데는 단어 하나면 되고 다음에 읽는 사람의 의문을 없애 주므로, 대부분의 스타일 가이드는 기본값과 같더라도 한정자를 쓰라고 합니다.
접근자 수준의 접근
속성이나 인덱서는 한 접근자에 속성 자체보다 좁은 한정자를 줄 수 있습니다:
출력:
report.pdf: 100%
접근자의 한정자는 속성의 것보다 더 제한적이어야 하고, 두 접근자 중 하나만 가질 수 있습니다.
일관성 없는 접근성
public 멤버는 자기보다 덜 보이는 타입을 노출할 수 없습니다. 호출하는 쪽이 이름조차 쓸 수 없는 것을 받게 되기 때문입니다:
internal class Discount { }
public class Checkout
{
public Discount Current() => null;
// error CS0050: Inconsistent accessibility: return type 'Discount' is less accessible than method 'Checkout.Current()'
}
같은 규칙이 매개변수 타입에는 CS0051을, 속성 타입에는 CS0053을 줍니다. 타입을 멤버만큼 보이게 하거나, 멤버를 타입만큼 숨겨서 해결하세요.
한정자 고르기
동작하는 가장 제한적인 한정자로 시작하고, 호출하는 쪽이 필요로 할 때만 넓히세요. 나중에 더 보이게 만드는 것은 쉽고 누구도 깨뜨리지 않지만, 덜 보이게 만들면 그것을 쓰던 모든 호출하는 쪽이 깨집니다. 실제로는 필드는 private, 클래스의 목적을 이루는 멤버는 public, 헬퍼는 private, 라이브러리 전용 타입은 internal입니다.
자주 묻는 질문
C#의 접근 한정자에는 어떤 것이 있나요?
여섯 가지입니다: public(모든 코드), private(포함하는 타입만), protected(포함하는 타입과 그로부터 파생된 타입), internal(같은 어셈블리의 모든 코드), protected internal(같은 어셈블리, 또는 어디서든 파생 타입), private protected(같은 어셈블리의 파생 타입, C# 7.2). C# 11은 한 소스 파일에서만 보이는 타입을 위한 file을 추가했습니다.
C#의 기본 접근 한정자는 무엇인가요?
클래스나 구조체의 멤버(필드, 메서드, 속성, 중첩 타입)는 기본적으로 private입니다. 최상위 타입(네임스페이스 안에 직접 선언한 클래스)은 기본적으로 internal입니다. 인터페이스 멤버와 열거형 멤버는 public입니다.
C#에서 protected는 무슨 뜻인가요?
protected 멤버는 자기 클래스 안과 그로부터 파생된 모든 클래스 안에서 보이지만, 다른 코드에는 보이지 않습니다. 파생 클래스는 자기 타입을 통해서만 이를 쓸 수 있습니다. Manager 안에서 this.baseSalary는 되지만, 다른 Employee 객체의 baseSalary에 접근하면 CS1540 오류입니다.
C#에서 internal은 무엇인가요?
internal은 타입이나 멤버를 같은 어셈블리(같은 프로젝트의 컴파일된 .dll이나 .exe)의 모든 코드에 보이게 하고, 그 밖에서는 보이지 않게 합니다. 라이브러리가 헬퍼 클래스를 공개 API에서 빼 두는 방법입니다. [assembly: InternalsVisibleTo("MyLib.Tests")]를 쓰면 테스트 프로젝트도 볼 수 있습니다.
"is inaccessible due to its protection level" 오류는 어떻게 해결하나요?
CS0122 오류로, 코드가 볼 수 없는 멤버를 쓰고 있다는 뜻입니다. 가장 흔하게는 기본값 private으로 남겨 둔 필드입니다. 외부 코드가 읽어야 한다면 필드를 그냥 public으로 만들지 말고 public 속성이나 메서드를 노출하세요. 다른 프로젝트의 타입이라면 아마 internal일 것입니다.