본문 바로가기
Study Note/Javascript

javascript #자바스크립트 객체와 SOLID 원칙

by 시뮝 2022. 5. 29.
728x90

자바스크립트 객체와 SOLID 원칙

  1. 원시형
  2. 객체 리터럴
  3. 모듈 패턴
  4. 객체 프로토타입과 프로토타입 상속
  5. new 객체 생성
  6. 클래스 상속
  7. 함수형 상속
  8. 멍키 패칭

1. 자바스크립트 원시형 (Primitive Type)

JavaScript에서 원시 값(primitive, 또는 원시 자료형)이란 객체가 아니면서 메서드도 가지지 않는 데이터다. 원시 값에는 7종류, string, number (en-US), bigint (en-US), boolean, undefined, symbol, 그리고 null이 존재한다.

대부분의 경우, 원시 값은 언어 구현체의 가장 저급(low level) 단계에서 나타낸다.

모든 원시 값은 불변하여 변형할 수 없다. 원시 값 자체와, 원시값을 할당한 변수를 혼동하지 않는 것이 중요하다. 변수는 새로운 값을 다시 할당할 수 있지만, 이미 생성한 원시 값은 객체, 배열, 함수와는 달리 변형할 수 없다.

원시형의 SOLID/DRY 요약표

원칙 결과
단일 책임 그 누가 원시형만큼 일편단심일 수 있으랴!
개방/폐쇄 변경에 폐쇄적, 원시형은 불변값(immutable)이다.
리스코프 치환 해당 없음
인터페이스 분리 때로 원시형에 인터페이스를 구현하고 싶은 마음이 굴뚝같지만, 안타깝게도 불가능한 일이다.
의존성 역전 해당 없음
DRY(반복하지 마라) 원시형을 반복하는 건 좋지 않다.

 

2. 객체 리터럴

객체 리터럴은 한 곳에서 다른 곳으로 데이터 뭉치를 옮길 때 쓰기 편하다.

 

객체 리터럴은 두 가지 생성 방법이 있다.  첫번째는 단순 객체 리터럴(bare object literal)이다.

var koko = { name: 'Koko', genus: 'gorilla', genius: 'sign language' };

 

두번째는 객체 리터럴이 함수 반환값인 경우다. 단순 객체 리터럴에서는 의존성 주입은 아예 시도조차 해볼 기회가 없지만, 리터럴을 생성/반환하는 함수는 애플리케이션 시작부에서 의존성을 주입하는 과정에 아주 잘 어울린다.

var amazeTheWorld = function() {
    // ...
    return { name: 'Koko', genus: 'gorilla', genius: 'sign language' };
}

객체 리터럴의 SOLID/DRY 요약표

원칙 결과
단일 책임 사실 단순 객체 리터럴은 아주 작은  편이어서 이 평가 항목에 문제가 될 만한 부분은 없다.
개방/폐쇄 객체 리터럴 특성상 제멋대로 확장될 수도 있으니 조심하자
리스코프 치환 해당 없음
인터페이스 분리 모듈 패턴 및 멍키 패칭을 참고하자
의존성 역전 단순 객체 리터럴은 내부에 의존성을 주입할 생성자가 없으니 의존성 역전은 불가능하다.
DRY(반복하지 마라) 싱글톤이 아닌 단순 객체 리터럴은 WET한 코드가 되기 일쑤다. 유념하자.

 

3. 모듈 패턴

개방/폐쇄 원칙이 최우선 관심사라면 모듈만한 것도 또 없다.

 

모듈 패턴은 데이터 감춤이 주목적인 함수가 모듈 API를 이루는 객체를 반환하게 한다. 임의로 함수를 호출하여 생성하는 모듈과 선언과 동시에 실행하는 함수에 기반을 둔 모듈이 있다.

 

예제) 임의 모듈 패턴 예시

// 해당 애플리케이션에서만 사용할 수 있는 모든 객체(모듈)를 담아 넣은
// 전역 객체를 선언하여 namespace처럼 활용한다.
var MyApp = MyApp || {};

