객체지향은 참 어렵다.

이제 객체지향을 그래도 좀 이해하고 프로그래밍을 한다는 생각을 하고 있지만 객체지향 언어로 개발을 해 오면서도 그 개념을 제대로 이해하지 못하고 지낸 시간이 훨씬 더 길다는 점을 생각해보면 지금의 이해한 것을 다른 사람들에게 잘 설명하는 것이 얼마나 힘든 일일지 상상이 간다.


개인적인 경험으로 보면, 객체지향 언어를 배우고 잘 사용하게 될 때까지의 난관을 보면 대략 다음과 같다. 


처음 : 개념 및 문법에 대한 이해

처음에는 객체지향 언어 문법을 열심히 공부하게 된다. 객체지향 언어의 난감한 점은 우리가 풀어 내려고 하는 문제에 대한 직접적인 구현(변수 선언, 함수 선언, 조건문, 반복문, 연산자 등)을 이해하는데 들어가는 노력에 비해 객체지향 개념(클래스, 객체, 캡슐화, 상속, 다형성?, 재정의, 인터페이스, 추상 클래스 등) 자체를 이해하는 것이 훨씬 더 어렵다는 점이다. 어려운 이유는 간단하다. 객체지향 개념이 필요한 이유를 문법을 공부하는 당시까지는 전혀 알 수 없기 때문이다. 하지만 이 단계를 일단 거치고 나면 이제 마치 객체지향을 제대로 다룰 수 있겠다는 생각이 든다.


난관 : 잘 짜여진 객체지향 코드를 접했을 때

이제 어떻게든 클래스와 객체도 알고, 돌아가는 프로그램을 작성할 줄도 알게 된다. 그래서 개발에도 속도가 좀 붙고, 기능을 이렇게 저렇게 만들어 가게 된다. 그러다가 어느 순간 오픈 소스 혹은 잘 짜여진 라이브러리들을 접하고 충격을 받게 된다. 처음에는 너무 난잡해 보여서 이해가 가질 않는다. 어떤 기능이 어떻게 동작하는지를 보려면 이 클래스, 저 클래스 파일을 넘나 들어야 하고, 굳이 상속을 왜 받아서 하위 클래스를 이해하는데 상위 클래스로 이동해야 하고, 상속을 넘어서 인터페이스로 넘어 가면 구현부가 없는 것을 보고 당황해 해야 한다. 온갖 기교를 써가면서 어떻게든 여러 클래스를 같은 클래스 혹은 인터페이스에 대한 하위 클래스로 만들기 위해 노력하는 이유를 대체 알 수가 없다.


깨닫기 전 : 이제 잘하고 있다고 생각했는데

이제 상속을 통한 기능의 확장에 대해서도 이해하고 나름 중복 코드를 보고 줄일 줄 알게 되고, SRP 쯤은 알아서 기능도 잘 구분해서 구현할 줄도 알게 되었다. 하지만 그래도 누군가 와서 내가 만든 코드를 보고 이렇게 이야기 한다. 객체지향의 핵심은 다형성이다, 인터페이스를 중심으로 설계하라, 상속을 통해 기능을 확장하지 마라 등등.



개인의 능력에 따라서는 이 과정이 생각보다 짧았을 수도 있을 것이다. 하지만 개인적인 경험에 비춰 보자면 앞으로 이야기 할 내용을 미리 알고 있었을 정도가 되었다면 객체지향을 접한지 최소 5년은 되었다고 봐야 한다. 그보다 더 오래되었어도 이해하지 못하는 경우도 있다.(좋은 개발 선배가 있었다면 모르지만.) 혹시 아래 내용을 보고 "겨우 그거 이야기 하려고 이런거야?" 라고 하신다면 내 능력이 부족한 것이니 할 말은 없다. 아까운 시간을 빼앗아 죄송스럽다는 말을 할 수 밖에.



이제 본론으로 넘어가서 그러면 과연 상속이란 무엇인가? 우선 위키를 찾아보면 다음과 같다.


1. 객체 지향 프로그래밍(OOP)에서, 상속은 객체들 간의 관계를 구축하는 방법이다. 

2. ......클래스는 기존의 클래스로부터 속성과 동작을 상속받을 수 있다.

3. 그 결과로 생기는 클래스를 파생 클래스, 서브클래스, 또는 자식 클래스라고 한다. 상속을 통한 클래스들의 관계는 계층을 형성한다.


사실 이 내용 중에는 이후에 이야기 할 내용에 딱 맞는 내용이 없다. 하지만 살펴볼 가치는 있다.


