item 23 : 태그 달린 클래스보다는 클래스 계층구조를 활용하라
1. 태그 달린 클래스
태그 달린 클래스는 하나의 클래스에서 여러 가지 다른 타입의 객체를 처리하는 방식으로 설계된다. 하지만 이렇게 하면 쓸모없는 데이터 필드와 코드가 생기며, 유지보수도 어렵고 실수로 인해 런타임 오류가 발생할 가능성도 커진다.
불필요한 필드: 각 도형에 필요하지 않은 필드도 클래스에 포함되어 있다. 예를 들어, 원에는
length
,width
가 필요하지 않고, 사각형에는radius
가 필요 없다.조건문으로 분기 처리: 태그 값에 따라
area()
메서드에서 조건문을 사용해 분기 처리하는 방식은 복잡하고 실수를 유발할 수 있다.확장성 부족: 새로운 도형을 추가하려면
switch
문에 새로운case
를 추가해야 하며, 태그 값을 처리할 수 있는 코드도 추가해야 한다.
1) 태그 달린 클래스란?
클래스 내부에 특정 태그 필드를 두고, 그 태그의 값에 따라 클래스의 동작이나 데이터를 다르게 처리하는 방식으로 설계된 클래스를 의미한다. 태그 필드
는 클래스의 상태나 모양을 나타내는 역할을 한다. 이 태그 필드에 따라 클래스가 어떤 방식으로 동작할지 결정하는데, 예를 들어 클래스가 여러 가지 다른 타입의 객체를 표현해야 하는 상황에서 태그 필드를 사용해 그 타입을 구분하게 되는 것임.
2) 태그 달린 클래스의 단점
열거 타입 선언, 태그 필드, switch 문 등 쓸데없는 코드가 많다. 여러 구현이 한 클래스에 혼합돼 있어서 가독성도 나쁘다.
다른 의미를 위한 코드도 언제나 함께 하니 메모리도 많이 사용한다. 필드들을
final
로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화해야 한다(쓰지 않는 필드를 초기화하는 불필요한 코드가 늘어난다).또 다른 의미를 추가하려면 코드 를 수정해야 한다. 예를 들어 새로운 의미를 추가할 때마다 모든 switch 문을 찾아 새 의미를 처리하는 코드를 추가해야 하는데 , 하나라도 빠뜨리면 역시 런타임에 문제가 불거져 나올 것이다.
마지막으로, 인스턴스의 타입만으로는 현재 나타내는 의미를 알 길이 전혀 없다.
한마디로, 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다.
2. 서브타이핑 (subtyping) : 클래스 계층 구조를 이용한 해결 방법
다행히 자바와 같은 객체 지향 언어는 타입 하나로 다양한 의미의 객체를 표현하는 훨씬 나은 수단을 제공한다.
바로 클래스 계층구조를 활용하는
서브타이핑 (subtyping)
이다.
클래스를 계층구조로 설계하면 된다. 가장 먼저 루트 클래스로 추상 클래스를 정의하고, 태그에 따라 달라지는 메서드를 추상 메서드로 선언한다. 그런 다음 각 도형에 해당하는 하위 클래스를 만들어 각각의 필드를 가지게 하고, 루트 클래스의 추상 메서드를 구현한다.
1) 클래스 계층 구조로 한 코드
2) 클래스 계층구조의 장점
불필요한 필드 제거: 각 도형에 맞는 필드만 남기고 불필요한 필드는 모두 제거했다.
분기 처리 제거: 조건문을 사용한 분기 처리가 필요 없다. 각 도형 클래스에서 고유한 로직을 구현할 수 있어 코드를 간결하게 유지할 수 있다.
유연한 확장성: 새로운 도형을 추가할 때마다 루트 클래스를 건드릴 필요 없이 독립적인 하위 클래스를 추가하면 된다.
컴파일 타임 검증: 컴파일러가 각 클래스가 필요한 필드를 모두 초기화하고, 추상 메서드를 구현했는지 확인해준다. 따라서 실수로 인한 런타임 오류가 줄어든다.
3) 추가적인 확장 (정사각형 예시)
계층 구조로 설계했을 경우, 새로운 도형을 쉽게 확장할 수 있다. 예를 들어, 정사각형을 사각형의 특별한 형태로 정의할 수 있다.
3. 추상 클래스를 쓰는 때
아이템 20에서는 추상클래스보다는 인터페이스를 고려하라고 했는데..? 왜 위에 계층 구조일 때는 추상클래스를 이용한 걸까? 🤔
추상 클래스를 사용하는 이유는 상황에 따라 다르다. 위에서 인터페이스가 더 유연하고 다중 상속이 가능하다고 했지만, 추상 클래스가 필요한 경우도 있다. 추상 클래스를 선택하는 주요 이유를 몇 가지 설명해보자
1) 공통 상태나 구현이 필요할 때
추상 클래스를 사용하는 가장 큰 이유는
공통 상태(필드)
를 공유하거나 기본 구현을 제공하기 위해서이다.
예를 들어, 여러 서브 클래스들이 공통적으로 사용할 인스턴스 변수나 메서드가 있다면, 추상 클래스로 이 부분을 상속받아 사용하게 할 수 있다.
예시: 모든 도형 클래스에서 색깔이나 위치 등의 공통 필드를 유지하고 싶다면, 추상 클래스가 적합하다. 이렇게 하면 공통된 상태를 상속받아 사용할 수 있고, 코드 중복을 줄일 수 있다.
이 경우 모든 도형은 색깔을 갖고 있고, 색깔을 변경할 수 있다. 이 기능을 모든 도형 서브 클래스에 일일이 구현하는 대신, 추상 클래스를 통해 공통으로 상속받는다.
2) 공통된 행동(메서드)을 정의할 때
인터페이스는 상태(필드)
를 가질 수 없고, 메서드의 기본 구현을 제공할 수 있는 디폴트 메서드도 일부 제한적이다. 하지만 추상 클래스는 상속받는 클래스에 기본 메서드 구현을 제공할 수 있다.
예를 들어, 도형 클래스에서 면적을 구하는 area()
메서드는 각 도형마다 다르게 구현되겠지만, toString()
같은 공통적인 동작은 추상 클래스에서 미리 정의해줄 수 있다.
모든 도형 서브 클래스는 toString()
메서드를 자동으로 상속받아 사용할 수 있게 된다. 만약 인터페이스였다면 이런 공통적인 구현을 제공하기가 어렵다.
3) 상속의 편리함
추상 클래스는 상속받는 클래스들이 자동으로 기본 구현을 가지게 하고, 필요한 부분만 오버라이드할 수 있게 도와준다. 이는 코드 중복을 줄이고, 일관된 방식으로 기능을 제공하는 데 유리하다.
예를 들어, 자바의 AbstractList
, AbstractMap
같은 추상 클래스는 모든 리스트나 맵 구현체가 공통적으로 사용하는 메서드를 미리 정의하고, 각 구현체는 그 메서드를 상속받아 사용할 수 있다. 이는 구체적인 구현을 간편하게 만들 수 있게 해준다.
4) 제한된 상속
추상 클래스는 클래스 계층구조 내에서 좀 더 엄격하게 상속을 제한하고자 할 때 사용된다. 인터페이스는 아무 클래스나 구현할 수 있지만, 추상 클래스는 하나의 부모 클래스만 상속할 수 있기 때문에, 일관된 계층구조를 유지하고 싶을 때 사용된다.
예를 들어, Shape
라는 추상 클래스가 있다면, 이 추상 클래스를 상속받은 Circle
, Rectangle
등은 모두 같은 상속 구조를 공유하게 된다. 이는 코드 유지보수성과 일관성을 높이는 데 기여할 수 있다.
5) 공통 인터페이스를 위한 골격 구현 제공
추상 클래스는 인터페이스와 함께 골격 구현(Skeletal Implementation)을 제공하는 데 매우 유용하다.
즉, 인터페이스로는 타입을 정의하고, 추상 클래스로는 기본 동작을 정의해서, 서브 클래스는 필요한 부분만 구현하면 된다.
예를 들어, 자바의 AbstractList
는 List
인터페이스를 구현한 추상 클래스로, List
의 많은 메서드를 기본적으로 구현하고 있다. 이렇게 하면 새로운 List
구현체를 만들 때, AbstractList
를 상속받아서 편리하게 구현할 수 있다.
이 방식은 새로운 List
구현체를 만들 때, 핵심 메서드만 구현하고 나머지는 자동으로 상속받을 수 있게 해준니다.
결론: 왜 추상 클래스를 사용하나?
인터페이스가 더 유연하고 다중 상속이 가능하더라도, 공통 상태(필드)를 공유하거나 기본 동작을 제공해야 하는 상황에서는 추상 클래스가 더 적합하다. 특히 공통된 데이터나 구현을 상속하고, 여러 하위 클래스에서 공통적으로 사용하는 공통 동작을 유지해야 할 때는 추상 클래스가 더 적합한 선택이 된다.
결론적으로, 상황에 따라 추상 클래스가 더 적합할 때가 있고, 이런 경우라면 추상 클래스를 선택하는 것이 좋다.
4. 인터페이스를 통한 서브타이핑
서브타이핑은 객체 지향 프로그래밍에서 하위 타입(subtype)이 상위 타입(supertype)의 역할을 할 수 있는 관계를 의미한다.
즉, 상위 타입의 메서드를 하위 타입이 모두 구현해서 하위 타입 객체가 상위 타입으로 동작할 수 있는 상황을 말합니다.
1) 서브타이핑과 인터페이스
서브타이핑의 핵심은 "하위 타입이 상위 타입의 모든 동작을 지원해야 한다"는 점이다. 인터페이스는 이 서브타이핑의 규칙을 따르는 대표적인 수단 중 하나이다. 인터페이스는 클래스가 반드시 구현해야 할 메서드들의 집합을 정의하므로, 그 인터페이스를 구현한 클래스들은 인터페이스 타입으로 참조할 수 있다.
즉, 인터페이스를 기반으로 계층 구조를 만들면, 상위 인터페이스 타입을 구현한 하위 클래스가 상위 인터페이스로 동작할 수 있기 때문에, 이것도 서브타이핑에 해당한다.
예시
위 코드에서 Shape
는 인터페이스이고, Circle
과 Rectangle
은 Shape
인터페이스를 구현한 클래스들이다. Circle
과 Rectangle
객체는 모두 Shape
타입으로 참조될 수 있고, Shape
인터페이스에 정의된 area()
메서드를 사용할 수 있다. 이것이 바로 서브타이핑
2) 추상 클래스 vs 인터페이스: 서브타이핑 관점
추상 클래스는 공통된 상태(필드)와 구현을 공유하는 데 강점이 있지만, 단일 상속만 가능하다. 즉, 한 클래스가 하나의 부모 추상 클래스만 가질 수 있다.
인터페이스는 상태를 가질 수 없지만, 다양한 구현체가 동일한 동작을 보장하도록 다중 상속이 가능하다. 즉, 하나의 클래스가 여러 개의 인터페이스를 구현할 수 있습니다.
둘 다 서브타이핑을 지원하지만, 인터페이스는 더 유연한 계층 구조를 설계할 수 있게 도와준다.
서브타이핑은 상위 타입(인터페이스나 추상 클래스)의 동작을 하위 타입이 구현해서 상위 타입처럼 동작할 수 있는 관계를 말하며, 인터페이스는 서브타이핑을 구현하는 중요한 방법 중 하나이다.
5. 스터디에서 추가 설명 : API SERVER APPLICATION 에서 Entity를 클래스 계층구조로 만드려면
출처 : https://antique-banon-928.notion.site/Effective-Java-20-25-10-17-11d82aee42598045ab1ded57b536635b
1) 퍼시스턴스 레이어 활용
퍼시스턴스 레이어에서 단순히 DB에 저장되어 있는 값을 꺼내오는 역할만 하지 않고, 계층구조의 타입으로 변환해주는 역할을 해준다.
레이어드 아키텍쳐
퍼시스턴스 레이어로 Repository, Dao Layer 총 두계층으로 나눈다.Dao는 DB에서 값을 꺼내오는 역할을, Repository는 계층구조의 타입으로 전환해주는 역할을 맡는다.
예시
이미지 퀴즈, 텍스트 퀴즈 총 2 종류의 퀴즈가 존재한다.
Dao
QuizEntity 타입값을 가지게 되기 때문에 객체지향적으로 좋다는 것
Repository
✨ 최종 정리
태그 달린 클래스를 써야 하는 상황은 거의 없다. 새로운 클래스를 작성하는 데 태그필드가 등장한다면 태그를 없애고 계층구조로 대체하는 방법을 생각해보자.
클래스 계층구조로 변환함으로써 코드가 더 간결해지고, 각 클래스의 역할이 명확해졌다. 태그 달린 클래스는 유지보수와 확장성이 떨어질 수 있지만, 클래스 계층구조로 변환하면 이러한 문제들을 해결할 수 있다.
기존 클래스가 태그 필드를 사용하고 있다면 계층구조로 리팩터링하는 걸 고민해보자.
Last updated