CodeGym /행동 /C# SELF /접근 제한자와 캡슐화 관련 실수들

접근 제한자와 캡슐화 관련 실수들

C# SELF
레벨 25 , 레슨 2
사용 가능

1. 소개

접근 제한자는 집에 있는 울타리랑 자물쇠 같은 거야: 누가 어느 방에 들어갈 수 있는지, 누가 열쇠구멍으로만 볼 수 있는지 정해주는 거지. 참고로, C#에서는 이런 게 있어:

제한자 누가 볼 수 있음
public
모두
private
현재 클래스 내부에서만
protected
현재 클래스랑 상속받은 애들
internal
프로젝트 전체 (어셈블리/assembly)
protected internal
프로젝트 전체 + 상속받은 애들
private protected
같은 프로젝트 내 상속받은 애들만

캡슐화의 목적은 클래스 구현 세부사항을 숨기는 거야. 그래서 네가 원하지 않으면 아무도 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이어도 필드 대신 속성 써. 나중에 고치기 쉬워.
2
과제
C# SELF, 레벨 25, 레슨 2
잠금
접근 제한자와 캡슐화 오류
접근 제한자와 캡슐화 오류
2
과제
C# SELF, 레벨 25, 레슨 2
잠금
캡슐화 구현 및 클래스 상태 보호
캡슐화 구현 및 클래스 상태 보호
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION