캡슐화와 접근 제어
캡슐화는 필드를 무조건 숨기는 규칙이 아니라 객체의 변경 경로를 제한해 유효한 상태를 지키는 설계입니다.
Java의 접근 제어자는 private, package-private, protected, public 순으로 보이는 범위를 넓힙니다.
필요한 범위보다 넓게 공개한 API는 나중에 바꾸기 어렵습니다.
getter와 setter를 모두 자동 생성하면 필드를 공개한 것과 크게 다르지 않을 수 있습니다.
이 절에서는 협력에 필요한 최소 표면만 노출합니다.
private은 객체 내부 구현을 닫는다
필드를 private으로 두면 외부는 공개 메서드를 통해서만 상태를 바꿉니다.
검증과 로깅, 표현 변경을 한 경계에 모을 수 있습니다.
패키지 경계는 가까운 협력자를 묶는다
접근 지정자를 생략한 package-private은 같은 패키지에서만 보입니다.
내부 구현 타입을 public API로 노출하지 않을 때 유용합니다.
접근 제어는 “누가 이 이름을 사용할 수 있는가”를 컴파일 시점에 제한합니다. 예제의 타입을 같은 패키지, 다른 패키지로 나누어 보면 package-private 경계가 단순한 표기 차이가 아니라 디렉터리 구조와 함께 작동한다는 점이 보입니다.
public class EncapsulatedWallet {
static final class Wallet {
private int balance;
void add(int amount) {
if (amount <= 0) throw new IllegalArgumentException("positive only");
balance += amount;
}
boolean spend(int amount) {
if (amount <= 0 || amount > balance) return false;
balance -= amount;
return true;
}
int balance() { return balance; }
}
public static void main(String[] args) {
Wallet wallet = new Wallet();
wallet.add(50);
System.out.println(wallet.spend(30));
System.out.println(wallet.spend(30));
System.out.println(wallet.balance());
}
}아래 출력은 private 잔액이 add와 spend를 통해서만 바뀐 결과입니다. package-private 범위를 확인하려면 Wallet을 별도 파일로 옮기고 같은 패키지와 다른 패키지의 호출 클래스를 각각 컴파일해 접근 가능 여부를 비교합니다.
true
false
20두 번째 spend는 잔액보다 커서 상태를 바꾸지 않고 false를 반환합니다.
호출자는 필드를 직접 대입하지 않고 spend(amount)처럼 의미 있는 동작을 요청합니다. 공개 표면에는 업무에 필요한 동사만 남습니다.
객체는 private 필드의 현재 상태와 요청 값을 함께 검사합니다. 잔액 부족이나 음수 입력 규칙이 여러 호출자에게 중복되지 않습니다.
검사를 통과한 한 경로에서만 필드를 변경하고 결과를 반환합니다. 실패가 객체를 절반만 바꾼 상태로 남기지 않도록 검증과 변경 순서를 고정합니다.
protected는 상속 계약까지 넓힌다
같은 패키지와 하위 클래스에 보이므로 단순히 public보다 조금 좁은 접근으로 생각하면 안 됩니다.
하위 클래스가 의존할 안정된 확장점에만 사용합니다.
protected 멤버는 다른 패키지의 아무 코드에나 보이는 것이 아니라 상속 관계의 하위 클래스에 확장 지점을 제공합니다. 하위 클래스가 아닌 외부 호출자도 써야 한다면 public, 상속 구현에만 필요하다면 protected를 검토합니다.
// 필드를 공개하면 어느 호출자나 불변식을 깨뜨릴 수 있다.
class OpenWallet {
public int balance;
}OpenWallet wallet = new OpenWallet();
wallet.balance = -1;// setBalance 대신 add/spend처럼 업무 의미가 있는 동작을 공개한다.setter 대신 의미 있는 동작을 제공한다
setBalance보다 deposit은 허용 값과 상태 변화 의미를 드러냅니다.
읽기 전용 조회가 필요할 때만 getter를 추가합니다.
공개 타입이 내부 타입을 새게 하지 않는다
public 메서드의 매개변수나 반환 타입에 내부 구현 클래스를 쓰면 사실상 그 타입도 계약이 됩니다.
표준 인터페이스나 공개 값 타입으로 경계를 유지합니다.
공개 메서드의 반환 타입이나 매개변수에 package-private 구현 타입을 노출하면 외부 사용자가 그 API를 제대로 호출할 수 없습니다. 공개 경계에는 공개된 추상 타입이나 값 타입을 두고 내부 구현은 패키지 안에 남깁니다.
접근 제어는 현재 호출을 막는 장치이면서 미래에 구현을 바꿀 수 있는 여유를 보존하는 장치입니다.