2017년 4월 1일 토요일

[번역] 객체로서의 함수 [FunctionAsObject]

이 포스트는 FunctionAsObject를 2017년 3월 30일 새벽에 번역한 글입니다. 원작자인 Martin Fowler의 허락을 받아 블로그에 게시합니다. 원문의 영어 단어나 표현이 필요하다고 생각한 경우에 괄호안에 원문을 같이 표기했습니다.

객체로서의 함수

프로그래밍에 있어서, 객체(object)의 가장 기본 개념은 데이터와 행동방식(behavior)을 함께 묶는 것입니다. 이렇게 하면 서로 관련있는 여러 함수를 만들 때, 공통의 데이터 컨텍스트(common data context)를 만들 수 있습니다. 그리고 그 데이터를 조작하는 인터페이스를 만들어 내서, 객체 내의 데이터에 대한 접근을 해당 객체가 제어할 수 있도록 할 수 있습니다. 그러면 관련있는 다른 데이터(derived data)를 만들어내기도 쉬워지고, 데이터를 잘못 수정하는 일도 미리 막을 수 있게 됩니다. 많은 프로그래밍 언어가 객체 정의를 위한 클래스를 선언하는 명시적인 방법을 가지고 있습니다. 하지만 1급함수(first-class function)와 클로저(closure)를 사용할 수 있는 프로그래밍 언어에서는, Function As Object 패턴을 이용해서 객체를 만들어 낼 수 있습니다. (Eugene Wallingford에 의해 처음 제안되었습니다.)

간단하게 사람을 객체로 만든 예제를 들어보겠습니다. 객체로서의 함수(function-as-object) 스타일을 JavaScript에서 사용했습니다. [1]

function createPerson(name) {
  let birthday;
  return {
    name: () => name,
    setName: (aString) => name = aString,
    birthday: () => birthday,
    setBirthday: (aLocalDate) => birthday = aLocalDate,
    age: age,
    canTrust: canTrust,
  };
  function age() {
    return birthday.until(clock.today(), ChronoUnit.YEARS);
  }
  function canTrust() {
    return age() <= 30;
  }
}

가장 바깥쪽의 형태가 객체로서의 함수인 함수입니다. 이는 생성자 함수(constructor function)라고도 불립니다. 이 함수의 호출 결과는 (엄밀히 따지면) 함수들의 해쉬맵(hashmap)입니다. [2] 이 해쉬맵은 메서드 셀렉터(method selector)의 역할을 합니다. 이 해쉬맵은 변수들의 상태를 클로저안에 있는 함수에 보관합니다. 이로써 각각의 함수 호출과는 상관없이 데이터를 유지할 수 있게 합니다. 그러면 이 해쉬맵은 전통적인 객체처럼 취급될 수 있습니다.

const kent = createPerson("kent");
kent.setBirthday(LocalDate.parse("1961-03-31"));
const youngEnoughToTrust = kent.canTrust();

객체로서의 함수를 전통적인 객체지향관점에서 분석해보겠습니다.
  • 객체의 필드(field)는 위 예제의 name처럼 생성자 함수의 인자의 형식, 또는 birthday처럼 지역 변수의 형식으로 표현됩니다.
  • 객체의 메서드는 생성자 함수 내부의 함수입니다. 보통의 객체의 메서드처럼, 내부의 함수들은 서로 자유롭게 호출이 가능하며 지역변수로 선언된 데이터를 조작할 수 있습니다.
  • 생성자 함수의 바깥에서는 내부의 변수에 접근할 수 없습니다. 이로 인해 데이터의 캡슐화(encapsulation)가 지켜집니다.
  • 객체의 퍼블릭(public) 메서드는 생성자 함수가 리턴하는 해쉬맵에 포함된 함수들입니다.
  • 생성자 함수 내부에서 선언된 함수들 중에서, 리턴 해쉬맵에 포함되지 않은 함수들이 바로 객체의 프라이빗(private) 메서드입니다.
  • 퍼블릭 메서드의 함수 이름은 리턴하는 해쉬맵의 키와 같습니다. 생성자 함수 내부에서 선언한 함수 이름이 아닙니다. 저는 불필요한 혼란을 피하기 위해서 내부의 이름과 외부로 노출되는 함수 이름을 같게 유지하는 것을 더 좋아합니다. (필요하면 간단한 방법으로 별도의 함수 별명을 지정할 수 있습니다.) [3]

이 패턴의 또 다른 일반적인 구현방법은 바로 메서드 셀렉터를 가진 함수를 리턴하는 것입니다. 해쉬맵처럼 JavaScript의 기본적인 메서드 셀렉터가 아니어도 됩니다. 메서드 셀렉터로 함수를 사용하기 위해서는, 첫번째 인자로 사용하려는 메서드의 이름을 받아들이는 함수를 리턴하면 됩니다. 함수의 내부 구현에서 인자로 들어온 메서드의 이름을 기준으로 조건을 주어 구별하면 됩니다. (Wallingford가 더 자세하게 작성한 문서가 있습니다.)

위와 같은 객체로서의 함수를 사용하는 방법은 예전 부터 있었습니다. lisp에서도 많이 볼 수 있었고, JavaScript에서도 폭넓게 사용되어 왔습니다. (ES6 이전에는 클래스에 대한 아주 제한적인 표현법만 있긴 했습니다.) 클래스를 위한 특별한 문법이 불필요하다는 논쟁이 종종 있었습니다. 이건 마치 객체애호가들이 메서드 1개짜리 클래스를 사용하면 되기 때문에 1급 함수가 필요없다고 했던 논쟁과 같은 것입니다. 그 결과로 JavaScript 진영의 많은 사람들이 ES6의 클래스 문법을 사용하는 것에 대해 아직 논쟁을 벌이고 있습니다. 1급 함수와 1급 클래스가 모두 있어서 좋습니다만, 개인적으로는 ES6의 클래스 문법을 더 좋아합니다.

더 읽어보기

Eugene Wallingford가 "객체로서의 함수"라는 이름을 만든 것은 1999년의 패턴 언어 "Envoy"에서 였습니다. 이 글을 보면 더 상세한 내용을 알수 있습니다. 함수를 메서드 셀렉터로 사용하는 것과 상속의 일부 개념을 지원하는 위임 형태로 사용하는 것에 대한 내용이 있습니다. Scheme을 예제로 사용하고 있습니다.

감사의 글

이 글의 초안에 대해서 의견을 보내준 Chris Ford, Fred George, James Shore, Kevin Yeung, Lucas Lego, Matteo Vaccari, Rob Miles, 그리고 Eugene Wallingford에게 감사드립니다.

주석

1. 날짜를 다루기 위해 js-joda를 사용했습니다. js-joda는 Java가 날짜와 시간에 대해서 엉망이었던 것을 깔끔하게 정리한 Joda-Time 라이브러리의 JavaScript 버전입니다. js-joda로 인해 JavaScript의 날짜와 시간에 대한 문제를 제대로 사용할 수 있게 되어 다행이라 생각합니다.

2. JavaScript 용어로 그걸 객체라고 부릅니다. 비록 우리가 만들려고 했던 전통적인 객체는 아니지만, 그래도 JavaScript의 객체입니다. 그래서 혹시 모를 혼란을 줄이기 위해 해쉬맵이라고 지칭했습니다.

3. ES6에서는 "age : age," 처럼 중복되는 내용은 그냥 "age,"로 대체 할 수 있습니다.