| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- 패스트캠퍼스후기
- base64
- 환급챌린지
- Framework
- 직장인공부
- unity
- ui
- C#
- TextMeshPro
- Job 시스템
- 게임개발
- adfit
- 패스트캠퍼스
- 직장인자기계발
- 오공완
- job
- Tween
- 암호화
- 샘플
- RSA
- 최적화
- 프레임워크
- 가이드
- sha
- DotsTween
- 2D Camera
- Dots
- AES
- Unity Editor
- Custom Package
- Today
- Total
EveryDay.DevUp
[PART2.클래스와 객체(6/7)] 접근 한정자 완전 정리 — public·internal·protected·private protected 본문
[PART2.클래스와 객체(6/7)] 접근 한정자 완전 정리 — public·internal·protected·private protected
EveryDay.DevUp 2026. 4. 5. 14:02접근 한정자 완전 정리 — public·internal·protected·private protected
각 한정자의 정확한 범위, CLR이 접근성을 강제하는 방식, 실무 설계 원칙까지
목차
Unity 프로젝트를 열어보면, 인스펙터에서 값을 바꾸려고 public으로 선언한 필드가 수십 개씩 보인다. 편하긴 하지만, 프로젝트가 커지면 어떤 스크립트든 그 필드를 마음대로 바꿀 수 있어 버그의 원인을 추적하기 어려워진다. 접근 한정자는 "이 코드는 누가 사용해도 되는가?"라는 질문에 답하는 도구다. 정확한 범위를 모르면 캡슐화가 무너지고, 팀원이 건드리면 안 되는 내부 구현까지 노출된다.
C#은 6가지 접근 한정자를 제공하며, 이 글에서는 각각의 정확한 범위, CLR(Common Language Runtime, C# 코드를 실행하는 런타임 환경)이 접근성을 검증하는 방식, Unity에서 올바르게 적용하는 방법까지 다룬다.
6가지 접근 한정자 — 각각의 정확한 범위
문제 제기
아래 코드에서 DamageSystem이 PlayerData의 필드를 직접 바꾼다. 어떤 필드를 바꿀 수 있고, 어떤 필드는 바꿀 수 없는가?
public class PlayerData
{
public int Score;
internal float InternalTimer;
protected int BaseHealth;
private string _secretKey;
}
public class DamageSystem
{
public void Process(PlayerData data)
{
data.Score = 0; // 될까?
data.InternalTimer = 0f; // 될까?
data.BaseHealth = 50; // 될까?
data._secretKey = "hack"; // 될까?
}
}
답을 확신할 수 없다면, 접근 한정자의 범위를 정확히 모르는 것이다. 하나씩 짚어보자.
개념 정의
접근 한정자는 타입이나 멤버에 붙여서 누가 접근할 수 있는지 범위를 지정하는 키워드다. 비유하면 건물의 출입 권한과 같다.
public— 누구나 들어올 수 있는 로비internal— 같은 건물(어셈블리) 직원만 출입 가능한 사무실protected— 가족(파생 클래스)만 들어올 수 있는 집private— 본인만 들어갈 수 있는 금고protected internal— 가족 또는 같은 건물 직원 (OR 조건)private protected— 같은 건물에 사는 가족만 (AND 조건)
어셈블리(Assembly) 컴파일된 .dll 또는 .exe 파일 하나를 말한다. Unity에서는 Assembly Definition(.asmdef) 파일 하나가 어셈블리 하나에 대응한다. 기본적으로 모든 스크립트는 Assembly-CSharp.dll 하나로 합쳐진다.

