[모던JS: Core] 클래스 (3)

KG·2021년 5월 22일
0

모던JS

목록 보기
16/47
post-thumbnail

Intro

본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.

정적 메서드와 프로퍼티

1) 정적 메서드

prototype이 아닌 클래스 함수 자체에 메서드를 설정할 수 있다. 이런 메서드를 정적(static) 메서드라고 부른다. 자바스크립트에서도 정적 메서드를 만들 수 있는데 다른 언어와 유사하게 static 키워드를 붙여 만들 수 있다.

class User {
  static staticMethod() {
    console.log(this === User);
  }
}

User.staticMethod();	// true

또한 정적 메서드는 아래와 같이 프로퍼티 형태로 직접 할당하는 것과 동일한 일을 한다.

class User {}

User.staticMethod = functio() {
  console.log(this === User);
};

User.staticMethod();	// true

이렇게 정적 메서드는 어떤 특정한 객체가 아닌 클래스에 속한 함수를 구현하고자 할 때 주로 사용된다. 할당 연산자(=)를 사용한다는 점에서 클래스 필드 문법과 착오가 있을 수 있는데, 클래스 필드는 클래스로 인해 생성되는 객체의 속성으로 들어가는 값이지만, 정적 메서드는 객체 생성없이 클래스 자체에서 제공해주는 값이라는 점에서 큰 차이가 있다.

예를 들어 객체 Article이 여러 개 있고, 이들을 비교해줄 함수가 필요하다고 가정해보자. 가장 먼저 아래와 같이 Article.compare를 추가할 수 있을 것이다.

class Article {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }
  
  static compare(articleA, articleB) {
    return articleA.date - articleB.date;
  }
}

let articles = [
  new Article("HTML", new Date(2019, 1, 1)),
  new Article("CSS", new Date(2019, 0, 1)),
  new Article("JavaScript", new Date(2019, 11, 1))
];

articles.sort(Article.compare);

여기서 Article.compare는 정적 메서드로 글 전체에 대한 무언가(여기서는 비교)를 행하는 메서드이다. 따라서 이는 글 하나하나 객체의 메서드가 아니라 이들 전체를 관망할 수 있는 메서드여야 하는 것을 알 수 있다. 때문에 이와 같은 기능의 메서드는 정적 메서드로 클래스 자체에서 접근할 수 있도록 지정해주는 것이다. 따라서 정적 메서드는 주로 항목 검색, 저장, 삭제 등의 데이터베이스 관련 클래스 또는 동일 대상에 대한 공통 로직을 구현할 때 많이 쓰인다.

2) 정적 프로퍼티

정적 프로퍼티는 스펙에 추가된 지 얼마 안 된 문법이다. 따라서 크롬을 제외한 브라우저는 정상적으로 동작하지 않을 수도 있다.

static을 사용하여 메서드뿐만 아니라 프로퍼티 역시 정적으로 생성할 수 있다. 클래스 필드와 유사하게 생겼지만 static 키워드와 함께 생성한다는 점이 다르다. 위에서도 다루었듯이 클래스 필드는 객체의 속성이지만, 정적 프로퍼티는 클래스 자체의 속성이 된다.

class Article {
  static publisher = "KG";
}

alert( Article.publisher ); // KG

// 또는 메서드와 유사하게 직접 할당도 가능하다
Article.publisher = 'KG'

정적 프로퍼티는 데이터를 클래스 수준에 저장하고 싶을 때 사용한다. 따라서 캐시, 고정 환경 설정 또는 인스턴스 간에 복제할 필요가 없는 기타 데이터에 유용하게 쓸 수 있다.

3) 정적 메서드와 프로퍼티 상속

정적 메서드와 프로퍼티 역시 상속된다. 아래 코드를 보면 Animal 클래스를 상속받은 Rabbit 클래스에서도 부모 클래스에서 선언한 정적 메서드와 프로퍼티에 접근하는 것을 볼 수 있다.

class Animal {
  // 정적 프로퍼티와 메서드 생성
  static planet = 'Earth';
  static compare(animalA, aimalB) {
    return animalA.speed - animalB.speed;
  
  constructor(name, speed) {
    this.name = naem;
    this.speed = speed;
  }
}

// Animal 클래스 상속
class Rabbit extends Animal {
  hide() {
    console.log(`${this.name}가 숨었다!`);
  }
}

let rabbits = [
  new Rabbit("흰 토끼", 10),
  new Rabbit("검은 토끼", 5)
];

rabbits.sort(Rabbit.compare);
Rabbit.planet;	// Earth

이 같은 참조가 가능한 이유는 역시 프로토타입 덕분이다. extends 키워드는 클래스의 prototype 참조뿐만 아니라 클래스 자체의 [[Prototype]]이 부모 클래스를 참조하도록 해준다.

위 그림을 보면 class Rabbit extends Animal은 두 개의 [[Prototype]] 참조를 만들어내는 것을 볼 수 있다.

