이제 현실세계에서 객체지향의 세계로 넘어갈 차례다. 아직까지도 왜 객체지향 언어가 다른 언어에 비해 더 유연한 언어인가에 대한 정확한 답을 얻지는 못했을 것이다. 객체지향 언어가 유연한 이유를 간단히 말하자면 현실세계의 유연성을 그대로 프로그래밍 언어에 접목시켰기 때문이다. 객체지향 언어가 가진 고유한 특성들을 이제부터 이야기 할텐데, 이 특성들과 유연성은 완벽하게 매치된다. 이제껏 객체지향의 특성들을 일단 공부하고 외우고 나서 어느 정도 객체지향 개념을 이해하게 된 사람들이나 이제 막 객체지향 언어에 발을 들여 놓은 사람들 모두 현실 세계의 유연성을 일단 이해하고 객체지향 개념을 접하게 된다면 그 개념이 왜 필요한지, 그리고 그것이 어떻게 유연성을 만들어 내는지를 정확히 이해할 수 있게 될 것이다.


객체지향 4대 특성

객체지향 언어는 다른 언어들과 차별화된 특성을 가지고 있다. 그리고 이러한 특성들 중에서 중요한 것을을 객체지향 4대 특성이라고 부른다. 


추상화(Abstraction)

유연성을 확보하는 각 단계 중에서 공통점을 추출하고 차이점을 감추는 작업에 대해서 이야기 했다. 이 단계에서 했던 일을 요약하자면 다음과 같다. 우리는 여러 요구사항들로부터

    1. 공통점을 추출하였고,

    2. 불필요한 공통점을 제거하였다.

오케스트라의 비유에서 우리는 연주한다는 공통점을 추출해냈고, 같은 국적이라는 공통점을 추출해 냈지만 국적은 연주와 상관없는 정보이기 때문에 제거하였다. 객체지향 언어에서 추상화(Abstraction)라고 부르는 특성은 우리가 유연성을 확보하기 위해 했던 과정과 동일하다. 추상화라는 말은 구체적인 것을 제거한다는 말이다. 이것은 여러 요구사항에 대한 공통 요약본을 만드는 과정이다. 따라서 무턱대고 추상화를 할 수는 없다. 우선 제거되는 대상은 공통되지 않은 것이어야 한다. 그리고 우리가 만드는 것은 요약본이기 때문에 공통점들 중에서도 목적한 바와 맞지 않는 것들은 다시 제거해야 한다. 이러한 과정을 통해서 문제와 밀접한 관계가 있고, 모든 요구사항들이 공통적으로 가지고 있는 부분들이 추출된다.



추상화(Abstraction)의 과정



캡슐화(Encapsulation)

캡슐화는 "캡슐로 만들다"라는 의미이다. 우리는 이미 "대상화" 한다는 개념을 통해 이 특성을 익혔다. 캡슐화의 의미는 크게 3가지로 나눌 수 있다.

1. 공통점들의 묶음에 명칭을 부여한다. 이 "묶음"은 다룰 수 있는 "대상"이 된다.

2. 속성과 행위를 하나로 묶는다.

3. 실제 구현 내용 일부를 은닉한다.


일반적으로 캡슐화라고 하면 2번과 3번을 떠올린다. 그리고 그것 만으로도 어느 정도 이해하는데 도움이 되기는 한다. 하지만 정작 중요한 것은 1번이다.

우리는 유연성에 대한 설명 중 이상화 시인의 시 "꽃"에 대한 이야기를 했었다. 2번처럼 속성과 행위를 하나로 묶었다고 하더라도 거기에 "명칭"이 부여되지 않으면 다룰 수 있는 "대상"이 되지 못한다. 일반적으로 객체지향에서 이 "명칭" 이라는 것은 타입(Type)이라고 말하고, "대상"은 객체를 말한다. 즉, 다룰 수 있는 대상인 객체가 되려면 타입을 가져야 한다는 말이다. 아래 코드는 이 캡슐화의 예이다.


class Musician{ // 명칭을 부여했다.

    public void play(){ /* nothing to do */ }

    public void stop(){ /* nothing to do */ }

} 

Musician은 이제 우리가 추출한 공통점들의 명칭이 된다. 그리고 중괄호({})의 시작과 끝에 의해서 그 명칭이 의미하는 내용이 묶여진다. 중괄호를 통해 공통점들이 묶이고 Musician이라는 명칭이 부여된 것이다. 명칭, 즉 타입(Type)이 중요한 이유는 이후 다른 객체지향 특성을 알고 나면 매우 명확해지게 될 것이다. 일단 아래의 코드를 보고 한가지만 이야기 하고 넘어가겠다.

Musician musician = new Musician();

위의 코드 한 줄은 다음과 같은 의미를 지닌다.

    1. 등호( = ) 오른쪽은 Musician 타입의 객체를 생성하는 코드이다. 

    2. 등호 왼쪽은 musician이라는 참조를 선언하는데 그 타입은 Musician 타입이다.

    3. 이 한 줄의 코드를 통해 Musician 타입의 객체가 생성되고, 이 객체는 Musician 타입의 참조 객체를 통해 다루어진다.

