상속이 더이상 사용되지 않는 이유
상속을 이용한 AbstractBird 구현 예시
public class AbstractBird {
public void fly() {}
}
public class Ostrich extends AbstractBird {
public void fly() {
throw new UnsupportedMethodException("에러메세지");
}
}
다른 날지 않는 새들에 관련된 클래스를 구현할 때마다 예외를 발생시켜야 하고, 날지도 않는 새에게 fly() 함수를 구현하는 것은 최소 지식 원칙(LOD)에도 위반된다.
위의 문제를 해결하기 위해서 특정 기준으로 나누어 클래스를 세분화 한다면,
AbstractBird
- AbstractFlyableBird
- AbstractUnflyableBird
AbstractBird
- AbstractFlyableBird
- AbstractFlyableTweetableBird
- AbstractFlyableUnTweetableBird
- AbstarctUnFlyableBird
- AbstractUnFlyableTweetableBird
- AbstractUnFlyableUnTweetableBird
클래스에 포함된 메서드와 속성을 파악하려면 상위 클래스로 계속 거슬러 올라가 확인해야 하고, 상위 클래스가 수정되면 하위 클래스도 수정해줘야 하는 번거로움이 생긴다.
결론적으로, 상속 단계가 깊고 복잡하게 되면 가독성과 유지보수성이 나빠지기 때문에 추천하지 않는다.
합성이 상속에 비해 나은 장점
상속의 is-a 관계 표현, 다형성 지원, 코드 재사용 기능은 굳이 상속을 사용하지 않더라도 다른 기술적 수단을 통해 충분히 구현 가능하다.
- is-a 관계: 합성과 인터페이스의 has-a 관계
- 다형성: 인터페이스 사용
- 코드 재사용: 합성 + 위임
그러므로, 복잡한 상속 관계를 사용하지 않거나 더 적게 사용하도록 주의를 기울이자.
public interface Flyable {
void fly();
}
public interface Tweetable {
void tweet();
}
public interface EggLayble {
void layEgg();
}
public class Ostrich implements Tweetable, EggLayable { // 타조
@Override
public void tweet() { ... }
@Override
public void layEgg() { ... }
}
public class Sparrow implements Flayable, Tweetable, EggLayable { // 참새
@Override
public void fly() { ... }
@Override
public void tweet() { ... }
@Override
public void layEgg() { ... }
}
이 때, Ostrich, Sparrow 클래스는 tweet(), layEgg() 를 모두 구현해야 하므로 중복된 코드에 대해 합성 + 위임할 수 있도록 다음과 같이 인터페이스를 구현한 클래스를 사용하는 방식을 사용하자.
public interface Flyable {
void fly();
}
public interface Tweetable {
void tweet();
}
public interface EggLayble {
void layEgg();
}
public class FlyAbility implements Flyable {
@Override
public void fly() { ... }
}
public class Tweetablility implements Tweetable {
@Override
public void tweet() { ... }
}
public class Egglayability implements EggLayable {
@Override
public void layEgg() { ... }
}
public class Ostrich implements Tweetable, EggLayable { // 타조
private TweetAbility tweetAbility = new TweetAbility(); // 합성
private EggLayAbility eggLayAbility = new EggLayAbility();
@Override
public void tweet() {
tweetAbility.tweet(); // 위임
}
@Override
public void layEgg() {
egglaybility.layEgg();
}
}
합성을 사용할지 상속을 사용할지 결정하기
상속보다 합성을 권장하지만, 항상 합성을 사용해야하는 것만은 아니다.
상속을 사용하면 좋은 경우
- 클래스 간의 구조가 안정적일 때
- 상속 단계가 2단계 이하일 때
합성을 사용하면 좋은 경우
- 클래스 상속 계층이 깊고 복잡할 때
- 합성을 사용한다는 것은 그만큼 인터페이스와 클래스를 더 많이 정의해야 한다는 뜻이므로 복잡성과 유지관리 측면에서 합성을 사용할 것인지, 상속을 사용할 것인지 선택해야 한다.
아래와 같이 외부 클래스와 메서드가 이미 정의되어있어 변경 불가능하고, 특정 메서드는 해당 클래스를 매개변수로 사용할 때
외부 클래스의 메서드를 재정의 하고 싶다면 반드시 상속을 이용해야 한다.
public class FeignClient { // 외부에서 정의된 클래스, 변경 불가
public void encode(String url) {
// ...
}
}
public void demoFunction(FeignClient feignClient) {
feignClient.encode(url);
}
public class CustomizedFeignClient extends FeignClient {
@Override
public void encode(String url) {
// ...구현
}
}
FeignClient client = new CustomizedFeignClient();
demofunction(client);
'BE 📙 > 디자인패턴의 아름다움' 카테고리의 다른 글
| [CH 05.01, 05.02] 리팩토링, 단위테스트 (4) | 2025.08.12 |
|---|---|
| [CH03.01] 설계 원칙 - 단일 책임 원칙 (1) | 2025.08.05 |
| [CH03.07-03.08] 설계원칙 (4) | 2025.07.31 |
| [CH01] 개요 (5) | 2025.07.21 |
댓글