다시 만난 OOP, 클래스 (feat. 낯 가리는 프로토타입)

인마헷·2023년 5월 11일
0

OOP, 객체 지향 프로그래밍은 전통적인 명령형 프로그래밍의 절차지향적인 관점에서 벗어나 여러 개의 독립적인 단위, 객체의 집합으로 프로그램을 표현하고자 하는 패러다임이다.

*절차지향적 프로그래밍? 프로그램을 연속적인 절차나 단계로 구성하는 것이다. 프로그램을 일련의 단계로 구분하고 그 단계 각각에 필요한 함수를 정의한 뒤에 데이터를 받아 처리하게 하는 패러다임이다. 즉 절차에 맞는 함수들이 연속적으로 호출되면서 동작하는데 데이터는 함수에 영향을 받아 변화하게 된다. 상태 추적 및 코드의 효율적인 유지보수를 위해서 이 방법은 지양되고 있다.

사실 OO를 설명하는 많은 자료들에서는 “real world를 modeling 위한 것”이라고 표현한다. 이에 대해서 엉클밥(uncle Bob)으로 잘 알려진 로버트 C. 마틴은 "이것은 nonsense"…라고 까지 표현한다. 학자마다 OOP를 해석하는 관점이 많이 다르지만, 엉클밥은 OO에 대해서 이렇게 설명한다.

OO is about managing dependencies by selectively reinverting certain key dependencies in your architecture so that you can prevent rigidity, fragility & non-reusability

그는 소프트웨어 아키텍쳐와 현실 세계 사이에 어느 정도 연관성은 있을 수 있지만, OOP의 주된 초점이 현실 세계를 직접적으로 표현하려는 노력이어서는 안 되며, 이보다는 비즈니스가 직면한 문제를 효과적으로 해결하고, 유지 관리 가능한 코드를 만들고, 건전한 디자인 원칙을 적용하는 데 맞춰져야 한다는 것을 강조했다. 즉, OOP를 통한 현실세계 모델링보다 아래 설명할 OO의 기본 개념이 잘 설계된 모듈화된 코드에 더 중점을 둔다.

이에 대해서는 언젠가 코드 레벨에서 온전히 이해할 날이 왔으면 한다. 그때쯤 정리해보고 싶다.


잘 설계된 객체지향 프로그래밍의 덕목

덕목이라고 표현했지만 '개념'이 더 어울리겠다.

캡슐화

캡슐화 (Encapsulation)는 관련 있는 데이터와 메서드를 묶는 것을 의미한다. 이를 통해 객체 내부상태에 대한 엑세스를 제어할 수 있다. 데이터에 대한 직접적인 접근을 제한하고, 메서드를 통해서만 해당 데이터를 조작할 수 있도록 한다. 이렇게 함으로써 외부에서는 묶인 데이터의 상태를 변경할 수 없고, 묶인 메서드를 통해서만 데이터에 접근하고 조작할 수 있게 된다. 이는 코드의 안정성과 유지보수성을 높여주는데 기여한다.

추상화

다음으로 추상화 (Abstraction)가 있다. 객체에서 공통된 특징을 추출하여 모델을 정의하는 방법이다. 객체의 복잡한 내부 동작을 단순화하는 것이다. 객체의 본질적인 특성과 동작에 포커스를 맞출 수 있게 하는 특징이다.

상속

상속 (Inheritance)은 반복적으로 나왔다시피 객체가 다른 객체의 특성과 동작을 물려받는 것을 의미한다. 부모 클래스의 특징과 동작을 자식 클래스가 물려받아 사용할 수 있다. 이를 통해 코드의 재사용성이 증가하고, 유사한 객체들 간에 일관된 인터페이스를 유지할 수 있게 된다.

다형성

개인적으로 가장 이해가 어려운 부분은 다형성 (Polymorphism)이다. 동일한 이름의 메서드가 서로 다른 객체에서 다르게 동작하는 것을 의미한다. 즉, 같은 메서드 호출이 다양한 방식으로 처리될 수 있는데 이는 코드의 유연성을 높여주며, 객체들 간의 상호작용을 단순화시킨다. 자식 클래스에서 상속한 부모 클래스의 메서드를 재정의하는 방식 등으로 다형성을 구현할 수 있다.

이 4가지 개념을 통해서 코드의 유지와 보수를 용이하게 하고자한다.


여기서 또 만나네, 클래스

클래스

