[번역]객체 지향 프로그래밍이여 안녕

05 Dec 2016

원본 : Goodbye, Object Oriented Programming

나는 수십년 동안 객체 지향 언어로 프로그래밍해왔다. 내가 처음 사용한 OO 언어는 C++이고 그 다음은 Smalltalk 그리고 마지막으로 .NET과 Java이다.

나는 패러다임의 세 가지 기둥인 상속Inheritance, 캡슐화Encapsulation 그리고 다형성Polymorphism의 이점을 열성적으로 최대한 활용하였다.

나는 재사용Reuse의 약속을 얻고 이 새롭고 흥미진진한 세상의 선각자들이 얻은 지혜를 최대한 활용하기를 열망했다.

나는현실 세계 객체를 그들의 클래스들과 매핑하고 모든 세상이 깔끔하게 자리를 잡을 것으로 예상 할 때 흥분을 감출 수 없었다.

나는 더 이상 잘못을 할 수 없었다.

상속, 실패의 첫 번째 기둥

언뜻보기에, 상속은 객체 지향 패러다임의 가장 큰 장점으로 보인다. 새로

새로 가르치기 위해 정렬된 Shape 계층의 극도로 단순화된 예제들은 모두 논리적으로 말이 되는 것처럼 보인다.

그리고 재사용reuse는 오늘의 단어이다. 아니… 이를 올해의, 어쩌면 영원히.

나는 이 모든 것을 그대로 받아들이고, 나의 새로운 통찰력과 함께 세상으로 돌진하였다.

바나나 원숭이 정글 문제

내 마음속의 종교와 해결해야 할 문제를 가지고, 나는 클래스 계층 구조를 만들고 코드를 작성하기 시작하였다. 그리고 모든 것이 세상과 잘 맞았다.

나는 이미 존재하는 클래스에서 상속받음으로써 재사용Reuse의 약속을 현금으로 바꿀 준비가 된 순간을 절대 잊지 못할 것이다. 이것은 내가 지금까지 기다리고 있었던 순간이었다.

새로운 프로젝트가 시작되었고, 나는 내가 지난 프로젝트에서 좋아했던 그 클래스를 떠올렸다.

문제 없다. 재사용resuse이 구조대다. 내가 해야 할 일은 단지 그 지난 프로젝트에서 클래스를 꺼내 사용하는 것이다.

글쎼.. 실제로는.. 그 클래스 뿐만이 아니다. 우리는 부모 클래스도 필요하다. 하지만.. 그렇다.

어.. 잠깐.. 우리는 부모의 부모도 필요할 거 같은데.. 그 다음.. 우리는 모든 부모들이 필요할 거 같다. 좋아.. 좋아.. 내가 처리하겠어. 문제없다.

이제 됬다. 이제 컴파일이 안된다. 왜?? 아.. 알았다.. 이 객체는 다른 객체를 포함하고 있다.

이제 그것도 필요할 거 같군. 나는 객체의 부모와 그 부모의 부모와 객체에 포함된 모든 것과 그것들이 포함한 것의 모든 부모들과 부모들과 부모들…

Ugh.

다음은 Erlang의 창시자인 Joe Armstrong의 위대한 인용구이다:

객체 지향 언어의 문제점은 그들이 이 모든 암묵적 환경들을 짊어지고 다닌 다는 것이다. 당신은 바나나를 원했지만 당신이 갖게 되는 있는 것은 바나나를 쥐고 있는 고릴라와 정글 전체이다.

바나나 원숭이 정글 해법

너무 깊은 계층 구조를 만들지 않아도 이 문제를 길들일 수 있다. 하지만 상속이 재사용의 핵심이라면, 그 메커니즘에 대한 제한이 재사용의 이점을 제한할 것이다. 그렇지 않은가?

그렇다.

쿨에이드의 건강한 도움을 받고 있는 불쌍한 객체 지향 프로그래머가 해야 할 일은 무엇인가?

Contain과 Delegate이다. 자세한 내용을 보자.

다이아몬드 문제

조만간, 다음 문제가 자신의 흉직함을 들어 올릴 것이다. 그리고 언어에 따라 해결할 수 없는 위기이다.

거의 모든 OO언어들은 이를 지원하지 않는다. 심지어 이것이 논리적으로 말이 되는 것처럼 보일 지라도. OO언어가 이것을 지원할 때의 어려움이 무엇일까?

다음의 pseudocode를 생각해보라:

Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
  function start() {
  }
}
Class Printer inherits from PoweredDevice {
  function start() {
  }
}
Class Copier inherits from Scanner, Printer {
}

