저번주에 스터디원인 '케이'가 상속과 조립은 어떤 차이가 있고, 무엇이 더 좋을까?
라는 질문을 해주었었다. 그리고 이와 관련된 주제가 이번주 스터디 주제인 재사용: 상속보다 조립
이다. 여기서 그 답을 찾아보자!
"우리는 상속이 무엇인가?" 라고 물으면 보통 공통된 기능을 묶어 상위클래스에 두고, 하위클래스에서의 코드의 재사용성을 높이는 것입니다. 라고 답하는 것이 일반적인 경우이다.
과연 위의 답이 "상속"이 무엇인지에 대한 정확한 답이 될 수 있을까?
상속의 사용할 경우 발생할 수 있는 몇가지 단점들을 살펴보고 상속을 다시 정의해보자. -> '상속은 그럼 언제?' 에서
상속을 사용하면 상위 클래스의 변경을 어렵게 만든다.
예를 들어 다음과 같은 구조를 이루는 클래스들이 존재한다고 하자.
[출처: 개발자가 반드시 정복해야할 객체지향과 디자인패턴(최범균), 89p]
만약 'AbstractController' 클래스가 변경되었다고 하자. 그렇다면 이는 'AbstractUrlViewController' 와 'BaseCommandController' 클래스에 변경을 유발할 수 있다. 왜냐하면 어떤 클래스를 상속한다는 것은 그 클래스에 "의존"한다는 것이기 때문이다.
그리고 이렇게 영향을 받은 'AbstractUrlViewController' 와 'BaseCommandController' 클래스에 의존하는, 즉 이 두 클래스를 상속하고 있는 그 하위의 클래스들의 변경 또한 유발할 수 있게 된다. 즉, 상위클래스에서의 변경이 파동처럼 계층도를 따라서 전파되는 것이다.
정리하면, 상속 계층을 따라 상위 클래스의 변경이 하위 클래스에 영향을 주기 때문에, 최악의 경우 상위 클래스의 변화가 모든 하위 클래스에 영향을 줄 수 있고, 이는 클래스 계층도에 있는 클래스들을 한 개의 거대한 단일 구조처럼 만드는 결과를 초래한다.
[출처: 개발자가 반드시 정복해야할 객체지향과 디자인패턴(최범균), 89p]
두번째로는 Class의 불필요한 증가를 야기할 수 있다는 것이다.
예를 들어, 유저의 데이터를 가공하여 반환해주는 기능을 하는 클래스(Process)가 있다고 하자. 그런데 여기에 "압축" 기능과 "암호화" 기능을 추가해달라는 요구사항이 들어오게 되었다.
그러면 우리는 Process를 상속하는 CompressedProcess와 EncryptedProcess를 생각해볼 수 있다.
그런데 여기서 갑자기 "캐싱"하는 기능 또한 추가된다고 하면 우리는 다시 또 CacheableProcess 라고 하는 하위 클래스를 구현하게 될 것이다.
이렇게 필요한 기능이 추가될 수록 해당 기능을 확장하는 과정에서의 클래스 증가가 발생하게 된다.
마지막으로는 상속의 오용이다. 상속은 "IS-A" 관계일 때 사용해야 적합하다.
예를 들어 '동물' 이라고 하는 상위 클래스가 있다고 하면 하위 클래스로 '척추동물', '연체동물' 등이 오게 되고, '척추동물' 아래에는 '사람', '강아지', '고양이' 등이 올 수 있게 되는 것이다.
(사람은 척추동물이다. 척추동물은 사람이다. 와 같이 이들은 "IS-A" 관계가 성립한다.)
교재에서 아주 좋은 예시를 들고 있다.
[출처: 개발자가 반드시 정복해야할 객체지향과 디자인패턴(최범균), 91p]
Container라고 하는 클래스가 있고, 이는 ArrayList 클래스를 상속하고 있는 구조로 설계하였다.
이 때 Container 클래스는 최대용량(int maxSize)가 존재하며 수화물을 놓을 때 마다 currentSize를 수화물 크기만큼 증가시킨다.
public void put(Luggage lug) throws NotEnoughSpaceException {
if (!canContain(lug)) {
throw new NotEnoughSpaceException();
}
super.add(lug);
currentSize += lug.size();
}
그런데 상위 클래스인 ArrayList에서 제공하는 기능인 add() 메소드를 Container 클래스에서 그대로 아무런 제약없이 사용 가능하지 않을까? 그리고 그렇게 사용한다면 put() 메소드에와 같이 현재 Container 클래스의 여분 size를 검사하지 못하므로 잘못된 동작을 유발하게 된다. (컨테이너의 크기를 초과하여 수화물을 적재할 수 있게 된다.)
이런 문제가 발생하는 이유는 Container "IS - A" ArrayList
가 아니기 때문이다. 즉 Container는 ArrayList가 아니다.
이렇게 상속을 오용하게 되면 상위 클래스에서 제공하는 기능을 잘못 사용할 수 있게 되는 문제가 발생할 수 있다.
composition(조립)
은 여러 객체를 묶어 더 복잡한 기능을 제공하는 객체를 만들어내는 것이다.
이렇게 말해서는 이해가 잘 가지 않을 수 있다.
쉽게 말해서 한 객체가 다른 객체에 대한 의존을 가지고(보통 필드로 다른 객체를 참조하는 방식) 사용한다는 의미이다.
이렇게 되면 앞서 보인 예시인 Process
와 같은 경우 각 기능을 가지는 객체를 Process
에서 사용하는 방식으로 설계할 수 있다. 그렇게 되면 우리는 기능이 추가되어도 앞서 상속과는 달리 불필요한 클래스의 증가를 방지할 수 있게 된다.
또한 composition
을 사용하면 실행시간에 사용할 객체를 교체할 수 있다. 앞서 상속의 경우에는 우리가 기능을 변경하고 싶을 때 다른 클래스를 상속하도록 변경을 하고 다시 컴파일을 하는 과정을 거쳐야하지만 composition
을 사용하면 실행시간에 유연하게 변경이 가능하게 된다.
하지만 그렇다고 모든 상황에서 Composition
을 사용하라는 것은 아니며, 이를 먼저 고려해보라는 의미이다.
Composition
또한 런타임에 구조가 복잡해진다는 단점이 존재하고, 구현이 어렵다는 단점이 존재하지만 우리가 계속해서 예시나 리팩토링을 진행하며 느껴왔듯이, 구현의 어려움보다는 변경의 유연함을 얻는게 더 좋기 때문에 기능을 재사용 해야 하는 경우에는 Composition
을 먼저 고려하자.
위임은 말 그대로 어떤 객체가 수행해야하는 기능을 다른 객체로 떠넘긴다라는 의미를 담고 있으며 위의 Composition
또한 다른 객체로 책임을 위임하는 방식이다.
(Process에 Composition
을 적용한다면 압축 기능을 다른 객체로 위임한다고 표현할 수 있다.)
앞서 잠깐 언급한 것과 같이 우리는 필드로써 Composition
을 보통 이루지만 어떤 메소드 내에서 객체를 생성하고 해당 객체의 메소드를 호출을 요청한다고 해도 위임
이란 의미에서 벗어나지 않는다.
public abstract class Figure {
public boolean contains(Point point) {
Bounds bounds = new Bounds(x, y, width, height);
return bounds.contains(point.getx(), point.getY());
// 메소드에서 생성한 인스턴스의 contains() 메소드 호출
// (데미테르의 법칙이 지켜진다.)
}
}
그럼 상속은 언제 사용해야 할까? 위의 글만 읽어보면 상속은 사용할 일이 전혀없는 단점 투성이이고, Composition만 쓰면 될 것 같다.
하지만 상속도 적절한 상황에 사용하게 되면 장점이 있다고 생각한다.
예를 들어 IS-A 관계가 명확한 경우에 사용하면 상속의 장점을 볼 수 있으며, 또 재사용
의 관점 보다는 기능의 확장
의 관점으로 보고 상속을 적용하면 적절하겠다 판단이 되면 사용하면 좋을 것이다.
즉, 내가 내린 사용을 사용하는 때는 다음과 같다.
우선 상속이란 "코드의 재활용"을 위한 즉 "재사용"을 위한 개념은 아니다. 상속은 "연관된 일련의 클래스들에 공통적인 규약을 정의"하기 위해 사용하는 것이고 재사용의 관점보다는 "기능의 확장(extends)의 관점"이다.
따라서 명확한 IS-A 관계이고, 상위 클래스의 기능을 확장해 나가는 경우 사용하면 좋다. 하지만 처음 상속을 적용했음에도 향후에 '상속의 단점'들이 보이게 된다면 그 때 리팩토링 과정을 통해Composition
으로 전환해보자.
위의 내가 정리한 상속
의 개념과 사용과 관련해서..
저번주 스터디 질문에 상속(extends)와 구현(implements)의 차이는 무엇인가?
에 대한 질문이 올라왔고, 해당 질문에 대한 답을 생각하며 내가 생각하는 상속
의 개념에 대한 정리를 잠깐 해보았다.
[Chapter 3 다형성과 추상 타입] 상속(extends)과 구현(implements)의 차이는 무엇일까요?