객체를 생성하고 객체와 타입의 참조를 통해 객체를 다룬다는 말이다. 같은 타입이니 당연히 객체를 다루는 것은 문제가 없다. 이제 다음에 나올 개념들을 이해할 차례다.


상속(Inheritance)

이제 우리는 공통점에 대해 해야 할 일들을 마쳤다. 그러면 이제 다양한 요구사항들의 차이점들에 집중할 때가 되었다. 오케스트라의 비유에서 각 악기 연주자들은 서로 다른 악기를 다룬다. 따라서 이들 악기로부터 나오는 소리는 모두 다 다를 것이다. 하지만 이들은 모두 연주한다(play()), 멈춘다(stop())는 공통점을 가지고 있어야 한다. 특히 유연성의 관점에서 보면 각 악기 연주자들은 연주한다와 멈춘다를 통해서 다루어져야 한다.

이것은 이미 만들어져 있는 공통점을 개별 악기 연주자가 이용할 수 있어야 함을 의미한다. 객체지향 언어에서 이미 만들어져 있는 타입을 다른 객체가 물려 받아 이용하는 것을 상속(Inheritance)이라고 한다.

상속을 통해 다른 객체로부터 물려 받는 것은 세가지가 있다.

    1. 타입(Type)

    2. 필드(field, = 속성 = 변수)

    3. 메소드(method, = 행위, 함수)

이 중에서 가장 중요한 것을 고르라면 당연히 타입이다. 일반적으로 많은 객체지향 관련 자료들이 놓치는 것이 상속을 통해 타입을 상속받는다는 사실이다. 이 타입 상속에 대한 이해가 없이는 객체지향을 온전히 이해할 수 없다. 타입 상속은 다음에 나올 다형성의 중요한 기반이 된다.

일단 상속이 어떤 방식으로 이루어지는지부터 알아보자. 우리는 공통점을 추출해 낸 Musician이라는 타입을 이미 가지고 있다. 그리고 이제 우리는 구체적인 요구사항, 즉 개별 악기 연주자들을 정의해야 한다. 그리고 개별 악기 연주자는 이미 정의된 Musician으로도 다루어 질 수 있어야 한다. 따라서 Musician이라는 타입을 "상속" 받아야만 한다. 일단 하나만 해보도록 하자.

class FluteMusician extends Musician{

    public void play(){

        System.out.println("playing flute");

    }

    public void stop(){

        System.out.println("stop flute");

    }

} 

FluteMusician 클래스는 Musician 클래스를 상속 받는다. 이를 통해 FluteMusician 클래스는 FluteMusician이라는 자신의 타입도 가지면서 동시에 Musician이라는 타입도 가지게 된다. 또한 Musician 클래스가 가지고 있던 공통점들, 즉 연주하다(play())와 멈추다(stop()) 역시 Musician 클래스로부터 상속을 받는다.(예제에서는 일단 이들 공통점들을 "재정의" 하고 있다.)

여기서 주목할 것은 타입과 함께 공통점들을 물려 받는 것이 왜 중요한가 하는 부분이다. 


다형성(Polymorphism)

다형성은 객체지향의 꽃이라고 불린다. 다형성이란 다(많은) + 형(타입) + 성(특성)이 합쳐진 말로, 하나의 대상이 여러 개의 타입을 가진다는 것을 의미한다. 하나의 대상이 여러개의 타입을 가진다는 것이 무슨 의미일까?

우리는 이미 다형성을 만들어내는 기법을 알고 있다. 바로 상속이다. 상속은 상속을 받는 클래스에게 자신의 타입과 필드와 메소드를 전달해 주는 역할을 한다. 앞서서 이 세가지 중에 가장 중요한 것이 타입이라고 말했다. 이 타입을 물려 줌으로써 물려 받은 클래스는 다형성을 가진 클래스가 되기 때문이다.

위의 예제에서 FluteMusician은 자기 자신의 타입을 가진다. 그와 함께 Musician을 상속 받음으로써 Musician 타입으로도 취급이 될 수 있다. 아래 코드를 보자.

FluteMusician fm = new FluteMusician();

위의 코드는 FluteMusician 클래스를 객체화 하고 FluteMusician 타입으로 다루겠다는 의미를 지닌다. 아래 코드를 한 번 더 보자.

Musician m = new FluteMusician();

위의 코드는 똑같이 FluteMisician 클래스를 객체화 하지만 Musician 타입으로 다루겠다는 것을 의미한다. 이처럼 똑같은 객체가 여러 타입으로 취급될 수 있는 특성이 다형성이다.


그렇다면 왜 다형성이 객체지향의 꽃이라고 불리는가?

