
접근 제한자는 클래스에 접근할 수 있는 권한의 범위를 결정합니다.
마치 집 안의 모든 방을 손님에게 공개하지 않고, 안방은 가족만,
금고는 나만 들어갈 수 있게 방문을 잠그는 것과 같죠.
중요한 건 숨기고 필요한 것만 외부에 공개해서 코드의 안정성을 높여줍니다.
public은 가장 개방적인 접근 제한자입니다. 누구나, 어디서든 접근할 수 있어요.
[BankAccount.cs]
namespace Bank
{
public class BankAccount
{
// 누구나 이 계좌에 입금할 수 있습니다.
public void Deposit(decimal amount)
{
// ... 입금 로직 ...
Console.WriteLine($"{amount}원이 입금되었습니다.");
}
// 누구나 잔액을 조회할 수 있습니다.
public decimal GetBalance()
{
// ... 잔액 반환 로직 ...
return 10000;
}
}
}
[Program.cs]
using Bank;
// 다른 파일, 다른 프로젝트에서도 가능
namespace Person
{
public class Program
{
public static void Main()
{
var myAccount = new BankAccount();
myAccount.Deposit(5000); // OK! public이라 어디서든 호출 가능
}
}
}
[실행 결과]
5000원이 입금되었습니다.
private은 가장 폐쇄적인 접근 제한자입니다.
오직 해당 클래스 내부에서만 접근할 수 있어요.
[코드]
public class BankAccount
{
// private 필드: 계좌 잔액은 외부에서 직접 수정할 수 없습니다.
private decimal _balance = 10000;
public void Deposit(decimal amount)
{
if (amount > 0)
{
Console.WriteLine($"입금 금액: {amount}");
_balance += amount; // OK! 클래스 내부에서는 접근 가능
}
}
public decimal GetBalance()
{
return _balance; // OK! 내부 데이터 조회는 가능
}
}
internal class Program
{
public static void Main()
{
var myAccount = new BankAccount();
Console.WriteLine($"초기 잔액: {myAccount.GetBalance()}");
myAccount.Deposit(5000);
Console.WriteLine($"현재 잔액: {myAccount.GetBalance()}");
// private 멤버는 외부에서 접근 불가
// myAccount._balance = 999999; // 컴파일 오류 발생!
}
}
[실행 결과]
초기 잔액: 10000
입금 금액: 5000
현재 잔액: 15000
protected는 해당 부모 클래스와 상속받은 자식 클래스에서만 접근할 수 있습니다.
[코드]
public class BankAccount
{
protected decimal _balance = 10000;
}
// BankAccount를 상속받은 SavingsAccount (적금 계좌)
public class SavingsAccount : BankAccount
{
public void AddInterest()
{
// 이자를 계산하기 위해 부모의 _balance에 접근
decimal interest = _balance * 0.05m; // protected 멤버는 자식 클래스에서 접근 가능
_balance += interest;
Console.WriteLine($"이자가 추가되어 잔액은 {_balance}원입니다.");
}
}
internal class Program
{
public static void Main()
{
var mySavings = new SavingsAccount();
// 부모 클래스와 상속받은 자식 클래스에서만 접근 가능
// mySavings._balance = 0; // 컴파일 오류 발생!
mySavings.AddInterest();
}
}
[실행 결과]
이자가 추가되어 잔액은 10500.00원입니다.
internal은 같은 어셈블리(Assembly) 안에서만 접근할 수 있도록
허용하는 접근 제한자입니다. 여기서 어셈블리는 프로젝트를
빌드(Build)했을 때 생성되는 결과물(dll 또는 exe 파일)이라고 보시면 됩니다.
대부분의 경우 하나의 프로젝트가 하나의 어셈블리가 되죠.
우선 internal의 특징을 보여주기 위한 별도의 실습 환경을 구성합니다.

1. 비주얼 스튜디오에서 콘솔 앱으로 새 프로젝트를 생성합니다.

2. 프로젝트 이름은 'MyApp'으로 정합니다.
3. 솔루션 이름은 'Assembly'로 정합니다.
4. [솔루션 및 프로젝트를 같은 디렉터리]의 체크를 해제합니다.

5. 비주얼 스튜디오 오른쪽의 솔루션 탐색기 창을 보세요.
6. 솔루션 항목에 마우스를 올리고 우클릭합니다.
7. 메뉴에서 [추가] → [새 프로젝트]를 선택합니다.

8. [클래스 라이브러리] → [다음] → [만들기]를 선택합니다.

9. 프로젝트 이름은 'MyCalculatorLib'로 설정하고 [다음]을 선택합니다.

10. 생성된 .cs 파일을 우클릭하고 [이름 바꾸기]를 선택합니다.

11. 파일 이름을 'Calculator.cs'로 설정합니다.

12. MyCalculatorLib 프로젝트 항목에 마우스를 올리고 우클릭합니다.
13. 메뉴에서 [추가] → [새 항목]을 선택합니다.

14. 새 항목 추가 창이 나타나면, 클래스(Class)를 선택합니다.
15. 아래쪽 이름 입력란에 'LogHelper.cs'를 적어주고 [추가]를 선택합니다.

