Polymorphism, 그리고 Interface

5386 단어 OOPOOP

객체지향에는 여러가지 실전적 요소가 있겠지만, 그중 큰 비중을 차지하는 다형성(polymorphism)을 실제로 코드에 적용할 수 있는 부분이 상속과 인터페이스이다.

상속은 흔히들 알고 있는 extends키워드를 이용하여 부모 클래스(superclass)의 메서드나 멤버 변수를 사용할 수 있게 하는 방법이다.

아, 그건 알고 있어요. 상속은 엄청 쉽던데요? 전체적인 코드량도 많이 줄여주더라구요.

인터페이스는 객체의 동작 방식을 정해 놓은 약속이라고 할 수 있다.

public interface Smartphone {

    public void call();
    public void takePicture();
    ...
    
}

"스마트폰은 전화와 사진 찍기 기능이 필요하다" 라고 약속을 해 놓고, 해당 인터페이스를 이용해서 만든 객체들은 모두 해당 기능들을 반드시 구현해서 만들어야 한다.

public class GalaxyTwenty implements Smartphone {

    @Override
    public void call() {
    	System.out.println("call someone");
    }
    
    @Override
    public void takePicture() {
    	System.out.println("cheese!");
    }

}

그것도 알고 있어요. 인터페이스는 여러 개발자들이 협업할 때 일종의 약속을 정하는 것이죠?

맞는 말이다. 하지만 뭔가 2% 부족했다.

솔직히 처음 자바를 공부할 때나 지금이나, 인터페이스가 뭔지는 알겠는데 실제로 어디에 쓰여야 좋을지 감이 잘 오지 않았다. 인터페이스를 써 가며 협업해야 할 만큼 규모가 있는 프로젝트도 없었고, 협업의 목적 외에는 인터페이스를 어디에 써먹어야 할 지 알지 못했다.

인터페이스도 상속처럼 전체적인 코드량을 줄여 주는 효과가 있지 않을까요?

인터페이스는 코드량을 줄여 주는 것이 아니다. 오히려(조금이지만) 코드량을 늘려 버린다. 대신, 인터페이스는 다형성이라는 특징을 이용해서 여러 객체들 사이의 결합도를 낮추어 주는 역할을 할 수 있다.

인터페이스는 실제로 구현되는 객체에 대해 모른 채로 작성한다. 인터페이스는 구현되는 객체에 대해 알 필요도 없다. 인터페이스가 객체에 대해서 모른다는 것은 둘 사이는 느슨한 결합을 만들었다는 것을 뜻한다.

다형성이나 느슨한 결합이 뭐에요?

public class You {

    private IPhoneX yourPhone;
    
    public void callYourFriend() {
    	yourPhone.iPhoneCall();
    }
    
    ...

}

당신이 스마트폰을 하나 가지고 있다고 해 보자. 당신은 아이폰X를 갖고 있다. 그런데, 당신은 실수로 폰을 분실해서 다른 폰을 사야 한다.

인터페이스로 구현되지 않았다면, 해당 부분을 수정할 때 You라는 클래스를 찾아서 IPhoneX라는 타입을 새로 살 폰인 GalaxyTwenty로 변경하고, GalaxyTwenty에 정의된 전화걸기 메서드로 기존 메서드를 수정해야 할 것이다.

public class You {

    private GalaxyTwenty yourPhone;
    
    public void callYourFriend() {
    	yourPhone.galaxyCall();
    }
    
    ...

}

매번 핸드폰을 바꿀 때마다 이런 작업을 하는 것은 매우 비효율적이다.

여기서 인터페이스가 빛을 발한다. 아까 전의 예시처럼 만약 모든 스마트폰을 하나의 인터페이스 타입으로 구현하자고 약속해 놓았다면?

public class You {

    private Smartphone yourPhone;
    
    public void callYourFriend() {
    	yourPhone.call();
    }
    
    ...

}

인터페이스 Smartphone타입으로 객체를 선언했다.

만약 위 코드가 정상적으로 컴파일된다면, 당신은 매번 핸드폰을 바꿀 때 마다 해당 부분의 코드를 수정할 필요가 전혀 없을 것이다.

핸드폰을 바꿀 때마다 타입이 바뀌어야 하지 않나요? 갤럭시면 Galaxy myPhone, 아이폰이면 IPhone myphone... 처럼 인스턴스를 선언해야 될 것 같은데... 인터페이스는 약속인데, 인터페이스로 객체를 선언해 버리면 컴파일이 안 될 것 같은데요?

