상속과 합성 개념 정리
프로그래밍을 할때 가장 신경 써야 할 것 중 하나가 바로 코드 중복을 제거하여 재사용 함으로써 변경, 확장을 용이하게 만드는 것이다.
그런 관점에서 상속과 합성은 객체지향 프로그래밍에서 가장 널리 사용되는 코드 재사용 기법이다.
상속(Inheritance) | 합성(Composition) |
부모 클래스와 자식 클래스 사이의 의존성은 컴파일 타임에 해결 | 두 객체 사이의 의존성은 런타임에 해결 |
is-a 관계 | has-a 관계 |
부모클래스의 구현에 의존 결합도가 높음. | 구현에 의존하지 않음. 내부에 포함되는 객체의 구현이 아닌 인터페이스에 의존. |
클래스 사이의 정적인 관계 | 객체 사이의 동적인 관계 |
부모 클래스 안에 구현된 코드 자체를 물려 받아 재사용 | 포함되는 객체의 퍼블릭 인터페이스를 재사용 |
상속 (Inheritance) 이란
상속(Inheritance)은 객체 지향 4가지 특징중 하나로서 클래스 기반의 프로그래밍에서 가장 먼저 배우는 개념이다.
클래스 상속을 통해 자식 클래스는 부모 클래스의 자원을 물려 받게 되며, 부모 클래스와 다른 부분만 추가하거나 재정의함으로써 기존 코드를 쉽게 확장할 수 있다.
그래서 상속 관계를 is-a 관계라도 표현하기도 한다.
class Mobile {
// ...
}
class Apple extends Mobile {
// ...
}
※ 참고
[ 상속을 통한 코드의 재사용 ]
'코드의 재사용' 이라는 단어가 머릿속에 그려지지 않아 정확히 어떤 것을 말하는지 모를수 있을텐데 이렇게 생각해보면 된다.
애초에 우리가 함수(function)을 만들어 쓰는 이유가 공통적으로 사용되는 코드를 묶어 재사용을 통해 코드 중복을 줄이기 위해서이다.
이런 관점에서, 객체 지향 프로그래밍에서 공통적으로 사용되는 코드가 있다면, 일일히 클래스마다 메소드를 만들어 사용하는게 아닌, 부모 클래스에 메소드 하나 만들어놓고 상속을 통해 부모의 것을 가져와 사용한다는 기법으로 코드의 재사용이라고 말하는 것이다.
다만, 엄밀히 말하면 상속은 그저 코드 재사용을 위한 기법이 아니다.
일반적인 클래스가 이미 구현이 되어 있는 상태에서 그보다 좀 더 구체적인 클래스를 구현하기 위해 사용되는 기법이며, 그로 인해 상위 클래스의 코드를 하위 클래스가 재사용 할 수 있을 뿐이다.
예를들어 백화점의 고객을 클래스로 표현하려고 할때, 고객도 백화점 매출의 기여도에 따라 VIP, Gold, Silver 등급으로 나누어 각 등급마다 차별된 서비스(할인 쿠폰, 포인트)를 제공할 수 있다.
이러한 경우 Customer 라는 최상위 클래스를 만들고, 이를 각각 상속 받아 VIPCustomer, GoldCustomer 등으로 자식 클래스를 구현하게 된다.
만일 고객 상속 구조에서 Cooper 등급 클래스를 새로 추가한다고 하였을때, 다른 클래스를 건드리지 않고 그냥 상위 클래스 Customer를 상속(extends)만 하면 무리없이 구조화된 클래스를 만들 수 있게 된다.
이처럼 '고객' 이라는 객체 주제는 같지만, 서로 다른 속성이나 기능들을 가지고 있을때, 이러한 구조를 상속 관계를 통해 논리적으로 개념적으로 연관 시키는 것을 상속이라 한다.
따라서 상속을 사용하는 경우는 명확한 is - a 관계에 있는 경우, 그리고 상위 클래스가 확장할 목적으로 설게되었고 문서화도 잘되어 있는 경우에 사용하면 좋다.
그러나 상속을 제대로 활용하기 위해서는 부모 클래스의 내부 구현에 대해 상세하게 알아야 하기 때문에 자식 클래스와 부모 클래스 사이의 결합도가 높아질 수 밖에 없다.
또한 상속 관계는 컴파일 타임에 결정되고 고정되기 때문에 코드를 실행하는 도중에 변경할 수 없다.
따라서 여러 기능을 조합해야 하는 설계에 상속을 이용하게 된다면 모든 조합별로 클래스를 하나하나 추가해주어야 한다.
이것을 클래스 폭발 문제라 한다.
더군다나 Java8부터는 인터페이스의 디폴트 메소드 기능이 나오면서 인터페이스내에서 로직 구현이 가능하여 상속의 장점이 약화되었다고 할 수 있다. 그래서 클래스 상속보다는 인터페이스 구현을 이용하라는 말을 한번쯤 들어봤을 것이다.
결과적으로 상속은 클래스간의 관계를 한눈에 파악할 수 있고 코드를 재사용할 수 있는 쉽고 간단한 방법일지는 몰라도 우아한 방법이라고 할 수는 없다.
합성 (Composition) 이란
합성(Composition)은 또다른 말로 조합이나 컴포지션이라고 불린다.
합성 기법은 기존 클래스를 상속을 통한 확장하는 대신에, 필드로 클래스의 인스턴스를 참조하게 만드는 설계이다.
예를들어 서로 관련없는 이질적인 클래스의 관계에서, 한 클래스가 다른 클래스의 기능을 사용하여 구현해야 한다면
합성의 방식을 사용한다고 보면 된다.
가령 학생(Student)이 수강하는 과목(Subject)들이나, 자동차(Car)와 엔진종류(Engine) 간의 관계같이 아주 연관이 없지는 않지만 상속 관계로 맺기에는 애매한 것들을 다루는 것으로 볼 수 있다.
class Car {
Engine engine; // 필드로 Engine 클래스 변수를 갖는다(has)
Car(Engine engine) {
this.engine = engine; // 생성자 초기화 할때 클래스 필드의 값을 정하게 됨
}
void drive() {
System.out.printf("%s엔진으로 드라이브~\n", engine.EngineType);
}
void breaks() {
System.out.printf("%s엔진으로 브레이크~\n", engine.EngineType);
}
}
class Engine {
String EngineType; // 디젤, 가솔린, 전기
Engine(String type) {
EngineType = type;
}
}
public class Main {
public static void main(String[] args) {
Car digelCar = new Car(new Engine("디젤"));
digelCar.drive(); // 디젤엔진으로 드라이브~
Car electroCar = new Car(new Engine("전기"));
electroCar.drive(); // 전기엔진으로 드라이브~
}
}
위의 초기화 코드에서 볼수 있듯이, 마치 new 생성자에 new 생성자를 받는 형식 new Car(new Engine("디젤")) 으로 쓰여진다.
즉, Car 클래스가 Engine 클래스의 기능이 필요하다고 해서 무조건 상속하지말고, 따로 클래스 인스턴스 변수에 저장하여 가져다 쓴다는 원리이다.
이 방식을 포워딩(forwarding)이라고 하며 필드의 인스턴스를 참조해 사용하는 메소드를 포워딩 메소드(forwarding method) 라고 부른다.
그래서 클래스간의 합성 관계를 사용하는데 다른 말로 Has-A 관계라고도 한다.
객체 지향에서 다른 클래스를 활용하는 기본적인 방법이 바로 합성을 활용하는 것이다.
※ 참고
합성을 이용할때 꼭 클래스 뿐만 아니라 추상 클래스, 인터페이스로도 가능하다.
상속 대신 합성을 이용하라
상속의 문제점
자바의 객체 지향 프로그래밍을 처음 배울때 클래스와 상속에 대해 배우기 때문에, 마치 상속이 코드 중복을 제거하고 클래스를 묶는 다형성도 이용할 수 있어서 마치 아주 좋은 객체 지향 기술 처럼 보여 무분별하게 상속을 남발하는 경우가 있다.
하지만 현업에서도 가능하면 extends를 지양하는 편이며 클래스 상속을 해야할때는 정말 개념적으로 연관 관계가 있을 때만 하는 상당히 제한적으로 선택적으로 다뤄진다.
왜냐하면 이제부터 소개할 상속이 갖는 치명적인 특징(단점) 때문에 그렇다.
왜 상속보단 합성(composition)을 사용하라고 권고하는지 그 이유를 알기위해 우선 상속의 단점을 알아보도록 하자.
※ 참고
Java의 창시자인 제임스 고슬링(James Arthur Gosling)이 한 인터뷰에서 "내가 자바를 만들면서 가장 후회하는 일은 상속을 만든 점이다" 라고 말할 정도 이다.
조슈야 블로크의 Effective Java에서는 상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라는 조언을 한다.
따라서 추상화가 필요하면 인터페이스로 implements 하거나 객체 지향 설계를 할땐 합성(composition)을 이용하는 것이 추세이다.
1. 결합도가 높아짐
결합도는 하나의 모듈이 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 의존 정도를 뜻한다.
객체지향 프로그래밍에서는 결합도는 낮을수록, 응집도는 높을수록 좋다. 그래서 추상화에 의존함으로써 다른 객체에 대한 결합도는 최소화하고 응집도를 최대화하여 변경 가능성을 최소화 할 수 있다.
하지만 상속을 하게 되면 부모 클래스와 자식 클래스의 관계가 컴파일 시점에 관계가 결정되어 결합도가 당연히 높아질수 밖에 없다.
컴파일 시점에 결정되는 관계는 유연성을 상당히 떨어뜨리고, 실행 시점에 객체의 종류를 변경하는 것이 불가능하여 유기적인 다형성 및 객체지향 기술을 사용할 수 없다.
예를 들어 클래스 B가 클래스 A를 상속(extends) 한다고 하면, 코드 실행(런타임) 중간에 클래스 C를 상속하도록 바꿀수 없다. (처음 실행되기 전에 미리 그렇게 결정되었으니까)
2. 불필요한 기능 상속
부모 클래스에 메소드를 추가했을때, 자식 클래스에는 적합하지 않는 메소드가 상속되는 문제이다.
예를 들어서 아래 그림과 같이 Animal 클래스에 fly() 라는 메소드를 추가했을때, Tiger 자식 클래스에서는 동작하지 않는 메소드가 되어 버린다.
물론 메소드를 구현하고 빈칸으로 놔두거나, 클래스를 분리하고 분리하여 해결은 할 수 있지만 결국 복잡해질 뿐이다.
※ 참고
물론 이는 인터페이스로 따로 implements 하면서 해결할 수 도 있다.
3. 부모 클래스의 결함이 그대로 넘어옴
만일 상위 클래스에 결함이 있다고 하면, 이를 상속을 하게 되면 부모 클래스의 결함도 자식 클래스에게 넘어오게 된다.
결국 자식 클래스에서 아무리 구조적으로 잘 설계하였다 하더라도 애초에 부모 클래스에서 결함이 있기 때문에 자식 클래스도 문제가 터지게 된다.
4. 부모 클래스와 자식 클래스의 동시 수정 문제
말 그대로 부모 클래스와 자식 클래스 사이의 개념적인 결합으로 인해, 부모 클래스를 변경할 때 자식 클래스도 함께 변경해야 하는 문제를 말한다.
예를들어 다음과 같은 코드에서 Food 부모 클래스에 count 필드를 하나 추가하고 싶다고 해서 추가해버리면,
class Food {
final int price;
Food(int price) {
this.price = price;
}
}
class Bread extends Food {
public Bread(int price) {
super(price);
}
}
public class Main {
public static void main(String[] args) {
Food bread = new Bread(1000);
}
}
class Food {
final int price;
final int count; // 코드 추가
Food(int price, int count) {
this.price = price;
this.count = count; // 코드 추가
}
}
class Bread extends Food {
public Bread(int price, int count) {
super(price, count); // 코드 수정
}
}
public class Main {
public static void main(String[] args) {
Food bread = new Bread(1000, 5); // 코드 수정
}
}
위와 같이 자식클래스는 물론 클래스 호출 부분 까지 전부 수정해주어야 한다.
5. 메서드 오버라이딩의 오동작
자식 클래스가 부모 클래스의 메서드를 오버라이딩할 때 자식 클래스가 부모 클래스의 메서드 호출 방법에 영향을 받는 문제이다.
부모의 public 메소드는 외부에서 사용하도록 노출한 메소드이다. 그런데 상속을 하게 된다면, 자식 클래스에서도 부모 클래스의 public 메소드를 이용할때 의도하지 않는 동작을 수반할 수 있게 될 수 있다.
이는 캡슐화를 위반하였다고 하기도 한다.
※ 참고
여기서 캡슐화란, 단순히 private 변수로 Getter / Setter 를 얘기하는 것이 아니다.
캡슐화(정보 은닉)은 객체가 내부적으로 기능을 어떻게 구현하는지를 감추는 것을 말한다. 그래서 우리는 클래스 자료형을 이용할때 내부 동작을 알필요없이 단순히 메소드만 갖다 쓰면 된다.
단, 내부 동작을 알 필요가 없다는 말은 신뢰성이 보장되어야 한다는 말이기도 하다.
캡슐화가 깨진건 이러한 신뢰성이 깨진것이라고 보면 된다.
다음 코드는 자바의 HashSet을 상속하고 부모의 메소드를 오버라이딩 하여 나만의 Set 클래스를 만들어 구축한 예제 코드이다.
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
class CustomSet<E> extends HashSet<E> {
private int addCount = 0; // 자료형에 몇번 추가되었는지 세는 카운트 변수
@Override
public boolean add(E e) {
// 만일 add되면 카운트를 증가 시키고, 부모 클래스 HashSet의 add() 메소드를 실행한다.
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
// 만일 리스트 자체로 들어와 통쨰로 add 한다면, 컬렉션의 사이즈를 구해 카운트에 더하고, 부모 클래스 HashSet의 addAll() 메소드를 실행한다.
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
public class Main {
public static void main(String[] args) {
CustomSet<String> mySet = new CustomSet<>();
mySet.addAll(Arrays.asList("가", "나", "다", "라", "마"));
mySet.add("바");
System.out.println(mySet.getAddCount()); // ! 6이 나와야 정상이지만 11이 나오게 된다.
}
}
메인 메소드에서 CustomSet 자료형에 스트링 데이터를 addAll() 메소드와 add() 메소드를 통해 총 6개 추가하였다.
원소가 6개 추가되니 당연히 addCount 변수는 6이 되어야 한다. 하지만 실행해보면 6이 아닌 11이 나오게 된다.
도대체 11이라는 숫자는 어디서 튀어나온 것일까?
문제는 부모의 addAll() 메소드를 함부로 오버라이딩 한채 super 키워드로 호출했기 때문이다.
addAll() 메소드 내부 로직을 보면, 매개변수의 길이에 따라 루프를 돌며 add() 메소드를 호출하고 있는 것을 볼 수 있다.
따라서 HashSet을 상속한 CustomSet 클래스에서 addAll() 메소드를 실행했을때 addCount 변수에 10이 더해져 버린 것이다.
이처럼 결국은 제대로 addAll() 동작을 구현하기 위해선 상속한 부모 클래스의 내부 로직을 뒤져서 자세히 살펴봐야 할 필요성이 있게 되며, 또한 만일 상위 클래스의 내부 구현이 달라지면 코드 한 줄 건드리지 않은 하위 클래스도 오동작을 일으 킬수 있다는 잠재적 위험성이 존재한다.
6. 불필요한 인터페이스 상속 문제
자바의 초기 버전에서 상속을 잘못 사용한 대표적인 사례는 java.util.Properties 와 java.util.Stack이다.
두 클래스의 공통점은 부모 클래스에서 상속받은 메서드를 사용할 경우 자식 클래스의 규칙이 위반 될 수 있다는 것이다.
즉, 자식 클래스에게는 부적합한 부모 클래스의 메소드가 상속되기 때문에 자식 클래스 인스턴스의 상태가 불안정해지게 된다.
예를 들어 Stack의 대표적인 동작은 push, pop 이지만, 상속한 Vector 클래스의 add 메소드 또한 외부로 노출되게 된다. 그러면서 아래와 같이 개발자가 add 메소드도 스택 클래스에서 사용할수 있는 메소드인줄 알고 사용했다가, 의도치 않은 동작이 실행되면서 오류를 범하게 된다.
Stack<String> stack = new Stack<>();
stack.push("one");
stack.push("two");
stack.push("three");
stack.add(0, "four"); // add 메소드를 호출함으로써 stack의 의미와는 다르게 특정 인덱스의 값이 추가
String str = stack.pop(); // three
System.out.println(str.equals("four")); // false
따라서 자바 공식 문서에 보면 애초부터 상속을 잘못하여 잘못 설계된 Stack 클래스보다 Deque 클래스를 사용하여 구현할 것을 권장하고 있다.
7. 클래스 폭발(class explosion)
상속을 남용하게 되면, 새롭게 만든 클래스에 하나의 기존의 기능을 연결하기 위해 상속을 하게 될것이고, 또다시 새롭게 만든 클래스에 기능을 연결하기 위해 상속을 하고, 이렇게 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 가리켜 클래스 폭발(class explosion) 문제 또는 조합의 폭발(combinational explosion) 문제라고 부른다.
클래스 폭발 문제는 자식 클래스가 부모 클래스의 구현과 강하게 결합되도록 강요하는 상속의 근본적인 한계 때문에 발생한다.
상속 관계는 컴파일 타임에 결정되고 고정되기 때문에 코드를 실행하는 도중에는 변경할 수 없다.
그래서 부모와 자식 클래스의 구현이 강하게 결합된 상속 관계 상태에서, 다양한 조합이 필요한 상황이 오면 결국 유일한 해결 방법은 조합의 수 만큼 새로운 클래스를 추가해 상속하는 것 뿐이다.
클래스 폭발 문제는 새로운 기능을 추가할 때뿐만 아니라 기능을 수정할 때에도 동일하게 발생한다.
그러므로 이러한 문제를 방지하기 위해서라도 상속보다는 합성을 이용해야 한다.
8. 단일 상속의 한계
자바에서는 클래스 다중 상속을 허용하지 않는다.
그렇기 때문에 상속이 필요한 해당 클래스가 다른 클래스를 이미 상속중이라면 문제가 발생할 수 있다. 결국 클래스를 또 나누고 나누어 구성해야하는데 결국 클래스 폭발로 이어지게 된다.
결국은 다중 상속 한계점 때문에 인터페이스를 사용하듯이 클래스 상속의 근본적인 문제는 단일 상속 밖에 못하다는 것이다.
※ 참고
클래스에 final 키워드를 붙여서 상속을 못하게 하는 문법을 한번 쯤은 들어본 적이 있을 것이다.클래스가 상속이 안되면 객체 지향이 무슨 의미가 있느냐 싶겠지만 위와 같은 문제점으로 이러한 기법이 있는 것이다.
합성을 사용 해야하는 이유
합성은 구현에 의존하지 않는 점에서 상속과 다르다.
왜냐하면 합성을 이용하면 객체의 내부는 공개되지 않고 인터페이스를 통해 코드를 재사용하기 때문에, 구현에 대한 의존성을 인터페이스에 대한 의존성으로 변경하여 결합도를 낮출 수 있기 때문이다.
상속과 합성은 재사용 기법으로 많이 쓰이는 방법 이지만, 대체로 상속을 최소화하고 합성을 사용하기를 권한다.
상속은 자식 클래스 정의에 부모 클래스의 이름을 덧붙이는 것(extends)만으로 코드를 재사용할 수 있으며 쉽게 확장할 수 있다. 그러나 상속을 제대로 활용하기 위해서는 부모 클래스의 내부 구현에 대해 상세히 알아야 하기 때문에 자식과 부모 사이의 결합도가 높아질 수 밖에 없다.
결과적으로 상속은 코드를 재사용하기 쉬운 방법이기 하지만 결합도가 높아지는 치명적인 단점이 있는 것이다.
※ 참고
보통 상속을 IS-A 관계, 합성을 HAS-A 관계로 표현하는데, 이는 단순히 관계를 설명할려고 단어를 만들어 낸 것이 아니다.상속을 지양하고 합성을 지향하라는 말은, 상속은 반드시 어떠한 특정한 관계 일 때만 사용하라고 엄격하게 제한하라는 말이며, 그 관계가 IS-A 관계 인 것이다.따라서 클래스간에 확실히 IS-A 관계로 구성될수밖에 없을때 그때 상속을 하고, 그외의 왠만한 경우에는 합성을 통해 클래스를 구성하면 된다.
또한 상속 관계는 클래스 사이의 정적인 관계인 데 비해 합성 관계는 객체 사이의 동적인 관계이다.
코드 작성 시점에 결정한 상속 관계는 변경이 불가능하지만(컴파일 타임), 합성 관계는 실행 시점에 동적으로 변경할 수 있기 때문이다 (런타임).
그래서 합성을 사용하고 인터페이스 타입을 사용한다면 런타임시에 외부에서 필요한 전략에 따라 교체하며 사용할 수 있으므로 좀 더 유연한 설계를 할 수 있다.
이러한 대표적인 사례가 디자인 패턴 중에 전략 패턴이 될 수 있다.
이처럼 합성은 내부에 포함되는 객체의 구현이 아닌 public 인터페이스 통신에 의존한다.
그래서 합성을 이용하면 포함된 객체의 내부의 구현이 변경되더라도 영향을 최소화 할 수 있기 때문에 변경/수정에 대해 어느정도 안정적인 것이다.
또한 상위 클래스에 의존하지 않기 때문에 변화에 유연하다. 그래서 부모 클래스에 의존하는 상속의 단점들을 대부분 해결 할 수 있다.
합성은 단순히 메소드 호출을 통해 값을 사용하면 되기 때문에 구현이 어렵지도 않다.
그러나 합성에도 단점이 존재하는데, 아무래로 상속과는 달리 클래스간의 관계를 파악하는데 있어 시간이 걸린다는 점이다. 한마디로 코드가 복잡해질 수 있다는 점을 떠안고 있다.
합성을 이용한 상속의 문제점 해결
지금 부터 위의 상속의 문제점에서 다룬 몇가지 케이스를 합성으로 코드를 변환 해보도록 하자.
불필요한 인터페이스 상속 문제 해결
위에서 알아본 Vector를 상속한 Stack 예제이다.
Vector를 상속 받아서 문제가 발생하였기 때문에, Stack 클래스 설계 때부터 상속을 제거하고 Vector 클래스를 내부 필드로 바꾸어 주면 된다.
public class Stack<E> {
private Vector<E> elements = new Vector<>(); // 합성
public E push(E item) {
elements.addElement(item);
return item;
}
public E pop() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() -1);
}
}
이렇게 구성하면, 이제 문제가 되었던 add와 같은 메소드를 사용하는 실수를 원천 봉쇄하였다.
이렇듯 상속을 합성 관계로 변경함으로써 Stack의 규칙이 어겨졌던 것을 막을수 있게 된다.
메서드 오버라이딩의 오동작 해결
HashSet을 상속하는 CustomSet 자료형 예제를 합성으로 변환해보자.
CustomSet 의 경우 addAll() 메서드가 동작하는 과정에서 super 키워드로 부모의 메서드를 호출하여 addCount 변수 증가가 원하는 결과를 같지 못하는 문제가 있었다.
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
class CustomSet<E> {
private int addCount = 0; // 자료형에 몇번 추가되었는지 세는 카운트 변수
private Set<E> set = new HashSet<>(); // 합성
public boolean add(E e) {
addCount++;
return set.add(e); // 합성된 객체의 메서드를 실행
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return set.addAll(c); // 합성된 객체의 메서드를 실행
}
public int getAddCount() {
return addCount;
}
}
public class Main {
public static void main(String[] args) {
CustomSet<String> mySet = new CustomSet<>();
mySet.addAll(Arrays.asList("가", "나", "다", "라", "마"));
mySet.add("바");
System.out.println(mySet.getAddCount()); // 6
}
}
단일 상속 문제 해소
또한 단일 상속 한계를 어느정도 해소해준다.
클래스 객체 기능이 필요하다면 필요한 만큼 필드에 정의해 두어 사용하면 되기 때문이다.
public class Phone {
private RatePolicy ratePolicy; // 클래스 합성
private List<Call> calls = new ArrayList<>(); // 클래스 합성
public Phone(RatePolicy ratePolicy) {
this.ratePolicy = ratePolicy;
}
public List<Call> getCalls() {
return Collections.unmodifiableList(calls);
}
}
참고