// 애플리케이션 이름공간에 속한 모듈
// 이 함수는 animalMaker라는 다른 함수에 의존하며 animalMaker는 주입 가능하다.
MyApp.wildlifePreserveSimulator = function(animalMaker) {
	// private 변수
    var animals = [];
    
    // API를 반환
    return {
    	addAnimal: function(species, sex) {
        	animals.push(animalMaker.make(species, sex));
        },
        getAnimalCount: function() {
        	return animals.length;
        }
    };
};

 

모듈은 다음과 같이 사용된다. 이 모듈은 객체 리터럴을 반환하나 animalMaker 같은 의존성을 외부 함수에 주입하여 리터럴에서 참조하게 만들 수도 있다. 다른 모듈에 주입할 수 있어 확장성이 좋다.

var preserve = MyApp.wildlifePreserveSimulator(realAnimalMaker);
preserve.addAnimal(gorilla, female);

 

예제) 즉시 실행 싱글톤 모듈 생성

var MyApp = MyApp || {};

MyApp.wildlifePreserveSimulator = (function() {
    var animals = [];
    
    return {
    	addAnimal: function(animalMaker, species, sex) {
        	animals.push(animalMaker.make(species, sex));
        },
        getAnimalCount: function() {
        	return animals.length;
        }
    };
})(); // <- 즉시 실행한다.

 

싱글톤은 이렇게 사용한다.

MyApp.wildlifePreserveSimulator.addAnimal(realAnimalMaker, gorilla, female);;

외부 함수는 애클리케이션 기동 코드의 실행과 상관없이 코드가 작성된 지점에서 즉시 실행된다. 따라서 함수 (즉시) 실행 시 의존성을 가져오지 못하면 외부 함수에 주입할 수 없다. 

 

모듈 생성의 원칙

  1. 한 모듈엔 한 가지 일만 시키자
  2. 모듈 자신이 쓸 객체가 필요하다면 의존성 주입 형태로 (직접 또는 팩토리 주입 형태로) 이 객체를 제공하는 방안을 고려하라
  3. 다른 객체 로직을 확장하는 모듈은 해당 로직의 의도가 바뀌지 않도록 분명히 밝혀라 (리스코프 치환 원칙)

 

모듈의 SOLID/DRY 요약표

원칙 결과
단일 책임 모듈은 태생 자체가 의존성 주입과 친화적이고 애스팩트 지향적이라 단일 책임 유지는 어렵지 않다.
개방/폐쇄 다른 모듈에 주입하는 형태로 얼마든지 확장할 수 있다. 통제해야 할 모듈은 수정하지 못하게 차단할 수 있다.
리스코프 치환 의존성의 의미를 뒤바꾸는 일만 없으면 별문제 없다.
인터페이스 분리 결합된 API 모듈 자체가 자바스크립트에서 분리된 인터페이스나 다름없다.
의존성 역전 임의 모듈은 의존성으로 주입하기 쉽다. 모듈이 어떤 형태든 다른 모듈에 주입할 수 있다.
DRY(반복하지 마라) 제대로만 쓴다면 DRY한 코드를 유지하는 데 아주 좋은 방법이다.

 

4. 객체 프로토타입과 프로토타입 상속

자바스크립트 객체는 생성 메커니즘과 무관하게 프로토타입 객체로 연결되어 프로퍼티를 상속한다.

 

기본 객체 프로토타입

객체 리터럴은 저절로 내장 객체 Object.prototype에 연결된다.

var chimp = {
    hasThumbs: true,
    swing: function() {
        return '나무 꼭대기에 대롱대롱 매달려 있네요';
    }
};

toString은 chimp 객체에 없는 함수이지만, 다음 코드를 실행해도 undefined 함수 에러는 나지 않는다.

// console에는 chimp 객체가 문자열로 표현된다. ('[object Object]')
chimp.toString();

Object.prototype에는 유용한 함수가 많다. 

https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes

 

프로토타입 상속

Object.prototype에 유용한 프로퍼티가 있긴 하지만, 자바스크립트의 프로토타입 상속은 기본 프로토타입을 맞춤형 프로토타입으로 대체할 때 그 진가가 드러난다.

ECMAScript 5 부터 등장한 Object.create 메서드를 사용하면 기존 객체와 프로토타입이 연결된 객체를 새로 만들 수 있다.

