접근 제어와 변경 경계
private·default·protected·public의 허용 범위를 구분하고 Study Log의 필드 대신 안전한 행동만 공개합니다.
객체가 불변식을 책임하려면 외부 코드가 내부 상태를 우회해 바꾸지 못하게 해야 합니다. 접근 제어자는 개발자를 불편하게 만드는 잠금이 아니라 허용된 변경 경로를 분명하게 만드는 도구입니다. 가장 좁게 숨기고 협력에 필요한 기능만 공개하는 것이 기본 전략입니다.
private 접근 제한
public final class PrivateFieldViolation {
public static void main(String[] args) {
StudyEntry entry = new StudyEntry("arrays", 40);
System.out.println(entry.minutes);
}
}
final class StudyEntry {
private final String topic;
private final int minutes;
StudyEntry(String topic, int minutes) {
this.topic = topic;
this.minutes = minutes;
}
}error: minutes has private access in StudyEntryPrivateFieldViolation과 StudyEntry가 같은 파일과 같은 패키지에 있어도 private 멤버는 선언한 클래스 밖에서 접근할 수 없습니다.
필요한 읽기 행동을 StudyEntry가 공개해야 합니다.
public int minutes() {
return minutes;
}단순 getter도 공개 계약입니다.
외부가 정말 원시 값을 알아야 하는지, isLongSession()이나 describe()처럼 의도 중심 행동이 더 적합한지 판단합니다.
네 가지 접근 수준
| 접근 제어자 | 같은 클래스 | 같은 패키지 | 다른 패키지 자식 | 그 밖의 외부 |
|---|---|---|---|---|
private | 허용 | 차단 | 차단 | 차단 |
생략(default) | 허용 | 허용 | 패키지가 다르면 차단 | 차단 |
protected | 허용 | 허용 | 상속 경로에서 허용 | 차단 |
public | 허용 | 허용 | 허용 | 허용 |
소스에 default라고 쓰는 접근 제어자는 없습니다.
아무 접근 제어자도 적지 않은 상태를 package-private 또는 default 접근이라고 부릅니다.
protected는 같은 패키지 접근과 다른 패키지의 상속 접근을 결합하므로 단순히 “자식만”이라고 외우면 틀립니다.
접근 범위가 넓을수록 좋은 API가 아닙니다.
public은 모든 호출자가 의존할 수 있어 변경 비용이 가장 큽니다.
내부 구현은 private, 패키지 협력만 필요한 타입과 메서드는 package-private, 외부가 사용해야 하는 최소 행동만 public으로 둡니다.
필드 은닉과 상태 변경
public final class ControlledStudyEntry {
public static void main(String[] args) {
StudyEntry entry = new StudyEntry("arrays", 40);
System.out.println(entry.extend(15));
System.out.println(entry.extend(600));
System.out.println(entry.describe());
}
private static final class StudyEntry {
private final String topic;
private int minutes;
StudyEntry(String topic, int minutes) {
if (topic == null || topic.isBlank() || minutes < 1 || minutes > 600) {
throw new IllegalArgumentException("invalid entry");
}
this.topic = topic;
this.minutes = minutes;
}
public boolean extend(int extraMinutes) {
if (extraMinutes <= 0 || minutes + extraMinutes > 600) {
return false;
}
minutes += extraMinutes;
return true;
}
public String describe() {
return topic + "=" + minutes;
}
}
}true
false
arrays=55외부는 minutes = 655를 직접 만들 수 없습니다.
extend가 현재 값과 상한을 검사하고 성공한 변경만 반영합니다.
필드가 private이므로 새 변경 경로가 필요할 때 클래스 작성자가 규칙을 선택합니다.
무조건 모든 필드에 setter를 만들면 이름만 메서드일 뿐 직접 대입과 같은 자유를 다시 엽니다.
public void setMinutes(int minutes) {
this.minutes = minutes;
}이보다 extend, correctMinutes, complete처럼 사용 의도와 허용 규칙을 담는 행동이 낫습니다.
클래스 접근 수준
최상위 클래스에는 public 또는 package-private만 사용할 수 있습니다.
public 최상위 클래스 이름은 파일 이름과 같아야 합니다.
한 소스 파일에 public 최상위 클래스를 여러 개 선언할 수 없습니다.
public final class StudyLog { }
final class EntryValidator { }StudyLog는 다른 패키지가 사용할 수 있고 EntryValidator는 같은 패키지 구현만 사용합니다.
private이나 protected 최상위 클래스는 허용되지 않지만, 중첩 클래스에는 네 접근 수준을 모두 적용할 수 있습니다.
패키지가 하위 경로처럼 보여도 자동 포함 관계가 없다는 점을 다시 확인합니다.
studylog.model
studylog.model.internal두 패키지는 서로 다른 package-private 경계입니다.
internal에서 model의 package-private 멤버에 접근할 수 없습니다.
StudyLog 공개 API
public final class AccessControlledStudyLog {
public static void main(String[] args) {
StudyLog log = new StudyLog(2);
log.add("arrays", 40);
log.add("methods", 50);
System.out.println(log.entryAt(0));
System.out.println("total=" + log.totalMinutes());
}
private static final class StudyLog {
private final StudyEntry[] entries;
private int size;
public StudyLog(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException("capacity");
entries = new StudyEntry[capacity];
}
public boolean add(String topic, int minutes) {
if (size == entries.length) return false;
entries[size++] = new StudyEntry(topic, minutes);
return true;
}
public String entryAt(int index) {
checkIndex(index);
return entries[index].describe();
}
public int totalMinutes() {
int total = 0;
for (int index = 0; index < size; index++) {
total += entries[index].minutes();
}
return total;
}
private void checkIndex(int index) {
if (index < 0 || index >= size) throw new IndexOutOfBoundsException(index);
}
}
private static final class StudyEntry {
private final String topic;
private final int minutes;
private StudyEntry(String topic, int minutes) {
if (topic == null || topic.isBlank() || minutes <= 0) {
throw new IllegalArgumentException("invalid entry");
}
this.topic = topic;
this.minutes = minutes;
}
private String describe() {
return topic + "=" + minutes;
}
private int minutes() { return minutes; }
}
}arrays=40
total=90외부는 entries와 size를 볼 수 없고 add, entryAt, totalMinutes만 호출합니다.
checkIndex는 공개할 이유가 없는 내부 보조 메서드라 private입니다.
StudyEntry 자체도 이 축약 예제에서는 StudyLog 구현 안에 숨겨져 있습니다.
연습 문제
0에서 시작하고 최대값을 생성자로 받는 Counter를 만드세요.
count 필드는 private으로 숨기고 increment()가 최대값을 넘으면 false, 성공하면 true를 반환하게 하세요.
현재 값은 value()로만 읽습니다.
해설 보기
public final class LimitedCounter {
public static void main(String[] args) {
Counter counter = new Counter(2);
System.out.println(counter.increment());
System.out.println(counter.increment());
System.out.println(counter.increment());
System.out.println("value=" + counter.value());
}
private static final class Counter {
private final int maximum;
private int count;
Counter(int maximum) {
if (maximum < 0) throw new IllegalArgumentException("maximum");
this.maximum = maximum;
}
boolean increment() {
if (count == maximum) return false;
count++;
return true;
}
int value() {
return count;
}
}
}true
true
false
value=2외부는 count를 최대값보다 크게 직접 대입할 수 없습니다.
상태 변경은 increment라는 한 경로로만 일어납니다.