객체지향 개념들간의 관계 UML


간략히 설명하자면 다음과 같다.

- OOP : Object Oriented Programming

- OOP는 유연성(Flexibility)을 가진다.

- 유연성은 캡슐화와 추상화, 다형성을 이용해서 달성된다.

- 상속은 캡슐화를 활용하고, 다형성은 상속을 이용해서 만들어진다.

- 캡슐화를 통해서 가시성(Visibility) 개념이 만들어지고, 클래스 개념이 만들어진다.

- 클래스는 타입과 필드, 그리고 메소드를 가지고 있다.

- 클래스 개념은 인터페이스(Interface), 추상 클래스(Abstract Class), 구체 클래스(Concrete Class) 개념을 파생시킨다.

- 객체(Object)는 구체 클래스(ConcreteClass)가 가진 개념을 포함한다.

- 객체는 Identity를 가지고 있다.(구체클래스를 통해 클래스가 가진 개념도 가지게 되므로 Method, Field, Type도 가지게 된다.)

- 메소드는 프로토타입을 가지며, 추상 메소드는 메소드의 일종이다.

- 필드는 Reference와 Primitive로 나뉜다.

- 클래스와 메소드, 필드는 가시성을 가진다.

- 클래스는 상속 가능(Inheritable)하다.

- 메소드는 재정의 가능(Overridable)하고, 오버로딩 가능(Overloadable)하다.

Posted by 이세영2
,

중간 정리

이쯤 해서 왜 "객체지향의 올바른 이해"라는 포스팅을 만들게 되었는지 그 동기를 설명하고 넘어가는 편이 전체적인 이해에 도움이 될 것이라고 생각한다.

이전 포스팅들에서 객체지향 4대 원칙이 나온 배경을 유연성의 예를 통해 설명하였다. 개인적으로 여러 책을 읽어보고 검색을 해 봤지만 이와 같은 방식으로 객체지향을 설명한 글은 없었다. 하지만 포스팅의 내용을 이해했다면 현실 세계에서 나온 개념을 프로그래밍 언어에서 차용했다고 보는 편이 더 타당하다는 점을 알 수 있을 것이다. 개인적으로 이 부분은 나름대로 확신하는 편이다. 하지만 그렇다고 해서 굳이 이미 사람들이 객체지향을 이해하고 있는 방식을 바꿔서 설명해야 할 필요가 있겠는지 하는 의문은 생길 것이다. 나는 그것이 필요하다고 봤다.


객체지향을 설명하는 일반적인 방식은 이렇다.

추상화(Abstraction)에 대해서는 공통된 속성과 행위를 추출하는 것으로 주로 설명한다. 이 설명이 틀린 것은 아니다. 하지만 공통된 속성과 행위를 추출하는 과정의 목적이 명확하지 않다. 유연성을 확보하기 위한 목적으로 설명하지 않기 때문에 추상화 과정이 필요한 이유를 설명하기가 어렵다. 그래서 어떻게든 가져다 붙이는 설명이 "속성과 행위를 중복 구현하지 않기 위해서"라고 말한다. 만약 추상화의 목적이 중복 구현의 방지, 즉 재사용이라면 일반적인 언어에서 말하는 작은 규모의 재사용, 즉 라이브러리와 무엇이 다른지를 명확히 해야 한다. 하지만 애초에 목표가 제시되지 않은 상태에서 추상화의 목적을 설명하기 때문에 그 본질적인 용도가 불명확해지는 것이다.

애초에 설명이 이렇게 시작하다보니 다른 개념들도 점차 모호해지기 시작한다. 캡슐화는 속성과 행위의 묶음이고, 이들 중에서 일부는 외부에 노출시키지 않는 것, 소위 정보 은닉을 통해 객체 내부 변경이 외부에 영향을 적게 미치도록 만든다고 말한다. 그래서 객체지향이 다른 언어보다 좋다고 이야기 한다. 캡슐화의 가장 큰 이점은 추상화를 통해 만들어진 객체(추상 객체라고 하자)가 그 고유한 타입을 가지게 되고, 이 추상화된 객체의 타입(추상 타입이라고 하자)을 통해서 여러 개별 객체가 공통으로 다루어 질 수 있음으로써 유연성이 확보된다는 점이다. 이 과정에서 캡슐화의 의미가 "공통의 속성과 행위에 타입을 부여하여 다루어질 수 있는 대상으로 만드는 것"이라는 정의를 성립시킬 수 있다. 따라서 캡슐화에서 가장 강조되어야 하는 부분은 타입의 부여이다. 하지만 캡슐화를 단순한 속성과 행위의 묶음 또는 일부 속성이나 행위를 외부에 노출시키지 않는 것이라고 정의한 덕분에 다른 개념들의 설명은 더욱 모호해진다.

상속에 대해서는 다른 객체의 속성과 행위 "만"을 가져 오는 것으로 보통 설명한다. 앞서 강조했듯이 캡슐화된 객체에 있어서 가장 중요한 것은 타입이다. 상속은 타입을 상속 받음으로써 그 타입으로 다뤄질 수 있도록 만드는 것이 핵심이다. 그래야 구체적인 타입이 아닌 상위 타입, 즉 추상 타입으로 다뤄지면서 유연성을 확보할 수 있게 된다.

