[프로그래밍 이야기 5] 점 두 개(..)가 가져온 혁명: Smalltalk의 유산과 Dart Cascade 연산자

Smalltalk의 유산과 Dart Cascade 연산자

소프트웨어 외주 개발 결과물이나 아웃소싱 코드를 검수하다 보면, Dart 언어로 작성된 프로젝트에서 독특하면서도 우아한 문법을 자주 마주치게 됩니다. 바로 점 두 개를 연달아 사용하는 **Cascade 연산자(..)**입니다.

처음에는 단순히 코드 길이를 줄여주는 문법 정도로 보입니다. 하지만 이 작은 연산자 속에는 객체지향 언어가 지난 수십 년 동안 고민해 온 메시지 전달(Message Passing), 객체 조작 방식, 그리고 API 설계 철학이 녹아 있습니다.


1부. Smalltalk에서 시작된 Cascade

Cascade 연산자의 기원을 따라가려면 객체지향 프로그래밍의 조상인 Smalltalk까지 거슬러 올라가야 합니다.

Smalltalk에서 객체는 함수를 호출(Call)하는 대상이 아니라 메시지를 전달(Message Passing)받는 존재였습니다.

동일한 객체에게 여러 메시지를 보낼 때는 객체 이름을 반복하지 않고 세미콜론(;) 하나만으로 계속 이어갈 수 있었습니다.

person := Person new.

person
    name: 'Alice';
    age: 30;
    printInfo.

객체는 한 번만 등장하고 이후의 모든 메시지는 같은 수신자에게 전달됩니다.

지금 봐도 상당히 세련된 문법입니다.

무엇보다 메서드를 추가하거나 삭제할 때도 마지막 줄에 한 줄만 더 붙이면 되므로 코드 수정이 매우 자연스럽습니다.


2부. Objective-C가 이어받은 메시징 철학

Smalltalk의 영향을 가장 크게 받은 언어 가운데 하나가 Objective-C입니다.

Objective-C는 C 언어 위에 Smalltalk의 메시지 전달 철학을 접목하면서 유명한 대괄호 문법을 도입했습니다.

Person *person = [[Person alloc] init];

[person setName:@"Alice"];
[person setAge:30];
[person printInfo];

하지만 Smalltalk의 Cascade 문법 자체는 계승하지 않았습니다.

동일한 객체에 연속적으로 메시지를 보내려면 객체 이름을 반복해서 작성하거나, 반환값을 이용해 중첩된 메시지를 작성해야 했습니다.

[[[[person setName:@"Alice"]
    setAge:30]
    setAddress:@"Seoul"]
    printInfo];

이 방식은 분명 동작하지만 사람이 작성하기에는 그다지 친절하지 않습니다.

몇 단계까지 메시지를 보낼 것인지 미리 생각해서 대괄호를 먼저 열어 두어야 하고, 중간에 호출을 추가하거나 삭제하면 괄호의 짝도 함께 수정해야 합니다.

오늘날에는 IDE가 자동으로 괄호를 맞춰주므로 큰 문제가 아니지만, 에디터의 도움이 없던 시절에는 상당히 불편한 문법이었습니다.

반면 Smalltalk의 Cascade는 처음부터 이런 불편함이 없었습니다.


3부. C++과 Java가 선택한 우회책

C++과 Java는 Smalltalk처럼 동일한 객체에 여러 메시지를 보내는 문법을 제공하지 않았습니다.

대신 메서드의 반환값에 다시 메서드를 호출할 수 있다는 일반적인 언어 문법을 이용해, 개발자들이 Method Chaining이라는 설계 기법을 만들어 사용하기 시작했습니다.

대표적인 방법이 return this;(C++에서는 return *this;)입니다.

class Button {
public:
    Button& setText(const std::string& text) {
        this->text = text;
        return *this;
    }

    Button& setEnabled(bool enabled) {
        this->enabled = enabled;
        return *this;
    }
};

button.setText("Click")
      .setEnabled(true);

이후 Fluent Interface와 Builder Pattern이 널리 알려지면서 return this는 사실상의 관용적인 API 설계 방식으로 자리 잡았습니다.


4부. return this가 만든 트레이드오프

하지만 이 방식에는 구조적인 한계가 있습니다.

메서드가 원래 반환해야 하는 값이 있다면 어떻게 될까요?

예를 들어 성공 여부를 알려주는 bool을 반환하고 싶을 수도 있고, 오류 코드나 생성된 객체를 반환해야 할 수도 있습니다.

그러나 체이닝을 위해서는 return this를 선택해야 합니다.

결국 메서드 본연의 반환값과 체이닝은 서로 경쟁 관계가 됩니다.

Builder처럼 객체를 구성하는 API에서는 문제가 되지 않지만, 일반적인 라이브러리에서는 종종 반환값 자체가 중요한 의미를 갖습니다.

즉, 언어가 아니라 API 설계자가 이 트레이드오프를 감수해야 했던 것입니다.


5부. Dart가 다시 언어 차원으로 가져온 Cascade

Dart는 이 문제를 API가 아니라 언어 문법 자체로 해결했습니다.

class Button {
  bool setText(String text) {
    return true;
  }

  void setEnabled(bool enabled) {}
}

void main() {
  var button = Button()
    ..setText("Click")
    ..setEnabled(true);
}

setText()bool을 반환합니다.

setEnabled()void를 반환합니다.

하지만 .. 연산자는 메서드의 반환값을 무시하고 항상 원래 객체를 다음 호출 대상으로 사용합니다.

즉, 체이닝을 위해 return this를 강제할 필요가 없습니다.

메서드는 자신의 고유한 반환값을 그대로 유지하면서도 연속적인 객체 조작이 가능합니다.

Smalltalk가 언어 차원에서 제공했던 Cascade의 장점을 현대적인 문법으로 되살린 것입니다.


마치며

Smalltalk는 객체지향 언어의 시초였지만 주류 개발 환경에서는 점차 사라졌습니다.

Objective-C는 메시지 전달 철학을 계승했지만 Cascade의 편리함까지는 이어받지 못했습니다.

C++과 Java는 return this를 이용한 Method Chaining이라는 우회책을 발전시켰지만, 이는 어디까지나 API 설계 패턴이었습니다.

그리고 Dart는 다시 언어 차원에서 Cascade를 부활시켰습니다.

그래서 저는 Dart의 .. 연산자를 볼 때마다 단순한 문법 설탕(Syntactic Sugar)으로 느껴지지 않습니다.

오히려 1980년대 Smalltalk가 제시했던 객체지향 철학을, 현대적인 정적 타입 언어에서 가장 자연스럽게 계승한 문법 중 하나라는 생각이 듭니다.

Smalltalk의 수많은 아이디어는 여러 언어로 흩어져 계승되었지만, Cascade 문법만 놓고 본다면 Dart는 가장 직접적으로 그 철학을 이어받은 현대적인 주류 언어라고 말해도 크게 무리가 없을 것입니다.

댓글