본문 바로가기
Design Pattern/Creational Design

[Design Pattern] (Creational) 생성자 패턴(Constructor Pattern)

by song.ift 2023. 3. 26.

디자인 패턴 유형

  • Creation Design Pattern
    • 객체의 생성에 관련된 패턴
    • 객체의 생성 로직을 숨김으로써 유연성을 향상시킨다.
  • Strutural Design Pattern
    • 클래스와 객체의 구성에 관련된 패턴
  • Behavioral Design Pattern
    • 객체와 객체 간의 통신에 관련된 패턴

 

[Creational Design Pattern] 생성자 패턴(Constructor Pattern)

애플리케이션이 복잡해질수록 모듈화가 필요하다. 자바스크립트도 객체로 비슷한 속성과 행위를 묶어서 모듈화 할 수 있다. 이렇게 좀 더 객체지향적인 방법으로 표현하는 방법을 생성자(constructor) 패턴을 통해 이해해보자.

먼저, 자바스크립트의 객체는 객체 리터럴로 쉽게 표현할 수 있다.

 

객체 리터럴

var healthObj = {
  name : "달리기",
  lastTime : "PM10:12",
  showHealth : function() {
    console.log(this.name + "님, 오늘은 " + this.lastTime + "에 운동을 하셨네요");
  }
}

이러한 코드를 객체리터럴이라고 하는데, 이 healthObj의 형태를 가진 객체가 여러 개 필요할 때가 있다. name이 "개발자"이고, 운동을 한 시간 lastTime이 AM9:30인 사람에 대한 객체가 필요하다고 가정해보겠다.

var healthObj2 = {
  name : "개발자",
  lastTime : "AM9:30",
  showHealth : function() {
    console.log(this.name + "님, 오늘은 " + this.lastTime + "에 운동을 하셨네요");
  }
}

필요한 정보를 담은 healthObj2라는 객체를 새로 만들어 처리할 수 있다.

하지만 이렇게 하면 코드에 불필요한 중복이 발생하고, 새로운 데이터가 추가될 때마다 비슷한 객체들을 계속해서 만들어 두는 것은 매우 비효율적이다. 또한, 유지보수할 때도 수정이 필요한 중복되는 코드를 모두 수정하려면 힘들어질 것이다.

이를 해결하기 위한 방법으로, 생성자 new와 prototype이 있다.

 

객체를 동적으로 생성하기 (new 키워드)

사용자 정의 생성자 함수

객체 리터럴 패턴이나 내장 생성자 함수를 쓰지 않고, 직접 생성자 함수를 만들어 객체를 생성할 수도 있다.

var Person = function (name) {
    this.name = name;
    this.say = function () {
        return 'I am ' + this.name;
    };
};

var jaehee = new Person('jaehee');
jaehee.say(); // 'I am jaehee'

new와 함께 생성자 함수를 호출하면 함수 안에서는 다음과 같은 일이 일어난다.

  • 빈 객체(사실 빈 객체는 아니다)가 생성된다. 이 객체는 this라는 변수로 참조할 수 있고, 해당 함수의 프로토타입을 상속받는다.
  • this로 참조되는 객체에 프로퍼티와 메소드가 추가된다.
  • 마지막에 다른 객체가 명시적으로 반환되지 않을 경우, this로 참조된 이 객체가 반환된다.

즉 이면에서는 다음과 같이 진행된다고 할 수 있다.

var Person = function (name) {
  // 객체 리터럴로 새로운 객체를 생성한다.
  // var this = {};

  // 프로퍼티와 메서드를 추가한다.
  this.name = name;
  this.say = function () {
    return 'I am ' + this.name;
  };

  // this를 반환한다.
  // return this;
};

한 가지 더 언급할 만한 사실이 있다. 먼저 생성자 내부의 이면에서는 다음과 같은 일이 벌어진다고 했다.

// var this = {};

