[Java] Item 23 : 태그 달린 클래스보다는 클래스 계층구조를 활용하라
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);
}
}
}
🫧 태그 달린 클래스의 단점
예시에서 볼 수 있다시피, 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다.
- 쓸 데 없는 코드 증가
-
가독성이 나쁨
-> 여러 구현이 하나의 클래스에 나타나 있으므로 -
필요없는 메모리 사용 발생
-> 한 의미를 표현하기 위해서는 다른 의미를 위한 코드 공간까지 사용해야 함. (쓰지 않는 필드 초기화 필요) -
확장성 X
-> 의미 추가 시 코드 수정 필요 -
오류 발생 확률 증가
-> 새로운 의미를 추가할 때마다 switch문을 찾아 코드를 일일이 넣어주어야 함. 하나라도 빠뜨릴 시 런타임 문제 발생. - 인스턴스 타입으로 의미 유추 X
따라서, 태그 달린 클래스보다는 클래스 계층 구조를 활용하는 서브 타이핑을 사용하자.
💡 왜 태그 달린 클래스에서는 컴파일러에서 문제가 잡히지 않고 런타임에 오류가 날까?
태그 달린 클래스의 예시를 보면 이해가 쉽다. 해당 예시는 글 위쪽에 태그 달린 클래스의 예시 코드 중 일부이다.
// 모양이 사각형일 때만 쓰는 필드
double length;
double width;
// 모양이 원일 때만 쓰는 필드
double radius;
이렇게 개발자가 작성해 둔 필드는 개발자 관점에서의 태그이다. 컴퓨터는 이를 이해할 수 없다. 가령 다음과 같은 코드로 작성한다면 런타임 오류가 발생할 것이다.
Shape shape = new Shape(10);
System.out.println(shape.height);
선언은 되었지만 초기하가 되지 않아 NullPointerException이 일어날 확률이 존재한다. 따라 태그 달린 클래스에서는 컴파일러로 이러한 오류를 찾아내기 쉽지 않다는 것이 문제점이다.
✨ cf) 서브 클래싱과 서브 타이핑
객체지향에서 중요한 기능인 서브클래싱과 서브타이핑에 대해 알아보자.
☁️ 서브클래싱
: 한 클래스가 다른 클래스의 기능을 상속받아 확장하는 방법
- “is-a” 관계를 형성하며, 서브클래스는 슈퍼클래스의 모든 특성(필드, 메서드)를 상속 받는다.
ex) “Dog” 클래스가 “Animal” 클래스를 상속받는 경우
☁️ 서브타이핑
: 타입 시스템과 관련된 개념으로 인터페이스의 구현을 포함한다.
- 리스코프 치환 원칙을 만족해야 한다 (하위 타입이 상위 타입의 계약을 만족해야 하며, 상위 타입을 대체할 수 있어야 한다.)
🫧 태그 달린 클래스를 클래스 계층 구조로 바꾸는 방법
- 계층구조의 루트가 될 추상 클래스 정의
-
태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언
ex) 해당 예에서 Figure의 area - 태그 값에 상관 없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가
- 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들을 루트 클래스에 추가
- 루트 클래스를 확장한 구체 클래스르 의미별로 하나씩 정의
- 각 하위 클래스에 각자의 의미에 해당하는 데이터 필드 추가 (final)
- 루트 클래스가 정의한 추상 메서드를 각자의 의미에 맞게 구현
✨ 클래스 계층 구조 예시 코드
해당 코드는 태그 달린 클래스 예시 코드
를 클래스 계층 구조
로 바꾼 예시 코드이다.
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 🫧 서브클래싱과 서브타이핑이란_티스토리 | 재미있는 개발 이야기