개발/Javascript

[You don't know JS] Part3 - 6장. 작동 위임

lanace 2020. 9. 6. 06:43

작동 위임

5장을 요약하면 다음과 같다.

[[Prototype]] 객체는 한 객체가 다른 객체를 잠조하기 위한 내부 링크이다.

[[Prototype]]을 사용하여 객체에 없는 필드나 메서드에 접근하기 위해 사용한다.

현재 객체에 없으면 [[Prototype]]을 따라 들어가 찾고, 다시 또 [[Prototype]]를 찾는다.

이를 프로토타입 체이닝이라 한다.

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

Javascript는 [[Prototype]]이 클래스와 근본부터 다른 디자인 패턴이다.

단, 클래스 지향 디자인 원칙을 모두 포기하라는것은 아니다.

6.1.1 클래스 이론

클래스 기반의 디자인 설계

  1. 부모가 되는 클래스를 설계
  2. 부모를 상속받은 자식클래스를 정의한다.
  3. 자식의 특성을 자식클래스에 정의한다.

위 과정에서 메서드를 오버라이드 할 것을 권장하고, 동작을 추가하는것을 권장한다.

또한 super를 통해 부모의 메서드에도 접근 가능하게 한다.

공통 요소는 추상화하여 부모클래스의 추상 메서드를 자식 클래스에서 구현하여 사용한다.

6.1.2 위임 이론

위임 기반의 디자인 설계

  1. 부모 객체를 정의, 자식들이 사용할 유틸리티 메서드가 포함된 구체적인 동작을 정의한다.
  2. 자식 객체를 정의하여 고유한 데이터와 동작을 정의한다.
  3. 부모 객체에 연결해 필요할때 특정 객체가 부모의 동작을 위임하도록 작성한다.

위 과정은 형제로부터 작동을 가져온다.

하지만 클래스 복사를 통해 이 둘을 조합하지 않아도 각자 별개의 객체로 분리된 상태에서 동작을 위임하는 구조이다.

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);
// do something for ABC 

위에서 Task, ABC, XYZ는 평범함 객체이다.

Object.create를 사용하여 [[prototype]]을 연결했다 (위임)

이러한 스타일을 OLOO 라 한다.

Object Linked to Other Objects

OLOO의 특징

  • 데이터 필드는 부모에 갖고있지 않고 자식들에게 위임된다.

    즉, id와 label은 Task에 있지 않고, 자식인 ABC와 XYZ가 직접 가지고 있다.

  • 가려짐을 피하기위해서 작동 방식을 잘 설명하는 서술적인 명칭이 좋다.

    클래스형에서는 outputTask라는 메서드를 오버라이드 하여 두 자식이 동일한 명을 사용하였지만 OLOO에서는 정반대이다.

    가려짐에 유의하기 위해 의미가 분명해지도록 코드의 가독성과 유지보수성을 높인다.

  • [[prototype]]을 이용하여 부모의 메서드를 찾고, this를 통해 자식의 데이터를 참조한다.

    this.setId는 [[prototype]] 를 통해 Task에 있는 setId를 찾고, 여기서 this 바인딩을 이용하여 참조한다.

작동 위임이란 찾으려는 프로퍼티/메서드 레퍼런스가 객체에 없으면 다른 객체로 찾는 작업을 위임하는것을 말한다.

부모 ↔ 자식 간의 관계를 생각하는 디자인 패턴과 다르게
모든 객체들이 서로 수평적으로 배열된 상태에서 위임 링크가 체결된 모습으로 생각하면 좋을것같다.

위임은 상세한 내부 구현 용도로 적절하다.
외부에 API 인터페이스를 노출시키지 않는다.

과연 이것이 장점인가??

상호 위임 (허용되지 않음)

복수의 객체가 양방향으로 상호 위임을 하면 발생하는 사이클은 허용되지 않는다.

⇒ [[prototype]]을 무한으로 찾는 루프에 빠지게 될 위험이 있기 때문

만약 모든 레퍼런스가 확실하게 존재한다면 유용할 수도 있음.

⇒ 과연? 어떤경우에서...?

결론적으론 엔진에서 프로퍼티를 체크할때 예외처리와 무한루프 참조하는것 사이에서 성능을 생각했을때 상호위임이 더 무겁다고 판단하여 사용이 불가능하다!

디버깅

자바스크립트 스팩에 디버깅에 대한 구체적인 정보가 없어서 브라우저 구현체마다 표현되는 방식이 조금씩 다름

function Foo() {}

var a1 = new Foo();

a1;

전지적 크롬시점

a1;  // Foo { }

크롬: Foo 라고 명명된 함수가 생성한 빈 객체이기 때문!

전지적 파폭시점

a1;  // Object {  }

파폭: {} 는 Object이니 Object이다!

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();

자식 클래스 Bar는 부모 클래스 Foo를 상속한 뒤 b1, b2 인스턴스를 생성.

