
아이템18. 상속보다는 컴포지션을 사용하라
컴포지션이 한국어로하면 구현, 구상 정도가 될텐데 이거 헤퍼디에서 나온 내용 같은데 지금 찾을 수가 없네
암튼
상속은 기초설계가 잘 되었고 문서화도 잘된 경우, 또는 모든 상속관계의 클래스들을 한 프로그래머가 통제 중이라면 안전하다. 하지만, 특정 클래스가 인터페이스를 구현하거나 인터페이스가 인터페이스를 상속하는 것 외, 구현클래스가 다른 구현클래스를 바로 상속하는 것은 위험하다.
상속은 캡슐화를 깨뜨린다.
class Member {
String name;
int age;
public Member() {
name = "윤마치";
age = 28;
}
public void addOne() {
add(1);
}
public void add(int plusAge) {
age += plusAge;
}
}
class Student extends Member {
int grade;
public Student() {
super();
grade = 4;
}
public void addOne() {
grade += 1;
super.addOne();
}
public void add(int plusAge) {
grade += plusAge;
super.add(plusAge);
}
}
이런 구조의 클래스들이 있다고하자. 핵심은 나이를 한 살 올리는 addOne 메서드와 나이를 입력받아 올리는 add 메서드다.
public static void main(String[] args) {
Student member = new Student();
System.out.println(member.grade);
member.addOne();
System.out.println(member.grade);
}
student 객체를 만들어 addOne 메서드를 호출한 경우, 나이인 age와 학년인 grade 모두 1씩 올라가야한다.

그런데 위의 코드를 실행한 경우 1씩 올라가지 않고 2씩 올라가며 문제가 생긴다.
문제의 원인은 Student 객체의 addOne 메서드에서 grade에 1을 더하고 인스턴스 변수인 나이에 1을 더하기 위해 상위클래스인 Member 객체의 add 메서드를 호출하는데, 이 경우 Member 객체의 add 메서드가 호출되는 것이 아니라 Student 객체의 add 메서드가 호출된다. 상속관계에 있는 메서드를 호출한 경우 상속트리 가장 아래에 있는 메서드가 호출되는데, 이를 동적 바인딩이라고 한다.
이렇게 구현체가 구현체를 상속하는 것은 상위클래스의 코드에 따라서 기대 결과가 달라질 수 있는 위험한 행동이다.
이 경우 하위클래스에서 add 메서드를 재정의하지 않는 방법이 있으나 이럴 경우 상속의 이점을 살리지 못할 뿐더러 만약 다른 개발자가 Member 클래스를 상속하여 add 메서드를 재정의할 경우 동적바인딩에 따라 상속트리에서 가장 낮은 위치에 있는 클래스의 메서드를 오버라이드하게된다. 그게 형제클래스 관계여도!
이외에도 메서드 추가로 인해 상위클래스에서만 의도한 기능이 하위클래스에서 작동하는 일도 있고 좀 골치아파진다 (물론 private로 선언해도 되지만..)
아니면 메서드를 재정의하는 것이 아닌 새 메서드를 호출해도 될테지만
public String addOne() {
grade += 1;
add(1);
return "wow!";
}