파이썬에서 만났던 클래스. 오랜만에 다시 만났지만 여전히 안 반갑...😂
클래스는 자바스크립트에서 OO를 구현하는 가장 기본적인 개념 중의 하나이다. 클래스는 일종의 코드 블루프린트라고 이야기하며 코드를 구조화하고 재사용 가능하게 한다. 사실 클래스는 너무나 당연하게도 정의를 작성해야 하기에 초반에는 오버헤드가 발생한다고 판단할 수 있다. 그러나 추후에 동일한 구조를 가지고 다른 데이터를 담은 인스턴스를 생성해야 할 때는 객체 리터럴 방식보다 훨씬 효율적으로 작업할 수 있다. 이렇게 정의하는 클래스에는 속성과 메서드가 있는데 속성은 객체가 가지는 변수이며, 메서드는 객체가 수행하는 함수이다.

인스턴스

클래스로부터 생성된 객체를 인스턴스(instance)라고 한다. 인스턴스는 클래스의 속성과 메서드를 모두 “상속”받게 된다. 인스턴스를 생성할 때는 new라는 키워드가 필요하며, 필드를 직접 하드코딩으로 입력할 수 있지만, 초기값을 주고 싶은 경우 constructor라는 생성자 함수 예약어를 사용할 수 있다.

ES5에서의 클래스 구현과 ES6의 클래스 구현

사실, 클래스는 자바스크립트에 처음부터 있었던 개념은 아니었다. ES5에서는 클래스를 생성하기 위해서 함수를 사용했다. 따라서 눈에 보이는 차이라고 한다면, ES5에서는 function으로 클래스를 생성하고 ES6에서는 class키워드를 사용한다는 점이다. 아래 예시를 보자.

function User(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function() {
    console.log("Hello, my name is " + this.name);
  }
}

const user1 = new User("A", 20);
user1.greet(); // "Hello, my name is A"

위 코드에서 User 클래스는 생성자 함수로 구현되어 있다. 클래스의 속성(name, age)과 메서드(greet)는 생성자 함수 내부에서 정의되었다.

반면, ES6에서는 클래스를 생성하기 위해서 class키워드를 사용하며, constructor를 통해서 생성자 함수를 좀 더 명시했다는 점에서 차이가 있다.

// 001: 초기값을 하드코딩 한 경우이다.
class User {
	name = 'A';
	age = 20;
}

class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  greet() {
    console.log("Hello, my name is " + this.name);
  }
}

const user1 = new User("A", 20);
user1.greet(); // "Hello, my name is A"

User 클래스는 class 키워드와 constructor 메서드로 구현되어 있다.

내가 아는 게 아니었던 프로토타입

위에서 클래스가 인스턴스 생성과 관련된 틀, 템플릿 역할을 함을 이해했다. 그럼 프로토타입은 무엇인가? 이것도 어떤 틀을 의미하는 걸까?

프로토타입

프로토타입은 자바스크립트에서 OOP를 위해 사용되는 매커니즘이라고 할 수 있다. 프로토타입을 기반으로 상속을 구현하기 때문이며, 자바스크립트의 모든 객체는 프로토타입을 가지고 프로토타입은 객체의 속성과 메서드를 포함하고 있다. 객체는 자신의 프로토타입에서 속성과 메서드를 상속받게 된다. 또한 자신의 프로토타입 체인을 따라 상위 프로토타입에서 속성과 메서드를 확인할 수 있다. 왜 갑자기 클래스를 말하다가, 프로토타입을 말하다가 객체를 말하는가?

자바스크립트에서 함수는 일급객체이다. (이 일급객체에 대해서도 추후 정리해봐야겠다) 일단 여기서 더 복잡하게 들어가지 말고, 함수도 객체이다. 앞서, 클래스는 생성자 함수의 일종이라고 했다. 따라서 클래스 또한 프로토타입을 가지고, 우리는 .prototype을 통해서 클래스의 프로토타입에 접근, 생성할 수 있다. 이런 클래스를 통해서 생성된 인스턴스는 그 프로토타입을 공유하게 된다.

간단히 말해, 클래스는 객체 생성과 관련된 템플릿이고, 프로토타입은 객체들이 속성과 메서드를 공유하기 위한 메커니즘이라고 할 수 있다.

자신이 어떤 객체의 프로퍼티를 공유받고 있는지는 .__proto__를 통해서 가능하다. 자바스크립트에서 모든 함수는 .prototype 프로퍼티를 가진다. 또한 이를 통해서 생성된 객체는 .__proto__프로퍼티를 가진다. .prototype를 통해서 이 함수를 통해 생성될 객체가 공유하게 될 프로토타입이 무엇인지 알아볼 수 있다.

프로토타입 체인을 살펴보기 전에 클래스와 프로토타입에 대해서 좀 더 정확히 보고 넘어가려 한다.

