Skip to content

Latest commit

 

History

History
345 lines (213 loc) · 10.7 KB

태그_달린_클래스보다는_클래스_계층구조를_활용하라(최준).md

File metadata and controls

345 lines (213 loc) · 10.7 KB

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


📌 목차

  1. 태그 달린 클래스
  2. 태그 달린 클래스 단점
  3. 클래스 계층구조
  4. 태그 달린 클래스 → 클래스 계층구조
  5. 클래스 계층 구조 장점

🔖 태그 달린 클래스

태그 달린 클래스란 두 가지 이상의 의미를 표현할 수 있는 클래스다.

그리고 여러 의미 중 현재 표현하는 의미를 태그 값으로 알려주는 클래스다.


📘 예시

아래는 도형을 의미하는 Figure 클래스인데 이 Figure 클래스는 원과 사각형을 표현할 수 있다.

원과 사각형이라는 의미를 태그로 표현하는 클래스이다.

따라서 Figure는 태그 달린 클래스다.


코드를 보자.

보면 shape라는 필드로 현재 모양을 나타내고 있다.

shape 필드가 태그 필드이다.

package effectivejava.chapter4.item23.taggedclass;

// 코드 23-1 태그 달린 클래스 - 클래스 계층구조보다 훨씬 나쁘다! (142-143쪽)
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);
        }
    }
}

👎 태그 달린 클래스 단점

태그 달린 클래스는 단점이 매우 많다.

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

  2. 여러 구현이 한 클래스에 혼합되어 있어 가독성이 나쁘다.

  3. 다른 의미를 위한 코드도 언제나 함께 하니 메모리 많이 사용한다.

  4. 해당 의미에 쓰이지 않는 필드까지 생성자에서 초기화 해야 한다.

  5. 생성자에서 엉뚱한 필드를 초기화 해도 런타임에 문제가 드러난다.

  6. 다른 의미 추가하려면 코드를 수정해야 한다.

  7. 인스턴스 타입만으로는 현재 나타내는 의미 알 수 없다.


4️⃣

4번 단점을 보면 필드를 final로 선언하려고 하면 생기는 문제점이다.

필드를 final로 선언하려고 하면 생성자에서 해당 필드들을 초기화 해야 한다.

그런데 쓰이지 않는 필드들도 생성자에서 같이 초기화 해야 한다는 단점이다.

쓰지 않는 필드를 초기화 하는 불필요한 코드가 생성자에 늘어나는 단점인 것이다.


5️⃣

5번 단점을 보면 생성자가 태그 필드를 설정하고 해당 의미에 쓰이는 데이터 필드들을 초기화 한다.

이때 현재 쓰일 필드말고 엉뚱한 필드를 초기화 해도 컴파일러가 도와줄 수 있는게 없다는 것이다.

그래서 런타임에 문제가 드러나는 것이다.

현재 사용하지 않는 필드 초기화 해도 컴파일러는 이 필드 지금 쓰는 것인지 안 쓰는 것인지 모른다.

그냥 초기화가 쉽게 된다.

따라서 필요한 필드가 초기화 되지 않고 런타임에 문제가 발생할 수 있는 것이다.


6️⃣

6번 단점을 보면 새로운 의미를 추가할 때마다 모든 switch문을 찾아서 새 의미를 처리하는 코드를 추가해야 한다.

그런데 하나라도 빠뜨리면 런타임에 문제가 생길 것이다.

따라서 다른 의미 추가하려면 코드 수정해야 한다는 단점이 생긴 것이다.


그래서 결론을 말하면 태그 달린 클래스는 장황하고, 오류 내기 쉽고, 비효율적이다.


👨‍👧‍👦 클래스 계층구조

그렇다면 여러 의미를 표현하려면 어떻게 해야 하는가?

자바 같은 객체 지향 언어는 타입 하나로 다앙햔 의미의 객체를 표현하는 좋은 수단을 제공한다.

바로 클래스 계층 구조를 활용하는 서브 타이핑이다.

태그 달린 클래스는 클래스 계층 구조를 어설프게 흉내낸 것이라고 한다.


서브 타이핑이 뭔지 알아보자.

상속의 용도는 크게 2가지다.

  1. 타입 계층을 구현
  2. 코드 재사용

여기서 타입 계층을 구현한다는 의미는 다음과 같다.

  • 부모 클래스는 자식 클래스의 일반화다.
  • 자식 클래스는 부모 클래스의 특수화다.

상속의 2가지 용도에 이름이 있는데 각각 서브 타이핑, 서브 클래싱이다.

정확한 의미를 보자.

서브 타이핑 : 타입 계층을 구성하기 위해 상속을 사용하는 경우

서브 클래싱 : 다른 클래스의 코드를 재사용할 목적으로 상속 사용하는 경우


상속은 두가지 용도 중 서브 타이핑 용도로 사용해야 한다고 한다.

서브 클래싱 용도는 상속보다는 composition을 사용하는 것이 맞다고 한다.