var ape = { // 공유 프로토타입(shared prototype)으로 사용됨
    hasThumbs: true,
    hasTail: false,
    swing: function() {
        return '매달리기';
    }
};

var chimp = Object.create(ape);

var bonobo = Object.create(ape);

bonobo.habitat = '중앙 아프리카';

// chimp와 bonobo 모두 ape에서 프로토타입을 물려받는다. bonobo에 직접 추가한 habitat 프로퍼티는 ape, chimp 와 공유하지 않는 고유 프로퍼티다.
console.log(bonobo.habitat); // '중앙 아프리카' (bonobo 프로퍼티)
console.log(bonobo.hasTail); // false (ape 프로토타입)
console.log(chimp.swing());  // '매달리기' (ape 프로토타입)

// ape는 공유 프로퍼티라서 수정하면 chimp와 bonobo 모두에 즉시 영향을 미친다.
ape.hasThumbs = false;
console.log(chimp.hasThumbs); // false
console.log(bonobo.hasThumbs);// false

 

프로토타입 체인(prototype chain)

프로토타입 체인이라는 다층 프로토타입을 이용하면 여러 계층의 상속을 구현할 수 있다.

var primate = {
    stereoscopicVision: true
};

var ape = Object.create(primate);
ape.hasHumbs = true;
ape.hasTail = false;
ape.swing = function() {
    return "매달리기";
};

var chimp = Object.create(ape);

console.log(chimp.hasTail);            // false (ape prototype)
console.log(chimp.stereoscopicVision); // true (primate prototype)

chimp.stereoscopicVision은 이 객체의 고유 프로퍼티가 아닌 까닭에 자바스크립트 엔진은 chimp의 프로토타입 체인을 따라 ape까지 올라가 결국 primate에서 이 프로퍼티를 발견한다. 만약 프로토타입 체인을 다 뒤져봐도 없으면 undefined를 반환한다.

 

너무 깊숙이 프로토타입 체인을 찾게 하면 성능상 좋을 게 없으니 너무 깊이 체인을 쓰지 않는 편이 좋다.

 

5. new 객체 생성

자바스크립트에서 객체를 new로 생성하는 구문 패턴은 C#, C++, Java 등의 언어와 모양새가 비슷하다.

 

예제) new 객체 생성 패턴으로 객체 인스턴스 생성

function Marsupial(name, nocturnal) {
	this.name = name;
    this.isNocturnal = nocturnal;
}

// maverick, slider라는 이름으로 생성한 두 Marsupial 인스턴스는
// 각각 고유한 프로퍼티 값을 가진다.
var maverick = new Marsupial('매버릭', true);
var slider = new Marsupial('슬라이더', false);

console.log(maverick.isNocturnal); // true
console.log(maverick.name); // "매버릭"

console.log(slider.isNocturnal); // false
console.log(slider.name); // "슬라이더"

자바스크립트 언어는 Marsupial 함수를 생성자 함수(new 키워드와 함께 사용하려고 작성한 함수)로 사용하라고 강요하지 않는다. 즉, new 키워드 없이 생성자 함수를 사용해도 이를 못하게 막을 보호 체계가 없다. 그래서 더러 개발자들은 파스칼 표기법(PascalCase)으로 생성자 함수를 따로 표기하여 구분하기도 한다.

 

new를 사용하도록 강제

자바스크립트 언어만으로는 new를 강제할 도리가 없다. 하지만 instanceof 연산자를 써서 우회적으로 강제하는 방법이 있다.

 

예제) instanceof 연산자로 new 사용을 강제

function Marsupial(name, noturnal) {
    if (!(this instanceof Marsupial)) {
        throw new Error("이 객체는 new를 사용하여 생성해야 합니다.");
    }
    this.name = name;
    this.isNocturnal = nocturnal;
}

var slider = Marsupial('슬라이더', true); // error

this instanceof Marsupial 결과가 false인지 확인한다. 자바스크립트에서 instanceof 연산자는 우변 피연산자의 프로토타입이 좌변 피연산자의 프로토타입 체인에 있는지 찾아본다. 만약 있으면 좌변 피연산자는 우변 피연산자의 인스턴스라고 결론 내린다.

