개요
'태그달린 클래스'라는 단어가 되게 생소하다. 이에 대한 의미를 이해하고, 이 클래스가 과연 어떤 단점을 갖길래 계층구조로 리팩토링 하라는지도 이해해보자.
태그달린 클래스란?
태그달린 클래스란 멤버 필드와 관련있다. 멤버 필드가 클래스의 유형을 나타내는 경우 해당 멤버 필드를 태그 필드라고 한다. 그리고 태그 필드를 갖는 클래스를 태그달린 클래스라고 한다.
태그 달린 클래스의 예
Figure 클래스는 shape 필드가 이 클래스의 유형을 나타낸다. 즉, 태그 필드이다. 유형마다 생성자가 따로 존재하며, area 메서드의 동작도 달라지는 것을 볼 수 있다. 이러한 클래스의 단점을 하나씩 짚어보자.
public class Figure {
enum Shape { RECTANGLE, CIRCLE };
// 태그 필드 - 현재 모양을 나타낸다.
final Shape shape;
// 다음 필드들은 모양이 사각형(RECTANGLE)일 때만 쓰인다.
double length;
double width;
// 다음 필드는 모양이 원(CIRCLE)일 때만 쓰인다.
double radius;
// 원 생성자
Figure(double radius){
shape = Shape.CIRCLE;
this.radius = radius;
}
Figure(double length, double width){
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area(){
switch (shape){
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError(shape);
}
}
}
태그달린 클래스의 단점
1. 쓸데없는 코드가 많다
열거 타입 선언, 태그 필드, switch 등 쓸데없는 코드가 많다. 이런 코드가 많은 이유는 이 클래스로 생성되는 인스턴스가 여러 유형(태그)을 가질 수 있기 때문이다.
2. 가독성 저하
한 클래스에 여러 유형에 대한 로직이 혼합돼어 있어 가독성이 저하된다.
3. 불필요한 초기화가 늘어난다.
멤버 필드의 불변성을 명시하기 위해 필드를 final로 선언한다. 위와 같은 코드는 필드를 final로 선언하려면 해당 태그에 쓰이지 않는 필드들도 생성자에서 초기화해야한다. 불필요한 초기화 코드가 늘어나는 것이다.
4. 태그 추가 시 클래스 전체를 수정해야한다.
또 다른 의미의 태그를 추가하려면 클래스 전체를 수정해야한다. 예를들어 삼각형이 추가될 경우 이에 대한 생성자가 추가되어야하고, area() 메서드도 수정이 되어야 한다.
5. 인스턴스 타입만으로는 어떤 태그인지 알 수 없다.
Figure 이라는 타입만으로 이 태그가 원인지, 사각형인지 알 수 없다.
태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다.
태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일 뿐이다. - 책에서
클래스 계층 구조로 변환하기
많은 단점을 갖는 태그달린 클래스를 계층 구조로 리팩토링 해보자. 리팩토링이 끝나면 기존 단점들을 얼마나 극복했는지도 확인해보자. 가장 먼저 계층구조의 루트가 될 추상 클래스를 정의해야한다.
1. 추상 메서드 선언
태그 값에 따라 동작이 달라지는 메서드를 추상 메서드로 선언해야한다. 그리고 각각의 하위 클래스에서 이 동작을 정의하도록 한다. area 메서드가 이에 해당한다.
2. 일반 메서드 선언
태그 값에 상관 없이 동작이 일정한 메서드들을 추상 클래스의 일반 메서드로 추가한다. Figure 클래스에는 이러한 메서드가 없기 때문에 넘어간다.
3. 멤버 필드 선언
모든 하위 클래스에서 공통으로 사용하는 필드들을 전부 추상 클래스의 필드에 추가한다. Figure 클래스에는 이러한 필드가 없기 때문에 넘어간다.
이를 토대로 추상 클래스를 작성하면 아래와 같다.
abstract class Figure {
abstract double area();
}
4. 구체 클래스 설계
이제 추상 클래스를 확장한 구체 클래스를 설계한다. Figure의 태그는 원(Circle)와 사각형(Rectangle)이 있으므로 이를 클래스로 분리한다. 이로써 계층구조로의 리팩토링이 끝났다. 이제 기존 단점들을 극복했는지 확인해보자.
public class Circle extends Figure{
private final double radius;
public Circle(double radius){
this.radius = radius;
}
@Override
double area() {
return Math.PI * (radius * radius);
}
}
public class Rectangle extends Figure{
private final double length;
private final double width;
public Rectangle(double length, double width){
this.length = length;
this.width = width;
}
@Override
double area() {
return length * width;
}
}
단점을 모두 날려버린 계층구조
1. 쓸데없는 코드가 많다
> 열거 타입 선언, 태그 필드, switch 등 쓸데없는 코드가 모두 없어졌다.
2. 가독성 저하
> 다른 유형의 로직이 혼합되어 있지 않다. 클래스는 자신에 대한 로직만을 관리하고 있다.
3. 불필요한 초기화가 늘어난다
> 사용하지 않아 불필요하게 초기화 해야했던 필드들이 모두 없어졌다.
4. 태그 추가 시 클래스 전체를 수정해야한다
> 이제 태그 추가가 아닌 클래스 추가로 변경되었다. 만약 삼각형이라는 클래스가 추가되어도 기존 클래스의 수정은 필요 없게 되었다.
5. 인스턴스 타입만으로는 어떤 태그인지 알 수 없다
> 타입만으로도 원인지, 사각형인지 알 수 있다.
태그 필드가 있다면 무조건 계층구조로?
그럼 태그 필드가 있는 클래스는 모두 계층구조로 바꿔야할까? 그건 아닌것 같다. 분리될 태그들이 상위 클래스와 is-a 관계일 때만 효용성을 가진다고 생각하기 때문이다. 물론 이런 관계를 갖지 않았다면 애초에 태그 필드를 도입하지 않았을 확률이 높지만, 코드에는 정답이 없고 사람이 짜는 것이니 기존 클래스의 구조를 분석한 후 계층 구조로 리팩토링 하는 것이 바람직하다고 생각한다.
정리
태그 달린 클래스를 써야 하는 상황은 거의 없다. 만약 태그 필드가 있다면 이를 없애고 계층 구조로 리팩터링 하는 것을 고려해야 한다.
'공부 > Effective Java' 카테고리의 다른 글
[Effective Java] Item 27. 비검사 경고를 제거하라 (1) | 2023.10.11 |
---|---|
[Effective Java] Raw Type / 로 타입(Raw Type)은 사용하지 말라 (0) | 2023.10.10 |
[Effective Java] Item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라. (0) | 2023.10.02 |
[Effective Java] Item 15. 클래스와 멤버의 접근 권한을 최소화하라 / 정보은닉 (0) | 2023.09.20 |
[Effective Java] Item 14. Comparable을 구현할지 고려하라 (0) | 2023.09.15 |