그리고 이것이 다가 아니다. 왜냐하면 '빈' 객체라는게 실제로는 텅 빈 것이 아니기 때문이다.  이 객체는 Person의 프로토타입을 상속받는다. 즉 다음 코드에 더 가깝다.

// var this = Object.create(Person.prototype);

 

다른 예시

// var Health = function (name, lastTime) {
function Health(name, lastTime) {
    this.name = name;
    this.lastTime = lastTime;
    this.showHealth = function() {
        return this.name + "님 오늘은 " + this.lastTime + "에 운동을 하셨네요";
    }
}

var healthObj = new Health("달리기","PM10:00");

위 코드의 healthObj에 Health {name: "달리기", lastTime: "PM 10:00", showHealth: function(){... }} 이런 객체 리터럴 형태의 객체 데이터가 담겨 있을 것이다.

만약 var healthObj = Health("달리기","PM10:00");라고 했다면, healthObj는 undefined였겠지만, new 키워드로 Health 함수를 불러내면서 저런 객체를 생성할 수 있었다. 그래서 Health를 생성자라고 한다. (생성자는 보통 대문자로 시작한다.)

  • new 키워드가 Health 함수를 불렀을 때 객체를 반환했던 건, 위에서 말했듯 new로 Health 함수를 호출하면 그 Health 함수 안의 this를 반환하게 되기 때문이다.
  • return this;가 내부에서 실행되는 것과 같이 동작한다고 생각하면 된다. 그래서 Health 함수의 인자로 받은 데이터가 this에 저장되어 return 되었기 때문에, 저런 결과 데이터를 받을 수 있었다.

이렇게 new 키워드로 객체를 동적으로 생성할 수 있는 방법을 알아보았다. 하지만 showHealth 함수는 여전히 객체 생성할 때마다 생성돼서 불필요한 메모리 공간을 차지하고 있다.

function makeObject(name,babo){
  return {
    name,
    babo,
    say : function() {
      console.log(this.name+"zz"+this.babo);
    }
  }
}

let object1 = makeObject('zz','melong');
let object2 = makeObject('gg','melllong');

이 코드에서도 동일 문제점이 있다. object1, object2는 서로 다른 객체라는것은 맞다. 하지만 객체의 메소드 say 또한 서로 다른 함수로 인지한다는 것이다. 2개의 객체를 생성하면 2개의 서로 다른 메소드를 생성한다는 의미가 된다. 이는 브라우저 메모리 낭비로 직결되기 때문에 좋지 않다. 이러한 패턴을 없애는 방법이 객체 생성자 패턴 Prototype이다.

 

Prototype 사용하기

생성자들은 prototype이라는 것들을 다 가지고 있다. 생성자의 prototype에 어떤 메서드를 넣어두면, 이 생성자로 만들어진 객체들은 prototype의 공간을 공유하고 있으므로 해당 메서드를 모두 사용할 수 있다.

function Health(name, lastTime) {
  this.name = name;
  this.lastTime = lastTime;
}

// prototype에 속성 추가
Health.prototype.showHealth = function() {
  console.log(this.name + "," + this.lastTime);
}

const h = new Health("달리기", "PM10:12");
console.log(h); // 크롬개발자도구를 열고 이 부분이 어떻게 출력되는지 확인
h.showHealth();

Health.prototype.xxx로 Health라는 생성자의 prototype에 속성을 추가하여 어떤 메서드를 생성할 수 있다. 위 코드처럼 showHealth를 Health가 아닌 protootype 객체에 추가했다.

자바스크립트는 현재 객체에 존재하지 않는 값이 호출되면, prototype chaining을 따라 탐색하는 성질을 가지고 있다. 이런 식의로 객체 생성자 패턴의 prototype에 함수를 지정하게 되면 추가로 메소드를 생성하는데 자원을 쏟지 않고, 프로토타입에 있는 함수를 사용하기 때문에 조금 더 효율적인 코드가 될 수 있다.

 

