객체 지향 방법론(Object Oriented Programming : OOP)의 기본 개념은
공통적으로 사용되는 부분을 미리 추상화 해놓는 것이다.
그것을 필요에 따라 구체화시켜 사용하는 것은 코드의 재사용성을 높이며,
그것은 곧 OOP의 지향점이 된다.
자바에서는 추상화의 기본 단위로 클래스(Class)와 인터페이스(Interface) 를 정의하고 있고, 이는 곧 자바의 심장과도 같다.
"4장 - 클래스와 인터페이스" 에서는,
쓰기 편하고, 견고하며, 유연한 클래스와 인터페이스를 만드는 방법에 대해 내용을 서술한다.
시작과 함께 먼저 아래의 예시를 봐보자.
class Point { public double x; public double y; }
간혹가다 위처럼 인스턴스 필드만을 모아놓는 경우가 생긴다.
(작가는 "퇴보한" 이라는 극적인 워딩을 사용했다....)
로직의 유무를 말하는게 아니다.
일반적으로 엔티티를 정의할 때도 저렇게 필드만 선언되니까 말이다.
문제는 바로 저 필드만 딱 있을때 발생한다.
모두 public으로 선언되어 있기 때문에,
다른 컴포넌트에서 해당 클래스 데이터 필드에 직접 접근이 가능하다.
그 말은, 캡슐화(Encapsulation)가 전혀 안되기 때문에 그에 따른 장점도 얻을 수 없다는 말이다.
사실 장점을 얻을 수 없는 차원이라기 보다, 많은 단점과 위험성이 존재한다고 하는게 맞는것 같다.
<단점>
- 캡술화의 이점이 없다.
- API를 수정하지 않고는 내부 표현을 바꿀 수 없다.
- 불변식을 보장할 수 없다.
- 외부에서 필드에 접근할 때 부수 작업을 수행할 수도 없다. (Thread가 불안전하다, Race Condition)
그래서 위 코드는 아래와 같이 바꾸는게 정석이다.
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; } }
모든 데이터 필드의 접근 가능성을 private으로 바꾼다.
그런 이후에 데이터 필드에 접근할 수 있는 별도의 메서드를 생성한다.
자바에서는 이러한 것을 접근자(Getter, Setter)라고 칭한다.
위와 같이 패키지 바깥에서 접근할 수 있는 클래스에 접근자를 제공하는 방법을 취하면,
클래스 내부 표현 방식을 언제든 유현하게 바꿀수 있게 된다.
<Comment.>
- 여기서 "내부 표현 방식"이 유연해진다 라는 의미는 "변수명"이라고 바꿔보면 조금 쉽게 이해가 가능했다.
- 접근자를 이용하지 않고 필드를 직접 노출하게 되면, 클라이언트는 해당 변수명을 그대로 이용하게 된다.
- 만약 내가 이 변수명을 바꾸려고 하면? 그것을 이용하는 모든 클라이언트가 다 바꿔야 하기 때문에 사실상 바꾸는게 불가능해진다.
- 그렇지만 클라이언트가 접근자 (Getter, Setter)를 이용하고 있다면 내부적으로 얼마든지 변수명을 바꿀수 있다는 것이다.
그렇다면 클래스가 public이 아닌, private-package나 private 중첩 클래스라면 어떨까?
이 경우엔 데이터 필드를 노출하던 안하던 별로 상관이 없다.
(어차피 외부에서 접근도, 수정도 못한다.)
이때는 그냥 본분을 다해 그 클래스가 구현하고자 하는 기능만 잘 구현해내면 된다.
실제로 이것은 클래스 선언 면에서나, 클라이언트 코드 면에서나 접근자보다 훨씬 깔끔해진다.
(사실 클라이언트 코드에서야 접근자를 사용하는 것보다 직접 필드를 사용하는게 더 깔끔하다.)
<Comment.>
- 아마도 이런 의문이 드는 사람이 있을 것이다.
- "이것도 그럼 변수명 바꾸면 클라이언트도 다 바꿔야 하는데?"
- public 클래스의 경우 공개 API이기 때문에 전세계의 모든 사람이 클라이언트지만, package-private은 그 패키지 내에서만 클라이언트가 존재한다.
- 선자는 바꾸는게 불가능하고, 후자는 바꾸는게 어렵지 않다. 아예 가능성 자체가 다르다.
중첩된 private 클래스의 경우는 더 쉽다.
어차피 그 클래스를 사용할 수 있는건 바로 바깥 클래스 하나이기 때문에 클라이언트가 될 수 있는 대상이 훨씬 좁다.
내부 표현 수정이 더욱 용이하다.
불변이라면야 직접 노출하는 거에 대해서 단점이 줄어들긴 한다.
불변식을 만족할 수는 있게 되지만 여전히 다른 단점은 존재하지 않는가.
내부 표현 방식을 바꿀수 없고, 필드를 읽을 때 부수 작업도 수행할 수 없다.
다음의 예를 보자.
public final class Time { private static final int HOURS_PER_DAY = 24; private static final int MINUTES_PER_HOUR = 60; public final int hour; public final int minute; public Time(int hour, int minute) { if (hour < 0 || hour >= HOURS_PER_DAY) throw new IllegalArgumentException("Hour: " + hour); if (minute < 0 || minute >= MINUTES_PER_HOUR) throw new IllegalArgumentException("Min: " + minute); this.hour = hour; this.minute = minute; } }
위는 입력으로 들어오는 시간이 유효한 시간임을 보장하는 클래스이다.
필자의 코멘트로 글을 마무리 한다.
Item16 정리
- public 클래스는 절대 가변 필드를 직접 노출해서는 안된다.
- 불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없다.
- package-private이나 중첩 private 클래스는 노출하는게 나을 때도 있다.