본문 바로가기

Reading/Effective Java

[Effective-Java] Item 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로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화해야 한다.(불필요한 코드 증가)
  • 엉뚱한 필드를 초기화해도 런타임에서야 문제가 발견된다.
  • 또 다른 모양이 추가된다면 area 계산을 위해 switch문에 새 의미 처리 코드를 추가해야하는데, 빠트리면 런타임 문제가 생긴다.
  • 인스턴스의 타입만으로 현재 나타내는 의미를 알 수 없다.

즉, 태그 달린 클래스는 장황하고 오류를 내기 쉽다. 클래스 계층구조를 어설프게 흉내냈을 뿐이다.

 

 

 

클래스 계층 구조로 변경


  1. 루트가 될 추상 클래스를 정의하고 태그 값에 따라 동작이 달라지는 메소드들을 루트 클래스의 추상 메소드로 선언
  2. 태그 값에 상관없이 동작이 일정한 메소드들을 루트 클래스에 일반 메소드로 추가
  3. 모든 하위 클래스에서 공통으로 사용하는 데이터 필드를 루트 클래스에 추가
  4. 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의
  5. 각 하위 클래스에 각자의 의미에 해당하는 데이터 필드 추가
  6. 루트 클래스가 정의한 추상 메소드를 각자의 의미에 맞게 구현

위 Figure 클래스는 태그 값에 상관없는 메소드, 모든 하위 클래스에서 사용하는 공통 데이터 필드가 없어서

루트 클래스에는 추상메소드 area만 남는다. 그럼 아래와 같이 변환할 수 있다.

 

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로 선언되어 초기화 여부와 추상메소드를 오버라이딩했는지를 컴파일 타임에 확인할 수도 있어졌다.

또한 타입 사이 자연스러운 계층 관계를 반영할 수 있어 유연성은 물론 컴파일타임 타입 검사 능력을 높여준다는 장점도 있다.

 

아래는 정사각형도 지원하기 위해 확장한 클래스다. 훨씬 간결하게 표현할 수 있음을 알 수 있다.

 

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

 

 

 

 

* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.