16. 이 상태에서 Calculator.cs와 LogHelper.cs의 코드를 아래와 같이 작성합니다.
[Calculator.cs]
namespace MyCalculatorLib
{
public class Calculator
{
// 사용자가 이 메서드를 호출할 수 있어야 하므로 public
public double Add(double a, double b)
{
// 내부적으로만 사용하는 LogHelper를 호출
LogHelper.Log($"덧셈 연산 수행: {a} + {b}");
return a + b;
}
}
}
[LogHelper.cs]
namespace MyCalculatorLib
{
// 우리 라이브러리 내부에서만 사용할 것이므로 internal
internal class LogHelper
{
public static void Log(string message)
{
// 실제로는 파일에 로그를 남기거나 복잡한 처리를 하겠죠?
Console.WriteLine($"[LOG] {message}");
}
}
}
17. 코드를 작성하면 메뉴 → 솔루션 빌드 또는 단축키(Ctrl+Shift+B)를 선택합니다.
18. MyCalculatorLib 프로젝트 폴더에서 bin/Debug/net8.0 폴더로 이동합니다.

19. 생성된 MyCalculatorLib.dll 파일을 확인합니다.

20. MyApp 프로젝트의 종속성 항목에 마우스를 올리고 우클릭합니다.

21. 참조 관리자 창에서 [찾아보기]를 선택합니다.

22. MyCalculatorLib.dll 파일을 추가합니다.

23. MyCalculatorLib.dll 파일이 선택되었으면 [확인]을 선택합니다.

24. 이제 Program.cs에서 코드를 아래와 같이 작성합니다.
[Program.cs]
using MyCalculatorLib; // 라이브러리 참조
namespace MyApp
{
internal class Program
{
static void Main()
{
// 1. public 클래스 사용
Calculator calculator = new Calculator(); // public이라서 외부에서 접근 가능
double result = calculator.Add(10, 20);
Console.WriteLine($"결과: {result}");
// 2. internal 클래스 사용 시도
// LogHelper helper = new LogHelper(); // 컴파일 오류 발생!
// 'LogHelper' is inaccessible due to its protection level.
// internal이라서 다른 어셈블리(MyApp)에서는 보이지도 않아요.
}
}
}
25. 코드가 작성되면 단축키(Ctrl + F5)를 눌려 솔루션을 빌드합니다.
[실행 결과]
[LOG] 덧셈 연산 수행: 10 + 20
결과: 30
MyApp 프로젝트에서는 public클래스인 Calculator는 잘 보이지만,
internal클래스인 LogHelper는 보이지도 않고 사용할 수도 없습니다.
덕분에 라이브러리 제작자는 사용자에게 꼭 필요한 API만 노출할 수 있고,
내부의 복잡한 구현은 internal에서 마음껏 리팩토링하거나 변경할 수 있어요.
이것이 바로 internal의 핵심 역할입니다.
두 가지 규칙을 합친 복합적인 접근 제한자도 있습니다.
같은 어셈블리에 있거나, 다른 어셈블리에 있더라도
자식 클래스라면 접근이 가능합니다. (OR 조건)
같은 어셈블리 내에 있는 자식 클래스에서만 접근 가능합니다. (AND 조건)
접근 제한자를 생략하면 C# 컴파일러가 접근 제한자를 자동으로 지정합니다.
'어디에 선언되었느냐'에 접근 제한자가 따라 달라져요.
우리가 일반적으로 클래스를 만드는 경우죠.
이때 접근 제한자를 생략하면 internal이 기본값으로 적용됩니다.
[코드]
// 접근 제한자를 생략했으므로 컴파일러는 'internal class Friend'로 인식합니다.
class Friend
{
public void SayHello()
{
Console.WriteLine("안녕하세요! 우리는 같은 프로젝트에 있어요!");
}
}
// 이것도 컴파일러는 'internal class Program'으로 인식합니다.
class Program
{
public static void Main()
{
// Friend 클래스는 같은 프로젝트 안에 있으므로 자유롭게 사용할 수 있습니다.
Friend myFriend = new Friend();
myFriend.SayHello();
}
}
[실행 결과]
안녕하세요! 우리는 같은 프로젝트에 있어요!
이번에는 클래스 안에 있는 메서드나 변수의 접근 제한자를 생략하는 경우입니다.
이때는 private접근 제한자가 기본값으로 적용됩니다.
[코드]
public class MyDiary
{
// 접근 제한자를 생략했으므로 컴파일러는 'private string _secret'으로 인식합니다.
string _secret = "사실 저는 C#을 좋아합니다.";
// 이것도 'private void WriteSecret()'으로 인식됩니다.
void WriteSecret()
{
Console.WriteLine(_secret);
}
// 외부에서 비밀을 볼 수 있도록 public 메서드를 하나 만들어 줍니다.
public void ShowSecretToFriend()
{
Console.WriteLine("친구에게만 살짝 보여줄게...");
// 클래스 내부에서는 private 멤버에 자유롭게 접근 가능!
WriteSecret();
}
}
internal class Program
{
internal static void Main()
{
MyDiary myDiary = new MyDiary();
myDiary.ShowSecretToFriend();
// 아래 코드들의 주석을 풀면 즉시 컴파일 오류가 발생합니다.
// myDiary._secret = "새로운 비밀";
// myDiary.WriteSecret();
}
}
[실행 결과]
친구에게만 살짝 보여줄게...
사실 저는 C#을 좋아합니다.
접근 제한자는 코드를 더 견고하고 안전하게 만들어주는 필수 도구입니다.
| 접근 제한자 | 접근 범위 |
|---|---|
public | 어디서든 접근 가능 |
private | 선언된 클래스 내부에서만 접근 가능 |
protected | 선언된 클래스 또는 자식 클래스에서만 접근 가능 |
internal | 같은 어셈블리 내에서만 접근 가능 |
protected internal | 같은 어셈블리 또는 자식 클래스에서 접근 가능 |
private protected | 같은 어셈블리 내의 자식 클래스에서만 접근 가능 |