본문 바로가기
개발 공부/Java

이펙티브 자바 아이템 23 - 태그 달린 클래스보다는 클래스 계층 구조를 활용하라 - 핵심 정리

by 개발인생 2022. 11. 30.
반응형

아이템 23 - 태그 달린 클래스보다는 클래스 계층 구조를 활용하라 - 핵심 정리

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

태그달린 클래스란 그 클래스가 가지고 있는 필드 중 일부가 그 클래스의 구체적인 타입을 나타내는 것을 말한다.

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);
        }
    }
}

위의 클래스는 사각형 아니면 원을 나타내는 클래스이다.

Shape 이라는 필드가 태그이다.

사각형에 필요한 기능과 필드가 있고, 원에 필요한 기능과 필드가 있을텐데 이런 것들을 모두 한 곳에 모아두게 된다.

RECTANGLE 관점에서 볼 때는 radius 는 불필요한 필드이다.

각각의 관점에 따라 쓸데없는 코드가 한 곳에 모이게 되고 가독성이 떨어지게 된다.

코드를 디버깅하고 개선해 나가기 힘들어진다.

메모리 관점에서도 인스턴스를 만들 때 불필요한 필드에 대한 메모리를 사용하게 되기 때문에 조금 더 많은 메모리를 사용하게된다.

만약 필드를 final 로 선언하려면 필요없는 필드들까지 초기화를 해주어야한다.

인스턴스의 타입만으로 사각형을 나타내는지 원을 나타내는지 알 수가 없다.

이런 문제들을 해결하기 좋은 방법으로는 상속 을 사용하는 방법이 있다.


abstract class Figure {
    abstract double area();
}

Figure 클래스에서는 모든 Figure 관련 클래스에서 공통으로 사용하는 메서드를 따로 빼낸다.

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; }
}

각각의 하위 클래스를 만든다.

Rectangle 클래스에는 사각형에 관련된 필드만 있으므로 final 로 만들기 수월하다.

사각형과 관련된 로직만 남길 수 있게된다.

이전 Figure 클래스에서 처럼 if 문이나 switch 문을 통해 각각의 도형을 계산할 필요가 없게된다.

만약 if 문이나 switch 문이 많다면 해당 클래스에 태그 역활을 하는 필드가 있지는 않은지

혹은 해당 클래스에 너무 많은 것들을 표현하려한 것은 아닌지 의심해봐야한다.

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

상속을 통해 다른 도형을 추가하기에도 편리해진다.

만약 하나의 클래스에서 Square 를 추가하려면

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);
      }
   }
}

위처럼 하나의 도형을 추가하기 위해서 많은 작업 필요해진다.

이렇게 도형을 추가할 때마다 클래스는 점점 더 복잡해지게 된다.

만약 태그를 사용하는 클래스가 있다면 상속 을 통해 풀어나가도록 하자.

주의할 점은 상속 을 사용하기 때문에 문서화에 좀 더 신경을 써야한다는 점이다.

반응형

댓글