new 키워드를 앞에 붙여 생성자 함수를 실행하면 일단 빈 객체를 하나 만들어 새 객체의 프로토타입을 생성자 함수의 프로토타입 프로퍼티에 연결한다. 그런 다음 생성자 함수를 this로 실행하여 새 객체를 찍어낸다.

new가 없다면 이런 일은 일어나지 않는다. 실행 후 생성자 함수와 새 객체는 아무 연관이 없고 예제는 전역 객체에 묶은다. 또한, 프로토타입 할당도 없으므로 instanceof 연산 결과는 false다.

 

예제) new를 자동 삽입하여 인스턴스를 생성

function Marsupial(name, nocturnal) {
    if (!(this instanceof Marsupial)) {
        throw new Marsupial(name, nocturnal);
    }
    this.name = name;
    this.isNocturnal = nocturnal;
}

var slider = Marsupial('슬라이더', true); // Marsupial {name: '슬라이더', isNocturnal: true}

new가 없을 때 에러를 내지 않고 자동으로 new를 붙여 인스턴스를 만들어 반환하게 할 수도 있다.

 

new 생성 객체의 SOLID/DRY 요약표

원칙 결과
단일 책임 가능하다. 하지만 객체가 반드시 한 가지, 한 가지 일에만 전념토록 해야 한다.
개방/폐쇄 가능하다.
리스코프 치환 상속을 잘 이용하면 가능하다.
인터페이스 분리 상속과 다른 코드 공유 패턴을 이용하면 가능하다.
의존성 역전 의존성은 어렵지 않게 생성자 함수에 주입할 수 있다.
DRY(반복하지 마라) new 객체 생성 패턴을 쓰면 아주 DRY한 코드가 된다.

 

6. 클래스 상속

자바스크립트는 클래스가 없지만 프로토타입 상속으로 어느 정도 흉내를 낼 수는 있다. class 로 클래스를 선언할 수는 있지만 사실 이 때의 class는 함수이다.

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes#class_%EC%A0%95%EC%9D%98

 

고전적 상속 흉내 내기

자바스크립트에서 고전적 상속(classical inheritance)을 모방할 수 있는 건 순전히 함수 프로토타입 덕분이다.

 

고전적 상속 흉내 내기의 SOLID/DRY 요약표

원칙 결과
단일 책임 고전적 상속 흉내 내기는 단일 책임 원칙을 지원하지만 강제하지는 못한다.
개방/폐쇄 이 패턴의 주제가 바로 개방/폐쇄 원칙이다.
리스코프 치환 이 패턴은 의존성을 수정하는 게 아니라 확장하려는 것이다. 따라서 리스코프 치환 원칙에 충실하다.
인터페이스 분리 해당 없음
의존성 역전 상속하는 객체의 생성자 함수에 의존성을 주입하는 형태로 실현할 수 있다.
DRY(반복하지 마라) 그다지 관련이 없다. 초기화 로직이 상속을 주고 받는 객체 모두의 생성 함수에 걸쳐 반복된다. 하지만 프로토타입을 공유하면 함수 사본 개수를 줄일 수 있다.

 

7. 함수형 상속

함수형 상속(functional inheritance)을 하면 데이터를 숨긴 채 접근을 다스릴 수 있다.

var AnimalKingdom = AnimalKingdom || {};

AnimalKingdom.marsupial = function(name, nocturnal) {
    var instanceName = name, instanceIsNocturnal = nocturnal;
    
    return {
        getName: function() {
            return instanceName;
        },
        getIsNocturnal: function() {
            return instanceIsNocturnal;
        }
    };
};

AnimalKingdom.kangaroo = function(name) {
    var baseMarsupial = AnimalKingdom.marsupial(name, false);
    
    baseMarsupial.hop = function() {
        return baseMarsupial.getName() + '가 껑충 뛰었어요!';
    };
    return baseMarsupial;
};

var jester = AnimalKingdom.kangaroo('제스터');
console.log(jester.getName()); // '제스터'
console.log(jester.getIsNocturnal()); // false
console.log(jester.hop()); // '제스터가 껑충 뛰었어요!'

 

함수형 상속 패턴의 SOLID/DRY 요약표

