반응형

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

 

솔직히 이번 아이템은 공감하기가 매우 어려웠다.

태그 달린 클래스를 사용하지 말라고 권장하는데 사실 저런 혼종 클래스는 처음 봤다. ㅋㅋㅋ

태그 달린 클래스란 두 가지 이상의 의미를 표현할 수 있는 클래스로 내부에 enum 열거 타입이 존재한다는 특징이 있다. 두 가지 이상의 의미를 가지고 있기 때문에 생성자도 따로 필드들도 따로 구현되어야 한다.

 

클래스는 하나의 기능만 가져야 하는 SRP(Single Responsibility Principle) 원칙을 지켜야 한다.

솔직히 이번 아이템은 공감하기가 매우 어려웠다.

태그 달린 클래스를 사용하지 말라고 권장하는데 사실 저런 혼종 클래스는 처음 봤다. ㅋㅋㅋ 태그 달린 클래스란 두 가지 이상의 의미를 표현할 수 있는 클래스로 내부에 enum 열거 타입이 존재한다는 특징이 있다. 두 가지 이상의 의미를 가지고 있기 때문에 생성자도 그 갯수만큼 있어야 하고, 필드 변수들도 그 갯수만큼 있어야 한다. 또 타입별로 다른 로직이 필요하므로 각종 switch 구문과 조건문들이 난재되어 있다.

 

public static 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 newAssertionError(shape);
        }
    }
}

 

가독성도 나쁘고, 다른 의미를 가진 코드들도 언제나 메모리에 상주해야 하며, 해당 의미에 쓰이지 않는 데이터 필드들을 생성자 시점에서 초기화해야 한다. 심지어 엉뚱한 필드를 초기화해도 런타임에야 문제가 드러난다. 다른 의미를 추가할 때마다 switch 문들도 수정해야 한다. 마지막으로 인스턴스의 타입만으로 현재 나타내는 의미를 알 길이 전혀 없다.

 

아무래도 아래 원칙을 강조하고자 위와 같은 혼종 클래스는 사용하지 말라고 권장하는 것 같다.

 

클래스는 하나의 기능만 가져야 하는 SRP(Single Responsibility Principle) 원칙을 지켜야 한다. 하나의 클래스에서는 하나의 기능만 가져야 유지보수하기도 편하고, 객체지향적으로 잘 작성할 수 있다. 여러가지 의미를 서술하고 싶으면 클래스 계층구조를 활용하라는 것이다.

 

가장 먼저 계층구조의 루트가 될 추상 클래스를 정의하고, 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언한다. 그러고 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들을 전부 루트 클래스로 올린다.

위에 서술한 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; }
}

 

관련없는 데이터들이 사라지고, 더러운 switch 문까지 모두 사라졌다. 실수로 빼먹은 case 문 때문에 런타임 오류가 발생할 염려도 없다. 이런 식으로 설계하면, 루트 클래스의 코드를 건드리지 않고도, 다른 프로그래머들이 계층구조를 확장해서 함께 사용할 수 있다.

 

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

 

"태그 달린 클래스를 가급적 지양하고 차라리 계층구조의 클래스를 사용해보자."

반응형

+ Recent posts