안은 감추고 창구만 열어 둔다
지금까지 필드를 전부 public 으로 열어 뒀습니다.
class Player
{
public int Hp;
}
Player p = new Player();
p.Hp = -500; // 막을 방법이 없습니다
체력이 -500 인 플레이어가 만들어졌습니다. 문법적으로 아무 문제가 없습니다.
문제는 이걸 누가 넣었는지 찾을 수가 없다는 것입니다.
p.Hp = ... 는 프로그램 어디에서나 쓸 수 있습니다.
값이 이상해졌을 때 후보가 코드 전체입니다.
밖에서 들어오는 길을 하나로 좁히면, 거기서 확인할 수 있습니다.
private 으로 막고 메서드로 연다
class Player
{
private int hp = 100;
public void TakeDamage(int amount)
{
if (amount < 0) return;
hp -= amount;
if (hp < 0) hp = 0;
}
public int GetHp()
{
return hp;
}
}
private 은 이 클래스 안에서만 쓸 수 있다는 표시입니다.
p.hp = -500; // 컴파일 에러
p.TakeDamage(30); // 이 길로만 들어옵니다
이제 hp 가 바뀌는 자리가 클래스 안뿐입니다.
값이 이상해지면 볼 곳이 이 파일 하나입니다.
그리고 들어오는 자리가 하나라 확인할 기회가 생깁니다. 음수 피해를 막고, 0 아래로 안 내려가게 하는 코드를 여기 한 번만 적으면 됩니다.
public 필드로 열어 두면 그 기회 자체가 없습니다. 확인할 자리가 없습니다.
접근 지정자
| 어디서 쓸 수 있나 | |
|---|---|
public |
어디서나 |
private |
그 클래스 안에서만 |
protected |
그 클래스와 물려받은 클래스에서 |
아무것도 안 적으면 클래스 멤버는 private 입니다.
그래서 앞의 예제에서 public 을 계속 붙였던 것입니다.
protected 는 상속을 쓸 때 필요합니다.
기본은 private, 필요한 것만 public 입니다.
반대로 하면 나중에 막을 수가 없습니다.
누가 이미 쓰고 있으면 private 으로 바꾸는 순간 그 코드가 다 깨지니까요.
감추는 것은 값이 아니라 방식이다
private 을 값을 숨기는 것으로만 보면 절반만 본 것입니다.
class Player
{
private int hp = 100;
private int shield = 50;
public int GetHp()
{
return hp + shield; // 안에서 어떻게 계산하든 밖은 모릅니다
}
}
밖에서는 GetHp() 만 압니다. 안에 필드가 하나인지 둘인지, 어떻게 계산하는지는 모릅니다.
그래서 안을 바꿔도 밖이 안 깨집니다. 나중에 방어구를 추가하고 계산식을 바꿔도
GetHp() 라는 창구가 그대로면 쓰는 쪽은 아무것도 안 고쳐도 됩니다.
public 필드는 이게 안 됩니다. p.Hp 라고 쓰던 코드가 밖에 흩어져 있으면
필드 이름 하나 바꾸는 것도 큰일이 됩니다.
밖에 드러난 것은 전부 약속입니다. 적게 드러낼수록 나중에 자유롭습니다.
읽기만 열어 두는 것도 있다
바꾸는 것은 막고 읽는 것만 열고 싶을 때가 많습니다.
class Player
{
private int hp = 100;
public int GetHp() { return hp; }
}
GetHp 는 있고 SetHp 는 없습니다. 밖에서는 볼 수만 있습니다.
바꾸는 것은 TakeDamage 나 Heal 처럼 의미가 있는 이름으로만 열어 둡니다.
SetHp(50) 은 무슨 상황인지 모릅니다. 맞은 것인지 회복한 것인지 치트인지요.
TakeDamage(50) 은 압니다. 그리고 그 안에서 무엇을 확인해야 하는지도 정해집니다.
필드마다 Get 과 Set 을 기계적으로 만들면 public 필드와 다를 게 없습니다.
창구를 여는 것이 아니라 벽에 구멍을 낸 것입니다.
생성자와 같이 쓴다
앞 글의 생성자와 합치면 이렇게 됩니다.
class Player
{
private string name;
private int hp;
public Player(string name, int hp)
{
if (hp <= 0) hp = 1;
this.name = name;
this.hp = hp;
}
public void TakeDamage(int amount) { ... }
public int GetHp() { return hp; }
}
이제 Player 객체는 태어날 때부터 끝까지 hp 가 0 아래로 안 내려갑니다.
- 생성자가 시작값을 확인합니다.
private이라 밖에서 못 건드립니다.- 바꾸는 길은
TakeDamage하나고 거기서도 확인합니다.
이렇게 “이 객체는 항상 이렇다” 를 보장할 수 있게 됩니다. 클래스를 쓰는 이유의 나머지 절반이 이것입니다. 데이터와 코드를 묶는 것에 더해, 그 데이터가 망가지지 않게 지키는 것입니다.
그런데 메서드가 번거롭다
GetHp() 는 괄호를 써야 하고, 필드처럼 자연스럽지 않습니다.
Console.WriteLine(p.GetHp());
Console.WriteLine(p.Hp); // 이렇게 쓰고 싶은데
밖에서는 필드처럼 쓰면서 안에서는 코드가 실행되게 하는 문법도 있습니다. 프로퍼티라고 합니다. 생김새가 무엇이든 노리는 것은 같습니다. 값이 바뀌는 자리를 한 곳으로 좁히는 것입니다.
정리
private은 그 클래스 안에서만 쓸 수 있다는 표시입니다. 아무것도 안 적으면private입니다.- 필드를 막고 메서드로만 열면 값이 바뀌는 자리가 한 곳으로 좁혀집니다.
- 자리가 좁아지면 확인할 기회가 생깁니다.
public필드는 그 기회 자체가 없습니다. - 감추는 것은 값만이 아니라 안에서 어떻게 하는지입니다. 그래야 나중에 안을 바꿔도 밖이 안 깨집니다.
- 밖에 드러난 것은 전부 약속입니다. 적게 드러낼수록 자유롭습니다.
- 바꾸는 것은 의미가 있는 이름으로 엽니다.
Set을 기계적으로 만들면public필드와 같습니다. - 생성자와 같이 쓰면 태어날 때부터 끝까지 상태를 지킬 수 있습니다.