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

    핵심 정리

    • 태그 달린 클래스의 단점
      • 쓸데없는 코드가 많다.
      • 가독성이 나쁘다.
      • 메모리도 많이 사용한다.
      • 필드를 final로 선언하려면 불필요한 필드까지 초기화 해야 한다.
      • 인스턴스 타입만으로는 현재 나타내는 의미를 알 길이 없다.
    • 클래스 계층 구조(상속)로 바꾸면 모든 단점을 해결할 수 있다. 

    태그가 달린 클래스는?

    태그 달린 클래스는 클래스가 가진 필드 중 일부가 클래스의 구체적인 타입을 나타내고, 이 타입에 따라 클래스의 전체 동작이 바뀌는 클래스다. 예시로는 Figure 클래스를 들 수 있다. 

    // 코드 23-1 태그 달린 클래스 - 클래스 계층구조(상속)보다 훨씬 나쁘다! (142-143쪽)
    public class Figure {
    
        enum Shape {RECTANGLE, CIRCLE}
    
        // 태그 필드 - 현재 모양을 나타낸다.
        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);
            }
        }
    
    
        public static void main(String[] args) {
            // 이 Figure는 원? 사각형?
            Figure figure = new Figure(1,2);
        }
    
    
    }

    위 태그 달린 클래스를 살펴보자.

    • Figure 클래스가 존재한다.
    • 내부에 Shape가 존재한다. Shape는 ENUM이고, 이 값에 따라 Figure 인스턴스가 직사각형 / 원인지를 나타낸다

    많은 단점이 존재하는데 아래에서 하나씩 설명해보고자 한다. 

     

    쓸데없는 코드가 많고, 가독성이 떨어짐.

    태그 달린 클래스라면 한 곳에 모이면 어색한 코드들이 모이게 된다. 이 어색한 코드들은 각 태그 관점에서 바라봤을 때 쓸모없는 코드가 된다. 또한 이런 코드가 많아지면 코드의 가독성이 떨어지게 된다. 아래를 살펴보자.

    • legnth, width는 원일 때는 사용되지 않는 필드임. → 원과 같이 있기 어색함. 
    • radius는 사각형일 때 사용되지 않는 필드임. → 사각형과 함꼐 있기 어색함. 
    • area() 메서드 → 어떤 종류의 태그냐에 따라 넓이 구하는 공식이 다름. → Switch 문으로 복잡해짐. 
    
        // 사각형일 때만 쓰인다
        double length;
        double width;
    
        // 원일 때만 쓰인다.
        double radius;
    
        double area() {
            switch (shape) {
                
                // 인스턴스마다 다르게 동작하는 코드
                case RECTANGLE:
                    return length * width;
                case CIRCLE:
                    return Math.PI * (radius * radius);
                default:
                    throw new AssertionError(shape);
            }
        }

    이처럼 같은 곳에 있으면 어색한 코드들이 많이 모여서 코드를 읽기 어렵게 만든다. 이런 코드들은 클래스를 분리하는게 더 좋은 방향이다.

     

    메모리를 많이 사용함.

    인스턴스 종류별로 서로 다른 필드를 사용하는데 그 필드가 한 클래스에 모여있게 된다. 클래스에 필드가 존재하는 것만으로 메모리가 사용된다. 만약 그 필드들이 final로 선언되어있다면 인스턴스가 생성될 때 항상 초기화 되어야 한다. 예를 들어 원을 생성하는데 필요없는 length, width 값을 모두 가져야한다. 이처럼 메모리 낭비도 문제가 된다. 

        // 사각형일 때만 쓰인다
        double length;
        double width;
    
        // 원일 때만 쓰인다.
        double radius;

     

     

    타입만으로 어떤 종류인지 알 수 없음. 

    클래스 타입만으로 어떤 종류인지 알 수 없기 때문에 의미가 명확하지 않게 된다. 이 클래스의 동작은 Figure라는 타입이 아니라 내부 태그인 Shape가 어떤 값이냐에 따라 서로 다른 의미를 가지고 동작하기 때문이다. 

    public static void main(String[] args) {
        // 이 Figure는 원? 사각형?
        Figure figure = new Figure(1,2);
    }

     

    태그 달린 클래스를 해결하는 방법은?  → 상속

    태그 달린 클래스는 상속을 사용해서 깔끔하게 해결할 수 있다. 위에서는 Figure 클래스에 Rectangle, Circle을 태그로 가지며, 태그에 따라 동작했었다. 상속으로 표현한다면 Figure를 Abstract 클래스로 생성하고, Rectangle / Circle이 이 클래스를 구현하면 된다. 이 때 Figure 추상 클래스에서 공통 메서드인 area()를 제공해주면 된다. 

    // 부모 클래스
    abstract class Figure {
        abstract double area();
    }
    
    // 자식 클래스
    public class Rectangle extends Figure{
    
        final double length;
        final double width;
    
        public Rectangle(double length, double width) {
            this.length = length;
            this.width = width;
        }
    
        @Override
        public double area() {
            return length * width;
        }
    }

    상속으로 할 경우 다음이 해결되었다.

    • 태그에 따른 불필요한 필드 → 상속으로 분리되어, 각 클래스는 필요한 것만 가짐.
    • 공통 메서드의 Switch case → 상속으로 분리되어 분기문이 없어짐. 

    각 하위 클래스는 필요로 하는 필드만 가지고 있다. 따라서 모든 필드를 final로 처리하고, 클래스가 생성될 때 안전하게 초기화를 할 수도 있다. 또한 이전 Figure에 존재하던 쓸데없는 If / Switch 문이 없어지게 된다. 만약 If / Switch문이 많다면, 한번쯤 "내가 이 클래스에 너무 많은 책임을 넣은 것은 아닐까?"를 고민해봐야 한다. 


    정리 

    • 태그가 달린 클래스는 클래스 내부의 변수에 의해 클래스 전체의 동작이 매번 바뀌는 클래스를 의미한다.
    • 태그가 달린 클래스는 단점이 너무 많기 때문에 클래스 계층 구조(상속)으로 바꾸면 해결할 수 있다. 
    • 만약 클래스 내부에 너무 많은 Switch, If 분기문이 있다면 태그 달린 클래스가 있는지 확인하고 구조를 변경한다. 

     

     

     

     

    댓글

    Designed by JB FACTORY