1. 소개
접근 제한자는 집에 있는 울타리랑 자물쇠 같은 거야: 누가 어느 방에 들어갈 수 있는지, 누가 열쇠구멍으로만 볼 수 있는지 정해주는 거지. 참고로, C#에서는 이런 게 있어:
| 제한자 | 누가 볼 수 있음 |
|---|---|
|
모두 |
|
현재 클래스 내부에서만 |
|
현재 클래스랑 상속받은 애들 |
|
프로젝트 전체 (어셈블리/assembly) |
|
프로젝트 전체 + 상속받은 애들 |
|
같은 프로젝트 내 상속받은 애들만 |
캡슐화의 목적은 클래스 구현 세부사항을 숨기는 거야. 그래서 네가 원하지 않으면 아무도 obj.Field = 99999; 이런 식으로 객체를 망가뜨릴 수 없게 하는 거지.
2. 접근 제한자 실수들
public으로 다 열어버리기
가장 흔한 실수는 클래스의 모든 필드랑 메서드를 public으로 선언하는 거야. 논리는 이래: "혹시 필요할 수도 있으니까? 남들도 내 멤버 아무 데서나 쓰게 하자!" 이런 식으로 하면 편해 보이지만, 누가 네 객체에 이상한 값을 넣거나 내부를 이상하게 써버리면 코드 바꾸는 게 무서워져.
실수 예시:
public class BankAccount
{
public string Owner;
public decimal Balance;
}
이제 아무나 이렇게 할 수 있어:
var acc = new BankAccount();
acc.Owner = "";
acc.Balance = -1000000M; // 갑자기, 백만의 빚!
뭐가 문제냐면?
보안이랑 상식 다 깨진 거지. 네 은행이 나뭇잎이나 "모노폴리" 게임 돈만 있어도, 마음대로 마이너스 잔고는 이상하잖아.
클래스를 "구조체 같은 클래스"로 만들어버리기
두 번째 흔한 실수는 필드뿐만 아니라 내부 속성이나 메서드까지 public으로 만들어서, 원래 외부에서 접근하면 안 되는 것까지 다 열어버리는 거야.
public class Vault
{
public string DoorPIN = "1234";
public void OpenDoor()
{
// 뭔가 마법
}
}
이제 어떻게 되냐면: 아무나 PIN을 알아내서 네 금고 문을 열 수 있어. "테스트용 금고"라도, 한 달 뒤 누가 이 구멍을 까먹으면 큰일 날 수도 있지.
올바른 방법:
클래스 인터페이스는 최소화하고 보호해야 해. 남이 써야 하는 건 public으로, 자기만 쓸 건 private으로. 상속받은 애들이 써야 하면 protected로.
3. 캡슐화 실수
.NET에서는 이런 합의가 있어: 클래스의 어떤 데이터(상태)도 public 필드에 저장하면 안 돼! 다 감추거나 최소한 속성으로 감싸야 해. 필드에 직접 접근하는 건 나쁜 스타일의 전형이고, 잡기 힘든 버그의 원인이야.
왜 필드는 private이어야 할까
필드는 객체 내부 상태의 핵심이야. public으로 열어두면 언제, 어떤 값이 들어갈지 통제 못 해. 객체가 이상한 값에 취약해지고, 나중에 형식 바꾸면 public으로 접근하던 코드가 다 깨져.
대신 이렇게 하자:
필드는 private로 선언하고, 외부엔 필요한 속성이나 메서드만 내보내:
public class BankAccount
{
private decimal balance;
public decimal Balance
{
get { return balance; }
private set
{
if (value < 0)
throw new ArgumentException("잔고는 음수일 수 없어");
balance = value;
}
}
public BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("0이나 그 이하로 입금할 수 없어!");
Balance += amount;
}
}
큰 실수: 바꿀 수 없는 데이터에 public set 주기
가끔 get만 필요해도, 개발자들이 "그냥 빨리 하려고" public { get; set; } 이렇게 써. 그러면 아무나 이렇게 할 수 있지:
account.Balance = 99999999; // 왜 안 돼?
더 나은 방법:
속성은 클래스 내부에서만 바꿔야 하면 set에 제한자를 붙여:
public decimal Balance { get; private set; }
아니면, 값이 객체 생성 때만 정해져야 하면 (C# 14):
public decimal Balance { get; init; }
4. protected도 함정일 때
상속받은 애들한테 다 열어버리기
protected는 상속받은 애들한테 기능 넘겨줄 때 좋은데, 어떤 데이터는 상속받은 애들한테도 주면 안 돼! 예를 들어, 자식 클래스가 중요한 필드를 직접 바꾸면 안 되는 경우.
예시:
public class SecureVault
{
protected string secretCode = "1234";
}
이제 아무 상속받은 클래스가 이렇게 할 수 있어:
public class HackerVault : SecureVault
{
public void Hack()
{
secretCode = "0000"; // 쉽게 비밀 바꿈!
}
}
추천:
protected는 진짜 상속받은 애들이 써야 하는 것만 써. 나머지는 private으로 두고, 필요한 작업은 protected 메서드로 만들어.
5. internal 실수와 어셈블리 꼬임
internal 제한자는 "프로젝트 안에서 다 쓸 수 있게 하자"는 유혹이 있어. 근데 프로젝트가 파일 여러 개로 커지거나 라이브러리 연결하면 충돌이 생겨. 학생들이 실수로 뭔가를 어셈블리 안에서 public으로 만들어버리기도 해. 사실 완전히 감춰야 하는 클래스인데도.
예시:
internal class Logger
{
internal void Write(string msg) { /* ... */ }
}
이제 프로젝트의 아무 파일에서나 Logger.Write를 부를 수 있어. 1년 뒤 누가 네 DLL을 다른 프로젝트에 붙이면? public 남겨둔 거 다 보이게 돼.
팁:
기술적인 클래스는 internal로 해도 되지만, 민감한 건 꼭 private로 남겨.
6. 실전: 은행 앱 예제
은행 앱 예제로 계속 가보자. 실수하기 얼마나 쉬운지, 어떻게 고치는지 직접 볼 수 있어.
실수 예시 1: Public 필드
public class BankAccount
{
public decimal Balance;
public string Owner;
}
문제: 아무나 망가뜨릴 수 있음.
속성으로 고치기
public class BankAccount
{
private decimal _balance;
private string _owner;
public string Owner => _owner; // 읽기 전용
public decimal Balance
{
get => _balance;
private set
{
if (value < 0) throw new ArgumentException("잔고는 음수일 수 없어");
_balance = value;
}
}
public BankAccount(string owner, decimal initialBalance)
{
if (string.IsNullOrWhiteSpace(owner))
throw new ArgumentException("소유자 이름은 필수야");
_owner = owner;
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("입금액은 양수여야 해");
Balance += amount;
}
}
이제 이렇게 해보면:
var acc = new BankAccount("레나", 1000m);
acc.Balance = -700m; // 에러! set이 private임.
아니면 이렇게 해도:
acc.Owner = "해커"; // 컴파일 에러, set 없음
— 안 돼. 생성자나 메서드로만 가능.
실수: "예쁘게" 속성 만들기
많이들 이렇게 써:
public int Age { get; set; }
근데 나이는 IncreaseAge 메서드(예를 들어 1월 1일에)로만 바뀌어야 하고, 직접 바꾸면 안 돼.
7. 시각적 요약: 이렇게 하면 안 되고, 이렇게 해야 함
graph TD
A[Public Fields] -->|아무 코드나| D[통제 안 되는 상태]
B[Private Fields + Public Methods] -->|잘 정의됨| E[통제되는 상태]
F[Public Setters] -->|누구나 바꿈| D
G[Private Setters] -->|클래스만| E
| 방법 | 상태 보호됨? | 검증 가능? | 확장 쉬움? |
|---|---|---|---|
| Public fields | ❌ | ❌ | ❌ |
| Properties (get/set public) | ❌ | 부분적으로 | 가능하지만 위험함 |
| Private fields + set private 속성 | ✅ | ✅ | ✅ |
8. 캡슐화 스타일 팁
- 진짜 클래스 사용자한테 필요한 것만 외부로 열어둬.
- 객체에 들어오는 데이터는 항상 검증해.
- 불필요하게 열지 마: 외부에서 바꾸면 안 되는 속성은 set을 private으로 해.
- 특이한 로직은 항상 메서드로 처리하고, 직접 접근은 피하자.
- 테스트한다고 public 필드 만들고 싶어도 참아.
- 읽기만 public이어도 필드 대신 속성 써. 나중에 고치기 쉬워.
GO TO FULL VERSION