본문 바로가기

JavaScript

[You don`t Know JS 정리] ch.6 작동 위임

Ch6. 작동 위임

  • 5장의 프로토타입 결론은 [[Prototype]] 체계는 한 객체가 다른 객체를 참조하기 위한 내부 링크라는 것이다.
  • 자바스크립트의 무한한 가능성을 이끌어낼 가장 중요한 핵심 기능이자 체계는 '객체를 다른 객체와 연결하는 것'이다.

6.1 위임 지향 디자인으로 가는 길

  • [[Prototype]] 사용 방법을 가장 쉽게 이해하려면 먼저 클래스와는 근본부터 다른 디자인 패턴이라는 사실을 인지해야 한다.

6.1.1 클래스 이론

  • 소프트웨어 모델링이 필요한 유사한 태스크 두개 (XYZ, ABC)가 있다. 클래스 기반의 설계라면 대략 아래와 같다.
  1. 가장 일반적인 부모 클래스와 유사한 태스크의 공통 작동을 정의한다.
  2. Task를 상속받은 2개의 자식 클래스를 정의한 후 이들에 특화된 작동은 두 클래스에 각각 추가한다.
  3. 클래스 디자인 패턴에서는 될 수 있으면 메서드를 오버라이드할 것을 권장하고 작동 추가뿐 아니라 때에 따라서 오버라이드 이전 원본 메서드를 super 키워드로 호출할 수 있게 지원한다.
  • 공통 요소는 추상화하여 부모 클래스의 일반 메서드로 구현하고 자식 클래스는 이를 더 세분화하여 쓴다. 이를 코드로 풀면 아래와 같다.
class Task {
  id;
  Task(ID) { id = ID;};
  outputTask() { output(id) }
}

class XYZ inherits Task {
  label;
  XYZ(ID, Label) { super(ID); label = Label};
  outputTask() {super(); output(label) };
}

class ABC inherits Task {
  // ...
}
  • 이제 클래스의 사본을 인스턴스화하고 그렇게 탄생한 인스턴스로 XYZ 태스크를 수행한다. 이 인스턴스는 Task의 일반 작동과 XYZ의 특수 작동 사본을 모두 포함한다. 인스턴스가 생성되면 원하는 작동은 인스턴스에 모두 복사되어 옮겨진 상태이므로 일반적으로 오직 인스턴스와 상호 작용을 한다.

6.1.2 위임 이론

  • 같은 문제를 작동 위임을 이용하여 생각해보자. 먼저 Task 객체를 정의한다. 이 객체에는 다양한 태스크에서 사용할 유틸리티 메서드가 포함된 구체적인 작동이 기술된다. 태스크별 객체를 정의하여 고유한 데이터와 작동을 정의하고 Task 유틸리티 객체에 연결해 필요할 때 특정 태스크 객체가 Task에 작동을 위임하도록 작성한다.
  • 각자 별개의 객체로 분리된 상태에서 필요할 때마다 객체가 Task 객체에 작동을 위임하는 구조다.
Task = {
  setID: function(ID) { this.id = ID};
  outputID: function() { console.log(this.id) }
}

XYZ = Object.create(Task);

XYZ.prepareTask = function(ID, Label) {
  this.setID(ID);
  this.label = Label
}

XYZ.outputTaskDetails = function() {
  this.outputID();
  console.log(this.label)
}

/*
ABC = Object.create(Task)
...
*/
  • 예제에서 XYZ는 create() 메서드를 이용하여 Task 객체에 Prototype 위임을 했다. 이 패턴을 저자는 OLOO라고 부른다.
  • 자바스크립트의 Prototype 체계는 객체를 다른 객체에 연결한다. 클래스 같은 추상화는 없다.
  • OLOO 스타일 코드의 특징
  1. 일반적으로 Prototype 위임 시 상탯값은 위임하는 쪽에 두고 위임받는 쪽에는 두지 않는다.
  2. 클래스 디자인 패턴에서는 부모/자식 양쪽에 메서드 이름을 일부러 같게하여 오버라이드를 이용했다. 작동 위임 패턴은 정반대로, 서로 다른 수준의 Prototype 연쇄에서 같은 명칭이 뒤섞이는 일은 될 수 있으면 피해야 한다.
  3. 위 코드 this.setID같이 존재하지 않는 메서드는 Prototype 위임 링크가 체결된 곳으로 이동하여 발견한다. 그리고 암시적 호출부에 따른 this 바인딩 규칙에 따라 setID 실행 시에는 this는 XYZ로 바인딩 된다.
  • 작동 위임이란, 찾으려는 프로퍼티/메서드 레퍼런스가 객체에 없으면 다른 객체로 수색 작업을 위임하는 것을 의미한다.
  • 자바스크립트에 상호 위임은 없다.
  • OLOO와 작동 위임 패턴으로 코딩하면 누가 어떤 객체를 '생성했는지'는 그다지 중요하지 않다.

6.1.3 멘탈 모델 비교

// OO 스타일 코드
function Foo(who) {
  this.me = who
}

Foo.prototype.identify = function () {
  return "I am" + this.me
}

function Bar(who) {
  Foo.call(this, who)
}

Bar.prototype = Object.create(Foo.prototype)
Bar.prototype.speak = function () {
  alert("Hello" + this.identify() + ".")
}

var b1 = new Bar("b1")
var b2 = new Bar("b2")

b1.speak()
b2.speak()
// OLOO 스타일 코드
Foo = {
  init: function (who) {
    this.me = who
  },
  identify: function () {
    return "I am " + this.me
  },
}

Bar = Object.create(Foo)

Bar.speak = function () {
  alert("Hello, " + this.identify() + ".")
}

var b1 = Object.create(Bar)
b1.init("b1")

var b2 = Object.create(Bar)
b2.init("b2")

b1.speak()
b2.speak()

6.2 클래스 vs 객체

  • 6.2, 6.3 챕터 모두 javascript 클래스 문법과 OLOO 문법을 비교하면서 설명한다.

6.2.1 위젯 클래스

  • 안민원-OO 코드 참조
  • 부모 클래스에는 render()만 선언해두고 자식 클래스가 이를 오버라이드하도록 유도한다. 여기서는 작동 기능을 덧붙여 기본 기능을 증강한다. Widget.prototype.render.call 호출부처럼 자식 클래스 메서드에서 부모 클래스의 기본 메서드로 돌아가기 위해 가짜 super 호출까지 동원한 명시적 의사다형성의 추한 단면을 볼 수 있다.

6.2.2 위젯 객체의 위임

  • 안민원-OLOO 코드 참조
  • OLOO 관점에서는 Widget이 부모도, BUtton이 자식도 아니다. 둘 다 객체이다. (물론 Widget과 위임 링크가 맺어진 상태)
  • 같은 이름의 메서드를 공유하는 대신, 각자 수행하는 임무를 더욱 구체적으로 드러낼 수 있는 다른 이름을 메서드에 부여한다.
  • 해당 코드를 보면 한 차례 호출로 끝났던 코드가 두 번으로 늘어난 부분이 보인다. 클래스 생성자는 보통 생성과 초기화를 한 번에 해야 하지만, OLOO 방식처럼 두 단계로 나누면 생성 및 초기화 과정을 굳이 한곳에 몰아넣고 실행하지 않아도 되니 관심사 분리의 원칙을 더 잘 반영한 패턴이다.

6.3 더 간단한 디자인

  • OO 스타일과 OLOO 스타일을 비교하는 예를 하나 더 든다.
  • 아래 코드는 로그인 페이지의 입력 폼을 처리하는 객체, 서버와 직접 인증을 수행하는 객체, 이렇게 두 컨트롤러 객체가 있다.
  • 전형적인 클래스 디자인 패턴에 의하면 Controller 클래스에 기본적인 기능을 담아두고 이를 상속받은 LoginController와 AuthController 두 자식 클래스가 각자 상황에 맞게 구체적인 작동 로직을 구현하는 식으로 구성된다.
// 부모 클래스
function Controller() {
  this.errors = []
}
Controller.prototype.showDialog = function (title, msg) {
  // 메시지 표시
}
Controller.prototype.success = function (msg) {
  this.showDialog("Success", msg)
}
Controller.prototype.failure = function (err) {
  this.errors.push(err)
  this.showDialog("Error", err)
}

// 자식 클래스
function LoginController() {
  Controller.call(this)
}

// 부모와 자식 연결
LoginController.prototype = Object.create(Controller.prototype)
LoginController.prototype.getUser = function () {
  return document.getElementById("login_username").value
}
LoginController.prototype.getPassword = function () {
  return document.getElementById("login_password").value
}
LoginController.prototype.validateEntry = function (user, pw) {
  user = user || this.getUser()
  pw = pw || this.getPassword()

  if (!(user && pw)) {
    return this.failure("ID와 비번 입력 부탁")
  } else if (pw.length < 5) {
    return this.failure("비밀번호는 5자 이상")
  }

  return true
}

// failure를 확장하기 위해 오버라이드
LoginController.prototype.failure = function (err) {
  Controller.prototype.failure.call(this, "Login invalid: " + err)
}

// 자식 클래스
function AuthController(login) {
  Controller.call(this)
  this.login = login
}

// 부모 클래스와 자식 연결
AuthController.prototype = Object.create(Controller.prototype)
AuthController.prototype.server = function (url, data) {
  return $.ajax({
    url: url,
    data: data,
  })
}
AuthController.prototype.checkAuth = function () {
  var user = this.login.getUser()
  var pw = this.login.getPassword()

  if (this.login.validateEntry(user, pw)) {
    this.server("/check-auth", {
      user: user,
      pw: pw,
    })
      .then(this.success.bind(this))
      .fail(this.failure.bind(this))
  }
}
// success를 확장하기 위해 오버라이드
AuthController.prototype.success = function () {
  Controller.prototype.success.call(this, "인증 성공!")
}
// failure를 확장하기 위해 오버라이드
AuthController.prototype.failure = function (err) {
  Controller.prototype.failure.call(this, "인증 실패: " + err)
}

var auth = new AuthController(new LoginController())
auth.checkAuth()
  • success(), failure(), showDialog()는 모든 컨트롤러가 공유하는 기본 작동이 구현된 메서드이다. 각각의 자식 클래스는 해당 메서드를 오버라이드하여 기본 작동을 증강한다. AuthController가 로그인 폼과 연동하려면 LoginController 인스턴스가 있어야 하는데, 그래서 아예 멤버 프로퍼티로 들고 있다.

6.3.1 탈클래스화

  • 모델링 과정에서 굳이 클래스와 구성까지 동원해야할까? OLOO 스타일을 이용하면 더 간단한 형태로 디자인할 수 있다.
var LoginController = {
  errors: [],
  getUser: function () {
    return document.getElementById("login_username").value
  },
  getPassword: function () {
    return document.getElementById("login_password").value
  },
  validateEntry: function (user, pw) {
    user = user || this.getUser()
    pw = pw || this.getPassword()

    if (!(user && pw)) {
      return this.failure("ID와 비밀번호 입력!")
    } else if (pw.length < 5) {
      return this.failure("비밀번호는 5자 이상!")
    }

    return true
  },
  showDialog: function (title, msg) {},
  failure: function (err) {
    this.errors.push(err)
    this.showDialog("에러", "로그인 실패: " + err)
  },
}

// Link `AuthController` to delegate to `LoginController`
var AuthController = Object.create(LoginController)

AuthController.errors = []
AuthController.checkAuth = function () {
  var user = this.getUser()
  var pw = this.getPassword()

  if (this.validateEntry(user, pw)) {
    this.server("/check-auth", {
      user: user,
      pw: pw,
    })
      .then(this.accepted.bind(this))
      .fail(this.rejected.bind(this))
  }
}
AuthController.server = function (url, data) {
  return $.ajax({
    url: url,
    data: data,
  })
}
AuthController.accepted = function () {
  this.showDialog("성공", "인증 성공!")
}
AuthController.rejected = function (err) {
  this.failure("인증 실패: " + err)
}

// 그냥 객체이기 때문에 인스턴스화 할 필요가 없이 아래 한 줄이면 끝.
AuthController.checkAuth()

// 위임 연쇄에 하나 이상의 객체를 추가로 생성해야 할 경우는 아래와 같이 하면된다.
var controller1 = Object.create(AuthController)
var controller2 = Object.create(AuthController)
  • 작동 위임 패턴에서 AuthController와 LoginController는 수평적으로 서로를 엿보는 객체일 뿐이며 클래스 지향 패턴처럼 부모/자식 관계를 억지로 맺을 필요가 없다.
  • 가장 주목할 부분은 이전에 3개였던 객체가 2개로 줄은것이다. 징검다리 역할을 하는 객체가 더는 필요없다.
  • 같은 기능을 훨씬 더 간단한 디자인으로 설계할 수 있다는 것이 OLOO 스타일 코드의 힘이자 작동 위임 디자인 패턴의 강력함이다.

6.4 더 멋진 구문

  • ES6에서 나온 class 구문은 클래스 메서드를 짧은 구문으로 선언할 수 있다. function 키워드도 단축 메서드 선언이 가능하여 OLOO 객체를 선언할 수도 있다.
// class 구문
class Foo {
  methodName() {}
}

// OLOO 스타일 객체
var LoginController = {
  errors: [],
  getUser() {},
  getPassword() {},
}

var AuthController = {
  errors: [],
  checkAuth() {},
  server(url, data) {},
}

// setPrototypeOf 메서드로 객체의 Prototype을 수정 가능
Object.setPrototypeOf(AuthController, LoginController)

6.4.1 비어휘적 식별자

  • 단축 메서드에는 결점이 있다. 바로 기명 함수가 익명 함수 표현식이 되버린 것이다. 익명 함수 표현식의 단점은 전작에도 나왔는데, 다음과 같다.
  1. 스택 추적을 통해 디버깅 하기가 곤란해진다.
  2. 재귀, 이벤트 바인딩 등에서 자기 참조가 어려워진다.
  3. 코드 가독성이 더 나빠진다.
  • 1,3번은 단축 메서드에는 해당하지 않는다. 하지만 자기 참조를 할 수 있는 어휘적 식별자는 없다. 아래 코드를 보면 알 수 있다.
var Foo = {
  bar: function(x) {
    if (x < 10) {
      return Foo.bar(x * 2)
    }
    return x
  }
  baz: function baz(x) {
    if (x < 10) {
      return baz(x * 2)
    }
    return x
  }
}
  • 저자는 단축 메서드는 자기 참조 시 방법이 마땅치 않아 이슈가 될 수 있으니 유의하자고 한다. 그리고 전작에서와 마찬가지로 함수 선언일 경우 이름 붙은 함수 표현식으로 사용하기를 권한다.

6.5 인트로스펙션

  • 타입 인스트로펙션은 인스턴스를 조사해 객체 유형을 거꾸로 유추하느 것이다. 클래스 인스턴스에서 타입 인트로스펙션은 주로 인스턴스가 생성된 소스 객체의 구조와 기능을 추론하는 용도로 쓰인다.

  • 이 부분은 읽어도 무슨 의도인지 잘 모르겠네요 ㅠㅠ