본문 바로가기

이펙티브 자바

[이펙티브 자바] 아이템 18. 상속보다는 컴포지션을 사용하라

상속 : 한 클래스가 다른 클래스의 속성과 메서드를 확장 혹은 재정의할 수 있도록 해주는 매커니즘

// 상속의 예시 : 
// 부모 클래스: 동물
class Animal {
    public void eat() {
        System.out.println("이 동물은 먹는다.");
    }
}

// 자식 클래스: 개
// Dog 클래스는 Animal 클래스를 상속받음으로써 "개는 동물이다"라는 Is-a 관계를 형성합니다.
class Dog extends Animal {
    public void bark() {
        System.out.println("개는 짖는다.");
    }
}

 

컴포지션 : 하나의 클래스가 다른 클래스의 인스턴스를 포함하여, 그 인스턴스의 메서드를 활용하는 방식

// 컴포지션의 예시 : 
// 독립된 기능을 가진 클래스: 소리 생성기
class SoundMaker {
    public void makeSound(String sound) {
        System.out.println(sound);
    }
}

// 개 클래스에서 소리 생성기를 사용
class Dog {
    private SoundMaker soundMaker;

    public Dog() {
        this.soundMaker = new SoundMaker();
    }

    // 전달 메서드: Dog 클래스의 bark 메서드는 내부적으로 SoundMaker의 makeSound 메서드를 호출합니다.
    public void bark() {
        soundMaker.makeSound("멍멍!"); // 위임: Dog 객체는 짖는 기능을 SoundMaker 객체에 위임합니다.
    }
}

 

 

상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다. 잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만들게 된다. 

상속의 문제점 :

  • 강한 결합 : 부모 클래스의 내부 변경이 자식 클래스에 영향을 줄 수 있어 유연성이 저하된다.
  • 캡슐화 위반 : 자식 클래스가 부모 클래스의 구현 세부 사항에 의존하게되면, 캡슐화가 약화된다.
  • 재사용성 저하 : 특정 구현에 강하게 결합된 상속 구조는 새로운 상황에 재사용하기 어렵다.

이러한 문제를 모두 피해가기 위해 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이런 설계를 컴포지션(compostition : 구성) 이라고 한다.

새 클래스의 인스턴스들은 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환한다. 이 방식을 전달(forwarding) 이라고 하며, 새 클래스의 메서드들을 전달 메서드라 부른다.

 

컴포지션과 전달(위임)의 장점 : 

  • 낮은 결합도 : 객체간의 결합도를 낮춰, 유연성과 확장성을 향상시킨다.
  • 캡슐화 강화 : 내부 구현을 숨기고, 필요한 인터페이스만 노출시켜 캡슐화를 강화.
  • 재사용성 향상 : 구성 요소를 쉽게 교체하거나 재사용할 수 있어, 다양한 상황에 맞게 시스템을 조정할 수 있다.

Is-a 관계는 상속을 이용하여 한 클래스가 다른 클래스의 특별한 형태임을 나타내는 관계다.

래퍼 클래스는 기본 데이터 타입의 값을 객체로 변환해야 할 때 사용된다.

// 래퍼 클래스의 예
int i = 5; // 기본 타입 int 사용

Integer integerObject = new Integer(5); // Integer 객체 생성 (래퍼 클래스 사용)
Integer autoBoxedInteger = 5; // 오토 박싱을 통한 Integer 객체 생성

// 오토 언박싱: Integer 객체에서 int 기본 타입 값으로 자동 변환
int unboxedInt = integerObject;

//오토 박싱은 기본 데이터 타입의 값을 해당하는 래퍼 클래스의 객체로 자동 변환하는 과정을, 
//언박싱은 그 반대 과정을 의미합니다.

 

 

결론 : 상속은 강력하지만 캡슐화를 해친다. 상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계일 때만 써야 한다. is-a 관계일 때도 하위 클래스의 패키지가 상위 클래스와 다르고, 상위 클래스가 확장을 고려해 설계되지 않았다면 여전히 문제가 될 수 있다. 이를 해결하려면 컴포지션과 전달을 사용해야 한다. 특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱 그렇다. 래퍼 클래스는 하위 클래스보다 견고하고 강력하다.