전체적인 스터디 내용은 JunHyeok96/effective-java에서 확인 가능!
🚫 흔하게 실수하는 클래스
“이따금 인스턴스 필드들을 모아놓는 일 외에는 아무 목적도 없는 퇴보한 클래스를 작성하려 할 때가 있다.”
class Point { public double x; public double y; }
초장부터 😮뜨끔! 하게 만드는 이야기다. 설마 누가 저런 클래스를 만들겠냐 생각했지만 실제로 우테코 프리코스 1주차 미션에서 많은 참여자 분들이 상수값을 묶어둔 public 클래스를 사용하는 것을 볼 수 있었다. 심지어 당시 나는 따라하려 했었다. 🤦♂️
- API의 필드를 수정하지 않고는 내부 표현을 바꿀 수 없다.
- getter/setter 메서드가 존재한다면 얼마든지 표현 변경이 가능하다.
- 불변식을 보장할 수 없다.
- 클라이언트가 직접 데이터를 변경할 수 있다.
- 외부에서 필드에 접근할 때 부수 작업을 수행할 수도 없다.
- 1차원적인 접근만 가능하고, 추가 로직을 삽입할 수 없다.
책에서는 해당 패턴을 가진 클래스의 문제점을 대표적으로 3가지 짚어주고 있다. 모두 캡슐화의 이점을 포기한 내용들이다.
👍 대표적인 해결방법: 클래스 캡슐화
“철저한 객체지향 프로그래머는 필드를 모두 private 로 바꾸고 접근자(getter) 메서드를 추가한다.”
class Point { private double x; private double y; public Point(double x, double y) { this.x = x; this.y = 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; } }
- getter/setter 메서드를 통해 언제든지 내부 표현을 바꿀 수 있다.
- 불변식이 보장된다.
- 클라이언트는 메서드를 통해서만 필드에 접근 가능하다.
- 외부에서 필드에 접근할 때 부수 작업을 수행 시킬 수 있다.
- getter/setter 메서드에 얼마든지 추가 가능하다.
위에서 이야기한 3가지 문제점을 모두 매꿔주는 캡슐화된 클래스다. 객체지향을 이해한다면, 객체지향의 장점을 이끌어내려면 아무리 단순한 데이터(필드)라도 이처럼 캡슐화를 진행하는 것이 좋다.
👍 또 다른 해결방법: private 중첩 클래스
“하지만 package-private 혹은 private 중첩 클래스라면 데이터 필드를 노출한다 해도 하등의 문제가 없다.”
public class TopPoint {
private static class Point {
public double x;
public double y;
}
public Point getPoint() {
Point point = new Point(); // TopPoint 외부에선
point.x = 3.5; // Point 클래스 내부 조작이
point.y = 4.5; // 불가능 하다.
return point;
}
}
위와 같이 private 클래스를 중첩시키면 TopPoint 클래스에서는 얼마든지 Point 클래스의 필드를 조작할 수 있지만, 외부 클래스에서는 Point 클래스의 필드에 직접 접근 할 수 없다.
package-private 클래스 역시 해당 클래스가 포함되는 패키지 내에서만 조작이 가능하고 패키지 외부에서는 접근이 불가능 하므로, 처음 제시된 3가지 문제점을 모두 매꿀 수 있다.
“클라이언트 코드가 이 클래스 내부 표현에 묶이기는 하나, 클라이언트도 어차피 이 클래스를 포함하는 패키지 안에서만 동작하는 코드일 뿐이다. 따라서 패키지 바깥 코드는 전혀 손대지 않고도 데이터 표현 방식을 바꿀 수 있다.”
즉, 클래스를 통해 표현하려는 추상 개념만 상세하고 올바르게 표현하면 클래스와 필드를 선언하는 입장에서나 클라이언트 입장에서나 훨씬 깔끔한 코드가 작성 가능하다는 것이다.
👎 public 클래스의 필드를 직접 노출시킨 사례
자바 플랫폼 라이브러리에도 public 클래스의 필드를 직접 노출하지 말라는 규칙을
어기는 사례가 종종 존재한다. 대표적으로 java.awt.package
패키지의
Point
와 Dimension
클래스가 있다.
java.awt.Component 클래스 내부
/**
* getSize() 메서드를 호출 할 때마다
* Dimension 인스턴스를 생성하고 있다.
*/
public Dimension getSize() {
return size();
}
@Deprecated
public Dimension size() {
return new Dimension(width, height);
}
public class Dimension extends ... {
public int width;
public int height;
Dimesion
클래스의 필드는 가변으로 설계되어 getSize를 호출하는 모든 곳에서
방어적 복사를 위해 인스턴스를 새로 생성해야만 한다. 단순히 작은 객체로 몇 개 생성하는 것은
요즘 VM, HW 사양으로 큰 문제가 없겠지만 수 만, 수 억개가 된다면 이야기가 달라진다.
final 키워드를 추가한 예시
public final class Time {
public final int hour;
public final int minute;
공개된 필드(public)에 final 키워드를 추가하여 불변을 보장하는 것은 당장 불변식은 보장할 수 있지만 API를 변경하지 않고는 표현 방식을 바꿀 수 없고, 필드를 읽을 때 부수 작업을 수행할 수 없다는 2가지 문제점을 여전히 매꿀 수 없다. (게다가 해당 필드가 배열이라면 이야기가 다르다. 배열 원소의 불변식을 보장하지 못한다.)
핵심 정리
public 클래스는 절대 가변 필드를 직접 노출해서는 안 된다.
불변 필드여도 노출해서 좋을게 없다.
단, package-private 클래스나 private 중첩 클래스를 통해서
필드를 노출하는 것이 도움이 될 때도 존재한다.
댓글남기기