우선 2번이 적절한 정의로서 확 와 닿을 것이다. 일반적으로 상속이라는 의미는 단어가 가진 의미와 마찬가지로 무엇인가를 "물려 받는다"는 의미이기 때문이다. 즉, 상위 클래스가 가진 속성(변수)과 행위(멤버 함수)를 내려 받는 것을 상속이라고 보통 말한다. 이것이 이해하기 쉬운 이유는 상속이라는 단어의 실생활 속에서의 의미와 가장 유사한 개념이고, 프로그래밍을 할 때 변수와 함수를 물려 받아 재 구현해야 하는 번거로움을 없애준다는 점에서 실질적인 이득을 주기 때문이다.


아주 마음에 들지는 않지만 1번과 3번이 그나마 좀 더 객체지향을 이해하는데 도움이 되는 정의이긴 하다.


자 그러면 저 정의들이 명시적으로 보여주지 못한 가장 중요한 부분은 무엇일까?


잘 이해가 안 갈 수도 있기 때문에 질문을 살짝 바꿔서, 상속을 받았을 때, 상속 받은 것 중에서 가장 중요한 것은 무엇인가?











정답은 "타입"이다.



사실 2번의 정의는 핵심을 빠뜨렸다. 클래스는 기존 클래스로부터 가장 중요한 것인 "타입"을 물려 받고, 속성이나 동작 "따위"의 소소한 것을 물려 받는다가 정확한 정의다.


객체지향에서 객체를 바라 볼 때 가장 중요한 것은 "외부적 관점"과 "내부적 관점"의 분리이다. 익히 알고 있는 속성과 행위가 아니다. 내부적 관점이라는 것은 객체의 동작을 어떻게 구현할 것인가 하는 점이다. 이 때 보이는 것이 속성과 행위이다. 이것을 잘 구현해야 하는 것은 당연한 것이다. 하지만 이것보다 중요한 것이 외부적 관점이다. 외부적 관점은 "그 객체를 어떤 객체로 인식할 것이며, 어떻게 다룰 것인가?" 이다.


만약 어떤 객체가 실제로는 Rectangle이라고 해도 (Shape을 상속 받았다 치고) 외부에서 인식하기를 Shape이라면 그것은 외부적 관점에서는 Shape이다. 그 실체가 Rectangle이라는 사실은 그 객체 내부에서나 중요한 것이지 밖에서 그 객체를 사용하는 입장에서는 아무런 의미가 없는 것이다. 그 객체는 그냥 Shape일 뿐이다. 오케스트라의 지휘자는 자신의 연주자들이 자신의 지휘에 맞춰 연주할 수 있느냐에 관심이 있지 그 사람이 바이올린 연주자인지 첼로 연주자인지는 관심을 가질 필요가 없다. 그저 지휘만 하면 알아서 연주를 할 것이고, 그것이면 충분하다.


그렇다면 외부적 관점, 즉 "타입"이 왜 그렇게 중요한지 간단한 예제를 통해 살펴보자.


List<Rectangle> rectangles = new ArrayList<Rectangle>();

rectangles.add(new Rectangle(1,2,3,4));

rectangles.forEach(each -> System.out.println(each));

List<Color> colors = new ArrayList<Color>();

colors.add(new Color(255,0,100));

colors.forEach(each -> System.out.println(each));


크게 이해하는데 어려움은 없을 것으로 생각된다. 두 개의 List를 만들어서 각각 Rectangle과 Color를 저장할 수 있도록 하고, 객체 하나씩을 만들어 넣은 후 이를 for 문을 이용해서 출력하는 구문이다. 반복되는 느낌은 있지만 어찌하겠는가? Rectangle과 Color는 엄연히 "타입"이 다른데!


그러면 다음을 보자.


List<Object> objects = new ArrayList<Object>();

objects.add(new Rectangle(1,2,3,4));

objects.add(new Color(255,0,100));

objects.forEach(each -> System.out.println(each));


예상했겠지만 둘의 출력 결과는 같다. 이것이 왜 가능한지도 이미 알고 있을 것이다. Java의 모든 객체는 Object 클래스를 상속 받았기 때문이다. 즉 Object "타입"이다. 코드 상에서, 즉 객체의 외부에서 (실제 객체는 서로 다르더라도) Object 타입으로 불려지게 되는 순간, 둘의 차이는 사라진다. 그리고 그와 함께 중복된 코드들도 함께 사라진다. 만약 서로 다른 종류의 객체가 훨씬 더 많았더라면 더욱 많은 코드들이 마법처럼 사라졌을 것이다. 마치 오케스트라 지휘자가 바이올린 연주하세요, 첼로 연주하세요, 플룻 연주하세요......와 같이 끝나지 않을 것 같은 XXX 연주하세요를 반복하지 않는 것과 같다. 부조리함이 많은 현실 세계에서도 이런 합리성을 추구하는데 매일 논리와 싸우는 우리 같은 사람들이야 어떠하겠는가?


