ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 23: Prefer class hierarchies to tagged classes
    독서/Effective Java 2021. 12. 26. 15:14

    태그 달린 클래스보다는 클래스 계층구조를 활용하라

     

    클래스 계층 구조를 활용하지 않고, 상태를 활용해서 개발된 클래스들은 다음과 같다.

    class Figure {
        enum Shape { RECTANGLE, CIRCLE };
        // Tag field - 해당 Figure의 모양을 나타냄.
        final Shape shape;
        // 직사각형일 경우에만 사용
        double length;
        double width;
        // 원일 경우에만 사용
        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);
            }
        }
    }

    해당 클래스는 문제점이 너무나 명확하고, 위험 투성이인 클래스가 되버린다.

    먼저, 하나의 클래스 내에 Enum Type, 각 태그별 field, switch문 등 필요없는 코드가 많다.

    위의 문제점 때문에 하나의 메서드 안에 여러 구현이 들어가고, 생성자도 각 태그별로 Overload해서 사용하기 때문에 가독성이 떨어진다.

    또한 사용자가 Circle Object를 생성한다고 하면 필요없는 length, width도 초기화해야 하니 메모리도 많이 사용하게 된다. 만약 length, width가 final 필드라면? 무조건 생성자에서 초기화해줘야 한다.

    또한 태그가 추가될 때마다 switch 문 내에서 해당 케이스를 추가해야 하는데 만약 태그가 수백개라면 추가하는 것도 공수를 많이 잡아먹는 일이 된다.

    가장 큰 문제는 이러한 클래스들은 컴파일 단계에서는 문제가 밖으로 나오지 않지만, 런타임에서 문제가 발생한다는 것이다. 해당 클래스를 사용하는 클라이언트는 컴파일은 잘 되니 안심하고 프로그램을 돌릴 것이고, 해당 문제는 런타임에 굉장히 큰 문제로 발생할 수 있다.

    추가적으로, 이러한 클래스들은 해당 클래스의 인스턴스 타입이 무엇인지를 알 길이 없다.

    Object-Oriented Language들은 타입 하나로 다양한 의미의 Object를 표현하는 더 나은 수단을 제공하는데, 이는 Subtyping이라 불린다. 위의 문제 많던 태그 달린 클래스는 클래스 계층 구조로 변경 가능하다.

     

    클래스 계층 구조로의 변환

    먼저 root abstract class를 정의한다.

    태그 값에 따라 동작이 달라지는 메서드들의 경우 추상 메서드로 선언해 하위 클래스에서 이를 Overriding할 수 있도록 한다.

    공통적으로 사용되는 메서드라면, Root Class에서 일반 메서드로 정의하자.

    이후, 해당 태그에 해당하는 클래스들을 하위 클래스로 생성한다.

    각 하위 클래스에서는 상위 클래스에서 태그에 따른 분기, 혹은 생성자 오버로딩을 사용했던 부분들을 모두 개별 클래스의 메서드들로 생성자부터, overriding 메서드까지를 정의한다.

    아래는 위의 태그 달린 클래스를 Root Class - Subclasses로 나눈 모습이다.

    // Root Class
    abstract class Figure {
        abstract double area();
    }
    
    // 원 구현체
    class Circle extends Figure {
        final double radius;
        Circle(double radius) { this.radius = radius; }
        @Override double area() { return Math.PI * (radius * radius); }
    }
    // 직사각형 구현체
    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; }
    }

    해당 클래스들은 태그 달린 클래스들과는 달리 각 클래스의 의미가 명확해졌고, 위험할 수 있는 분기 행위를 하지 않으며, 변수들마저도 각 클래스에 맞게 넣어뒀기 때문에 각 클래스에 필요한 변수들마저도 명확해졌다.

    가장 좋은 점은 런타임에서 발생할 수 있는 오류들을 컴파일 단계에서 검증할 수 있도록 변경한 것이다. 이제 클라이언트 코드에서 해당 구현체들을 사용할 때 의미에 맞게 명확하게 사용하지 않으면 컴파일 단계에서부터 에러가 날 것이다.

    또한, 이러한 계층 구조를 가지게 되면서 추가 구현체가 필요할 경우 아래와 같이 의미에 따른 세분화가 가능해진다는 점도 존재한다.

    class Square extends Rectangle {
        Square(double side){
            super(side, side);
        }
    }

    정리

    태그 달린 클래스는 잊자. 태그 클래스를 달아야 한다면 먼저 계층 구조로 대체할 수 있는 지 확인하자. 기존 클래스가 태그 달린 클래스라면 계층구조로 리팩터링하는 것을 진지하게 고민하자.

    댓글

Designed by Tistory.