ScannerPrinter클래스는 둘 다 start라고 불리는 함수를 구현함을 주목하라.

그렇다면 Copier 클래스는 어떤 start 함수를 상속받아야 하는가? Scanner의 것? Printer의 것? 둘 다는 될 수 없다.

다이아몬드 해법

해법은 간단하다. 그렇게 하지 마라.

그렇다. 그것이 맞다. 대부분의 OO 언어들은 당신이 이렇게 하도록 내버려 두지 않는다.

하지만, 만약 내가 이렇게 모델링 해야 한다면? 나는 나의 재사용을 원한다!

그렇다면 당신은 ContainDelegate를 해야 한다.

Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
  function start() {
  }
}
Class Printer inherits from PoweredDevice {
  function start() {
  }
}
Class Copier {
  Scanner scanner
  Printer printer
  function start() {
    printer.start()
  }
}

여기 Copier 클래스는 이제 PrinterScanner의 인스턴스를 포함contain한다. 이것은 start함수를 Printer클래스의 구현에 위임delegate한다. 스캐너의 경우에도 마찬가지로 쉽게 위임할 수 있다.

이 문제는 아직 상속Inheritance 기둥의 또 다른 균열이다.

깨지기 쉬운 기반 클래스Base class 문제

그래서 나는 계층 구조를 얕게 만들고 순환적cyclical이 되지 않도록 하고 있다. 다이아몬드들은 나에게 없다.

그리 모든 것이 세상과 잘 맞았다. 그것이 나타나기 전까지..

언젠가, 잘 동작하던 내 코드가 다음 날에는 동작하지 않았다. 이건 뜻 밖의 사건이다. 나는 내 코드를 변경하지 않았다.

음, 아마 버그가 있는가 보다.. 그런데 잠깐… 무엇인가 변경되었어.

하지만 그건 내 코드의 변경이 아니야. 내가 상속하였던 클래스에서 변경이 발생하였다.

어떻게 기반 클래스의 변경이 내 코드를 깨지도록 만들 수 가 있지?

이것은 어떻게..

다음 기반 클래스를 상상해보라 (Java로 작성되었지만, 당신이 Java를 모른다고 하여도 이해하기 쉬울 것이다.)

import java.util.ArrayList;
 
public class Array
{
  private ArrayList<Object> a = new ArrayList<Object>();
 
  public void add(Object element)
  {
    a.add(element);
  }
 
  public void addAll(Object elements[])
  {
    for (int i = 0; i < elements.length; ++i)
      a.add(elements[i]); // this line is going to be changed
  }
}

IMPORTANT: 주석 처리된 라인을 확인하라. 이 라인은 나중에 변경되어 일을 망치게 될 것이다.

이 클래스는 자신의 인터페이스에 add()addAll(), 두 개의 함수를 가지고 있다. add()함수는 단일 요소을 추가할 것이고 addAll()add()를 여러 번 호출하여 다수의 요소들을 추가할 것이다.

그리고 이것은 그 파생 클래스이다.

public class ArrayCount extends Array
{
  private int count = 0;
 
  @Override
  public void add(Object element)
  {
    super.add(element);
    ++count;
  }
 
  @Override
  public void addAll(Object elements[])
  {
    super.addAll(elements);
    count += elements.length;
  }
}

ArrayCount클래스는 일반적인 Array클래스의 특화입니다. 유일한 동작상 차이점은 ArrayCount는 요소의 개수인 count를 간직한다는 것이다.

두 클래스들을 자세히 살펴보도록 하자.

Array add()는 요소를 로컬 ArrayList에 추가한다.

Array addAll()는 각 요소에 대해 로컬 ArrayList의 add를 호출한다.

ArrayCount add()는 자신의 부모의 add()를 호출한 뒤 count를 증가시킨다. ArrayCount addAll()는 자신의 부모의 addAll()를 호출한 뒤 count를 요소의 개수만큼 증가시킨다.

그리고 모든 것이 잘 동작한다.

이제 파괴의 변경이다. 기반 클래스 코드의 주석 처리된 라인은 다음과 같이 변경된다:

  public void addAll(Object elements[])
  {
    for (int i = 0; i < elements.length; ++i)
      add(elements[i]); // this line was changed
  }

기반 클래스의 소유자에 관한 한, 그것은 광고한 대로 잘 동작한다. 그리고 모든 자동화 테스트는 여전히 성공한다.

하지만 소유자는 그 파생된 클래스를 잊기 쉽다. 그리고 파생된 클래스의 소유자는 거칠게 자각하게 된다.