클래스와 프로토타입은 어떤 관계가 있나?

// 클래스 정의
class User(name) {
	constructor (name) {
		this.name = name;
	}
}

// 인스턴스 생성
const user1 = new User("A");

// 프로토타입에 메서드 추가
User.prototype.greet = function() {
  console.log("Hello, my name is " + this.name);
};

// 인스턴스에서 메서드 호출
user1.greet(); // "Hello, my name is A"

위의 코드에서 User은 class 키워드를 통해 정의된 클래스이다. User클래스에서 new를 사용하여 user1 인스턴스를 생성했고 User.prototypegreet라는 메서드를 추가한다. 이제 user1 인스턴스에서 greet 메서드를 호출할 수 있게 되었다.

user1.greet()을 호출할 때, user1 객체는 자신의 프로토타입인 User.prototype에서 greet 메서드를 찾습니다. 그리고 해당 메서드를 실행하게 됩니다.

자바스크립트에서 클래스와 인스턴스는 프로토타입으로 연결되어있고 인스턴스는 프로토타입에서 상속된 속성과 메서드를 이용할 수 있다.

프로토타입 체인

프로토타입 체인(Prototype Chain)은 객체가 특정 프로퍼티나 메서드에 접근할 때, 해당 객체의 프로토타입을 찾아가는 과정을 말한다. 객체에서 프로퍼티를 찾을 때 해당 객체의 프로토타입에 접근하고, 그 프로토타입이 다른 프로토타입을 가지고 있을 경우 계속해서 상위 프로토타입을 찾아가는 방식이다.

Object와 .__proto__ , .prototype

자바스크립트에서 모든 객체는 Object라는 최상위 객체를 상속합니다. 모든 객체는 Object의 프로토타입 체인 상에 위치하게 됩니다.

.__proto__는 모든 객체가 가지고 있는 속성으로, 해당 객체의 프로토타입을 나타낸다. 이를 사용해서 해당 객체의 상위 프로토타입에 직접 접근할 수 있는데 예를 들어, obj.__proto__를 사용하면 obj의 프로토타입에 접근할 수 있습니다.

.prototype은 함수 객체만이 가지는 속성으로, 해당 함수로부터 생성된 인스턴스들의 프로토타입을 가리킵니다. .prototype 속성에 추가한 프로퍼티와 메서드는 해당 함수로부터 생성된 모든 인스턴스가 가질 수 있다.

프로토타입 체인 예시

// 생성자 함수
function User(name) {
  this.name = name;
}

// 프로토타입 메서드 추가
User.prototype.greet = function() {
  console.log("Hello, my name is " + this.name);
};

// 인스턴스 생성
const user1 = new User("A");

// 프로퍼티 및 메서드 접근
console.log(user1.name); // "A"
user1.greet(); // "Hello, my name is A"

// 프로토타입 체인 검색
console.log(user1.hasOwnProperty("name")); // true
console.log(user1.hasOwnProperty("greet")); // false

// 객체와 object, '__proto__', '.prototype'의 관계
console.log(user1.__proto__ === User.prototype); // true
console.log(User.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null

위의 코드에서 생성자 함수 User을 정의하고, 해당 함수의 프로토타입에 greet 메서드를 추가했다. 그리고 user1 인스턴스를 생성하고 프로퍼티 name과 메서드 greet를 확인했다.

user1 객체의 프로퍼티 name 은 다이렉트 속성으로 가지고 있으므로 hasOwnProperty() 를 통해 확인할 수 있지만 greet 메서드는 user1 객체에 다이렉트 속성으로 존재하지 않으므로 hasOwnProperty()는 false를 리턴하게 된다. 그러나 user1 객체는 User를 통해서 생성되었기에 User.prototype을 프로토타입으로 가지고 있으므로 User.prototype에서 greet()를 찾아 실행할 수 있다. 이는 실제로 인스턴스에서 해당 메서드를 호출하면 프로토타입 체인을 따라 올라가며 상위 프로토타입에서 해당 메서드를 찾아 실행함을 의미한다. 다만, __proto__를 통해 확인하는 시점에서는 추가된 메서드가 보이지 않을 뿐이다.

또한, user1.__proto__User.prototype을 가리키고, User.prototype.__proto__Object.prototype을 가리킨다. 마지막으로, Object.prototype.__proto__는 null이다. 이렇게 프로토타입 체인을 따라 올라가면서 상속 관계를 형성하고, 최상위인 Object.prototype에서 끝나게 된다.

profile
비공개 글이 너무 많다...My code may sink, but at least I can swim🤿

0개의 댓글