6가지 한정자를 범위가 넓은 순서로 정리하면 이렇다:
public — 제한 없음. 어셈블리 안팎 어디서든 접근할 수 있다. 외부에 공개하는 API에 사용한다.
protected internal — protected 또는 internal 조건 중 하나만 만족하면 접근 가능하다. 같은 어셈블리면 누구나, 다른 어셈블리라도 파생 클래스라면 접근된다.
internal — 같은 어셈블리 안에서만 접근 가능하다. 어셈블리 외부에서는 완전히 숨겨진다.
protected — 선언된 클래스 자신과 파생 클래스에서만 접근 가능하다. 어셈블리 경계와 무관하게, 상속 관계만 보고 판단한다.
private protected — protected 그리고 internal 두 조건을 모두 만족해야 접근 가능하다. 같은 어셈블리 안의 파생 클래스에서만 접근된다.
private — 선언된 클래스 내부에서만 접근 가능하다. 가장 강력한 캡슐화 수단이다.
이제 이 한정자들이 IL(Intermediate Language, C# 코드가 컴파일되면 생성되는 중간 언어) 수준에서 어떻게 표현되는지 확인해보자.
public class AccessDemo
{
public int PublicField;
internal int InternalField;
protected int ProtectedField;
private int PrivateField;
protected internal int ProtectedInternalField;
private protected int PrivateProtectedField;
public void PublicMethod() { PublicField = 1; }
internal void InternalMethod() { InternalField = 2; }
protected void ProtectedMethod() { ProtectedField = 3; }
private void PrivateMethod() { PrivateField = 4; }
}
.class public auto ansi beforefieldinit AccessDemo
extends [System.Runtime]System.Object
{
.field public int32 PublicField // public → public
.field assembly int32 InternalField // internal → assembly (어셈블리 범위)
.field family int32 ProtectedField // protected → family (상속 계열)
.field private int32 PrivateField // private → private
.field famorassem int32 ProtectedInternalField // protected internal → family OR assembly
.field famandassem int32 PrivateProtectedField // private protected → family AND assembly
.method public hidebysig
instance void PublicMethod () cil managed // public → public
{
IL_0001: ldarg.0
IL_0002: ldc.i4.1
IL_0003: stfld int32 AccessDemo::PublicField // public 필드에 1 저장
IL_0008: ret
}
.method assembly hidebysig
instance void InternalMethod () cil managed // internal → assembly
{
IL_0001: ldarg.0
IL_0002: ldc.i4.2
IL_0003: stfld int32 AccessDemo::InternalField
IL_0008: ret
}
.method family hidebysig
instance void ProtectedMethod () cil managed // protected → family
{
IL_0001: ldarg.0
IL_0002: ldc.i4.3
IL_0003: stfld int32 AccessDemo::ProtectedField
IL_0008: ret
}
.method private hidebysig
instance void PrivateMethod () cil managed // private → private
{
IL_0001: ldarg.0
IL_0002: ldc.i4.4
IL_0003: stfld int32 AccessDemo::PrivateField
IL_0008: ret
}
}
IL에서 접근 한정자가 어떤 키워드로 변환되는지 주목하자.
| C# 한정자 | IL 키워드 | 의미 |
|---|---|---|
public |
public |
제한 없음 |
internal |
assembly |
같은 어셈블리 |
protected |
family |
상속 계열 |
private |
private |
선언 타입 내부 |
protected internal |
famorassem |
family OR assembly |
private protected |
famandassem |
family AND assembly |
famorassem과 famandassem이라는 IL 키워드 이름에서 protected internal은 OR 조건이고 private protected는 AND 조건이라는 것이 직관적으로 드러난다.
앞서 던진 질문의 답: DamageSystem은 PlayerData와 같은 어셈블리에 있고 파생 클래스가 아니다. 따라서 Score(public)와 InternalTimer(internal)는 접근 가능하고, BaseHealth(protected)와 _secretKey(private)는 컴파일 에러가 발생한다.
기본 접근 수준 — 한정자를 생략하면 어떻게 되는가
문제 제기
class Foo { } // 이 클래스의 접근 수준은?
class Bar
{
int _value; // 이 필드의 접근 수준은?
void DoWork() { } // 이 메서드의 접근 수준은?
}
접근 한정자를 명시하지 않으면 컴파일러가 기본값을 적용한다. 이 기본값을 모르면 의도치 않게 타입이나 멤버가 너무 넓게(또는 너무 좁게) 노출된다.
개념 정의
C# 컴파일러는 "명시하지 않으면 가능한 한 좁게"라는 원칙을 따른다.
| 대상 | 기본 접근 수준 | 허용 가능한 한정자 |
|---|---|---|
| 최상위 타입 (namespace 바로 아래 class, struct, interface) | internal |
public, internal 만 |
| 클래스 멤버 (필드, 메서드, 프로퍼티, 중첩 타입) | private |
6가지 모두 |
| 구조체 멤버 | private |
public, internal, private 만 |
| 인터페이스 멤버 (C# 8 이전) | public |
public 만 (명시 불가) |
| 인터페이스 멤버 (C# 8 이후) | public |
6가지 모두 (기본 구현 시) |
| enum 멤버 | public |
변경 불가 |
// 최상위 타입: 기본값 = internal
class InternalByDefault { } // internal
public class ExplicitPublic { } // public
// protected class Invalid { } // ❌ 컴파일 에러 — 최상위 타입에 protected 불가
// 클래스 멤버: 기본값 = private
public class MyClass
{
int _field; // private
void Method() { } // private
class Nested { } // private
}
// 구조체 멤버: 기본값 = private, protected 계열 불가
public struct MyStruct
{
int _field; // private
// protected int _x; // ❌ 컴파일 에러 — struct는 상속 불가
}
// 인터페이스 멤버: 기본값 = public
public interface IExample
{
void Method(); // public (암시적)
}
구조체에서 protected를 사용할 수 없는 이유는 단순하다 — 구조체는 상속이 불가능하므로 "파생 클래스에서만 접근"이라는 개념 자체가 성립하지 않는다.
핵심은 이것이다: 한정자를 생략하면 클래스 멤버는 private, 최상위 타입은 internal이 된다. 둘 다 "가능한 한 좁게"라는 방향이므로, 의도적으로 넓혀야 할 때만 명시적으로 접근 한정자를 붙이면 된다.
protected internal vs private protected — OR와 AND의 결정적 차이
문제 제기
이 두 한정자는 이름이 비슷해서 혼동하기 쉽다. 하지만 접근 범위가 근본적으로 다르다. 핵심 차이는 어셈블리 밖의 파생 클래스에서 접근이 가능한지 여부다.
개념 정의

protected internal은 "protected 또는 internal" — 두 조건 중 하나만 만족하면 접근이 허용된다. private protected는 "protected 그리고 internal" — 두 조건을 모두 만족해야 접근이 허용된다.
코드로 확인해보자.
// ── AssemblyA.dll ──
public class Base
{
protected internal int OrField = 1; // OR 조건
private protected int AndField = 2; // AND 조건
}
// 같은 어셈블리, 파생 클래스
public class DerivedSame : Base
{
void Test()
{
_ = OrField; // ✅ 같은 어셈블리 + 파생 — 둘 다 만족
_ = AndField; // ✅ 같은 어셈블리 + 파생 — 둘 다 만족
}
}
// 같은 어셈블리, 파생 아님
public class NonDerivedSame
{
void Test(Base b)
{
_ = b.OrField; // ✅ 같은 어셈블리 — OR 조건 충족
_ = b.AndField; // ❌ 파생 클래스 아님 — AND 조건 미충족
}
}
// ── AssemblyB.dll (AssemblyA 참조) ──
public class DerivedOther : Base
{
void Test()
{
_ = OrField; // ✅ 파생 클래스 — OR 조건 충족
_ = AndField; // ❌ 다른 어셈블리 — AND 조건 미충족
}
}
차이가 드러나는 지점은 DerivedOther — 다른 어셈블리의 파생 클래스다. protected internal은 파생 클래스라는 조건 하나로 충분하지만, private protected는 같은 어셈블리가 아니므로 차단된다.
IL에서도 이 차이가 명확하다. OrField는 famorassem(family OR assembly), AndField는 famandassem(family AND assembly)으로 표현된다.
언제 private protected를 쓰는가? 프레임워크 내부에서 상속 계층을 지원하면서도, 외부 사용자가 상속받아 내부 멤버에 접근하는 것을 막고 싶을 때 사용한다. 예를 들어, Unity 패키지 내부에서만 확장 가능한 기반 클래스를 만들 때 적합하다.
내부 동작 — CLR이 접근성을 강제하는 방식
문제 제기
접근 한정자는 컴파일할 때만 검사되는 것일까? 만약 누군가 IL 코드를 직접 수정해서 private 멤버에 접근하려 하면 어떻게 될까?
개념 정의
접근 한정자는 이중으로 검증된다.
1단계: 컴파일 타임 — C# 컴파일러가 코드를 분석하며 접근 규칙 위반을 컴파일 에러로 차단한다. 대부분의 위반은 여기서 걸린다.
2단계: 런타임(JIT 컴파일 시) — CLR의 JIT(Just-In-Time) 컴파일러가 IL을 네이티브 코드로 변환할 때 메타데이터를 다시 확인한다. 접근 위반이 발견되면 FieldAccessException 또는 MethodAccessException을 던진다.
런타임 검증이 필요한 이유는 어셈블리가 독립적으로 컴파일되기 때문이다. A.dll에서 public이었던 멤버가 이후 private으로 변경되어 재컴파일되면, B.dll은 여전히 옛날 메타데이터로 접근을 시도한다. 이때 JIT가 런타임에서 차단한다.
접근성 캡핑 규칙
internal 클래스 안에 public 멤버를 선언하면 어떻게 될까?
internal class InternalClass
{
public int PublicField; // public이지만...
public void PublicMethod() // public이지만...
{
PublicField = 42;
}
}
PublicField는 public으로 선언되었지만, 이 필드를 포함하는 InternalClass 자체가 internal이다. 외부 어셈블리에서는 InternalClass에 접근할 수 없으므로, PublicField도 실질적으로 internal 범위로 제한된다. 이것이 접근성 캡핑(Accessibility Capping) 규칙이다 — 멤버의 접근성은 자신을 포함하는 타입의 접근성을 넘을 수 없다.
중첩 클래스의 특별한 접근 권한
중첩 클래스(nested class)는 한 가지 특별한 권한이 있다 — 외부 클래스의 private 멤버에 직접 접근할 수 있다.
public class Outer
{
private int _secret = 42;
public class Inner
{
public int GetSecret(Outer outer)
{
return outer._secret; // ✅ private 멤버에 직접 접근 가능
}
}
}
.class nested public auto ansi beforefieldinit Inner
extends [System.Runtime]System.Object
{
.method public hidebysig
instance int32 GetSecret (class Outer outer) cil managed
{
IL_0001: ldarg.1 // outer 인스턴스 로드
IL_0002: ldfld int32 Outer::_secret // private 필드에 직접 접근!
IL_0007: stloc.0
IL_000a: ldloc.0
IL_000b: ret
}
}
IL을 보면 Inner.GetSecret()이 Outer::_secret에 ldfld로 직접 접근한다. 합성 접근자(synthetic accessor) 메서드를 생성하지 않고, 중간 우회 없이 바로 필드를 읽는다.
이것이 가능한 이유는 C# 언어 명세에서 중첩 타입을 외부 타입의 멤버로 간주하기 때문이다. Inner는 Outer의 멤버이므로, Outer 내부의 다른 메서드와 동일한 접근 권한을 갖는다.
이 특성 덕분에 빌더 패턴 등에서 외부 클래스의 생성자를 private으로 만들고, 중첩 빌더 클래스만 인스턴스를 생성하도록 강제할 수 있다.
실전 적용 — 최소 권한 원칙과 Unity 설계
문제 제기
"일단 public으로 해놓고 나중에 바꾸자"는 습관이 위험한 이유가 있다. 한번 public으로 공개된 API는 외부 코드가 이미 의존하고 있을 수 있어, 나중에 접근 수준을 좁히면 기존 코드가 깨진다. 반대로 private에서 시작해서 필요할 때 넓히는 것은 언제든 가능하다.
개념 정의
최소 권한 원칙 (Principle of Least Privilege) 모든 멤버는 자신의 역할을 수행하는 데 필요한 최소한의 접근 수준만 가져야 한다는 설계 원칙이다.
의사결정은 private에서 시작해서, 필요한 만큼만 넓힌다:

실전 적용 — Unity에서의 접근 한정자
🎮 [SerializeField] private — 캡슐화와 인스펙터 접근의 양립
Unity 초보자가 가장 많이 하는 실수가 인스펙터에서 값을 바꾸려고 필드를 public으로 선언하는 것이다.
[SerializeField]— 직렬화 필드 어트리뷰트 Unity의 직렬화 시스템이 이 어트리뷰트가 붙은private필드를 인식하여 인스펙터에 노출하고 씬 파일에 저장한다. C# 접근 수준은private으로 유지되므로 다른 스크립트에서 직접 접근할 수 없다.
// ❌ 나쁜 패턴: public 필드로 인스펙터 노출
public class EnemyController : MonoBehaviour
{
public float health = 100f; // 누구나 직접 수정 가능
public float moveSpeed = 5f; // 다른 스크립트에서 마음대로 변경
public NavMeshAgent agent; // 내부 구현까지 외부에 노출
void Update()
{
// 누군가 외부에서 health를 음수로 바꿨을 수 있다
if (health <= 0) Destroy(gameObject);
}
}
// ✅ 좋은 패턴: [SerializeField] private + 프로퍼티
public class EnemyController : MonoBehaviour
{
[SerializeField] private float _maxHealth = 100f; // 인스펙터에 노출, 코드에선 private
[SerializeField] private float _moveSpeed = 5f;
private float _currentHealth;
private NavMeshAgent _agent;
// 외부에는 읽기만 허용
public float HealthRatio => _currentHealth / _maxHealth;
public bool IsAlive => _currentHealth > 0f;
// 상태 변경은 메서드를 통해서만 — 유효성 검사 가능
public void TakeDamage(float damage)
{
if (!IsAlive) return;
_currentHealth = Mathf.Max(0f, _currentHealth - damage);
}
private void Awake()
{
_agent = GetComponent<NavMeshAgent>();
_currentHealth = _maxHealth;
}
}
[SerializeField]가 동작하는 원리: Unity의 직렬화 시스템은 리플렉션(Reflection, 런타임에 타입 정보를 분석하는 기능)을 통해 private 필드에도 접근한다. C#의 접근 한정자는 유지되면서 Unity 에디터에서만 값을 편집할 수 있게 된다.
🎮 MonoBehaviour 생명주기 메서드는 왜 private이어도 되는가
public class Player : MonoBehaviour
{
// ✅ private으로 선언해도 Unity가 정상 호출한다
private void Awake() { }
private void Start() { }
private void Update() { }
private void OnDestroy() { }
}
Awake, Start, Update 같은 생명주기 메서드는 C# 메서드 호출이 아니라 Unity 엔진의 네이티브(비관리) 코드에서 리플렉션 또는 내부 메시지 시스템으로 호출된다. 따라서 private이어도 Unity가 찾아서 실행한다. 오히려 public으로 두면 다른 스크립트가 player.Update()처럼 직접 호출할 위험이 생기므로, private이 권장된다.
🎮 Assembly Definition과 internal의 진정한 힘
Unity 프로젝트는 기본적으로 모든 스크립트가 Assembly-CSharp.dll 하나로 컴파일된다. 이 상태에서는 internal의 범위가 프로젝트 전체와 같아서 public과 차이가 거의 없다.
Assembly Definition(.asmdef) 파일을 사용해 어셈블리를 분리하면, internal이 모듈 경계로서 의미를 갖게 된다.
// ── MyGame.Core.asmdef 어셈블리 ──
namespace MyGame.Core
{
// public — 다른 어셈블리(Gameplay, UI 등)에서 접근 가능
public class GameManager
{
public static GameManager Instance { get; private set; }
public int Score { get; private set; }
public void AddScore(int points) { Score += points; }
// internal — Core 어셈블리 내부에서만. Gameplay에서는 접근 불가.
internal void ResetForTesting() { Score = 0; }
}
// internal 클래스 — Core 어셈블리 외부에서는 존재 자체를 모른다
internal class ScoreCalculator
{
internal int Calculate(int baseScore, float multiplier)
{
return (int)(baseScore * multiplier);
}
}
}
// ── MyGame.Core/AssemblyInfo.cs ──
using System.Runtime.CompilerServices;
// 테스트 어셈블리에게만 internal 접근 허용
[assembly: InternalsVisibleTo("MyGame.Tests")]
[InternalsVisibleTo]— 프렌드 어셈블리 선언 특정 어셈블리에게 자신의internal멤버에 대한 접근 권한을 예외적으로 부여하는 어트리뷰트다. 테스트 프로젝트에서 내부 구현을 테스트해야 할 때 사용한다. 프로덕션 코드를 테스트 목적으로public으로 변경하는 위험을 피할 수 있다.
Assembly Definition을 사용하면 얻는 추가 이점:
| 항목 | .asmdef 없이 | .asmdef 사용 |
|---|---|---|
| internal 범위 | 프로젝트 전체 | 해당 어셈블리만 |
| 컴파일 속도 | 코드 수정 시 전체 재컴파일 | 변경된 어셈블리만 재컴파일 |
| 순환 참조 탐지 | 어려움 | 컴파일 에러로 즉시 탐지 |
함정과 주의사항
❌ sealed 클래스에서 protected 멤버 선언
sealed— 봉인 클래스 키워드 클래스에sealed를 붙이면 다른 클래스가 이 클래스를 상속할 수 없다. 더 이상 확장할 필요가 없는 최종 구현에 사용한다.
예시:public sealed class FinalBoss : MonoBehaviour { }다른 클래스가FinalBoss를 상속하려 하면 컴파일 에러가 발생한다.
// ❌ 의미 없는 protected — 컴파일러 경고 CS0628
public sealed class FinalBoss : MonoBehaviour
{
protected float _attackPower = 50f; // 경고: sealed 클래스에서 protected는 의미 없음
}
// ✅ sealed 클래스에서는 private 사용
public sealed class FinalBoss : MonoBehaviour
{
private float _attackPower = 50f;
}
sealed 클래스는 상속이 불가능하다. 따라서 "파생 클래스에서만 접근"이라는 protected의 의미가 성립하지 않는다. 컴파일은 되지만 경고(CS0628)가 발생하며, 의도와 맞지 않는 코드다.
❌ 접근성 캡핑을 무시한 설계
// ❌ internal 클래스에 public 멤버 — 외부에서 접근 불가한데 public인 척
internal class HelperUtils
{
public static void DoWork() { } // internal로 캡핑됨
}
// ✅ 의도를 명확히 — internal 클래스의 멤버도 internal로
internal class HelperUtils
{
internal static void DoWork() { } // 실제 접근 범위와 일치
}
internal 클래스 안의 public 멤버는 실질적으로 internal 범위로 제한된다. 코드를 읽는 사람에게 혼란을 주므로, 실제 접근 범위와 일치하도록 명시하는 것이 좋다. 단, 인터페이스를 구현하는 경우(public void IDisposable.Dispose())는 public이어야 하므로 예외다.
❌ Unity에서 public 필드 남발
// ❌ 프로젝트 어디서든 player.health = -999를 할 수 있다
public class Player : MonoBehaviour
{
public float health = 100f;
public float mana = 50f;
public int level = 1;
}
// ✅ 필드는 private, 필요한 것만 프로퍼티로 노출
public class Player : MonoBehaviour
{
[SerializeField] private float _health = 100f;
[SerializeField] private float _mana = 50f;
[SerializeField] private int _level = 1;
public float Health => _health;
public int Level => _level;
public void TakeDamage(float damage)
{
_health = Mathf.Max(0f, _health - damage);
}
}
public 필드는 프로젝트 내 어디서든 값을 직접 바꿀 수 있다. 유효성 검사 없이 음수를 넣거나, 의도하지 않은 시점에 값을 변경하는 버그가 발생하기 쉽다. [SerializeField] private + 읽기 전용 프로퍼티 + 상태 변경 메서드 조합이 안전하다.
C# 버전별 변화
C# 7.2 — private protected 추가
C# 7.2 이전에는 "같은 어셈블리 내의 파생 클래스에서만 접근"을 표현할 수 없었다.
// C# 7.2 이전 — 정확한 표현 불가
public class FrameworkBase
{
// internal: 파생 클래스가 아닌 곳에서도 접근 가능 → 과다 노출
internal void InternalHelper() { }
// protected: 다른 어셈블리의 파생 클래스에서도 접근 → 과다 노출
protected void ProtectedHelper() { }
}
// C# 7.2 이후 — 정확한 범위 지정
public class FrameworkBase
{
// 같은 어셈블리의 파생 클래스에서만 접근 — 정확히 원하는 범위
private protected void ExactHelper() { }
}
C# 8.0 — 인터페이스 멤버에 접근 한정자 허용
C# 8 이전 인터페이스 멤버는 항상 public이었고, 접근 한정자를 명시할 수 없었다. 기본 인터페이스 구현(Default Interface Implementation)이 도입되면서 인터페이스 내에서도 접근 한정자를 사용할 수 있게 되었다.
// C# 8 이전 — 인터페이스 멤버는 무조건 public
public interface ILogger
{
void Log(string message); // public (한정자 명시 불가)
}
// C# 8 이후 — 기본 구현 + 접근 한정자
public interface ILogger
{
void Log(string message); // public (기본값)
// private 헬퍼 — 인터페이스 내부의 기본 구현에서만 사용
private string Format(string message)
=> $"[{DateTime.Now:HH:mm:ss}] {message}";
// 기본 구현이 있는 public 메서드
void LogWithTimestamp(string message)
{
Log(Format(message)); // private 헬퍼 호출
}
}
private 인터페이스 멤버는 기본 구현 내부에서만 사용하는 헬퍼 메서드를 정의할 때 유용하다. 구현 클래스에서는 접근할 수 없으며, 인터페이스 자체의 기본 구현에서만 호출된다.
정리
- 기본 선택은
private— 모든 멤버는private에서 시작하고, 필요할 때만 접근 수준을 넓힌다 - 최상위 타입의 기본값은
internal, 멤버의 기본값은private— 한정자를 생략하면 컴파일러가 "가능한 한 좁게" 적용한다 protected internal은 OR 조건,private protected는 AND 조건 — 다른 어셈블리의 파생 클래스 접근 여부가 갈린다- IL에서 접근 한정자는
assembly,family,famorassem,famandassem등으로 변환되어 런타임까지 강제된다 - 접근성 캡핑 — 멤버의 접근성은 자신을 포함하는 타입의 접근성을 넘을 수 없다
- Unity에서는
[SerializeField] private— 인스펙터 접근과 코드 캡슐화를 동시에 달성한다 - Assembly Definition을 사용해야
internal이 진정한 모듈 경계로 기능한다 sealed클래스에서protected는 의미 없음 —private을 사용한다
'C# 심화' 카테고리의 다른 글
| [PART3.상속과 다형성(1/4)] 상속 vs 컴포지션 — 언제 상속을 쓰고 언제 쓰지 않는가 (0) | 2026.04.05 |
|---|---|
| [PART2.클래스와 객체(7/7)] 연산자 오버로딩 — 사용자 정의 타입에 연산자를 부여하는 방법 (1) | 2026.04.05 |
| [PART2.클래스와 객체(5/7)] object 클래스 — 모든 타입의 조상 (1) | 2026.04.05 |
| [PART2.클래스와 객체(4/7)] 선택적 매개변수와 명명된 인수 — 기본값의 함정 (0) | 2026.04.05 |
| [PART2.클래스와 객체(3/7)] 메서드 오버로딩 — 컴파일러는 어떤 메서드를 고르는가 (0) | 2026.04.05 |