이제 ArrayCount addAll()자신의 부모의 addAll()를 호출한다. 내부적으로 그 addAll()은 파생된 클래스에 의해 OVERRIDEadd()를 호출한다,

이로 인해 파생된 클래스의 add()이 호출될 때 마다 count가 증가되며, 그것의 파생된 클래스의 addAll()에서 요소의 개수만큼 다시 증가된다.

그것은 두 번 셈 하였다.

이런 일이 발생할 수 있는 경우, 파생된 클래스의 작성자는 반드시 기반 클래스가 어떻게 구현되었는지를 알고 있어야 한다. 그리고 파생된 클래스는 예측할 수 없는 방식으로 파괴될 수 있으므로, 그들은 기반 클래스의 모든 변경을 통보받아야 한다.

아! 이 커다란 균열은 귀중한 상송 기둥의 안정성을 끝없이 위협하고 있다.

깨지기 쉬운 기반 클래스 해법.

다시 한 번 ContainDelegate가 구조대이다.

Contain과 Delegate를 사용하여, 우리는 화이트 박스 프로그래밍에서 블랙 박스 프로그래밍으로 이동한다. 화이트 박스 프로그래밍을 사용하면 우리는 기반 클래스의 구현을 살펴보아야 한다.

블랙 박스 프로그래밍을 사용하면 우리는 기반 클래스 안으로 그것의 함수를 override하여 코드를 주입하는 것을 할 수 없으므로 기반 클래스의 구현을 전혀 몰라도 된다. 우리는 인터페이스에만 신경을 쓰면 된다.

이 유행는 충격적이다.

상속은 재사용을 위해 크게 이길 것으로 되어 있었다.

객체 지향 언어는 Contain과 Delegate를 쉽게 하도록 만들어져있지 않다. 그들은 상속을 쉽게 할 수 있도록 설계되어 있다.

만약 당신이 내가 마음에 든다면, 우리는 상속이라는 일에 의문을 가지기 시가해야 한다. 하지만 더 중요한 건, 이것이 계층을 통한 분류의 힘에 대한 당신의 신뢰를 흔들어야 한다.

계층Hierarchy 문제

내가 새 회사에서 일을 시작할 때 마다, 회사 문서들을 넣을 장소를 만들 때 문제에 봉착한다. 예를 들어 직원 핸드북.

Documents라는 폴더를 만들고 그 안에 Company라는 폴더를 만들어야 하는가?

아니면 Company라는 폴더를 만들고 그 안에 Documents라는 폴더를 만들어야 하는가?

둘 다 동작한다. 하지만 어느 것이 맞는가? 어느 것이 가장 좋은가?

범주 계층Categorical Hierarchy의 개념은 더 일반적인 기반(부모) 클래스들이 있고 그 클래스들의 더 전문화된 버전인 파생된(자식) 클래스들이 있다는 것이다. 그리고 우리가 상속 체인 아래로 내려갈 때 더욱 전문화된다. (위의 Shape 계층을 보라)

하지만 부모와 자식이 임의로 자리를 바꿀 수 있다면, 이는 모델에 무엇인가 명백한 잘못이 있다는 것이다.

계층 해법

무엇이 잘못되었나면,

범주 계층은 동작하지 않는다.

그럼 계층 구조는 어디에 좋은 것일까?

봉쇄Containment.

만약 당신이 현실 세계를 본다면, 당신은 봉쇄(또는 독점 소유Exclusive Ownership) 계층을 어디서나 볼 수 있을 것이다.

당신이 찾을 수 없는 것은 범주 계층이다. 잠시 더 깊이 들어가보자. 객체 지향 패러다임은 현실 세계를 기반으로 둔고, 그 세계는 객체로 채워져있다. 하지만 잘못된 모델을 사용하면, 즉, 범주 계층을, 현실 세계와의 유사성analogy가 존재하지 않는다.

하지만 현실 세계는 봉쇄 계층으로 가득 차 있다. 봉쇄 계층의 가장 좋은 예는 당신의 양말이다. 그것들은 당신의 집에 포함된, 당신의 침실에 포함된, 당신의 옷장에 포함된, 한 서랍장에 포함된, 양말 서랍에 있다.

당신의 하드 드라이브에 있는 디렉토리들은 봉쇄 계층의 또다른 예이다. 그들은 파일을 포함하고 있다.

그렇다면 어떻게 분류를 해야 하나?

글쎼, 회사 문서들을 생각한다면, 내가 그것들을 어디에 두는지는 중요하지 않다. 나는 Document나 Stuff라고 불리는 폴더에 넣을 수 있다.

