상속과 합성.. 이게 뭐야?

김재연·2023년 12월 28일
0

상속의 개념(is -a)

상속은 클래스가 클래스를 물려 받는 것을 의미합니다.

즉, 부모-자식 클래스 관계에서, 자식이 부모의 모든 것을 상속받는 것을 의미하죠

코드로 에시를 들면 아래와 같습니다.

public class Parent {

	public final int parentField = 1;

	...

}

public class Child extends Parent {

	...

}

public class Main {

	public static void main(String[] args) {
		Child child = new Child();
		System.out.println(child.parentField); // 1
	}

}

합성의 개념 (has-a)

합성은 다른 클래스를 본인의 Field 로 가지고 있는 것을 말합니다.

이전 포스팅에서 다뤘던, 전략 디자인 패턴, 어댑터 디자인 패턴에서 사용이 되는 개념이죠.

코드로 설명하면 다음과 같습니다.

이해를 위해 상속에서 사용했던 코드에서 약간의 변경만을 하겠습니다.

public class Parent {

	public final int parentField = 1;

	...

}

public class Child {

	public final Parent parent;
	
	public Child(Parent parent) {
		this.parent = parent;
	}
	...

}

public class Main {

	public static void main(String[] args) {
		Child child = new Child(new Parent());
		System.out.println(child.parent.parentField); // 1
	}

}

위와 같습니다.

상속의 단점

상속의 단점을 간단하게 정리해보겠습니다.

결합도 증가

상속 관계는 is-a 관계입니다.

관계의 명칭에서부터 부모-자식 클래스간의 결합도가 얼마나 강한지가 나옵니다.

소프트웨어 아키텍쳐를 설계할 때, 결합도는 굉장히 중요합니다.

결합도가 의존성을 결정짓기 때문이죠.

서로 다른 클래스간의 결합도가 높아져 의존성이 높아진다는 것은 꽤나 많은 문제를 불러 일으킬 수 있습니다.

지금부터 이야기 할 상속의 단점들은 대부분 이런점들에서 오게 됩니다.

불필요한 기능 상속

잘못된 설계로 인해서 불필요한 기능을 상속하게 될 수 있습니다.

물론 설계를 잘하면 되겠지만, 불필요한 기능을 상속하지 않기 위해서, 많은 고민들을 해야합니다.

아마, 이 과정에서 실수를 하여 아래와 같은 관계도가 나오지 않았을까 생각합니다.

Vector 의 기능을 상속받기 때문에, Stack 에는 add 라는 메서드가 있습니다.

이는 부모 클래스에서 add 를 정의하고, 이를 public 으로 정의했기 때문입니다.

Stack 이 Vector 를 상속받는 한 해당 인터페이스를 막을 방법이 없습니다.

부모 클래스의 결함마저 상속

당연히 모든 것을 상속받기 때문에, 부모 클래스에 결함이 발생한 순간, 자식 클래스에게도 전파되게 됩니다.

부모 클래스에서 변경이 일어나면, 그 사항이 자식까지 전파됨

이는, 유지보수에 굉장히 큰 영향을 줄 수 있습니다.

public class Parent {

	public final int f1;

	public Parent(int f1) {
		this.f1 = f1;
	}

	...

}

public class Child extends Parent {

	public Child(int f1) {
		super(f1);
	}

	...
	
}

public class Main {

	public static void main(String[] args) {
		Child child = new Child(1);
		System.out.println(child.parentField); // 1
	}

}

이와 같은 상태에서, Parent 에 f2 필드가 추가적으로 생겼다고 가정해봅시다.

public class Parent {

	public final int f1;
	public final int f2;

	public Parent(int f1, int f2) {
		this.f1 = f1;
		this.f2 = f2;
	}

	...

}

public class Child extends Parent {

	public Child(int f1, int f2) {
		super(f1, f2);
	}

	...
	
}

public class Main {

	public static void main(String[] args) {
		Child child = new Child(1, 1);
		System.out.println(child.parentField); // 1
	}

}

이와 같이, 부모 클래스에서 필드를 하나 추가했을 뿐인데, 실제로 이를 호출하는 부분까지 수정해야만 했습니다.

불필요한 인터페이스 상속

이는 Stack 이 Vector 의 add 를 상속받은 것과 같은 문제로 정의할 수 있습니다.

클래스 폭팔

Stack 과 Vector 의 관계에서 보면 알 수 있듯이, 설계를 잘 진행하는 것이 정말 중요합니다.

필요한 기능만을 상속하기 위해서, 혹은 필요한 기능만을 상속받기 위해서 클래스를 나누고 나눠야 할 것입니다.

이는 클래스가 너무 많아지는, 클래스 폭팔로 이어질 수 있습니다.

단일 상속의 한계

C++ 은 다중 상속을 지원합니다.

하지만, 이는 너무 복잡하고, 다이아몬드 문제를 유발하게 됩니다.

이는 다음 포스트에서 다뤄보도록 하겠습니다.

상속의 단점을 해결해주는 합성

이 모든 단점들을 합성이 해결해줍니다.

아무래도 상속관계와 다르게 컴파일 시간에 클래스 간의 관계가 정해지지 않고, 전략 패턴에서 활용하는 것과 같이 런타임에 결합이 정해집니다.

A 가 B 를 상속 받고 있는데, 런타임에 B 가 C 를 상속받는 것으로 바뀔 수가 없는 것처럼, 컴파일 시간에 관계가 정해진다.

결합도 증가

이는 위에서 설명했던 것과 같이 합성을 통해서 해결이 되게됩니다.

불필요한 기능 상속

합성을 활용하는 클래스에서, 원하는 기능들만 호출하면 됩니다.

부모 클래스의 결함마저 상속

이는 위와 같이 사용하지 않으면 됩니다.

그래도 실수로 인해서 사용될 수는 있겠네요

부모 클래스에서 변경이 일어나면, 그 사항이 자식까지 전파됨

Main 문에서는 코드의 변경이 필요할지도 모르겠습니다.

변경이 일어난 클래스를 생성하고, 주입해주는 과정에서 말이죠.

불필요한 인터페이스 상속

Stack, Vector 의 관계를 예를 들면, Stack 에서 Vector 를 활용해 pop, poll 과 같은 메서드를 구현하고 add 를 구현하지 않으면 됩니다.

클래스 폭팔

본인의 필드로 사용할 클래스를 추가만 하면 되기 때문에, 클래스가 늘어나지 않습니다.

하지만, 상속은 이 모든 것들을 클래스로 구현해내야하죠.

단일 상속의 한계

클래스 폭팔에서 설명했던 부분으로 인해서 해결이 되게 됩니다.

결론

Java 의 창시자 제임스 고슬링이 "내가 자바를 만들면서 가장 후회하는 일은 상속을 만든 점이다" 라고 말할 정도로 상속은 잘못 사용될 수 있는 여지가 많은 것 같습니다.

상황에 맞게 사용하면 정말 좋겠지만 말이죠.

앞으로 개발을 진행할 때, 현재 상황에서 상속이 꼭 필요한지 체크해보고, 되도록이면 합성을 활용할 수 있도록 노력해봅시다!

참고 자료

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EC%9D%98-%EC%83%81%EC%86%8D-%EB%AC%B8%EC%A0%9C%EC%A0%90%EA%B3%BC-%ED%95%A9%EC%84%B1Composition-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0

profile
끊임없이 '성장'하는 개발자 김재연입니다.

0개의 댓글