간단한 예제였지만 상속의 진정한 가치와 의미를 이해하는데는 어려움이 없었을 것으로 생각된다. 상속 개념에는 분명 속성과 행위를 내려 받아 중복된 코드를 작성하는 것을 방지해 준다는 이점도 있다. 하지만 상속이 진정한 위력을 발휘 하는 것은 이러한 내부적 관점이 아니라 "타입" 상속이라는 외부적 관점이다. 이 관점이 위력적인 이유는 아무리 실질적으로는 다른 객체들이라도 동일하게 취급할 수 있고, 동일하게 취급되는 순간 유사하긴 하지만 반복적으로 구현되었던 수많은 중복 코드들을 사라지게 만들 수 있다는 점이다. 이러한 관점에서 보면 속성이나 행위 따위를 상속 받는 것은 매우 소소한 것으로 취급될 수 밖에 없다.


그래서 한 번 쯤 더 생각해 볼만한 부분이 바로 인터페이스와 추상 클래스의 개념이다. 인터페이스는 속성과 실제 구현부라는 실체는 눈 씻고 찾아봐도 없고 눈으로 보기에는 빈 껍데기에 불과한 API만을 제공한다. 하지만 이 빈껍데기는 "외부적 관점"을 제공해 주고, 이 관점을 이용하는 외부에서 그 객체를 다루는데는 전혀 어려움이 없다. 따라서 같은 인터페이스를 상속 받은 객체들에 대해서는 같은 관점을 유지할 수 있고, 같은 관점을 유지할 수 있다는 것은 동일하게 취급할 수 있다는 것이고, 이들 객체에 대해서 별도의 코드들을 작성할 필요가 없다는 것이다. 추상 클래스의 경우 인터페이스에 비해 조금 더 많은 것을 주긴 하지만 물려 받은 것에 비해 역시 더 중요한 것은 추상 클래스의 "타입"이다. 이 "타입" 이라는 외부적인 관점을 이용해서 인터페이스와 같은 위력을 발휘할 수 있다. 덤으로 내부적 관점에서의 중복 코드도 줄일 수 있다는 장점과 함께.


타입이 같다는 것이 주는 또 하나의 장점은 적절한 시기에 기존의 객체 대신 실질적으로는 다른 객체로 대체시킬 수 있다는 점이다. 이것은 마치 컴퓨터라는 하드웨어에 내부 소프트웨어만 바꿔 실행하면 여러 기능을 하는 범용 컴퓨터와 유사한 개념이다. 이것은 구조적으로 동일하게 구현된 코드 상에서 서로 다른 로직을 구현한 객체들을 바꾸어 가면서 사용함으로써 소스 코드의 양을 줄이면서도 다양한 기능을 수행하게 만들 수 있다는 말이다. 또한 기능의 확장을 위해서 다른 코드를 수정하지 않고 동일한 "타입"의 새로운 객체 하나만 만들어 넣으면 된다. 이것은 수정이나 기능의 확장을 위해서도 도움이 된다는 말이다. 


만약 상속을 통해 외부적 관점의 통일성, 즉 "타입"을 상속 받지 못하고 단순히 속성과 기능만을 상속할 수 있는 것이었다면 이러한 이점은 생각지도 못할 일이다. 아무리 속성과 기능을 동일한 클래스로부터 상속 받았다 해도 하위 클래스들은 서로 엄연히 다른 관점으로 봐야 했을 것이고, 그렇다면 어떤 방법으로도 이들을 다루는 코드들을 통합 시킬 수 없었을 것이다. 그리고 기존의 객체를 새로운 객체로 간단히 교체한다는 것은 상상하기도 힘들었을 것이다. 상속은, 특히 "타입"의 상속은 이처럼 위대한 개념이다.


혹시 이러한 개념을 잘 생각하지 않고 있었던 분들이라면 이 타입 상속이라는 개념을 염두해 두고 어떻게 하면 동일한 외부 관점을 유지시킬 수 있을 것인지 항상 생각하면서 프로그래밍을 했으면 좋겠다. 그것이 개발자로서의 수명(신체적인 수명까지도;)을 길게 가져가는 길이기도 하니까 말이다.


Posted by 이세영2
,