기타2020. 7. 13. 15:42

https://medium.com/@cscalfani/goodbye-object-oriented-programming-a59cda4c0e53

 

Charles scalfani의 글이 흥미로워서 정리해보았다.

 

수십년간 개발자로 살아온 나에게도 OOP는 종교였다. 

상속, 캡슐화, 다형성은 이 페러다임의 3개의 기둥이다.

 

이 기둥 3개로 우리는 실세계의 모든 상황을 모델링할 수 있다고 배웠다.

 

필자는 더이상 잘못된 방향으로 살고 싶지 않았다며 본인의 감회를 풀어낸다.

 

먼저 상속..

 

 

첫번째 문제 상속

 

OOP의 가장 큰 장점으로 설명되며 아주 전통적인 예로 Shape를 가지고 설명들을 한다.

 

 

 

 

많이 보았을 거다. 그리고 우리는 재사용성이라는 외마디를 외치게 된다. 신념에 차서.

 

재사용을 위한 모든 준비는 되어 있고 언제든 이를 다시 사용할 시기만 학수고대 할것이다.

 

Banana Monkey Jungle Problem

 

그리고 마침내 새로운 프로젝트가 시작되고 나의 소중한 코드들을 가져와서 사용하려고 할것이다.

 

이런! 부모클래스도 필요하네?

이런! 부모의 부모클래스, 그 부모의 부모클래스도 필요하네??

아 하지만 문제 없어 난 할수 있어~

이런 다 했는데 왜 컴파일이 안되지? 오 이런 이 클래스가 저 클래스를 참조하자나.. 저것도 가져와야 해.. 

머 가져오지 머.. 문제 없어.

 

오~ 이런 난 이런 클래스들은 필요가 없다구. 먼가 잘못되었어..

 

Erlang의 창시자인 Joe Amstrong의 말이 있지.

 

"OOP의 문제는 당신은 단지 바나나를 원하지만 그 바나나는 바나나와 정글전체를 쥐고 있는 고릴라라는 것이다"

 

이를 두고 banana monkey jungle 문제라고 한다.

 

이 문제를 해결하기 위해 필자는 hierarchy를 너무 깊이 설계하지 않는다. 

하지만 재사용성을 상속의 핵심이라고 한다면 메커니즘에 이런 제한을 주는것은 재사용의 이점에 대한 제한이 될것이다.

 

그러면 진짜 속시원한 해결책은 무엇일까?

 

포함(contain)과 위임(delegate)라고 생각한다.

 

Diamond Problem(다중상속문제)

 

 

 

 

대부분의 객체지향언어는 논리적으로 작성된것 같지만 이를 지원하지 않는다. 객체지향언어가 이를 지원하기 위해 어려운 이유에 대해서는 다음 의사코드를 한번 보자

 

Class PoweredDevice {

}

 

Class Scanner inherits from PowerdDevice {

  function start() {

  }

}

 

Class Printer inherits from PoweredDevice {

  function start() {

  }

}

 

Class Copier inherits from Scanner, Printer {

}

 

위 의사코드를 보면 Scanner와 Printer 클래스가 모두 start함수를 구현하고 있다.

Copier의 start를 호출하면 Scanner나 Printer의 어떤 start가 호출될 수 있을까? 알수 없다.

 

이에 대한 해결책은 간단하며 위와 같이 설계하지 않고 따로 따로 구현하면 된다.

하지만....... 어떻게 모델링해야 하지?? 재사용은 포기하고 싶지 않고?

 

이 때 포함 위임을 통해서 해결리 가능하다.

 

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클래스가 이제 Printer와 Scanner의 인스턴스를 포함하고 있다.  start함수의 구현은 Printer 클래스에 위임하고 있다. 이는 쉽게 Scanner에 위임이 가능하다.

 

 

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

 

이제 나는 상속을 얕게 만들고 다중상속되지 않도록 하여 Diamond문제는 발생하지 않게 하였다.

 

모든건 잘 될거라고 생각하지만...

 

어떤날은 잘 동작하고 어떤날은 동작을 멈춘다.. 코드는 변경된바가 없는 체로...

 

버그가 있겠지.. 하지만 변경한적이 없는걸...

 

이라고 개발자들은 자기위안을 삼지만... 이 코드의 세계는 그렇게 호락호락하지 않다.. 거기에 버그는 있게 마련이다. 반드시~

 

내가 작성한 코드에는 문제가 없었지만 내가 상속한 클래스에서 변경이 발견되었다면... 가능하다.

 