서브 타이핑의 관계가 유지되기 위해서는 서브 타입이 슈퍼 타입이 하는 모든 행동을 동일하게 할 수 있어야 한다.

즉 상속 관계는 부모 클래스를 새로운 자식 클래스로 대체해도 시스템이 문제 없이 동작할 것이라는 것을 보장해야 한다고 한다.


그래서 위에서 말하는 클래스 계층 구조를 활용한 서브 타이핑이라는 것은 상속으로 클래스 계층 구조를 표현한 것을 의미하는 것이다.


🔄 태그 달린 클래스 → 클래스 계층구조

이제 안 좋은 태그 달린 클래스는 쓰면 안된다.

지금까지 써 왔다면 클래스 계층 구조로 바꿔야 한다.

태그 달린 클래스를 클래스 계층 구조로 바꾸는 방법을 알아보자.


  1. 계층 구조의 root가 될 추상 클래스 정의한다.

  2. 태그 값에 따라 동작이 달라지는 메서드들을 root 클래스의 추상 메서드로 선언한다.

  3. 태그 값에 상관 없이 동작이 일정한 메서드들을 root 클래스의 일반 메서드로 추가한다.

  4. 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들도 root 클래스로 올린다.

  5. root 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의한다.

  6. 각 하위 클래스에 각자의 의미에 해당하는 데이터 필드들 넣는다.

  7. root 클래스가 정의한 추상 메서드를 각자의 의미에 맞게 구현한다.


14까지 root 클래스를 정의하는 것이고 57까지 하위 구체 클래스를 만드는 것이다.


2️⃣

2번 단계를 보면 태그 값에 따라 동작이 달라지는 메서드들을 root 클래스의 추상 메서드로 선언하라고 되어 있다.

위의 태그 달린 클래스에서 area 메서드가 이에 해당한다.


3️⃣, 4️⃣

3, 4번 단계를 보면 동작 일정한 메서드와 공통 사용 데이터 필드를 root 클래스로 올리라고 한다.

위의 태그 달린 클래스에는 이런 것들이 없다.

따라서 4번 단계까지 진행하면 root 클래스에는 abstract double area() 밖에 없는 것이다.


5️⃣

5번 단계를 보면 root 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의하라고 한다.

위의 태그 달린 클래스에서는 Figure 클래스 (root 클래스)를 확장한 Circle 클래스와 Rectangle 클래스를 만들면 되는 것이다.


6️⃣

6번 단계를 보면 각 하위 클래스에 각자의 의미에 해당하는 데이터 필드를 넣으라고 한다.

위에서 만든 Circle, Rectangle 클래스에 다음과 같은 필드 넣으면 된다.

  • Circle 클래스 : double radius;

  • Rectangle 클래스 : double length; double width;

각각 위의 태그 달린 클래스에서 다른 의미일 때 사용되지 않던 필드들이다.


이렇게 해서 태그 달린 클래스 Figure를 클래스 계층 구조로 바꾼 코드를 보자.

먼저 root 클래스인 Figure 클래스다.

// 코드 23-2 태그 달린 클래스를 클래스 계층구조로 변환 (144쪽)
abstract class Figure {
    abstract double area();
}

다음은 Circle 클래스다.

// 코드 23-2 태그 달린 클래스를 클래스 계층구조로 변환 (144쪽)
class Circle extends Figure {
    final double radius;

    Circle(double radius) { this.radius = radius; }

    @Override double area() { return Math.PI * (radius * radius); }
}

마지막으로 Rectangle 클래스다.

// 코드 23-2 태그 달린 클래스를 클래스 계층구조로 변환 (144쪽)
class Circle extends Figure {
    final double radius;

    Circle(double radius) { this.radius = radius; }

    @Override double area() { return Math.PI * (radius * radius); }
}

👍 클래스 계층 구조 장점

이렇게 클래스 계층 구조로 바꾸니 태그 달린 클래스의 단점을 모두 날린다.

그리고 다음과 같은 장점이 생기는 것이다.

  • 간결하고 명확해진다.

  • 태그 달린 클래스에 포함된 쓸데 없는 코드들이 모두 사라졌다.

  • 각 의미를 독립된 클래스에 담아 관련 없던 데이터 필드를 모두 제거했다.

  • 살아 남은 필드들은 모두 final이다.

  • 각 클래스의 생성자가 모든 필드를 남김없이 초기화 한다.

  • 추상 메서드를 모두 구현했는지 컴파일러가 확인해준다.

  • root 클래스의 코드 건들지 않고도 독립적으로 계층 구조 확장하고 함께 사용할 수 있다.

  • 타입이 의미별로 따로 존재해 변수의 의미를 명시하거나 제한할 수 있다.

  • 타입이 의미별로 따로 존재해 특정 의미만 매개변수로 받을 수 있다.

  • 타입 사이의 자연스러운 계층 관계를 반영할 수 있어 유연성이 생긴다.

  • 타입 사이의 자연스러운 계층 관계를 반영할 수 있어 컴파일타임 타입 검사 능력 높여준다.


📍 references