분류하는 방법은 Tag를 지정하는 것이다. 나는 다음과 같은 tag들로 파일을 테그한다:

Document
Company
Handbook

Tag는 순서나 계층을 가지지 않는다.(이는 다이아몬드 문제도 해결한다.)

당신은 문서와 관련된 다양한 유형들을 가질 수 있기에 Tag는 인터페이스와 유사하다.

하지만 많은 균열로, 상속 기둥이 무너진 것으로 보인다.

상속이여, 안녕.

캡슐화, 실패의 두 번째 기둥

언뜻보면, 캡슐화는 객체 지향 프로그래밍의 두번째로 큰 장점으로 보인다.

객체 상태 변수들은 외부 접근에서 보호된다. 즉, 객체는 캡슐화되어 있다.

더 이상 우리는 누군지 모르는 누군가가 접근할 수 있는 전역 변수에 대해 걱정할 필요가 없다.

캡슐화는 당신의 변수에게 안전하다.

이 캡슐화라는 것은 엄청나다!!

캡슐화여 영원하라..

그런데…

참조 문제

효율성을 위해, 객체는 값이 아니라 참조로 함수에게 전달된다.

이는 함수는 객체를 전달 받지 않고, 대신 객체에 대한 참조나 포인터를 전달 받는 다는 것이다.

만약 참조로 전달된 객체가 객체 생성자에게 전달되면, 그 생성자는 그 객체 참조를 캡슐화에 의해 보호되는 private 변수에 저장할 수 있다.

하지만 전달된 객체는 안전하지 않다!

왜일까? 이는 다른 일부 코드, 즉 생성자를 호출한 그 코드가 그 객체에 대한 포인트를 가지고 있기 때문이다. 그것은 객체에 대한 참조를 가지고 있어야 한다. 그렇지 않다면 어떻게 생성자에게 전달할 수 있단 말인가?

참조 해법

생성자는 전달된 객체를 복제해야 한다. 또한 얕은 복제shallow clone가 아니라 깊은 복제deep clone이여야 한다. 즉 전달된 객체 내부의 모든 객체와 그 객체들 안의 모든 객체들, 그 안의 객체들, 그 안의 객체들…

너무 효율적이다.

그리고 여기 함정이 있다. 모든 객체가 복제될 수 있는 건 아니다. 일부는 운영 체제의 리소스를 가지고 있어 이를 복제하는 것은 최고의 경우 소용이 없고 최악의 경우에는 불가능하다.

그리고 모든 주류 OO 언어들은 이 문제를 가지고 있다.

캡슐화여 안녕.

다형성, 실패의 세번째 기둥

다형성은 객체 지향 삼위 일체의 Reaheaded stepchild이다.

이는 그 그룹의 Larry Fine같은 존재이다.

그들이 가는 곳마다 그가 있지만, 그는 단지 떠받쳐주는 역활 뿐이다.

이는 다형이 대단하지 않다는 것이 아니다. 이는 단지 다형성만을 위해 OO 언어를 필요로 하지 않다는 것이다.

인터페이스는 당신에게 다형성을 줄 것이다. 그리고 나머지 OO의 짐들은 필요 없다.

그리고 인터페이스를 사용하면 혼합할 수 있는 동작의 수에 제한이 없다.

그래서 많은 야단법석이 없이, 우리는 OO 다형성과 잘별하고 인터페이스 기반 다형성과 인사를 하면 된다.

깨진 약속들

초기에는 OO는 확실히 많은 것을 약속했엇다. 그리고 이러한 약속들은 여전히 순진한 프로그래머들을 교실에 앉게 하고, 블로그 글들을 읽게 하고, 온라인 과정을 밟게 했다.

OO가 나에게 어떻게 거짓말을 했는지 깨닫는 데 수년이 걸렸다. 나 역시 너무 순진했고, 경험이 없었고, 믿었다.

그리고 나는 된통 당했다.

객체 지향 프로그래밍이여 안녕.

그럼 어떻게 해야 하나?

Hello, Functional Programming. 지난 몇 년간 당신과 일하는 것이 너무 좋았습니다.

당신도 아시다시피, 당신의 약속을 액면 그대로 믿지 않았습니다. 나는 직접 봐야만 믿을 것입니다.

한 번 불에 데면, 두 번 피하게 된다.

You understand.

If you liked this, click the💚 below so other people will see this here on Medium.

If you want to join a community of web developers learning and helping each other to develop web apps using Functional Programming in Elm please check out my Facebook Group, Learn Elm Programminghttps://www.facebook.com/groups/learnelm/

My Twitter: @cscalfani