DEV ℧ Developer Diary

[EffectiveJava] item23 - 태그달린 클래스보다는 클래스 계층구조를 활용하라

이번 내용은 OOP에 관련된 내용이다. 객체지향언어인 자바를 좀더 객체지향적으로 사용할 수 있도록 정리된 글이다.

태그 달린 클래스 (tagged class)란 하나의 클래스안에 두 가지 이상의 의미를 표현할 수 있도록 현재 표현하는 의미의 태그값을 통해 클래스가 구분되고 있는 클래스이다.

태그 달린 클래스 (tagged class)

태그클래스에 대한 예시를 들어보자.

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

Figure 클래스는 원과 사각형을 생성해주고, 면적을 구하는 메소드가 있는 클래스이다. shape라는 열거타입 필드를 이용해 구분하고 있다.

태그 달린 클래스는 단점이 한가득이다.

우선 열거타입 선언, 태그필드, switch 문등.. 쓸데없는 코드들이 추가가 된다. 또한 여러 구현이 한 클래스에 들어가 있기 때문에 코드의 가독성이 떨어진다. 필드들을 final로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화 해야 한다.

또한 새로운 의미가 추가될 때마다. 모든 switch 문을 찾아 새로운 의미를 처리하는 코드를 추가해야하는데, 하나라도 빠뜨리면 Exception이 발생할 것이다.

예제 코드에는 area만 있지만, 각도나, 가로의 길이 세로의 길이 등등 여러 기능을 하는 메소드가 추가된다면 모든 곳에 추가해주는 일은 정말 끔찍할 것이다.

한마디로, 태그 달린 클래스는 장황하고, 오류를 내기 쉽고 비효율 적이다.

클래스 계층구조

다행히 자바와 같은 객체 지향 언어는 타입 하나로 다양한 의미의 객체를 표현하는 훨씬 나은 수단을 제공한다. 바로 클래스 계층 구조를 활용하는 서브 타이핑(subtyping)이다.

그렇다면 태그 달린 클래스에서 계층구조로는 어떻게 변환할 수 있을까?

가장 먼저 계층 구조의 루트(root)가 될 추상 클래스를 정의하고, 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언한다.

그런 다음 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가한다. 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들도 전부 루트 클래스로 올린다.

다음으로, 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의한다.

설명보다는 예시를 보는 것이 더 이해가 쉬울것이므로 한번 위의 예시를 계층 구조로 변환하여 살펴보자.

abstract class Figure {
  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;
    }
}

Figure 클래스를 추상화하고, 공통으로 사용하던 area 메소드는 가장 root 클래스에 배치하여 구현체 클래스가 상속받도록 만들었다. 그리고 Circle과 Rectangle각각 구현체 클래스를 만들고, 해당 의미에서만 사용하는 필드들을 분리하여 각 의미에 맞는 클래스를 구현했다.

이렇게 클래스 계층 구조는 태그 달린 클래스의 단점을 모두 날려버린다. 간결하고 명확하며 switch와 같은 쓸데없는 코드도 모두 사라졌다.

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

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

만약 정사각형을 추가하고 싶다면 처음 작성한 태그 달린 클래스에서는 어떻게 구현해야할지 머리가 아팠을 것이다.

클래스 계층 구조를 이용하면 아래와 같이 간단하게 구현할 수있다.

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