크롬 개발자 도구 콘솔에서 console.log(h);를 실행해보면,

 

이런 결과가 나온다. h 객체 속성 중 __proto__라는 속성이 보일 것이다.

이것이 크롬 개발자 도구에서 prototype 객체를 나타내는 속성 이름인데, 위 사진처럼 Health의 prototype에 showHealth 함수 속성이 있음을 알 수 있다.

const h = new Health("달리기", "PM10:12");
const h2 = new Health("개발자", "AM9:20");
console.log(h.showHealth === h2.showHealth); //결과 값: true

이렇게 h, h2 두개의 객체를 생성했을 때, 각 객체의 showHealth 메서드는 참조하는 값이 같음을 알 수 있다. h, h2 모두 prototype 안의 showHealth를 불러오기 때문이다. 도식으로 표현하면 아래 사진과 같다.

상속과 같은 느낌이 난다. 상속을 위한 건 아니지만, 이 prototype을 이용하여 상속 구조를 만들 수도 있다고 한다.

 

생성자의 반환값

위에서 다 말했듯 생성자 함수를 new와 함께 호출하면 항상 객체가 반환된다. 기본값은 this 로 참조되는 객체다. 생성자 함수 내에서 아무런 프로퍼티나 메소드를 추가하지 않았다면 '빈'(즉, 생성자의 프로토타입에서 상속된 것 외에는 '비어있는') 객체가 반환될 것이다. 함수 내에 return문을 쓰지 않더라도 생성자는 암묵적으로 this를 반환한다.

그러나 반환 값이 될 객체를 따로 명시적으로 정할 수도 있다.

var Objectmark = function () {
  // 생성자가 다른 객체를 반환하기로 결정했기 때문에
  // 다음의 'namme' 프로퍼티는 무시된다
  this.name = 'This is it';

  // 새로운 객체를 생성하여 반환한다.
  var that = {};
  that.name = "And that's that";
  return that;
};

// TEST
var o = Objectmark();
console.log(o.name); // "And that's that"

이와 같이 생성자에서는 어떤 객체라도(객체이기만 한다면) 반환할 수 있다.

  • 객체가 아닌 것(예를 들면 문자열이나 false 값)을 반환하려고 시도한다면, 에러가 발생하진 않지만 그냥 무시되고 this에 의해 참조된 객체가 대신 반환된다.

 

new를 강제하는 패턴

앞서 언급했듯이 생성자란 new와 함께 호출될 뿐 여전히 별다를 것 없는 함수에 불과할 뿐이다. 그렇다면 생성자를 호출할 때 new를 빼먹으면 어떻게 될까?

  • 문법 오류나 런타임 에러가 발생하지는 않지만, 논리적인 요류가 생겨 예기치 못한 결과가 나올 수 있다. new를 빼먹으면 생성자 내부의 this가 전역 객체를 가리키게 되기 때문이다.(브라우저에서라면 this가 window를 가리키게 된다)
  • 생성자 내부에 this.member와 같은 코드가 있을 경우 이 생성자를 new 없이 호출하면, 실제로는 전역 객체에 member라는 새로운 프로퍼티가 생성된다. 이 프로퍼티는 window.member 또는 그냥 member를 통해 접근할 수 있다. 알다시피 전역 네임스페이스는 항상 깨끗하게 유지해야 하기 때문에, 이런 동작 방식은 대단히 바람직하지 않다.
// 생성자
function Coffee() {
    this.tastes = 'dalcom';
}

// 새로운 객체
var morning_coffee = new Coffee();
console.log(typeof morning_coffee);  // 'object'
console.log(morning_coffee.tastes);  // 'dalcom'

// 안티 패턴
// 'new' 를 빼먹음
var morning_coffee = Coffee();
console.log(typeof morning_coffee);  // 'undefined'
console.log(window.tastes);  // 'dalcom'

명명규칙

