2012년 5월 8일

인터페이스 디자인하기 - Design Pattern Part1

1. 객체지향언에서 다중상속이 갖는 Diamond problem 란 무엇인가?



그림 1-1 다중상속 다이어그램


그림 1-1 을 보면, 동일한 클래스 A에서 파생된 두개의 클래스 B 와 C 가 있다. 그리고 B 와 C 에서 다중 상속을 사용하여 파생된 클래스 D 가 있다. "Diamond Problem"  은 위의 다이어그램의 모양에서 유래되었다고 한다.

문제는 클래스 D 의 인스턴스 객체를 생성할 때 클래스 A 에 정의된 메소드를 어떻게 호출해야 하는가를 알 수 없게된다는 것이다.

/*
The Animal class below corresponds to class 
A in our graphic above
*/
        
class Animal { /* ... */ }; // base class
{
int weight;

public:

int getWeight() { return weight;};

};

class Tiger : public Animal { /* ... */ };

class Lion : public Animal { /* ... */ } 
      
class Liger : public Tiger, public Lion { /* ... */ }; 


위의 C++ 코드에서 Liger 의 getWeight 함수는 Lion 에서 상속받은 getWeight 또는 Tiger 에서 상속받은 getWeight  함수 중 어떤 함수를 호출해야 하는가를 알 수 없게된다.


자바언어는 다중 상속을 지원하지 않기 때문에 C++ 에서와 같은 "Diamond Problem"은 발생하지 않는다. 자바에는 다중 상속을 인터페이스를 통하여 구현하고 있다.

2. 인터페이스(Interface)와 다형성(Polymorphism)

인터페이스를 이해하기 위한 중요한 키는 다형성(polymorphism)이다.  다형성(polymorphism)은 고대 그리스어에 그 바탕을 두며, "여러가지 모양들" 이라는 의미를 가진다. (여기에서는 클래스가 여라가지 형태를 가질 수 있다는 의미.)

자바언어에서 다형성은 자바 가상머신이 메소드 이름(method name)과 인자의 형태(descriptor) 그리고 메소드가 호출된 객체의 클래스에 기반하여 메소드를 선택하는 다이나믹 바인딩(dynamic binding) 메카니즘에 의해 가능하게 된다.

그림 2-1 Vehicle 의 클래스 계층구조
그림 2-2 추상 슈퍼클래스을 이용한 Vehicle 의 클래스 계층 구조( [Source: Laszlo Böszörmenyi, Carsten Weich; Programming in Modula-3; 1996]  )


public interface Vehicle {

    public long speed();

}

public class SpeedMetor {

    public static long getSpeed(Vehicle  vehicle ){
        return vehicle.speed();
    }
}


예를들어 자바 컴파일러는 어떤 객체의 클래스가 getSpeed() 의 인자로 전달될지 알 수 없다. 오직 인자로 전달되는 객체가 Vehicle 의 구현이라는 것만을 아는 것이다. 앞에서 언급되었던 다이나믹 바인딩(dynamic binding) 은 런타임에 자바가상머신이 인자로 전달되는 객체에 기반하여 메소드를 결정하는 것을 의미한다. 만일 전달되는 객체가 Truck 이라면, 가상머신은 Truck 의 speed() 메소드를 호출할 것이다. 결국 다이나믹 바인딩(dynamic binding)은   다형성(polymorphism)과 수퍼클래스(superclass) 에 대한 서브클래스(subclass) 의 고유성(subsitutability)을 가능하게 하는 메카니즘이라 할 수 있을 것이다.

3. 상속(Inheritance)과 콤포지션(Composition)

객체지향 디자인의 기본 중 하나는 클래스 간의 관계를 구성하는 것이며, 여기에는 상속(inheritance)과 콤포지션(composition) 이라 불리우는 2가지 방법이 있다.

3.1 상속(inheritance)
상속에서는 부모클래스(또는  슈퍼클래스)에 대하여  "부서지기 쉬운(fragile)" 이라는 표현을 자주 사용한다. 이는 부모 클래스의 작은 변화가 다른 수 많은 자식 클래스 코드(또는 서브 클래스)를 수정하는 결과를 가져올 수 있기 때문이다. 만일 부모 클래스가 객체지향 스타일을 사용하여 인터페이스와 구현으로 구분되어 디자인되어 있다면, 부모클래스의 구현을 수정하더라고 다른 연관된 클래스에 영향을 주는 일은 없을 것이다 (부모 클래스 구현을 수정하는 일이 자식 클래스 또는 연관된 클래스를 수정하게 하지 않는다는 의미).  그러나 부모 클래스의 인터페이스를 변경하는 것은 자식 클래스 와 연관된 클래스에  커다란 영향을 주게된다. 예를들어 Vehicle 의 speed 메소드 리턴 타입을 변경하게 되면, 이를 사용하거나 상속하는 모든 클래스에서  이 메소드를의 코드를 수정하여야 한다.

