C#에서 클래스나 구조체를 정의할 때 다양한 멤버들을 캡슐화가 가능합니다. 가장 기본적인 멤버는 데이터를 캡슐화하기 위한 멤버 필드와 수행할 작업에 대한 논리적 코드를 정의하는 메서드입니다. 그리고 사용하는 곳에서는 캡슐화된 데이터처럼 보이지만 실제로는 수행할 작업을 정의할 수 있는 특별한 메서드인 속성을 제공합니다.
배열이나 연결 리스트처럼 요소 개체들을 보관하는 컬렉션에서는 사용자가 인덱스 연산을 통해 요소에 접근할 수 있는 인덱서를 제공하고 있습니다. 그리고 정적 멤버인 상수 멤버와 읽기 전용이 있습니다. 이 외에도 개체를 생성할 때 수행할 작업을 정의할 수 있는 생성자와 메모리에서 개체를 제거할 때 작업을 정의하는 소멸자가 있습니다. 그리고 형식 내에 서브 형식을 정의할 수 있으며 연산자를 사용하였을 때의 작업을 정의할 수도 있습니다.
이 외에도 콜백에서 자주 사용하는 대리자(delegate)형식의 이벤트 멤버를 캡슐화할 수 있는데 9장 대리자와 이벤트에서 설명할게요.
5.1.1 멤버 필드
멤버 필드는 클래스나 구조체의 캡슐화되어 있는 일부 데이터입니다. 멤버 필드는 클래스나 구조체 블록 내에 멤버 형식 및 필드 이름을 차례로 선언하면 됩니다. 또한, C#에서는 선언과 동시에 초기값을 지정할 수 있습니다.
class Man { string name; int hp = 0; //멤버 필드 초기화 }
접근 한정자에 대한 설명하면서 다시 다루겠지만 멤버 필드는 형식 외부에서 접근할 수 없게 private으로 지정하는 것이 바람직합니다. C#에서는 접근 한정자를 명시하지 않을 때 private으로 설정되어 형식 외부에서 접근할 수 없습니다. 만약, 형식 외부에서 멤버 필드의 값을 얻어오거나 설정할 필요성이 있으면 멤버 속성이나 멤버 메서드를 이용하여 제공하는 것이 바람직합니다.
5.1.2 멤버 속성
멤버 속성은 멤버 필드에 있는 값을 얻어오거나 변경할 때 사용할 수 있게 제공하는 특별한 메서드입니다. 멤버 속성을 캡슐화하기 위해서는 형식과 속성 명을 선언하고 전용 필드의 값을 얻어올 때 사용하는 get 블록과 설정하는 set 블록을 선택적으로 정의할 수 있습니다. 각 블록에서는 메서드처럼 내부에서 수행할 작업에 대한 코드를 작성할 수 있으며 필요에 따라 get 블록과 set 블록의 접근 한정을 다르게 지정할 수도 있습니다. get 블록에서는 선언한 형식을 반환해야 하고 set 블록에서는 value 이름으로 전달된 값을 사용할 수 있습니다.
class Man { string name; int hp = 0; public string Name //멤버 속성 - get 블록만 선택적으로 정의 { get { return name; } } public int Hp //멤버 속성 - public으로 접근 지정 { get //멤버 속성에 대한 접근 지정을 따름(public) { return hp; } private set //private으로 접근 지정 { hp = value; //value를 전달 받은 값을 사용할 수 있음 } } }
이와 같은 멤버 속성은 멤버 필드의 신뢰성을 높이는 데 큰 역할을 할 수 있습니다. 외부에서 멤버 필드를 사용할 수 있게 접근을 허용한다면 잘못된 사용으로 신뢰성 없는 값을 갖게 될 수 있습니다. 예를 들어 사람의 hp를 0에서 200사이이 값이 유지하게 하고 일을 하면 hp가 5가 증가하게 하려고 합니다. 사람 형식의 멤버 필드 hp 접근을 public으로 지정하여 형식 외부에서 접근할 수 있게 하면 사용하는 곳에서 잘못된 값으로 hp를 지정하는 경우가 발생할 수 있습니다.
▶ 신뢰성이 없는 값을 사용한 예
using System; namespace Ex_MemberProperty { class Man { public string name; //멤버 필드의 접근을 public으로 지정 public int hp = 0; //멤버 필드의 접근을 public으로 지정 public Man(string name) { this.name = name; } public void Work() { hp += 5; } } class Program { static void Main(string[] args) { Man man = new Man("홍길동"); Console.WriteLine("{0} HP:{1}", man.name, man.hp); man.hp = 937; //잘못된 사용 Console.WriteLine("{0} HP:{1}", man.name, man.hp); } } }
▶ 실행 결과
홍길동 HP:0 홍길동 HP:937
이 같은 경우에 개발자 사이에서는 Man을 잘못 정의한 것인지 사용을 잘못한 것인지 의견이 서로 다를 수 있습니다. OOP에서는 Man을 정의하는 곳에서 사용하는 곳에서 접근할 수 있는 멤버를 적절하게 지정할 수 있게 해 주는 문법을 제공합니다. 따라서 C#과 같은 OOP 언어에서는 위 경우 Man을 잘못 정의한 것으로 볼 수 있습니다. 다음 예는 멤버 필드의 접근을 막고 이에 대해 필요한 수준으로 접근 가능한 멤버 속성을 정의한 예입니다.
▶ 신뢰성을 높인 예
using System; namespace Ex_MemberField { class Man { string name; //멤버 필드 int hp = 0; //멤버 필드 public Man(string name) //생성자 { this.name = name; } public string Name //멤버 속성 { get { return name; } } public int Hp //멤버 속성 { get //가져오기는 외부 형식에서 접근 가능하게 지정 { return hp; } private set //설정은 내부에서만 접근 가능하게 지정 { if(value > 200) { value = 200; } if(value < 0) { value = 0; } hp = value; } } public void Work() { Hp += 5; } } class Program { static void Main(string[] args) { Man man = new Man("홍길동"); Console.WriteLine("{0} HP:{1}", man.Name, man.Hp); man.Work(); Console.WriteLine("{0} HP:{1}", man.Name, man.Hp); } } }
▶ 실행 결과
홍길동 HP:0 홍길동 HP:5
단순히 get 블록에서는 값을 반환하기만 하고 set 블록에서는 필터링 없이 value를 설정하기를 원한다면 멤버 필드를 선언하지 않고 멤버 속성의 get 블록과 set 블록을 선언만 해도 됩니다.
class Foo { public int ExP { get; set; } }
[그림 12]는 ildasm.exe 유틸리티를 통해 위 예제를 덤핑한 화면입니다. 이를 통해 C# 컴파일러에서는 Hp라는 멤버 속성의 get 블록은 get_Hp, set 블록은 set_Hp 이름의 멤버 메서드로 번역을 하는 것을 알 수 있습니다.