다형성(Polymorphism)의 경우 역시 왜곡이 심하다. 다형성을 메소드 오버로딩과 혼돈하는 경우가 많고, 기껏해야 상위 타입으로 호출된 메소드를 하위 타입의 메소드로 대체되어 호출되는 것 정도로 말하는 경우가 대부분이다. 다형성은 하나의 타입이 서로 다른 타입으로 지칭될 수 있는 특성이다. 이 다형성은 객체지향 언어에서 상속을 통해 확보된다. 상속이라는 특성은 속성과 행위 뿐 아니라 타입도 상속시킨다. 이를 통해 상속을 받은 클래스는 자기 고유 타입 뿐 아니라 상위 타입도 가지게 된다. 이러면 가지고 있는 타입이 두 개가 되는데 이것이 다형성(multiple type)이다. 따라서 다형성이라는 단어만으로는 유연성을 이해하기 어렵다.


많은 소프트웨어 개발자들은 새로운 언어를 빨리 배우는 것이 자신의 능력과 연결된다고 생각한다. 그것 자체는 틀린 것이 아니다. 하지만 C언어와 같은 절차지향 언어를 쓰다가 Java와 같은 객체지향 언어를 배우게 되는 경우에도 이런 생각에 사로잡힌다. 특히 개발자들은 자신이 하는 분야에 대한 자부심이 크기 때문에 다른 분야 업무를 경시하는 경향이 있다. C언어와 객체지향 언어는 사용되는 분야가 많이 다르다. 언어를 빨리 배워야 뛰어난 개발자라는 생각과 다른 분야에 대한 경시가 객체지향 언어에 대한 이해 부족으로 나타난다. 빨리 배워서 기능 하나 빠르게 개발해서 보여주고 "쉽네!" 라고 빨리 말할 수 있어야 한다는 강박관념에서 언어를 배우는 것이다. 일반적인 형식의 객체지향 설명이 추구하는 방향이 딱 이런 정도다.

객체지향 언어는 애초에 프로그래밍을 시작할 단계에서부터 유연성을 염두해 두고 시작해야 하는 언어다. 프로그램을 구성해야 하는 요소(즉 객체)가 둘이 되었을 때부터 둘 간의 관계에 대해서 생각해보고, 이들의 관계가 확장될 소지가 있는지, 즉 유연성을 확보해야 할 필요가 있는지를 고려해야 한다. 또 이미 구현되어 있는 프로그램이라고 해도 서로 유사성이 있는 요소는 없는지, 그리고 유사한 요소들을 함께 다룰 수 있도록 함으로써 유연하고 간결한 프로그램으로 만들 수는 없는지를 생각해야 한다. 이런 과정에서 유연성을 확보하는 방법에 대한 일종의 청사진을 가지고 있다면 그 일이 매우 수월해질 것이다. 이것이 현실 세계에서 오케스트라 지휘자의 문제를 보여주고, 이를 객체지향 4대 원칙과 비교하면서 설명하게 된 계기이다.


이것을 통해 얻은 것은 무엇인가? 

우선 객체지향 언어를 배우는 과정에서 많이 간과되거나 잘못 설명되었던 개념들을 정확하게 정립할 수 있었다. 특히 객체지향 언어에서 "타입"이 얼마나 중요한지를 재발견할 수 있게 해주었다. 이제껏 일반적인 객체지향 도서들이나 블로그 들에서는 타입에 대해 거의 언급하지 않았거나 단편적으로만 설명했을 뿐이다. 하지만 이 포스팅을 통해서 객체가 속성과 행위의 집합이 아닌 타입과 속성과 행위의 집합으로 이해될 수 있었다. 그리고 캡슐화가 타입을 부여하는 과정이라는 점, 상속은 속성과 행위만이 아니라 타입까지도 물려 준다는 점, 마지막으로 다형성의 의미가 여러 타입을 가질 수 있는 특성이라는 점까지 이해할 수 있게 되었다.

객체지향 언어의 4대 특성이 왜 도입되었는지를 알 수 있었다. 객체지향이 실제 세계의 반영이라는 점은 많은 글들에서 설명하고 있다. 그러나 단순히 실세계를 반영하기 위해서 4대 특성을 도입했다는 것은 설득력이 부족하다. 앞서서도 이야기 했듯이 객체지향의 개념은 절차지향에 비해 이해하기가 어렵다. 굳이 더 어려운 개념을 도입해서 언어를 개발하고, 그렇게 개발된 언어가 지금처럼 널리 쓰일 수 있게 된 이유가 있어야만 한다. 유연성이라는 목적은 그 이유에 부합한다. 그리고 이해하기 어려운 개념임에도 불구하고 유연성이라는 목적을 명확히 이해하고 사용하면 객체지향을 빠르게 익히는데 많은 도움이 된다.

객체지향 언어의 목적이 유연성이라는 점을 이해하고 있고, 이 목적에 맞게 4대 특성을 이해하고 있으면 이제 디자인 패턴과 같은 설계적인 특성에도 빨리 다가갈 수 있다. GoF의 디자인 패턴 23가지 중 약 10가지는 그 목적이 유연성 확보에 있고, 앞서 설명한 유연성 확보 방식을 기반으로 구현된다. 나머지 패턴들 중 일부는 일부 특수한 목적을 위한 것이거나 객체지향 4대 특성의 일부를 활용한 것들이다.


Posted by 이세영2
,

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


객체지향 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
,