시그니처가 같지만 반환타입이 다르다던가하면 컴파일에러가 터지고 반환타입마저 같다고 그냥 상속한 것이므로 같은 문제가 터질 가능성을 배제할 수 없다.
이런 상황을 타개하기 위해서 컴포지션(구성)이 쓰일 수 있다.
컴포지션은 특정 클래스를 만든 후, 기존 클래스의 인스턴스를 private 필드로 참조하는 방식으로 사용된다.
package hongikUniv;
interface addable {
void addOne();
void add(int plusAge);
}
class Member implements addable {
String name;
int age;
public Member() {
this.name = "윤마치";
this.age = 28;
}
public void addOne() {
}
public void add(int plusAge) {
}
}
class ForwardingMember implements addable {
private final addable a;
String name;
int age;
public ForwardingMember(addable a) {
this.a = a;
this.name = "윤마치";
this.age = 28;
}
public void addOne() { a.addOne(); }
public void add(int plusAge) { a.add(plusAge); }
}
class Student extends ForwardingMember {
int grade;
public Student(addable a) {
super(a);
grade = 4;
}
public void addOne() {
grade += 1;
super.addOne();
}
public void add(int plusAge) {
grade += plusAge;
super.add(plusAge);
}
}
public class Stack {
public static void main(String[] args) {
Member member = new Member();
Student student = new Student(member);
student.addOne();
System.out.println(student.grade);
}
}
뭐 이렇게 볼 수 있겠다.. 사실 억지로 얼기설기 짠 코드이기 때문에 실제 예시를 들긴 좀 뭐하고.. 대충 개념정도만 알면 되지 않을까 싶다.
static class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> set) {
this.s = set;
}
@Override
public boolean add(E e) {
return s.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
}
static class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> set) {
super(set);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
암튼 컴포지션은 한 객체가 다른 객체를 자신의 일부로 포함하여 그 객체의 기능을 위임해 재사용하는 방법이다
HashSet의 상위 인터페이스인 Set 인터페이스를 활용하여 설계되었다.
InstrumentedHashSet 은 Set 객체를 받아 몇 개의 기능을 덧씌워 추가적인 기능을 부여했다. 이렇게 InstrumentedHashSet 처럼 Set 인스턴스를 덧씌워 기능을 추가하는 클래스를 래퍼 클래스라고 칭하며, 기능 덧씌우기를 통해 데코레이터 패턴이라고도 불린다.
ForwardingSet 클래스가 메서드 호출을 Set에게 위임하고있으며, InstrumentedHashSet 클래스는 Set에 기능을 추가한다.
이처럼 상속기능을 고민할 때는 is-a 관계를 만족하는지 봐야한다. 자신이 없으면 상속해서는 안된다. 컴포지션을 사용해야한다. 확장하려는 클래스에 아무 문제가 없는가? 라는 질문을 끊임없이 던지고, 상속해야한다. 단, is-a 관계더라도 상위클래스가 확장을 고려하지 않았더라면 상속 대신 컴포지션을 사용하는 것이 좋다.
사실 데코레이터 패턴은 이전에 다룬 적이 있어서..
https://dev-dx2d2y-log.tistory.com/62
[2025백엔드] 헤드퍼스트 디자인패턴 독서 #3 - 3장. 객체 꾸미기
데코레이터 패턴은 특정 객체나 클래스에 기능을 추가, 위임하는 패턴이다.대표적으로는 커피 주문 등의 예시가 있으며, 음료 주문은 기존 음료 클래스에, 휘핑크림이나 샷 추가 등 부가적인 기
dev-dx2d2y-log.tistory.com
이걸 참고하는게 더 좋을 것 같다.
암튼.. 아이템18이 좀 더 어려웠던 것 같다. 디자인패턴은 하면할수록 어떻게하는지 감을 못잡겠단 말이지. 더닝 크루거 효과는 역시 옳아
중간고사 끝나면 자바의정석도 좀 읽고 디자인패턴도 공부해야할 것 같다.
백준도 하고
'언어공부 > Java | Kotlin' 카테고리의 다른 글
| [CS] 이펙티브 자바 독서 #3 - 아이템85. 직렬화를 피하라! (0) | 2025.10.23 |
|---|---|
| [CS] 스레드란 무엇일까? (0) | 2025.10.15 |
| [CS] 이펙티브자바 독서 #1 - 아이템15 ~ 17 (0) | 2025.10.11 |
| [CS] 자바의 정석 독서 #13 - 내부클래스 (0) | 2025.10.10 |
| [CS] 자바의 정석 독서 #12 - 추상클래스와 인터페이스 (0) | 2025.10.09 |