  1. 클래스 Rabbit은 프로토타입을 통해 클래스 Animal을 상속받는다.
  2. Rabbit.prototype은 프로토타입을 통해 Animal.prototype을 상속받는다.
class Animal {}
class Rabbit extends Animal {}

// 정적 메서드
alert(Rabbit.__proto__ === Animal); // true

// 일반 메서드
alert(Rabbit.prototype.__proto__ === Animal.prototype); // true

다음 예제를 보며 extends를 통한 상속에서 클래스 간, 그리고 클래스 프로토타입 간 두 개의 [[Prototype]] 참조가 발생하는 것을 다시 한 번 확인해보자.

class Person {}
class Student extends Person {}
const student = new Student();

private와 protected 프로퍼티 및 메서드

객체 지향 프로그래밍에서 가장 중요한 원리 중에 하나는 내부 인터페이스와 외부 인터페이스를 구분 짓는 것이다. 이를 보통 캡슐화(Encapsulation)이라고도 하는데, 이는 내부의 정확한 로직과 동작 방식을 모르더라도 제공되는 기능을 통해 적절하게 사용할 수 있는 방식과도 같다. 프로그래밍에서는 특별한 문법과 컨벤션을 사용해 안쪽 세부 사항을 숨기고 외부에는 이에 대해 접근 및 설정할 수 있는 기능을 알려 사용을 가능케한다.

1) 내부 및 외부 인터페이스

객체 지향 프로그래밍에서는 프로퍼티와 메서드를 두 그룹으로 분류할 수 있다.

  • 내부 인터페이스 : 동일한 클래스 내 다른 메서드에선 접근이 가능하지만, 클래스 밖에서는 접근 불가한 프로퍼티 및 메서드
  • 외부 인터페이스 : 클래스 밖에서도 접근 가능한 프로퍼티 및 메서드

이러한 특징의 가장 큰 장점은, 외부 인터페이스만 알더라도 객체를 가지고 어떤 조작을 할 수 있다는 점이다. 즉 객체 안이 어떻게 동작하는지 세부사항은 알 필요가 없다.

객체 지향 언어에서는 이 두 가지 인터페이스를 접근자를 통해 제어한다. 자바스크립트 역시 동일하게 접근자를 제공하지만 다른 언어와는 살짝 문법이 다르다. 자바스크립트에서는 아래와 같은 두 가지 타입의 객체 필드가 있다.

  • public : 어디서든 접근이 가능하며 외부 인터페이스르 구성. 앞서 다루었던 모든 프로퍼티와 메서드는 public이다.

  • private : 클래스 내부에서만 접근이 가능하며 내부 인터페이스를 구성.

만약 다른 언어를 통해 객체 지향 패러다임을 먼저 접했다면 다음과 같은 의문이 들 수 있다. protected 접근 제어자는 지원되지 않는것인가? 자바스크립트는 문법적으로 protected 키워드를 제공하지는 않지만, 이를 사용하면 편리한 점이 많기 때문에 이를 모방하여 사용하는 관습이 남아있다.

protected 필드는 private과 비슷하지만 자손 클래스에서도 접근이 가능하다는 점이 다르다. 때문에 protected 역시 내부 인터페이스를 구성할 때 유용한데, 자손 클래스에서 부모 클래스로 접근해야 하는 경우가 많을때 특히 유용하다.

자바스크립트는 동적 언어고 때문에 타입에 대한 제약이 적다. 이 같은 점은 장점으로 작용하기도 하지만 거대한 규모에서 개발의 생산성과 협업의 기준에서는 오히려 큰 단점으로 작용하기도 한다. 이 같은 동적 언어의 단점을 커버하기 위해 자바스크립트는 superset으로 타입스크립트가 존재한다. 오늘날 대부분 환경에서 타입스크립트로 작업을 많이 진행하고 있는데, 이 챕터와 관련된 접근제어자(private/protected/public) 역시 타입스크립트에서는 지원이 되고 있다. 이 외에도 타입스크립트는 다양한 타입 관련 기능을 제공한다.