가장 간단한 대안은 명명규칙을 사용하는 것이다. 즉, 생성자 함수명의 첫글자를 대문자로 쓰고(Myconstructor) '일반적인' 함수와 메서드의 첫글자는 소문자를 사용(myFunction)하는 것이다

that 사용

명명 규칙을 따르는 것도 꽤 도움이 되지만, 이는 올바른 동작 방식을 권고할 뿐 강제하지는 못한다.

그래서 생성자가 항상 생성자로 동작하도록 해주는 패턴이 있다. this에 모든 멤버를 추가하는 대신, that에 모든 멤버를 추가한 후 that을 반환하는 것이다.

function MyConstructor() {
    var that = {};
    that.name = 'jaehee';
    return that;
}

위 코드와 같이 간단한 객체라면 that이라는 지역 변수를 만들 필요도 없이 객체 리터럴을 통해 다음과 같이 객체를 반환해도 된다.

function MyConstructor() {
   return {
       name : 'jaehee'
   }
}

위의 MyConstructor() 구현 중 어느 것을 사용하더라도 호출 방법과 상관없이 항상 객체가 반환된다.

var first = new MyConstrutor(),
    second = MyConstrutor();

console.log(first.name); // 'jaehee'
console.log(second.name); // 'jaehee

하지만 위 패턴의 문제는 프로토타입과의 연결고리가 끊어지게 된다는 점이다. 즉, MyConstructor() 프로토타입에 추가한 멤버를 객체에서 사용할 수 없게 된다.

that 이라는 변수명은 관습적인 것으로, 언어에 정의되어 있진 않다. 어떤 이름이라도 쓸 수 있다. 흔히 사용되는 변수명으로는 self와 me 등이 있다.

 

스스로를 호출하는 생성자

앞서 언급한 패턴의 문제점을 해결하고, 인스턴스 객체에서 프로토타입의 프로퍼티들을 사용할 수 있게 하려면, 다음의 접근법을 고려하면 될 것이다. 생성자 내부에서 this가 해당 생성자의 인스턴스인지를 확인하고, 그렇지 않은 경우 new와 함께 스스로를 재호출하는 것이다.

function MyConstrutor() {
    if (!(this instanceof MyConstrutor)) {
        return new MyConstrutor();
    }

    this.name = 'jaehee';
}

MyConstrutor.prototype.anotherName = true;

// 호출확인
var first = new MyConstrutor(),
    second = MyConstrutor();

console.log(first.name); // 'jaehee'
console.log(second.name); // 'jaehee'

console.log(first.anotherName);
console.log(second.anotherName);

인스턴스를 판별하는 또다른 범용적인 방법은 생성자 이름을 하드코딩하는 대신 arguments.callee와 비교하는 것이다.

function MyConstrutor() {
    if(!(this instanceof arguments.callee)) {
        return new arguments.callee();
    }

    this.name = 'jaehee';
}

이것은 모든 함수가 호출될 때, 내부적으로 arguments라는 객체가 생성되며, 이 객체가 함수에 전달된 모든 인자를 담고 있다는 점을 활용한 패턴이다. arguments의 callee라는 프로퍼티는 호출된 함수를 가리킨다. 

 

ex)

옛날 스타일

function OldPerson(name, age) {
  this.name = name;
  this.age = age;
}

// prototype에 속성 추가
OldPerson.prototype.say = function() {
  console.log(`[OLD] ${this.name} is ${this.age} years old!`);
}

// 인스턴스 생성
const oldPerson = new OldPerson('John', 20);
oldPerson.say(); // [OLD] Output - “John is 20years old!”

class

ES6 부터는 자바스크립트가 class를 지원해주고 있다.

class NewPerson {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  say() {
    console.log(`[NEW] ${this.name} is ${this.age} years old!`);
  };
}

// 인스턴스 생성
const newPerson = new NewPerson('John', 20);
newPerson.say(); // [NEW] Output - “John is 20years old!”

댓글