본문 바로가기
개발관련 서적 정리/Effective Java

아이템 23. 태그 달린 클래스 보다는 클래스 계층 구조를 활용하라

by Backchus 2023. 2. 5.

여기서 태그달린 클래스라는 건 클래스가 가지고 있는 필드 중에 일부가 그 클래스의 구체적인 타입을 나타내는 경우가 있습니다. 예를 들어 아래와 같은 Figure클래스가 있습니다.

package me.whiteship.chapter04.item23.taggedclass;

// 코드 23-1 태그 달린 클래스 - 클래스 계층구조보다 훨씬 나쁘다! (142-143쪽)
class Figure {
    enum Shape { RECTANGLE, CIRCLE, SQUARE };

    // 태그 필드 - 현재 모양을 나타낸다.
    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) {
        if (this.length == this.width) {
            shape = Shape.SQUARE;
        } else {
            shape = Shape.RECTANGLE;
        }

        this.length = length;
        this.width = width;
    }

    double area() {
        switch(shape) {
            case RECTANGLE, SQUARE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError(shape);
        }
    }
}

Figure 클래스를 보면 Shape을 enum타입으로 RECTANGLE, CIRCLE, SQUARE중 한가지를 나타낼 수 있도록 구현이 되어 있습니다. 이렇게 Shape이라는 태그가 달려있는 클래스입니다. 이러한 클래스에는 여러가지 단점이 존재합니다. 예를 들어 RECTANGLE에 필요한 필드들이 있고, CIRCLE에 필요한 필드들이나 메소드가 있을텐데 그런것들이 모두 한 클래스에 정의되어 있습니다. 그래서 RECTANGLE관점에서는 CIRCLE과 관련있는 radius필드가 필요없고 CIRCLE관점에서는 RECTANGLE과 관련있는 length, width필드가 필요없습니다. 이렇게 쓸때 없는코드가 한 곳에 모이게 됩니다. 또한 Shape마다 필요한 기능들이 모여있어 가독성도 나빠집니다. 메모리 관점에서 보면 인스턴스 생성시 Figure에 모든 필드가 다 만들어지면서 상관없는 메모리까지 차지하게됩니다. 만약 필드를 final로 선언하고 싶다면 필요없는 필드까지 초기화를 시켜줘야 합니다. 그리고 Figure의 인스턴스를 생성해서 쓸 때 이 Figure가 RECTANGLE을 나타내는지 CIRCLE인지 SQUARE인지 단순하게 인스턴스의 타입만 보고 알 수 없습니다. 그래서 이런 문제점을 해결하기 위해서 상속을 사용하면 깔끔하게 해결할 수 있습니다.

package me.whiteship.chapter04.item23.hierarchy;

// 코드 23-2 태그 달린 클래스를 클래스 계층구조로 변환 (144쪽)
abstract class Figure {
    abstract double area();
}

위와 같이 Figure 추상클래스에서는 태그값과 상관없는 모든 Figure에서 공통적으로 쓰이는 추상메서드를 선언하고 각각의 하위 클래스를 생성합니다.

package me.whiteship.chapter04.item23.hierarchy;

// 코드 23-2 태그 달린 클래스를 클래스 계층구조로 변환 (144쪽)
class Rectangle extends Figure {
    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width  = width;
    }
    @Override double area() { return length * width; }
}
package me.whiteship.chapter04.item23.hierarchy;

// 코드 23-2 태그 달린 클래스를 클래스 계층구조로 변환 (144쪽)
class Circle extends Figure {
    final double radius;

    Circle(double radius) { this.radius = radius; }

    @Override double area() { return Math.PI * (radius * radius); }
}
package me.whiteship.chapter04.item23.hierarchy;

// 태그 달린 클래스를 클래스 계층구조로 변환 (145쪽)
class Square extends Rectangle {
    Square(double side) {
        super(side, side);
    }
}

이렇게 각각의 모양에 필요한 필드들을 하위클래스에 정의해서 final로 초기값을 설정해서 사용할 수 있습니다. 따라서 처음에 선언했던 태그값이 포함되어있는 Figure클래스의 area메서드는 swich문을 통해서 각 태그마다 area를 계산하도록 했지만 상속구조로 바뀐 후 이런 switch문을 제거할 수 있게 되었습니다. 클래스에서 분기문이 많다면 이것을 상속으로 변환해야하는지 한번 고려해 봐야합니다.