어떻게 내가 상속했던 기반클래스이 변경이 왜 나의 코드에 예외를 발생하는것일까?

 

다음의 자바코드를 살펴보자

 

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 element[]) {

    for (int i=0; i<element.length; i++) {

      a.add(element[i]); // this line is going to be changed..

    }

  }

}

주석된 라인의 코드에 주목하자. 여기서 바로 향후 예외가 발생할 수 있다.

 

여기 add와 addAll함수가 있다. add는 엘리먼트하나를 addAll은 다수의 엘리먼트를 추가하는데 add를 호출하여 추가하고 있다.

 

이제 상속한 클래스를 하나 정의해보겠다.

 

public class ArrayCount extends Array {

  private int count = 0;

  @Overrie

  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에 엘리먼트들을 추가

 

ArrayCount add는 부모의 add를 호출하고 count를 추가한다.

ArrayCount addAll은 부모의 addAll을 호출하고 엘리먼트의 개수만큼 count를 추가한다.

 

예상한데로 정확히 동작한다.

 

자 이제 예외를 유발하는 변경을 주석된 라인부분을 다음과 같이 수정되었다고 하자.

 

public void addAll(Object elements[]) {

  for (int i=0; i<elements.lenggh; ++i) {

    add(elements[i];

  }

}

 

기반클래스의 소유자가 의도한 대로 자동 테스트는 여전히 패스된다.

 

하지만 소유자는 상속한 사실을 잘 잊게 마련이고 미련하기도 하다.

 

ArrayCount addAll은 부모의 addAll을 호출하고 내부에서 오버라이드된 상속된 클래스의 add를 호출하게 된다. 이는 상속된 클래스에서 addAll에서 추가되면서 카운트가 한번더 증가될수 있다. 

 

중복해서 카운트가 증가될 수 있다는 것이다. 실제 엘리먼트 개수와 다르게..

 

만일 이런 문제가 발생한다면 개발자는 기반클래스가 어떻게 구현되어 있는지 확인이 필요하게 되며 기반클래스 작성자는  변경이 발생하게 되면 이 클래스를 사용하는 개발자들에게 모두 알려야 한다. 

 

이런 잠재적인 문제는 상속이라는 기둥에 대해 불신하게 한다.

 

이에 대한 해결책은 다시한번 포함과 위임이다.

 

포함과 위임으로 우리는 화이트박스프로그래밍에서 블랙박스프로그래밍으로 갈수 있다. 화이트박스프로그래밍에서는 기반클래스의 구현을 알아야 한다.

 

블랙박스프로그래밍은 기반클래스에 코드를 주입할 수 없기 때문에 완전하게 무시할 수 있다. 단지 인터페이스에 집중할 수 있다.

 

이 트렌트는 혼란스럽다.

 

상속은 재사용에 있어서 큰승리로 여겨졌다.

객체지향언어는 포함과 위임보다 상속이 쉽게 설계되었다.

 

이젠 상속에 대해 근원적인 의시밍 생기기 시작했을것이다. 중요한것은 계층을 통한 분류를 맹신하지 않아야 한다는 것이다.

 

 

계층화의 문제

 

새로운 회사를 시작할 때 마다 회사문서를 어디에 위치시킬지 고민하게 된다. 예를 들어 직원명부등..

 

문서폴더를 만들고 회사폴더를 만들건지. 또는 회사폴더를 만들고 문서폴더를 만들지...

 

어떤게 맞고 최선인건가....?

 

분류기반 계층화에서 기반클래스는 좀더 일번적인것이고 상속(자식)클래스는 좀더 특화된 것들이다. 그리고 좀 더 특화된 것들이 상속체인의 끝부분에 위치하게 된다.

 

하지만 그 위치를 변경하면 이 모델에서 먼가 확실하게 잘못되게 된다.

 

이게 머가 잘못된 것일까...?

어떤 계층구조가 맞는 것인가..?

 

포함

 

실세계를 살펴본다면 포함계층화(배타적소유계층화)를 어디에서든지 보게 될것이다. 찾아볼수 없는것은 분류기반계층화이다. 객체기반 페러다임은 실세계 모델링을 기반으로 하며 하나는 객체로 간주된다. 이는 잘못된 모델이다. 실세계는 없다.

실제로 실세계는 포함계층화에 가깝다. 당신의 양말이 아주 훌룡한 예이다. 양말은 서랍에 포함되고 옷장에 포함되고 침실에 포함되고 집에 포함되어 있다.

 

PC 저장장치도 그 예 인데. 파일들을 포함한다.

 

그래서 이제 어떻게 분류할건가?

 

자 이제 회사문서를 생각해보자면 어디에 위치하던지 문제가 안된다. document폴더나 stuff폴더에 위치시킬수 있다.

그리고 분류는 태크로 가능해진다. 다음과 같은 태그로 태그할 수 있다.

 

Document

Company

Handbook

 

태그는 순서나 계층이 없다. 이는 diamond 문제도 해결한다.

태그는 문서에 대해 여러 타입을 지정할 수 있어서 인터페이스와 유사하다.

 

균열이 너무 많으면 상속기둥이 무너진것처럼 보인다.

 

 

두번째문제 캡슐화

 

캡슐화는 객체지향이 갖고 있는 두번째 큰 장점이다.

객체의 상태는 외부에서 보호된다. 

누군가에 의해 접근되는 글로벌변수에 대한 걱정을 더이상 할 필요가 없다.

 

캡슐화는 변수들에 대해 안전하다. 아주 끝내준다. 원원하라 캡슐화여..

 

참조의 문제

 

캡슐화는 참조의 문제가 있다. 

객체는 함수에 전달될 때 값이 아니라 참조로 전달된다.

 

이는 함수는 객체를 전달하지 않으며 대신에 참조나 객체의 포인터를 전달하게 된다는 것이다.

 

만일 객체가 객체생성자에 참조로 전달된다면 이 생성자에서는 캡슐화로써 내부에 private 변수로 참조를 저장할 수 있다.

 

이제 전달된 객체는 안전하지 않다.

 

왜냐구? 일련의 다른 코드에서 객체의 포인터를 갖게 되기 때문이다. 그 코드는 생성자로 호출된다. 그렇다면 생성사로 전달 할 수 없는 객체에 대한 참조를 가져야 하나?

 

해결책은 생성자는 전달되는 객체를 복재해야 한다. 그리고 얕은 복제가 아니고 깊은 복제이어야 한다. 즉 객체가 포함하는 객체들이 있다면 모두 복제되어야 한다.

 

여기서 망하는 지점이 있다. 모든 객체가 복제가 가능한것이 아니다. 일부 운영체제 리소스들은 불가할 수 있다.

 

싱글메인스트림의 객체기반 언어들을 이 문제를 가지고 있다.

 

안녕.. 캡슐화

 

 

세번째 문제 다형성

 

다형성은 객체지향 삼위일체의 빨간머리 의붓자식이다. 일종의 Larry Fine이다.

그가 어디 있던지 그는 하나의 캐릭터이다.

 

다형성이 위대하지 않다는것이 아니다  객체지향언어에서 취할필요는 없다는 것이다.

 

인터페이스가 이를 지원할것이다. 그리고 인터페이스는 얼마든지 다양한 행위에 대해서 섞어서 제한없이 구현이 가능하다.

 

따라서 우리는 객체지향다형성에 대해서는 안녕을 고하고 인터페이스기반의다형성을 반겨야 할 때 이다.

 

 

 

약속의 깨기

 

자 객체지향은 그동안 오랬동안 약속을 해왔다. 이런 약속은 교실에 앉아있는 네이티브프로그래머, 블로그의 읽을거리들 그리고 온라인코스를 학습자들에게 여전히 유효하다.

 

객체지향의 거짓말을 깨닫는데 몇년이 걸렸다. 눈은 없어서 경험이 없고 신뢰를 했다.

그리고 나는 완전히 타버렸다.

 

안녕.. 객체지향!

 

 

 

그럼 어쩌라고?

 

함수형 프로그래밍~ 이다. 수년동안 아주 잘 사용되어 왔다. 

 

한번 타버리면 창피함이 두배가 된다 

 

 

 

세줄정리

 

1. 상속은 재사용의 어머니 이지만 나에게 필요없는 넘들까지 줄줄이 딸려온다.

2. 캡슐화는 객체가 참조로 전달될 수 있다는 점에서 쉽게 깨진다.

3. 다형성은 복잡하기만 하며 인터페이스로 단순하게 얼마든지 다양하게 구현이 가능하다.

 

이를 보완하기 위한 방법들은 포함과 위임이 있으며 최근에는 함수형프로그래밍으로 대체가 가능하므로 객체지향과는 안녕을 고하자..

 

 

Posted by 삼스