그리고 b1, b2는 Bar.prototype과 연결되며, Bar는 Foo.prototype과 연결된다.

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);
var b2 = Object.create(Bar);

b1.init('b1');
b2.init('b2');

b1.speak();
b2.speak();

OLOO 방식에서도 b1은 Bar와 연결되어 있으며, Bar는 Foo와 연결되어있다.

중요한것은 잡다한 것들이 정리되고, 단순해졌다는 것이다.

생성자, 프로토타입, new호출을 하면서 클래스처럼 보이게 한 장치를 쓰지 않고, 객체를 연결해준거다.

6.2 클래스 vs 객체


6.2.1 위젯 클래스

// 부모 클래스
function Widget(width, height) {
    this.width = whith;
    this.height = height;
    this.$elem = null;
}

Widget.prototype.render = function($where) {
    if (this.$elem) {
        this.$elem.css({
            width: this.width + 'px',
            height: this.height + 'px',
        }).appendTo($where);
    }
}

// 자식 클래스
function Button(width, height, label) {
    Widget.call(this, width, height);
    this.label = label || 'default label';
    this.$elem = $('<Button>').text(this.label);
}

// Button은 widget으로부터 상속받음
Button.prototype = Object.create(Widget.prototype);

// 상속받은 render를 오버라이드 한다.
Button.prototype.render = functioon ($where) {
    // super 호출
    Widget.prototype.render.call(this, $where);
    this.$elem.click(this.onClick.bind(this));
}

Button.prototype.onClick = function(event) {
    console.log(this.label + 'is Clicked');
}

$(document).ready(function() {
    var $body = $(document.body);
    var button1 = new Button(125, 30, 'Hello1');
    var button2 = new Button(125, 30, 'Hello2');

    button1.render($body);
    button2.render($body);
});

OO 디자인 패턴을 사용하여 위와같이 구현함

render만 선언해두고, 자식 클래스가 이를 오버라이드 하도록 유도한다.

버튼에 해당하는 기능에 추가하여 살을 덧붙이는 방식으로 개발

ES6의 Class 간편 구문

class Widget {
    constructor(width, height) {
        this.width = width || 50;
        this.height = height || 50;
        this.$elem = null;
    }

    render($where) {
        if (this.$elem) {
            this.$elem.css({
                width: this.width + 'px',
                height: this.height + 'px'
            }).appendTo($where);
        }
    }
}

class Button extends Widget {
    constructor (width, height, label) {
        super(width, height);

        this.label = label;
        this.$elem = $('<Button>').text(this.label);
    }

    render($where) {
        super($where);
        this.$elem.click(this.onClick.bind(this));
    }

    onClick() {
        console.log(this.label + 'is Clicked');
    }
}

$(document).ready(function() {
    var $body = $(document.body);
    var button1 = new Button(125, 30, 'Hello1');
    var button2 = new Button(125, 30, 'Hello2');

    button1.render($body);
    button2.render($body);
});

ES의 Class를 사용하면 그래도 조금 깔끔하게 처리할 순 있다.

super도 사용 가능하다.

6.2.2 위젯 객체의 위임

var Widget = {
    init: function(width, height) {
        this.width = width;
        this.height = height;
        this.$elem = null;
    },

    insert: function($where) {
        if (this.$elem) {
            this.$elem.css({
                width: this.width + 'px',
                height: this.height + 'px'
            }).appendTo($where);
        }
    }
};

var Button = Object.create(Widget);
Button.setup = function(width, height, label) {
    this.init(width, height);
    this.label = label;
    this.$elem = $('<Button>').text(this.label);
}

Button.build = function($where) {
    this.insert($where);
    this.$elem.click(this.onClick.bind(this));
}

Button.onClick = function(event) {
    console.log(this.label + 'is clicked');
}

$(document).ready(function() {
    var $body = $(document.body);
    var button1 = Object.create(Button);
    var button2 = Object.create(Button);

    button1.setup(120, 40, 'btn1');
    button2.setup(120, 40, 'btn2');

    button1.build($body);
    button2.build($body);
})

OLOO 관점에선 Widget이나 Button은 부모자식이 아니다.

Widget은 다른 위젯들이 편하게 가져다 사용할 수 있는 유틸리티 저장소 정도의 역할이다.

장점들

  • render같은 공통된 메서드를 상속받아 구현하지 않아도 된다.
  • 생성자, prototype, new 등의 구문을 쓸필요가 없다.
  • 생성자에서 한번에 하는걸 나누는것이 장점이다.(?) ⇒ 관심사 분리...

6.3 더 간단한 디자인


코드는 161~163p 참조

class Controller {
    ...
}

class AuthController extends Controller {
    ...
}

class LoginController extends Controller {
    ...
}

일반적인 클래스 지향 디자인

function AuthController(login) {...}

...

var auth = new AuthController();
auth.checkAuth(
    // 상속 + 구성
    new LoginController()
)