상속은 약한 캡슐화 (weak encapsulation) 라고도 한다. 이는 Automobile 와 같은 서브클래스를 직접 사용하는 코드가 있다면, 이 코드는 부모 클래스 변경을 통하여 Vehicle 과 관계를 파괴시킬 수 있기 때문이다.

public interface Vehicle {

    public long speed();

}


public abstract class  Automobile implements Vehicle {

    public abstract long speed();

}

public class Sedan extends Automobile {

    public long speed(){
        return 100;
    }
} 

3.2 콤포지션(composition)
콤포지션(composition)은 코드 변경이 좀더 쉬운 접근 방식이다. 이방식에서 부모 클래스(슈퍼클래스)는  백엔드(back-end) 클래스가되고, 자식 클래스(서브 클래스)는  프론트 엔드(front-end)가 된다. 상속의 경우는 자식클래스가 오버라이드 하지 않는 부모 클래스의 private 를 제외한 모든 메소드와 변수들을 자동으로 상속하지만,  콤포지션(composition) 에서는 프론트 엔드(front-end) 클래스는 묵시적으로 반듯이 백엔드(back-end) 클래스의 메소드를 호출하여야 한다. 이런 묵시적 호출은 백엔드(back-end) 클래스에 더 강력한  캡슐화 (strong encapsulation) 을 제공하게 된다. 이 경우 백엔드(back-end) 클래스의 메소드의 리턴 타입 변경은 프론트 엔드(front-end) 클래스에 의존하는 어떠한 코드에도 영향을 주지 않게된다. 이런 이유에서 상속과 비교하자면 아주 유연하다고 말할 수 있다.

public class Sedan extends Automobile {

    public long speed(){
        return 100;
    }
} 
public class NewSedan implements Automobile {

    private Sedan sedan = new Sedan();

    public long speed(){
        return sedan.speed() ;
    }
} 

3.3 상속(inheritance) 과 콤포지션(composition) 비교
콤포지션(composition)에서 묵시적 메소드 호출 (forwarding 또는 delegation ) 접근 방법은 상속된 슈퍼클래스 메소드에서 구현하는 인터페이스 메소드 호출과 비교하면 성능면에서 많은 비용을 치루어야 한다. 유연성 (flexibilility) 에 대한 비용으로 볼 수 있다.   유연성(flexibilility) 측면에서  콤포지션(composition) 의 장점은
  • 상속(inheritance) 관계의 관련 클래스보다 콤포지션(composition) 관계의 관련 클래스를 변경하는 것이 쉽다. 
  • 콤포지션(composition)에서는 필요한 시점이전까지 백엔드(back-end) 클래스 생성을 지연할 수 있다(필요하지 않다면 생성하지 않음..). 또한 백엔드(back-end) 클래스 객체를 프론트 엔드(front-end)객체의 라이프타임(lifetime) 동안 동적으로 변경할 수 있다. 상속의 경우, 서브클래스가 생성될때 서브클래스의 인스턴스에서 슈퍼클래스의 인스턴스를 얻기 때문에 서브클래스의 라이프타임(lifetime) 동안 슈퍼클래스는 서브 클래스의 일부로 남아있게 된다. 
유연성(flexibility) 측면에서 상속(inheritance)의 유일한 장점은

  • 다형성(polymorphism)이라는 특성이 있어 새로운 서브클래스(상속)를 추가하는 것이 새로운 프론트 엔드(front-end) 클래스을 만드는 것보다 쉽다. 만일 슈퍼클래스 인터페이스에 대한 의존성을 가지는 코드가 있다면, 이 코드는 변경 없이 새로운 서버클래스와 동작할 수 있다. 콤포지션(composition)에서는 인터페이스를 사용하여 구현되지 않는다면 어려운 일이다. 
마직막으로  상속(inheritance) 은 그다지 안전하지 않다. 그러나 콤포지션(composition) 에서도 인터페이스를 사용하면 다형성을 구현하는 것이 가능하다.

interface Vehicle {

    long speed ();
}

class Automobile {

    public long speed () {
        System.out.println("Automobile ");
        return 1;
    }
}

class Sedan implements Vehicle {

    private Automobile auto = new Automobile ();

    public long speed () {
        return auto.speed();
    }
}

class SpeedMetor{

    static long getSpeed (Vehicle item) {
        return item.speed();
    }
}

class Example {

    public static void main(String[] args) {

        Sedan sedan = new Sedan ();
        SpeedMetor.getSpeed(sedan );
    }
}

새로운 클래스를 추가한다면 아래와 같이 할 수 있다.

class Convertible implements Vehicle {

    private Automobile auto = new Automobile ();

    public long speed () {
        return auto.speed();
    }
} 

이 단일 디자인은 콤포지션(composition)의 유연성과 다형성(polymorphism)의 유연성를 모두 가질 수 있다.

출처 

One Programmer's Struggle to Understand the Interface by Bill Venners First Published in JavaWorld, November 1998

댓글 없음:

댓글 쓰기