다형성은, 여러 타입의 객체를 하나의 타입으로 여길 수 있다는 개념이다. 모든 스마트폰은 interface Smartphone을 상속했기 때문에, 아래와 같이 선언할 수 있다.

public class GalaxyTwenty implements Smartphone { ... }

public class IPhoneX implements Smartphone { ... } 

...

Smartphone myPhone = new Smartphone();
Smartphone myPhone = new IPhoneX();

// also possible
GalaxyTwenty myPhone = mew GalaxyTwenty();

위 코드에서 GalaxyTwenty 타입의 객체와 IPhoneX 타입의 서로 다른 객체가 인터페이스를 상속했기에 하나의 Smartphone 이라는 타입으로 선언할 수 있게 되었다.

다형성이 뭔지 알것 같아요. 그런데 다형성은 대체 무슨 장점이 있나요?

모든 핸드폰은 Smartphone인터페이스를 상속하고 있기 때문에 당신이 어떤 핸드폰을 가져와도 private Smartphone yourPhoneyourPhone.call()부분의 코드는 변하지 않는다. 즉, 폰을 아무리 바꿔도 당신은 You클래스에 손 하나 대지 않아도 된다. 즉, 두 객체의 결합도를 낮춤으로써 유지 보수가 매우 편리해지는 구조가 된 것이다.

public class You {

    private Smartphone yourPhone;
    
    public void callYourFriend() {
    	yourPhone.call();
    }
    
    ...

}

아까 You클래스가 컴파일 되지 않을 것 같다고 했는데, 정말 You클래스는 컴파일되지 않는다. 왜냐하면 인터페이스는 자신이 만드는 객체에 대한 정보가 없기 때문에, 인터페이스 타입인 Smartphone타입으로 선언한 yourPhone은 실체가 없는 객체인 것이다. 아까도 보았겠지만, 인터페이스를 들여다 보면 추상 메서드만 존재하고 메서드 몸통에는 아무것도 구현되어 있지 않고, 심지어 멤버 변수도 없다(혹시 있더라도 상수로 취급된다).

그렇다면, 인터페이스를 구현한 객체를 이 클래스 내에서 선언하지 말고, 외부에서 받아온다면 어떨까?

public class You {

    private Smartphone yourPhone;
    
    public You(Smartphone phone){
    	this.yourPhone = phone;
    }
    
    public void callYourFriend() {
    	yourPhone.call();
    }
    
    ...

}

의존성 주입(Dependency Injection)이란 마틴 파울러라는 사람이 제시한 개념이다. 코드를 작성할 때는 의존적인 코드는 만들지 말고, 의존성이 필요한 부분은 외부에서 만들어서 주입해주자는 것이다.

생성자를 통해서 yourPhone을 주입받으면, You라는 클래스는 당신이 수백 수천번 스마트폰을 새로 바꾸어도 단 한 단어도 수정하지 않아도 되는, 결합도가 느슨한 완벽한 객체지향적 코드가 되는 것이다.

물론 외부 어딘가에서 해당 인터페이스를 상속한 실제 객체를 만들어서 주입시키는 코드가 있어야 되서, 결국 전체 코드량은 오히려 더 많아진다. 하지만 이미 느껴지지 않는가? 복잡한 시스템에서 인터페이스를 이용하여 의존성 주입을 해 놓으면, 유지보수가 매우 용이할 것이고 각 클래스는 겹치는 관심사가 없이 자신의 일에 몰두할 수 있게 된다.

유지 보수가 그렇게 중요한 것이었군요.

실제로 개발을 하다 보면 고객의 요청이 수시로 바뀌기도 하고, 코드 규모가 매우 커졌을 때 수정해야되는 부분이 생기면(거의 99%확률로 수정해야될 일이 생긴다) 결합도가 높은 코드는 손 댈 수가 없을 정도로 유지 보수가 힘들다. 설사 일일히 객체끼리 연관된 모든 부분을 비교해가며 수정한다 하더라도, 과정에서 에러가 발생할 확률이 높아질 수밖에 없다.

따라서 인터페이스를 사용한 것처럼, 객체지향적인 유연한 코드를 만드는 것은 미래를 위해서 매우 중요하다고 할 수 있다.

좋은 웹페이지 즐겨찾기