Effective Java를 읽고 정리한 정리본입니다.

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

🫧 태그 달린 클래스

: 두 가지 이상의 의미를 표현할 수 있으며 그 중 현재 표현하는 의미를 태그 값으로 알려주는 클래스.

예시 코드는 다음과 같다.

✨ 태그 달린 클래스 예시 코드

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

🫧 태그 달린 클래스의 단점

예시에서 볼 수 있다시피, 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다.

  1. 쓸 데 없는 코드 증가
  2. 가독성이 나쁨
    -> 여러 구현이 하나의 클래스에 나타나 있으므로

  3. 필요없는 메모리 사용 발생
    -> 한 의미를 표현하기 위해서는 다른 의미를 위한 코드 공간까지 사용해야 함. (쓰지 않는 필드 초기화 필요)

  4. 확장성 X
    -> 의미 추가 시 코드 수정 필요

  5. 오류 발생 확률 증가
    -> 새로운 의미를 추가할 때마다 switch문을 찾아 코드를 일일이 넣어주어야 함. 하나라도 빠뜨릴 시 런타임 문제 발생.

  6. 인스턴스 타입으로 의미 유추 X

따라서, 태그 달린 클래스보다는 클래스 계층 구조를 활용하는 서브 타이핑을 사용하자.

💡 왜 태그 달린 클래스에서는 컴파일러에서 문제가 잡히지 않고 런타임에 오류가 날까?

태그 달린 클래스의 예시를 보면 이해가 쉽다. 해당 예시는 글 위쪽에 태그 달린 클래스의 예시 코드 중 일부이다.

// 모양이 사각형일 때만 쓰는 필드
double length;
double width;

// 모양이 원일 때만 쓰는 필드
double radius;

이렇게 개발자가 작성해 둔 필드는 개발자 관점에서의 태그이다. 컴퓨터는 이를 이해할 수 없다. 가령 다음과 같은 코드로 작성한다면 런타임 오류가 발생할 것이다.

Shape shape = new Shape(10);
System.out.println(shape.height);

선언은 되었지만 초기하가 되지 않아 NullPointerException이 일어날 확률이 존재한다. 따라 태그 달린 클래스에서는 컴파일러로 이러한 오류를 찾아내기 쉽지 않다는 것이 문제점이다.

✨ cf) 서브 클래싱과 서브 타이핑

객체지향에서 중요한 기능인 서브클래싱과 서브타이핑에 대해 알아보자.

☁️ 서브클래싱

: 한 클래스가 다른 클래스의 기능을 상속받아 확장하는 방법

  • “is-a” 관계를 형성하며, 서브클래스는 슈퍼클래스의 모든 특성(필드, 메서드)를 상속 받는다.

ex) “Dog” 클래스가 “Animal” 클래스를 상속받는 경우

☁️ 서브타이핑

: 타입 시스템과 관련된 개념으로 인터페이스의 구현을 포함한다.

  • 리스코프 치환 원칙을 만족해야 한다 (하위 타입이 상위 타입의 계약을 만족해야 하며, 상위 타입을 대체할 수 있어야 한다.)

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

  1. 계층구조의 루트가 될 추상 클래스 정의
  2. 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언
    ex) 해당 예에서 Figure의 area

  3. 태그 값에 상관 없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가
  4. 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들을 루트 클래스에 추가
  5. 루트 클래스를 확장한 구체 클래스르 의미별로 하나씩 정의
  6. 각 하위 클래스에 각자의 의미에 해당하는 데이터 필드 추가 (final)
  7. 루트 클래스가 정의한 추상 메서드를 각자의 의미에 맞게 구현

✨ 클래스 계층 구조 예시 코드

해당 코드는 태그 달린 클래스 예시 코드클래스 계층 구조로 바꾼 예시 코드이다.

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

만약 여기서 정사각형도 지원한다고 하면?

다음과 같은 코드를 추가하면 된다.

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

이렇듯, 태그 달린 클래스를 계층 구조 클래스로 전환한다면 태그 달린 클래스의 단점을 모두 상쇄시킬 수 있다.

만일 기존 클래스가 태그 필드를 사용하고 있다면 계층구조로 리팩터링하는 것을 고민해보자.

🫧 참고 자료

23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라 ( 클래스 계층구조 )_간펴니 tistory 🫧 서브클래싱과 서브타이핑이란_티스토리 | 재미있는 개발 이야기