var auth = new AuthController(new LoginController());
auth.checkAuth();

6.3.1 탈클래스화

OLOO스타일로 변경해보자

코드는 164~166p 참조

일단 AuthController와 LoginController는 단순 객체이다.

따라서 new 를 통해 인스턴스화 할 필요가 없다.

여러개의 인스턴스를 만들고 싶다면 다음과 같이 하면 된다.

AuthController.checkAuth();

var controller1 = Object.create(AuthController);
var controller2 = Object.create(AuthController);

차이점

  • 서로 공유하기 위해 만들었던 Controller가 없어도 된다.
  • 인스턴스화 과정이 없다.
  • 동일한 이름의 메서드를 사용하지 않아도 된다.

6.4 더 멋진 구문


ES6부턴 function 키워드를 사용하지 않고 함수를 생성할 수 있다.

var LoginController = {
    errors: [],
    getUser() {

    },
    getPassword() {

    }
}

단, ,를 통해 구분해야된다는 점이 class와 차이다.

[[prototype]]을 다음과 같이 Object.setPrototypeOf로 수정할 수 있다.

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

Object.setPrototypeOf(AuthController, LoginController);

6.4.1 비어휘적 식별자

var Foo = {
    bar: function() {...}, 
    baz: function() {...}
}

위의 bar와 baz는 단축 메서드이다.

단축 메서드는 익명함수의 단점중 스텍추적과 가독성을 향상시켰다.

익명함수의 단점은 다음과 같다.

  • 스텍 추적을 통한 디버깅이 어렵다.
  • 재귀, 이벤트 바인딩 등에서 자기 참조가 어렵다.
  • 가독성이 좋지 않다.
var Foo = {
    bar: function(x) {
        if (x < 10) {
            return Foo.bar(x * 2);
        }

        return x;
    },

    baz: function(x) {
        if (x < 10) {
            return baz(x * 2);
        }

        return x;
    }
}

위의 코드처럼 Foo.bar를 통해 접근하는 방법이 있긴 하다.

하지만 권장하는 방식은 함수 이름까지 포함하는 것이다.

baz: function baz() {...}

아직 익명함수의 단점에 대해서 잘 모르겠다. 진짜로 그렇게 불편하고, 가독성이 좋지 않은가?
진짜로 this가 걸림돌이 되는가?
음...

6.5 인트로스펙션

객체의 유형을 거꾸로 유추하는 것

인스턴스가 생성된 소스 객체의 구조와 기능을 추론할때 주로 쓰인다.

function Foo() {
    ...
}

Foo.prototype.something = function() {

}

var a1 = new Foo();

if (a1 instanceof Foo) {
    a1.something();
}

Foo.prototype은 a1의 [[prototype]] 연쇄에 존재하므로 a1이 인스턴스인것처럼 표현된다.

실제로는 인스턴스가 아니고 단순 prototype이 연결된것 뿐이다.

따라서 instanceof 를 통해 인트로스펙션을 하는건 헷갈리고, 간접적이다.

instanceof 이외에 타입 인트로스펙션 방법으로는 덕 타이핑이 있다.

덕 타이핑

  • 사람이 오리처럼 행동하면 오리로 봐도 무방하다라는게 덕 타이핑(Duck Typing)이다.
  • 타입을 미리 정하는게 아니라 실행이 되었을 때 해당 Method들을 확인하여 타입을 정한다.
  • 장점
    • 타입에 대해 매우 자유롭다.
    • 런타임 데이터를 기반으로 한 기능과 자료형을 창출하는 것
  • 단점
    • 런타임 자료형 오류가 발생할 수 있다 런타임에서, 값은 예상치 못한 유형이 있을 수 있고, 그 자료형에 대한 무의미한 작업이 적용된다.
    • 이런 오류가 프로그래밍 실수 구문에서 오랜 시간 후에 발생할 수 있다
    • 데이터의 잘못된 자료형의 장소로 전달되는 구문은 작성하지 않아야 한다. 이것은 버그를 찾기 어려울 수도 있다.
if(a1.something) {
    a1.something();
}

덕 타이핑의 사례도 Promise가 있다.
then을 가지고 있으면 promise로 보고, 그 외에 스펙을 만족한다고 가정하고 동작된다.

그래서 결론적으로 덕 타이핑은 위험하다.

제한적으로 조절 가능한 조건에서만 사용하자

6.6 정리하기


클래스는 구조를 잡기위한 유일한 방법은 아니다.

작동 위임이라는 선택지가 더 있다.

작동 위임은 객체를 부모/자식 클래스 관계가 아닌 동등한 입장에서 서로 위임하는 형태로 연결된다.

javascript는 [[prototype]]을 사용한다.

객체만으로 구성하면 단순해지고, 코드 아키텍쳐도 가벼워진다.

OLOO는 직접 객체를 생성 및 연계한다.