복잡한뇌구조마냥

[JAVA] Effective Java - 태그 달린 클래스보다는 클래스 계층구조를 활용하라 (23) 본문

BE/JAVA

[JAVA] Effective Java - 태그 달린 클래스보다는 클래스 계층구조를 활용하라 (23)

지금해냥 2025. 8. 11. 00:01

요약

 

  • “태그 달린 클래스(tagged class)”는 한 클래스 안에 kind 같은 태그값으로 여러 형태를 구분하는 방식.
  • 이 방식은 필드/메서드가 뒤섞여 지저분해지고, 생성자·검증·equals/hashCode 같은 공통 로직이 복잡해지며, 실수가 생기기 쉽다.
  • 추상 클래스/인터페이스 기반 계층구조로 바꾸면 타입별 필드만 갖게 되고, 컴파일러가 누락/오류를 잡아주며, 가독성과 유지보수성이 크게 좋아진다.
  • 현대 자바(17+)라면 sealed 클래스/인터페이스로 더 안전하게 닫힌 계층을 만들 수 있다.

1) 태그 달린 클래스가 뭔가요?

public class Shape {
    public enum Kind { CIRCLE, RECTANGLE }

    private final Kind kind;

    // CIRCLE 전용
    private final double radius;

    // RECTANGLE 전용
    private final double width;
    private final double height;

    public Shape(double radius) {
        this.kind = Kind.CIRCLE;
        this.radius = radius;
        this.width = 0;
        this.height = 0;
    }

    public Shape(double width, double height) {
        this.kind = Kind.RECTANGLE;
        this.radius = 0;
        this.width = width;
        this.height = height;
    }

    public double area() {
        switch (kind) {
            case CIRCLE: return Math.PI * radius * radius;
            case RECTANGLE: return width * height;
            default: throw new AssertionError(kind);
        }
    }
}

문제점

  • 지저분한 필드: 한 형태에만 쓰는 필드가 공존 → 미사용 필드/더미 값 발생.
  • 생성자·검증 폭탄: 태그별로 다른 불변식(예: 원은 radius > 0, 직사각형은 width, height > 0)을 한 클래스에서 모두 처리 → 누락/버그 위험.
  • 조건 분기 남발: switch(kind)가 이곳저곳에 퍼짐 → 새 형태 추가 시 전부 수정해야 함.
  • 타입 안전성 부족: 논리적으로 불가능한 상태(예: kind=CIRCLE인데 width 접근)를 컴파일 타임에 막기 어려움.
  • equals/hashCode/toString의 취약성: 태그마다 다르게 계산해야 해 코드가 길어지고 실수하기 쉽다.

2) 클래스 계층구조로 바꾸기

public abstract class Shape {
    public abstract double area();
}

public final class Circle extends Shape {
    private final double radius;

    public Circle(double radius) {
        if (radius <= 0) throw new IllegalArgumentException("radius > 0");
        this.radius = radius;
    }

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

public final class Rectangle extends Shape {
    private final double width;
    private final double height;

    public Rectangle(double width, double height) {
        if (width <= 0 || height <= 0) throw new IllegalArgumentException("width,height > 0");
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }
}

장점

  • 필드 정합성: 각 타입은 자기에게 필요한 필드만 가짐 → 불필요·미사용 필드 사라짐.
  • 불변식 캡슐화: 유효성 검사가 각 생성자에 자연스럽게 위치 → 누락 가능성↓.
  • 조건 분기 제거: 다형성으로 switch(kind) 사라짐 → 새 타입 추가도 폐쇄적 수정.
  • 타입 안전성 향상: 컴파일러가 메서드/필드 사용을 체크.
  • 가독성과 테스트 용이성: 각 타입이 작고 응집도 높아짐.

3) 현대 자바라면: sealed(자바 17+)

닫힌 계층을 만들고 싶다면 sealed로 확장 가능 타입을 통제할 수 있다.

public sealed abstract class Shape permits Circle, Rectangle {
    public abstract double area();
}

public final class Circle extends Shape { /* ... */ }
public final class Rectangle extends Shape { /* ... */ }
  • 이외 타입이 임의로 상속할 수 없음 → 모델의 완결성/안전성 보장.
  • switch 표현식과 함께 쓰면 총합 타이핑(algebraic data type) 느낌으로 안전하게 패턴 매칭 가능(미래의 패턴 매칭 for switch와 궁합 좋음).

4) 언제 태그 달린 클래스가 “그나마” 괜찮을까?

  • 아주 작고 임시인 데이터 구조, 또는 JVM 간 직렬화 포맷 호환을 위한 얇은 DTO처럼 변형이 거의 없고 규칙이 단순한 경우.
  • 그래도 가능하면 enum + 분기 대신 작은 타입 분리를 검토하는 게 장기적으로 안전하다.

5) 실전 적용 팁(리팩터링 체크리스트)

  • 클래스 안에 서로 다른 의미의 필드 묶음이 공존하는가?
  • 메서드 곳곳에 if/switch (kind)가 반복되는가?
  • 생성자/유효성 검증이 태그값에 따라 분기하는가?
  • 새 “종류”를 추가할 때 여러 파일을 돌아다니며 수정하는가?
    → 그렇다면 추상 타입 + 하위 타입으로 쪼개자. 필요하면 sealed로 닫자.

 

6) 한 줄 결론

“태그로 형태를 구분하는 한 덩어리 클래스를 만들지 말고, 다형성이 드러나는 클래스 계층으로 쪼개라.
그러면 필드·불변식·행동이 자연스럽게 자기 자리로 돌아온다.”

 

LIST