원칙 결과
단일 책임 함수형 상속은 모듈 패턴을 사용하므로 의존성 주입과 애스팩트 장식에 친화적이다. 상속한 모듈에는 반드시 한 가지 책임만 부여해야 한다.
개방/폐쇄 함수형 상속은 모듈 확장에 관한 한 완벽한 메커니즘이다.
리스코프 치환 함수형 상속은 수정 없이 모듈을 확장할 수 있게 해주므로 상속받은 모듈은 자신이 상속한 모듈로 대체될 수 있다.
인터페이스 분리 함수형 상속은 모듈 패턴의 변형이다. 응집된 모듈 API 자체가 분리된 인터페이스다.
의존성 역전 임의 모듈 생성 방식으로 만든 모듈을 상속에 사용했다면 의존성은 쉽게 주입할 수 있다.
DRY(반복하지 마라) 설계만 잘한다면 모듈을 이용한 함수형 상속은 DRY한 코드로 향하는 이상적인 지름길이다.

 

8. 멍키 패칭

멍키 패칭(monkey-patching)이란 추가 프로퍼티를 객체에 붙이는 것이다.

 

예제) 멍키 패칭

var MyApp = MyApp || {};

MyApp.Hand = function() {
    this.dataAboutHand = {}; // etc.
};
MyApp.Hand.prototype.arrangeAndMove = function(sign) {
    this.dataAboutHand = '새로운 수화 동작';
};

MyApp.Human = function(handFactory) {
    this.hands = [ handFactory(), handFactory() ];
};
MyApp.Human.prototype.useSignLanguage = function(message) {
    var sign = {};
    
    // 메시지를 sign에 인코딩한다.
    this.hands.forEach(function(hand) {
        hand.arrangeAndMove(sign);
    });
    return '손을 움직여 수화하고 있어. 무슨 말인지 알겠니?';
};

MyApp.Gorilla = function(handFactory) {
    this.hands = [ handFactory(), handFactory() ];
};

MyApp.TeachSignLanguageToKoko = (function() {
    var handFactory = function() {
        return new MyApp.Hand();
    };
    // (빈자의 의존성 주입)
    var trainer = new MyApp.Human(handFactory);
    var koko = new MyApp.Gorilla(handFactory);
    
    koko.useSignLanguage = trainer.useSignLanguage; // 멍키 패칭이 일어난다.
    
    // 실행 결과: '손을 움직여 수화하고 있어. 무슨 말인지 알겠니?';
    console.log(koko.useSignLanguage('안녕하세요!'));
});

다음 줄 끝에서 멍키패칭이 일어난다.

koko.useSignLanguage = trainer.useSignLanguage;

조련사(trainer)의 수화(sign language) 능력을 코코(Koko)에게 패치한다.

  1. koko.useSignLanguage()를 호출한다.
  2. 멍키 패칭을 했으니 MyApp.Human.prototype.useSignLanguage가 실행된다.
  3. 이 함수는 this.hands에 접근한다.
  4. 여기서 this는 useSignLanguage를 호출한 객체, 즉 MyApp.Gorilla 객체(koko)다. 따라서 MyApp.Gorilla 객체도 수화할 수 있는 손을 가지게 되었다.

멍키 패칭은 '메서드 빌림(method borrowing)'이라는 그럴싸한 타이틀이 있다. 다중 상속에 가깝다.

 

 

멍키 패칭의 SOLID/DRY 요약표

원칙 결과
단일 책임 기증받은 기능 다발이 단일 책임으로 이루어진다 해도 빌림 자체로 빌리는 객체에 책임을 전가하는 게 아닌가 하는 점이 있다.
개방/폐쇄 멍키 패칭을 분별 있게 잘 쓰면 지킬 수 있다.
리스코프 치환 빌린 함수가 새 집과 옛 집에서의 의미가 같다면 문제없다.
인터페이스 분리 인터페이스 분리 원칙은 멍키 패칭이 추구하는 바로 그 자체다.
의존성 역전 의존성은 보통 빌려주는 객체 또는 빌리는 객체 어느 쪽에도 주입될 수 있다.
DRY(반복하지 마라) 잘 쓰면 도움이 된다.

 


참고자료

https://book.naver.com/bookdb/book_detail.nhn?bid=11262780 

https://developer.mozilla.org/ko/docs/Glossary/Primitive

 

728x90

댓글