티스토리 뷰

(Effective Java) 규칙23. 태그달린 클래스보다는 클래스 계층구조를 활용하라


태그 달린 클래스


  • 두 가지 이상의 의미를 표현할 수 있으며, 그중 현재를 표현하는 의미를 태그 값으로 알려주는 클래스를 본 적이 있을 것이다.

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

 

태그 달린 클래스의 단점


열거 타입 선언, 태그 필드, switch문 등 쓸데 없는 코드가 많음

  • 여러 구현이 한 클래스에 혼합돼 있어서 가독성도 나쁨
  • 다른 의미를 위한 코드도 언제나 함께 하니 메모리도 많이 사용함

필드들을 final로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화해야 함

  • 생성자가 태그 필드를 설정하고 해당 의미에 쓰이는 데이터 필드들을 초기화하는 데 컴파일러가 도와줄 수 있는 건 별로 없음
  • 엉뚱한 필드를 초기화해도 런타임에야 문제가 드러남

또 다른 의미를 추가하려면 코드를 수정해야 함

  • 새로운 의미를 추가할 때마다 모든 switch문을 찾아 새 의미를 처리하는 코드를 추가해야 하는데, 하나라도 빠뜨리면 런타임에 문제가 불거져 나올 것임

인스턴스의 타입만으로는 현재 나타내는 의미를 알 길이 전혀 없음

  • 태그 달린 클래스는 장황하고, 오류를 내기 쉽고 비효율 적임

 

클래스 계층 구조


  • 다행히 자바와 같은 객체 지향 언어는 타입 하나로 다양한 의미의 객체를 표현하는 훨씬 나은 수단을 제공한다.
    • 클래스 계층구조를 활용하는 서브타이핑(subtyping)
  • 태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일 뿐이다.

 

태그 달린 클래스를 클래스 계층구조로 바꾸는 방법


  1. 계층구조의 루트(root) 가 될 추상 클래스를 정의하고, 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상메서드로 선언한다.
    • 위 예제에서 Figure 클래스의 area가 이러한 메서드에 해당함
  2. 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가한다.
    • 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들도 전부 루트 클래스로 올림
    • Figure 클래스에서는 태그 값에 상관없는 메서드가 하나도 없고, 하위 클래스에서 사용하는 공통 데이터 필드도 없음
      • 그 결과 루트 클래스에는 추상 메서드인 area 하나만 남게 됨
  3. 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의한다.
    • 위 예제에서는 Figure를 확장한 원(Circle)클래스와 사각형(Rectangle)클래스를 만들면 됨
      • 각 하위 클래스에는 각자의 의미에 해당하는 데이터 필드들을 넣음
  4. 루트 클래스가 정의한 추상 메서드를 각자의 의미에 맞게 구현한다.

 

클래스 계층구조로의 수정


  • Figure 클래스를 클래스 계층구조 방식으로 구현한 코드
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; }
}
  • 기존의 쓸데없는 코드들을 전부 날리면서 각 의미를 독립된 클래스에 담아 관련 없던 데이터 필드를 모두 삭제 했다.

    • 살아 남은 필드들은 모두 final
  • 각 클래스의 생성자가 모든 필드를 남김없이 초기화하고 추상 메서드를 모두 구현했는지 컴파일러가 확인해준다.

    • 실수로 빼먹은 case문 때문에 런타임 오류가 발생한 일도 없음
  • 타입 사이의 자연스러운 계층 관계를 반영할 수 있어서 유연성은 물론 컴파일타임 타입 검사 능력을 높여준다는 장점도 있다.

    • 만약 정사각형도 지원하도록 수정한다고 생각해보자

      //클래스 계층구조에서라면 다음과 같이 정사각형이 사각형의 특별한 형태임을 아주 간단하게 반영이 가능함
      class Square extends Rectangle {
        Square(double side) {
          super(side, side);
        }
      }

 

결론


  • 태그 달린 클래스를 써야 하는 상황은 거의 없다.

  • 새로운 클래스를 작성하는 데 태그 필드가 등장한다면 태그를 없애고 계층구조로 대체하는 방법을 생각해보자.

    • 기존 클래스가 태그 필드를 사용하고 있다면 리팩터링을 고민해볼 것

 


끝으로

이 글이 도움이 되었다면, 하단의 Google 광고 👎👎👎 한번씩 클릭 부탁 드립니다. 🙏🙏🙏

광고 클릭은 많은 힘이 됩니다! 

반응형
댓글