이펙티브 자바 완벽 공략 1부
아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
- 아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
- 아이템 16. 핵심 정리
- 아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
- public 필드를 직접 공개하면 캡슐화가 깨진다
- 내부 표현을 변경하기 어려워진다
- 접근자 메서드를 사용하면 내부 구현을 숨길 수 있다
- 필드 이름을 변경해도 클라이언트 코드를 유지할 수 있다
- setter에서 값을 검증할 수 있다
- getter에서도 부가 작업을 수행할 수 있다
- setter를 반드시 제공해야 하는 것은 아니다
- public 클래스에서는 접근자 메서드를 사용해야 한다
- package-private 클래스는 예외가 될 수 있다
- public final 필드는 완전한 해결책이 아니다
- final은 객체 내부까지 불변으로 만들지 않는다
- 가변 객체를 전달할 때는 복사본을 고려해야 한다
- 가변 public 필드는 스레드 안전성도 해친다
- 접근자 메서드 사용 시 주의할 점
- public 필드와 접근자 메서드 비교
- 실무에서의 권장 방향
- 핵심 정리
- 한 줄 정리
- 아이템 16. 핵심 정리
아이템 16. 핵심 정리
아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
이번 아이템의 핵심은 단순하다.
public 클래스의 필드는 직접 공개하지 말고, private으로 감춘 뒤 접근자 메서드를 제공하라.
필드를 public으로 공개하면 외부 코드가 객체의 내부 상태에 직접 접근하게 된다. 이는 캡슐화를 무너뜨리고, 이후 클래스의 내부 구현을 변경하기 어렵게 만든다.
public 필드를 직접 공개하면 캡슐화가 깨진다
다음과 같은 Point 클래스가 있다고 가정해보자.
public class Point {
public double x;
public double y;
}
클라이언트 코드는 다음과 같이 필드에 직접 접근하게 된다.
Point point = new Point();
point.x = 10;
point.y = 20;
System.out.println(point.x);
System.out.println(point.y);
이 구조에서는 외부 코드가 객체의 내부 상태를 직접 읽고 변경한다.
즉, Point 클래스는 자신의 상태를 통제하지 못한다.
public 필드의 문제점
필드를 직접 공개하면 다음과 같은 문제가 발생한다.
- 내부 표현을 변경하기 어렵다.
- 필드에 저장되는 값을 검증할 수 없다.
- 값을 읽거나 변경할 때 부가 작업을 수행할 수 없다.
- 객체의 불변식을 유지하기 어렵다.
- 외부 코드가 언제 어디서 값을 변경했는지 추적하기 어렵다.
결국 클래스 내부 구현과 클라이언트 코드가 강하게 결합된다.
내부 표현을 변경하기 어려워진다
현재 Point가 x, y 필드로 좌표를 표현한다고 가정해보자.
public class Point {
public double x;
public double y;
}
외부 코드가 다음처럼 직접 사용하고 있다면
point.x
point.y
나중에 필드 이름을 변경하기 어렵다.
예를 들어 내부 표현을 다음처럼 바꾸고 싶다고 해보자.
private double horizontal;
private double vertical;
기존에 point.x, point.y를 사용하던 모든 코드를 수정해야 한다.
public 클래스는 어디에서 사용되고 있는지 완전히 파악하기 어려울 수 있다. 라이브러리나 외부 공개 API라면 변경 영향 범위는 더 커진다.
접근자 메서드를 사용하면 내부 구현을 숨길 수 있다
필드를 private으로 감추고 getter와 setter를 제공하면 내부 구현을 외부에 노출하지 않을 수 있다.
public class Point {
private double x;
private double y;
public double getX() {
return x;
}
public double getY() {
return y;
}
public void setX(double x) {
this.x = x;
}
public void setY(double y) {
this.y = y;
}
}
클라이언트는 다음과 같이 사용한다.
Point point = new Point();
point.setX(10);
point.setY(20);
System.out.println(point.getX());
System.out.println(point.getY());
접근자 메서드의 장점
접근자 메서드를 사용하면 다음과 같은 장점이 생긴다.
- 내부 필드 이름을 자유롭게 변경할 수 있다.
- 값 설정 전에 검증할 수 있다.
- 값 조회 시 추가 계산을 수행할 수 있다.
- 로깅이나 이벤트 발행 같은 부가 작업을 추가할 수 있다.
- 가변 객체를 불변 객체로 전환하기 쉽다.
- 외부 코드와 내부 구현의 결합도를 낮출 수 있다.
필드 이름을 변경해도 클라이언트 코드를 유지할 수 있다
기존 필드가 다음과 같다고 가정해보자.
private double x;
private double y;
이후 내부 표현을 다음과 같이 변경할 수 있다.
private double horizontal;
private double vertical;
접근자 메서드 이름을 유지하면 클라이언트 코드는 변경하지 않아도 된다.
public double getX() {
return horizontal;
}
public double getY() {
return vertical;
}
외부에서는 여전히 다음 코드를 사용할 수 있다.
point.getX();
point.getY();
내부 구현이 변경되어도 공개 API가 유지되는 것이다.
setter에서 값을 검증할 수 있다
필드에 직접 접근하면 값이 올바른지 확인할 방법이 없다.
public int age;
외부에서는 다음과 같이 잘못된 값을 넣을 수 있다.
person.age = -10;
하지만 setter를 사용하면 값을 검증할 수 있다.
public class Person {
private int age;
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException(
"나이는 0 이상이어야 합니다."
);
}
this.age = age;
}
public int getAge() {
return age;
}
}
객체가 자신의 상태를 스스로 통제할 수 있게 된다.
getter에서도 부가 작업을 수행할 수 있다
접근자 메서드는 단순히 필드 값을 반환하는 역할만 하는 것이 아니다.
필요하다면 다음과 같은 작업을 추가할 수 있다.
- 값 계산
- 지연 초기화
- 로깅
- 보안 검사
- 방어적 복사
- 표현 형식 변환
예를 들어 내부에는 섭씨 온도를 저장하지만 외부에는 화씨 온도를 제공할 수 있다.
public class Temperature {
private double celsius;
public double getFahrenheit() {
return celsius * 1.8 + 32;
}
}
필드를 직접 공개했다면 이런 추상화를 제공하기 어렵다.
setter를 반드시 제공해야 하는 것은 아니다
필드를 private으로 만들었다고 해서 반드시 setter를 제공해야 하는 것은 아니다.
오히려 불변 객체를 만들고 싶다면 setter를 제공하지 않는 것이 좋다.
public final class Point {
private final double x;
private final double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
}
이 객체는 생성 이후 상태가 변경되지 않는다.
getter와 setter가 캡슐화의 전부는 아니다
필드를 private으로 바꾸고 모든 필드에 getter와 setter를 제공했다고 해서 완벽한 캡슐화가 되는 것은 아니다.
다음과 같은 코드는 여전히 객체의 상태를 외부에서 자유롭게 변경하게 만든다.
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
도메인 규칙이 있다면 단순 setter보다 의미 있는 메서드를 제공하는 것이 좋다.
public void deposit(BigDecimal amount) {
validatePositive(amount);
balance = balance.add(amount);
}
public void withdraw(BigDecimal amount) {
validateWithdraw(amount);
balance = balance.subtract(amount);
}
객체가 자신의 비즈니스 규칙을 직접 보호할 수 있게 된다.
public 클래스에서는 접근자 메서드를 사용해야 한다
이 아이템의 권장은 특히 public 클래스에 중요하다.
public 클래스는 외부 패키지나 외부 프로젝트에서 사용할 수 있다. 한 번 필드를 공개하면 해당 필드가 사실상 공개 API가 된다.
외부 사용자는 다음과 같이 직접 의존하게 된다.
point.x
point.y
그 순간부터 필드의 이름, 타입, 의미를 쉽게 변경하기 어려워진다.
따라서 public 클래스에서는 필드를 private으로 숨기고 메서드를 통해 접근하도록 만들어야 한다.
package-private 클래스는 예외가 될 수 있다
책에서는 package-private 클래스나 private 중첩 클래스라면 데이터 필드를 직접 노출해도 큰 문제가 없을 수 있다고 설명한다.
class Point {
double x;
double y;
}
이 클래스는 같은 패키지 안에서만 사용할 수 있다.
따라서 변경 영향 범위도 해당 패키지 내부로 제한된다.
package-private 클래스는 통제 범위가 작다
package-private 클래스는 다음 특성을 가진다.
- 외부 패키지에서 사용할 수 없다.
- 사용 범위를 비교적 쉽게 파악할 수 있다.
- 변경 영향을 패키지 내부로 제한할 수 있다.
- 구현 세부사항으로 관리하기 쉽다.
이런 이유로 책에서는 package-private 클래스의 필드 노출을 어느 정도 허용한다.
그래도 접근자 메서드가 더 안전할 수 있다
package-private 클래스라고 해도 필드 직접 접근에는 여전히 단점이 있다.
- 값 검증을 추가하기 어렵다.
- 내부 표현을 변경하기 어렵다.
- 부가 작업을 넣기 어렵다.
- 필드 변경 시 사용 코드를 한꺼번에 수정해야 한다.
따라서 package-private 클래스라도 변경 가능성이 있거나 의미 있는 상태를 가진다면 접근자 메서드를 사용하는 편이 더 안전할 수 있다.
단순한 데이터 전달 객체나 아주 제한된 내부 구현이라면 필드를 직접 사용할 수도 있지만, 습관적으로 모든 필드를 노출하는 것은 피하는 것이 좋다.
public final 필드는 완전한 해결책이 아니다
다음과 같이 필드를 public final로 공개할 수도 있다.
public class Time {
public final int hour;
public final int minute;
public Time(int hour, int minute) {
this.hour = hour;
this.minute = minute;
}
}
final이므로 생성 이후 다른 값을 대입할 수 없다.
time.hour = 10; // 컴파일 오류
가변 public 필드보다는 안전하다.
public final 필드가 줄여주는 문제
public final 필드는 다음 문제를 줄여준다.
- 외부에서 필드를 다시 대입할 수 없다.
- 객체 상태가 생성 이후 변경되지 않는다.
- 여러 코드에서 값이 임의로 변경되는 문제를 방지한다.
하지만 캡슐화 문제를 완전히 해결하지는 못한다.
public final 필드에도 남는 문제
필드 자체가 공개 API라는 사실은 변하지 않는다.
time.hour
를 사용하는 외부 코드가 존재하면 다음 변경이 어려워진다.
- 필드 이름 변경
- 필드 타입 변경
- 내부 표현 변경
- 값 반환 시 추가 계산
- 접근 시 검증이나 로깅 추가
즉 불변성은 높아지지만 정보 은닉의 장점은 여전히 얻지 못한다.
final은 객체 내부까지 불변으로 만들지 않는다
필드가 가변 객체를 참조한다면 final만으로는 충분하지 않다.
public class Group {
public final List<String> members =
new ArrayList<>();
}
members 필드가 다른 리스트를 참조하도록 바꿀 수는 없다.
하지만 리스트의 내용은 변경할 수 있다.
group.members.add("new member");
group.members.clear();
따라서 public final로 공개하려면 참조 대상도 불변이어야 한다.
가변 객체를 전달할 때는 복사본을 고려해야 한다
가변 객체를 다른 메서드나 클래스에 그대로 전달하면 호출된 코드가 객체를 변경할 수 있다.
public void doSomething(Point point) {
point.x = 100;
point.y = 200;
}
호출자는 메서드 실행 이후 값이 바뀌었는지 쉽게 알기 어렵다.
Point point = new Point();
point.x = 10;
point.y = 20;
doSomething(point);
System.out.println(point.x); // 100
방어적 복사
외부 변경으로부터 보호해야 한다면 복사본을 만들어 사용할 수 있다.
public void doSomething(Point point) {
Point localPoint = new Point();
localPoint.x = point.x;
localPoint.y = point.y;
localPoint.x = 100;
}
원본 객체에는 영향을 주지 않는다.
다만 매번 복사해야 한다는 사실 자체가 가변 객체 관리 비용을 보여준다.
가능하다면 처음부터 객체를 불변으로 설계하는 편이 더 안전하다.
가변 public 필드는 스레드 안전성도 해친다
public 가변 필드는 여러 스레드가 동시에 접근할 수 있다.
public class Counter {
public int count;
}
여러 스레드가 다음 코드를 실행하면
counter.count++;
레이스 컨디션이 발생할 수 있다.
객체가 상태 변경을 통제하지 못하기 때문에 동기화나 원자적 연산을 적용하기도 어렵다.
메서드를 통해 상태를 변경하도록 만들면 내부에서 동기화 정책을 적용할 수 있다.
public synchronized void increment() {
count++;
}
public 필드를 직접 공개하면 이런 변경도 클라이언트 코드 수정 없이 적용하기 어렵다.
접근자 메서드 사용 시 주의할 점
접근자 메서드는 public 필드보다 낫지만, 무조건 getter와 setter를 생성하는 방식은 피해야 한다.
모든 필드에 setter를 만들지 않는다
setter가 필요하지 않다면 제공하지 않는다.
가변 객체를 그대로 반환하지 않는다
다음 코드는 내부 컬렉션을 그대로 노출한다.
public List<String> getMembers() {
return members;
}
외부에서 내부 상태를 변경할 수 있다.
group.getMembers().clear();
불변 뷰나 복사본을 반환하는 것이 더 안전하다.
public List<String> getMembers() {
return List.copyOf(members);
}
의미 있는 행동을 제공한다
단순 setter보다 객체의 의도를 표현하는 메서드를 제공한다.
order.changeShippingAddress(address);
account.withdraw(amount);
member.updateProfile(profile);
이렇게 하면 비즈니스 규칙과 상태 변경을 객체 내부에 모을 수 있다.
public 필드와 접근자 메서드 비교
| 구분 | public 필드 | 접근자 메서드 |
|---|---|---|
| 내부 구현 은닉 | 어려움 | 가능 |
| 값 검증 | 불가능 | 가능 |
| 부가 작업 | 불가능 | 가능 |
| 내부 표현 변경 | 어려움 | 상대적으로 쉬움 |
| 불변 객체 설계 | 어려움 | 가능 |
| 변경 추적 | 어려움 | 상대적으로 쉬움 |
| API 확장성 | 낮음 | 높음 |
| 스레드 안전 정책 추가 | 어려움 | 가능 |
실무에서의 권장 방향
public 클래스를 설계할 때는 다음 원칙을 적용하는 것이 좋다.
- 필드는 기본적으로
private으로 선언한다. - 외부에 필요한 동작만 public 메서드로 공개한다.
- 단순 setter보다 의미 있는 상태 변경 메서드를 제공한다.
- 변경이 필요하지 않다면 setter를 제공하지 않는다.
- 가능하면 불변 객체로 만든다.
- 컬렉션이나 배열은 방어적 복사 또는 불변 뷰로 반환한다.
- package-private 클래스라도 변경 가능성이 크다면 필드를 직접 노출하지 않는다.
- public final 필드도 공개 API라는 사실을 기억한다.
핵심 정리
- public 클래스의 필드는 직접 공개하지 않는다.
- 필드는 private으로 감추고 접근자 메서드를 제공한다.
- 메서드를 통하면 검증, 로깅, 계산 등의 부가 작업을 추가할 수 있다.
- 내부 필드 이름이나 표현을 변경해도 공개 메서드를 유지할 수 있다.
- setter는 반드시 제공해야 하는 것이 아니다.
- 단순 getter와 setter보다 의미 있는 행동을 제공하는 것이 좋다.
- package-private 클래스는 필드를 노출해도 영향 범위가 제한적이지만, 접근자 메서드가 더 안전할 수 있다.
- public final 필드는 가변 필드보다 안전하지만 캡슐화를 제공하지는 못한다.
- final은 참조만 고정할 뿐 참조 대상까지 불변으로 만들지는 않는다.
- 가변 객체는 복사본이나 불변 뷰를 통해 노출해야 한다.
한 줄 정리
public 클래스는 내부 상태를 필드로 직접 노출하지 말고, 메서드를 통해 상태를 통제해야 변경에 유연하고 안전한 설계를 만들 수 있다.