2) 프로퍼티 보호하기

class CoffeeMachine {
  waterAmount = 0;	// 물통의 양 (클래스 필드)

  constructor(power) {
    this.power = power;
    console.log(`전력량: ${power}`);
  }
}

let coffeeMachine = new CoffeeMachine(100);

coffeeMachine.waterAmount = 200;	// 물 추가

현재 클래스에 존재하는 모든 프로퍼티는 public이다. 따라서 손쉽게 waterAmountpower를 읽고 또는 원하는 값으로 변경할 수 있다. 이때 각 프로퍼티를 protected로 변경하여 접근을 제어해보자.

protected 키워드는 문법적으로 제공되지 않지만, 이를 알리기 위해 보통 관습적으로 언더바(_)를 추가로 덧붙여준다. 이는 강제되는 사항은 아니지만, 개발자 사이에서 외부 접근이 불가능한 프로퍼티나 메서드를 나타내는 일종의 약속으로 많이 쓰인다.

문법적으로 강제되는 것이 아님에 주의하자! 가시적으로 구분을 할 수 있지만, 여전히 public이기 때문에 문법적으로는 정상적으로 접근과 설정이 가능하다.

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }

  constructor(power) {
    this._power = power;
  }
}

let coffeeMachine = new CoffeeMachine(100);

// getter/setter를 이용해 접근/설정
coffeeMachine.waterAmount = 20;
coffeeMachine.waterAmount;	// 20

3) 읽기 전용 프로퍼티

power 프로퍼티를 읽기 전용 프로퍼티로 만들어보자. 프로퍼티를 생성할 때만 값을 할당하고, 그 이후로는 변경이 불가능 하도록 설정해야 할 때 읽기 전용 프로퍼티를 생성할 수 있다. 읽기 전용 프로퍼티를 만들려면 setter를 제외하고 getter만 정의함으로써 쉽게 생성할 수 있다.

역시 문법적으로 강제되는 것은 아니다. 타입스크립트에서는 별도로 readonly 제어자를 지원하여 해당 기능을 강제할 수 있다.

class CoffeeMachine {
  constructor(power) {
    this._power = power;
  }
  
  get power() {
    return this._power;
  }
}

클래스에서는 보통 getter/setter를 활용하기도 하지만, 별도로 get.../set... 형태의 메서드를 직접 생성하기도 한다. 이 경우 다수의 인자를 처리할 수 있기 때문에 좀 더 유연하기 때문이다. 어떤걸 사용해야 한다는 규칙은 없기에 본인이 조금 더 익숙하고 편한 방식을 사용하자.

class CoffeeMachine {
  _waterAmount = 0;
  setWaterAmount(value) {
    this._waterAmount = value;
  }
  getWaterAmount() {
    return this._waterAmount;
  }
}

4) private 프로퍼티 및 메서드

private 키워드 역시 스펙에 추가되지 얼마되지 않은 문법이다. 때문에 일부 브라우저에서는 해당 문법을 아직 지원하지 않을 수 있다는 점을 염두해두자.

private 프로퍼티와 메서드는 #으로 시작한다. #이 붙어있다면 클래스 안에서만 접근이 가능하다.

class CoffeeMachine {
  #waterLimit = 200;
  
  #checkWater(value) {
    if (value < 0) throw Error('음수 불가');
    if (value > this.#waterLimit) throw.Error('용량 초과');
  }
}

let coffeeMachine = new CoffeeMachine();

coffeeMachine.#checkWater();	// Error
coffeeMachine.#waterLimit;	// Error

privateprotected와는 달리 자손 클래서에서도 접근이 불가하다. 또한 최신 문법이지만, 자바스크립트에서 문법적으로 지원해주고 있기에 강제할 수 있다는 것 역시 큰 장점이다. 만약 자손 클래스에서 private한 프로퍼티나 메서드에 접근하고자 한다면 getter/setter 메서드를 통해 접근해야 한다. 보통 이런 제약사항은 너무 엄격하기 때문에 특정 경우가 아닌 이상은 protected를 통해 내부 인터페이스를 구성하기도 한다.

또한 private 필드와 public 필드는 서로 상충하지 않는다. 따라서 private 프로퍼티인 #waterLimitpublic 프로퍼티 waterAmount를 동시에 선언할 수 있다.

References

  1. https://ko.javascript.info/classes
  2. https://helloworldjavascript.net/pages/270-class.html
  3. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes
profile
개발잘하고싶다

0개의 댓글