Iyoungman Back-end Developer

상속(Inheritance) vs 컴포지션(Composition)


상속의 단점

[1] 캡슐화를 위반한다.

  • 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있기 때문이다.
  • 상위 클래스의 내부 구현이 달라지면 하위 클래스를 고쳐야할 수 있다.


[2] 설계가 유연하지 못하다.

  • 컴파일 시점에 객체의 Type이 정해지기 때문이다.


[3] 다중상속

  • 자바는 다중상속이 불가능하다.
  • 따라서 다른 클래스를 상속받고있다면 추가적으로 상속을 받을 수 없다.


컴포지션이란?

  • 다른객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 메서드를 호출하는 기법이다.
  • 해당 인스턴스의 내부 구현이 바뀌더라도 영향을 받지 않는다.
  • 또한, 다른객체의 인스턴스이므로 인터페이스를 이용하면 Type을 바꿀 수 있다.


적용 예제

이전 예제를 컴포지션으로 바꿔본다.


//ConnectionMaker
public interface ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException;
}

//DConnectionMaker
public class DConnectionMaker implements ConnectionMaker {

    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        // D사 DB Connection 생성코드
        return null;
    }
}

//NConnectionMaker
public class NConnectionMaker implements ConnectionMaker {

    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        // N사 DB Connection 생성코드
        return null;
    }
}

//UserDao
public class UserDao {

    private ConnectionMaker connectionMaker;//컴포지션 사용

    public UserDao(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();
        ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();
        ...
    }
}

//Client
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao userDao = new UserDao(connectionMaker);


언제 상속을 사용할까?

  • 상위 클래스와 하위 클래스의 관계가 정말 is-a 관계일때 사용한다.
    확신할 수 없다면 컴포지션을 사용하자.

HashTable과 Properties

  • 자바 플랫폼 라이브러리에서 위의 원칙을 위반한 예시이다.
    설명에 필요한 Properties API의 일부 코드만 가져왔다.
//Properties
public class Properties extends Hashtable<Object,Object> {

    @Override
    public synchronized Object put(Object key, Object value) {
        return map.put(key, value);
    }

    public synchronized Object setProperty(String key, String value) {
        return put(key, value);
    }

    public void store(Writer writer, String comments)
        throws IOException
    {
        store0((writer instanceof BufferedWriter)?(BufferedWriter)writer
                                                 : new BufferedWriter(writer),
               comments,
               false);
    }
}

//Client
public class Main {

    public static void main(String[] args) {
        File path = new File("./src/main/java/inheritance_composition/TEST_FILE");
        try (FileWriter file = new FileWriter(path)) {
            Properties p = new Properties();
            p.put(1, "id");//상위 클래스의 메서드
            p.setProperty("id", "id");//Properties의 메서드
            p.setProperty("pw", "pw");
            p.store(file, "user");//Runtime Exception!
        } catch (Exception e) {
            System.out.println("fail));
        }
    }
}

Properties는 키와 값으로 문자열만 허용하도록 설계되었다.

  • 따라서 Properties의 setProperty 메서드의 인자인 Key, Value는 String 값이다.


하지만 상위 클래스의 메서드를 호출하면 위와 같은 불변식을 깨버린다.

  • 상위 클래스를 오버라이드한 put 메서드를 호출하면 메서드의 인자인 Key, Value는 Object가 된다.


불변식이 깨지기때문에 store와 같은 Properties API의 메서드를 사용할 수 없다.

  • 위 예제에서 Properties의 Key, Value에 String 이외의 값이 들어갔기 때문에
    store 메서드를 호출했을때 Exception이 발생한다.


예제의 결론

  • 상속보다는 컴포지션을 사용했으면 좋았을 것이다.


Reference


Comments

Content