우리는 유연성에 대한 예로부터 Musician 객체를 추출해냈다. 그리고 각각의 클래스가 가진 차이점을 나타내는 클래스들 중에서 FluteMusician이라는 클래스를 만들어 보았다. 이 FluteMusician 클래스는 Musician 클래스를 상속 받도록 되어 있다. 이제 하프 연주자, 피아노 연주자도 같은 방식으로 만들어 볼 수 있다.

class HarpMusician extends Musician{

    public void play(){

        System.out.println("playing harp");

    }

    public void stop(){

        System.out.println("stop harp");

    }

}

class PianoMusician extends Musician{

    public void play(){

        System.out.println("playing piano");

    }

    public void stop(){

        System.out.println("stop piano");

    }

}

이제 3명의 연주자로 구성된 오케스트라(좀 빈약 하지만)를 지휘한다고 생각해보자. 만약 다형성이 없었다면, 즉 FluteMusician은 오직 FluteMusician이라는 타입만 가질 수 있고, Musician 타입을 가질 수 없다면, 그리고 다른 클래스들도 이와 같다면 연주는 다음과 같이 이루어질 수 밖에 없다.

public void playAll(){

    FluteMusician fm = new FluteMusician();

    HarpMusician hm = new HarpMusician();

    PianoMusician pm = new PianoMusician();

    // ......

    fm.play();

    hm.play();

    pm.play();

    // ......

    fm.stop();

    hm.stop();

    pm.stop();

    // ......

}

개별 악기 연주자들은 서로 공통점으로 묶어 다룰 수 없다. 다형성이 없을 경우 각각의 클래스는 자기 고유 타입 한가지만 가질 수 있기 때문이다. 따라서 각각의 클래스를 통해 만들어진 객체들은 한번에 취급될 수 없고 각각 다뤄질 수 밖에 없다. 마치 플루트 연주하세요 / 하프 연주하세요 / 피아노 연주하세요 라고 지휘자가 따로따로 외치는 것과 같다. 객체지향에서 다형성이 없다는 것은 현실 세계에서는 마치 개별 연주자들을 공통된 명칭, 즉 연주자라고 부를 수 없는 것과 같다. 플루트 연주자는 현실 세계에서 플루트 연주자이면서 동시에 그냥 연주자라고 불릴 수 도 있다. 하프 연주자도 하프 연주자로 불릴 수 있지만 그냥 연주자라고 불릴 수도 있다. 따라서 현실 세계에서는 연주자 여러분 연주하세요라고 말하면 자연스럽게 동시에 모든 악기 연주자들이 연주를 시작할 수 있다.


객체지향에서는 다형성을 이용하여 현실 세계에서의 동시 연주 효과를 낼 수 있다. 다형성을 통해 개별 연주자 객체들은 Musician이라는 공통 타입을 통해 다루어질 수 있다.

public void playAll(){

    List<Musician> musicians = new ArrayList<Musician>();

    musicians.add(new FluteMusician());

    musicians.add(new HarpMusician());

    musicians.add(new PianoMusician());

    // ......

    musicians.forEach(each -> each.play());

    musicians.forEach(each -> each.stop());

}


개별 연주자들은 각자 고유한 타입을 가지면서도 Musician이라는 타입으로 다뤄질 수 있다. 따라서 Musician 타입 객체를 넣을 수 있는 musicians라는 List에 함께 집어 넣을 수 있다. 그리고 forEach와 같은 순회 메소드를 이용하여 Musician 클래스가 가진 play() 메소드나 stop() 메소드를 동시에 호출함으로써 동시 연주와 같은 효과를 낸다.


유연성과 객체지향 특성

자 이제 다시 한번 정리를 해보자. 우리는 현실세계에서 여러가지 방식으로 유연성을 확보하고 있다. 이미 익숙한 방식을 재활용하는 것은 훌륭한 전략이다. 따라서 그리고 객체지향 언어는 현실 세계에서의 유연성 전략을 그대로 따르고 있다.

현실 세계에서의 유연성은 4가지 단계로 나뉘어진다. 그리고 이 단계들은 객체지향의 4대 특성과 1:1로 매칭된다.

일단 다루어야 할 여러 요구사항의 공통점을 찾고 문제 해결과 관계가 없는 공통점들을 제거한다. 이 과정을 객체지향 언어에서는 추상화(Abstraction)라고 한다.

그런 다음에는 공통점을 묶어 대상화 한다. 객체지향에서는 이를 캡슐화(Encapsulation) 라고 한다.

이 다음에는 개별적인 차이점들을 구현해 주어야 한다. 이 때 이미 뽑아 놓은 공통점은 모든 차이점들에게도 포함을 시켜 줘야 하는데, 이 과정을 객체지향에서는 상속(Inheritance)이라고 한다.

이제 차이점 대신 공통점을 통해 대상을 다루어야 한다. 공통점을 통해 대상을 다루려면 각각의 요구사항들이 공통점을 통해 다뤄 질 수 있어야 하는데, 객체지향에서는 이를 다형성(Polymorphism)을 통해 지원한다.

Posted by 이세영2
,