Post thumbnail

불편한 객체지향 프로그래밍: 그래도 객체지향 사랑하시죠?

· by 박승재

객체지향 프로그래밍: 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 객체의 모임으로 파악하고자 하는 패러다임

객체지향 프로그래밍은 상당히 불편하다.

객체 간의 관계, 상속 등 고려해야 할 것도 많고, 관성에 따라 생각 없이 프로그램을 만들면 무수한 extends가 악수 요청을 하는 라자냐 코드가 나오기 일쑤이다.

또한, 빅데이터의 등장으로 대용량의 데이터를 고속으로 처리해야 하는 최근 메타에는 함수형 프로그래밍이 더 적합하기도 하다.

불편한 객체지향 프로그래밍, 꼭 배워야 할까?

이 글은 아래 두 영상의 내용을 바탕으로 제 생각을 정리한 글입니다.

OOP는 중요하다. 하지만…

OOP는 허접한 개발자 때문에 발전했다?

동영상 업로드 날짜는 꽤 오래되었지만, 내용은 아직 유효하다.

상속

객체지향을 풀어서 설명하자면,

물체와 물체의 상태를 클래스라는 단위로 묶어서 프로그래밍하는 것

이다.

처음 들었을 때는 세상의 모든 문제를 해결할 수 있는 만능 도구처럼 보인다. (사람은 현실을 물체 단위로 인식할 수 있으므로)

하지만, 막상 현실의 문제를 객체지향에 적용하면 결론이 이상해진다.

예를 들어보자.

여기 난로(Stove)가 있다.

class Stove {}

객체지향적 사고방식에서 Stove는 열을 내는 부품인 Heater상속받아 On/Off 기능을 추가해 확장한 것이다.

class Stove extends Heater {
    void on() {}
    void off() {}
}

그러나 현실은 다르다.

난로는 그 자체로 난로일 뿐이지 어떤 물체가 진화(상속)해 난로가 된 것이 아니다.

현실에 가깝게 Stove를 표현하기 위해서는 Composition이 되어야 한다.

class Stove {
    Heater heater;
    void on() {}
    void off() {}
}

실제로 코딩할 때도 상속(extends)보다는 Composition을 많이 사용하고, 그 편이 깔끔한 코드를 유지하는 데 도움이 된다.

여러 클래스를 동시에 상속하는 다중 상속도 여러 문제를 일으킨다.

그리고, 다중 상속으로 일어나는 문제를 해결하려 만든 문법이 인터페이스이다.

이 부분은 아래 블로그 링크로 대체한다.

다중 상속을 지양해야 하는 이유

클래스의 의미

생태계 시뮬레이션을 만들어보고자 한다.

오리(Duck)는 새(Bird)를 확장한 클래스이다.

class Bird extends Animal {
    void fly() {}
}

class Fish extends Animal {
    void swim() {}
}

class Duck extends Bird {}

오리는 새지만 수영할 수 있다.

swim 함수를 재사용하기 위해 Fish상속받는다.

class Bird extends Animal {
    void fly() {}
}

class Fish extends Animal {
    void swim() {}
}

class Duck extends Bird, Fish {}

코드 상에서 오리(Duck)는 새(Bird)이면서 물고기(Fish)로 표현되어 있다.

하지만 오리는 물고기의 한 종류가 아니라 수영을 할 수 있는 새이다.

즉, 객체지향의 제약으로 코드의 표현력이 떨어진 것이다.

위의 문제는 인터페이스를 이용해 해결할 수 있다.

interface Flyable {
    void fly()
}

interface Swimable {
    void swim()
}

class Duck implements Flyable, Swimable {
   void fly() {}
   void swim() {}
}

하지만, 인터페이스를 이용하면 함수의 구현을 재사용할 수 없는 문제가 발생한다.

더 나은 해결책으로는 Strategy Pattern을 고려해 볼 수 있다.

상속의 장점인 코드의 재사용도 실제 코드에서는 많이 쓸 일이 없다.

class Bird extends Animal {
    void fly() {
        System.out.println("파닥파닥");
    }
}

class Duck extends Bird {
    @Override
    void fly() {
        System.out.println("푸드덕푸드덕");
    }
}

부모의 함수를 수정하는 상황에서는 재사용의 장점도 사라지게 된다.

상속의 문제로 인해, 최근에 등장한 일부 언어는 상속 문법(extends)이 없는 경우도 있다.

Rust에서는 trait(=고오오급 인터페이스)과 enum을 이용해 객체지향을 구현한다.

trait Fly {
     fn fly(&self);
}

struct Bird {}

impl Fly for Bird {
    fn fly(&self) {
        println!("fly");
    }
}

상속이라는 개념이 존재하지 않기 때문에 Composition으로 함수를 재사용하며 traitenum을 이용해 다형성을 제공한다.

객체지향의 핵심(중요)

객체지향에서 가장 중요한 점은 상속이 아닌,

객체의 동작을 정의하는 인터페이스와 객체의 상태가 코드상에서 같은 곳에 위치하는 점이다.

또한, 인터페이스를 상속 받아 객체 간의 공통된 동작을 정의하고 실행할 수 있는 관리의 편의성을 제공하는 점이다. (클래스를 상속받아 동작을 재사용한다는 의미와 다르다)

상속은 객체지향을 구현하는 많고 많은 방법 중 하나일 뿐, 제일 잘 난 방법이 아니다.

상속이 깊어지거나 다중상속이 되면 뭔가 잘못하고 있는 가능성이 높으며,

상속이 깊어질수록 읽기 어려워지기 때문에 Composition으로 상속 계층을 풀어주는 거도 고려해 볼 수 있다.

책임 소재

객체지향에서 모든 것(특히 Java에서)는 접근 제어자(Access Modifier)를 거친다.

이는 누구 한 명이 이 클래스를 책임지고 관리하고, 다른 사람은 해당 클래스를 못 고치는 규칙을 도입하기에 더할 나위 없이 좋다.

패러다임 자체적으로 개인에게 프로그래밍 책임을 분리할 수 있으며, 뉴비 프로그래머가 다양한 코드를 망가뜨리지 못하게 하기 위한 제약을 걸기에는 매우 유용하다.

객체지향 광신도들에게

객체지향 5원칙이라 불리는 SOLID는 올바른 이론이긴 하나, 이론적인 강의고 현실에는 안 맞는 경우도 있다.

너무 객체지향 이론에 집착하면 오히려 유지보수가 어려워질 수 있고 탁상공론적인 디자인이 나오는 경우도 있다.

“세상에서 가장 핵심적인 프로그램들의 대부분은 객체지향 없이 만들어졌다.”