프로그래밍-학습기록/객체 지향 프로그래밍

재사용, 상속보다는 조립으로

leesche 2021. 1. 7. 20:28

재사용: 상속보단 조립

상속을 사용하면 재사용을 쉽게 할 수 있는 것은 분명하다. 하지만 상속을 사용할 경우 몇 가지 문제점이 있다. 이 글에서는 상속을 통한 재사용 과정에서 발생할 수 있는 문제점을 살펴보고, 또 다른 재사용 방법인 객체 조립을 통해 상속을 통한 재사용의 단점을 해소하는 방법을 알아본다.

상속을 통한 재사용의 단점

상위 클래스 변경의 어려움

어떤 클래스를 상속받는다는 것은 그 클래스에 의존한다는 뜻이다. 따라서 의존하는 클래스의 코드가 변경되면 영향을 받을 수 있다. 즉, 상위 클래스에서 변경의 여파가 하위 클래스로 전파된다.
최악의 경우 상위 클래스의 변화가 모든 하위 클래스에 영향을 줄 수 있다. 이는 클래스 계층도에 있는 클래스들을 한 개의 거대한 단일 구조처럼 만들어 주는 결과를 초래한다.

클래스의 불필요한 증가

예를 들어, 파일 보관소를 구현한 Storage 클래스가 있을 때, 제품이 출시된 이후 보관소의 용량을 아낄 수 있는 방법을 제공해달라는 요구가 발생했다. 이 요구를 수용하기 위해 Storage 클래스를 상속 받아 압축 기능을 추가한 CompressedStorage 클래스를 추가한다. 또 보안이 문제가 됐기 때문에 EncryptedStorage 클래스를 추가한다. 그러면 다음과 같은 클래스 계층도 만들어진다.

  • Storage
    • CompressedStorage
    • EncryptedStorage

그런데, 만약 압축을 먼저 하고 암호화 하는 저장소가 필요하다면 어떡할까? 또는 암호화를 먼저 하고 압축을 해 달라고 하면? 아니면 성능 향상을 위해 캐시를 제공하는 저장소가 필요하고, 추가로 암호화된 저장소에 캐시를 적용하려면?
상속을 통해서 이 기능을 구현하려면 CompressedStorage클래스와 EncryptedStorage 클래스를 상속받은 CompressedEncryptedStorage 클래스를 만들어야 한다. 하지만 자바에서는 두 개의 클래스를 동시에 상속 받는 것은 불가능하기 때문에 별도의 기능 구현이 필요하다.
이렇게 필요한 기능의 조합이 증가할수록, 상속을 통한 기능 재사용을 하면 클래스의 개수는 함께 증가하게 된다.

상속의 오용

예를 들어, 어떤 개발자가 ArrayList를 상속 받아 Container 클래스를 구현했다. Container 클래스에서는 put 메서드를 사용해야 한다. 하지만 개발자가 Container 클래스의 인스턴스를 사용할 때 자동완성으로 나오는 ArrayList의 add 메서드를 사용한다면 문제가 발생한다.

이렇듯 '같은 종류'가 아닌 클래스의 구현을 재사용하기 위해 상속을 받게 되면, 잘못된 사용으로 인한 문제가 발생하게 된다.

조립을 이용한 재사용

객체 조립(composition)은 여러 객체를 묶어서 더 복잡한 기능을 제공하는 객체를 만들어내는 것이다.

객체 지향 언어에서 객체 조립은 보통 필드에서 다른 객체를 참조하는 방식으로 구현된다.

한 객체가 다른 객체를 조립해서 필드로 갖는다는 것은 다른 객체의 기능을 사용한다는 의미를 내포한다.

앞선 예에서 Storage에 기능을 추가할 때, 새로 하위 클래스를 만드는 것이 아니라 해당 기능을 제공하는 클래스를 조립해서 사용하면 된다. 따라서 불필요한 클래스 증가를 방지할 수 있다.

조립을 사용하면 상속을 잘못 사용해서 발생했던 문제도 제거된다.

조립 방식의 또 다른 장점은 런타임에 조립 대상 객체를 교체할 수 있다는 것이다. 상속의 경우 소스 코드를 작성할 때 관계가 형성되기 때문에 런타임에 상위 클래스를 교체할 수 없다.

상속에 비해 조립을 통한 재사용의 단점은 다음과 같다.

  • 상대적으로 런타임 구조가 더 복잡해진다.
  • 상속보다 구현이 더 어렵다.

하지만 장기적 관점에서 구현/구조의 복잡함보다 변경의 유연함을 확보하는 데서 오는 장점이 더 크기 때문에, 기능을 재사용해야 할 경우 상속보다는 조립하는 방법을 먼저 고려해야 한다.

위임

위임은 내가 할 일을 다른 객체에게 넘긴다는 의미를 담고 있다. 보통 조립 방식을 통해 위임을 구현한다.

보통 위임은 조립과 마찬가지로 요청을 위임할 객체를 필드로 연결한다. 하지만, 꼭 필드로 정의해야 하는 것은 아니다. 위임의 의도는 다른 객체에게 내가 할 일을 넘긴다는데 있으므로, 객체를 새로 생성해서 요청을 전달한다 해도 위임이란 의미에서 벗어나지 않는다.

🗒️ 위임을 사용하면 해당 객체가 바로 실행할 수 있는 것을 다른 객체에 한 번 더 요청하게 된다. 이 과정에서 메서드 호출이 추가되기 때문에 실행 시간은 다소 증가한다. 연산 속도가 매우 중요한 시스템에서는 많은 위임 코드가 성능에 문제를 일으킬 수 있다. 하지만 대부분의 경우, 위임으로 인해 발생하는 성능 저하 보다 위임을 통해서 얻을 수 있는 유연함/재사용의 장점이 더 크다.

상속은 언제 사용하나?

상속은 재사용이라는 관점이 아닌, 기능의 확장이라는 관점에서 상속을 적용해야 한다. 또한 추가로 명확한 IS-A 관계가 성립되어야 한다.

단, 최초에는 명확한 IS-A 관계로 보여서 상속을 이용해 기능을 확장했다 하더라도, 이후에 클래스의 개수가 불필요하게 증가하는 문제가 발생하거나 상위 클래스의 변경이 어려워지는 등 상위 클래스를 상속받을 때의 단점이 발생하다면, 조립으로 전환하는 것을 고려해야 한다.


참고 및 출처

  • 책 <개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴>

'프로그래밍-학습기록 > 객체 지향 프로그래밍' 카테고리의 다른 글

주요 디자인 패턴  (0) 2021.01.15
DI(Dependency Injection)와 서비스 로케이터  (0) 2021.01.14
SOLID 설계 원칙  (0) 2021.01.11
다형성과 추상 타입  (0) 2021.01.06
객체 지향  (0) 2021.01.05