본문 바로가기
언어/JavaScript

24/08/21 - JavaScript(5 - 2): 클래스(정의, Getter와 Setter)

by Jini_Lamp 2024. 8. 21.

객체 지향 언어의 가장 대표적인 특징은 클래스와 인스턴스, 객체이다.

 

클래스설계도와 비슷하다. 객체를 정의하는 틀로서, 클래스에 멤버 변수와 멤버 함수를 선언(이것들은 객체의 속성이 된다.)한다. 객체, 또는 인스터스클래스를 바탕으로 만들어진 것으로, 설계도에 그려진 그대로 속성이나 메서드를 다 가지고 있다.

 

클래스와 객체 관계는 붕어빵 틀과 붕어빵 관계와 비슷하다. 붕어빵 틀른 클래스, 붕어빵은 객체에 비유된다.

출처: https://danmilife.tistory.com/19

 

이렇듯 클래스는 결국 객체를 생성해 내기 위한 방법이다. 

클래스를 생성하기 위해선, 먼저 class 키워드를 사용해야 한다.

// 사람 클래스를 만들어 보자.
class Person {
  constructor(name, age) {
	// 사람의 필수 정보(name, age) 입력
	// =new라는 키워드를 이용해서 인스턴스를 만들 때, 기본적으로 넣어야 하는 값들을 의미
	// 여기서 말하는 this는 만들어질 인스턴스를 의미
    	this.name = name;
    	this.age = age;
  }

	// 다양한 메소드를 아래와 같이 정의할 수 있다.
	// 여기서 this.name으로 내부 값을 접근해야 한다.
  sayHello() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }
}

// 클래스(설계도)를 통해 인스턴스(실제 사물) 생성
const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);

// 만든 객체를 토대로 메서드 호출해보기
person1.sayHello(); // 출력: "Hello, my name is Alice and I am 30 years old."
person2.sayHello(); // 출력: "Hello, my name is Bob and I am 25 years old."

 

클래스가 필요한 이유는 객체를 정확하고 빠르게 많이 만들 수 있기 때문이다.

 

여기서 constructor()은 클래스의 생성자 함수이다. 생성자 함수는 객체를 생성할 때 호출되며, 객체를 초기화하는 역할을 한다.

 

 

Getter와 Setter

constructor()을 통해 클래스의 속성들을 정의했다. 해당 함수에서 만들어진 속성들은 이후 만든 객체를 기반으로 직접 접근할 수 있다.

console.log(person1.name);	// Alice 출력

 

하지만 이러한 방식은 좋은 방법이 아니다. 왜냐하면 constructor()을 통해 만들어진 속성들은 내부적인 변수이기 때문에 위험하고, 검증하기도 어렵기 때문이다.

따라서 제안하는 방식은 getter 메소드와 setter 메소드의 사용이다. 하는 역할은 다음과 같다.

  • getter: 속성 값을 반환
  • setter: 속성 값을 설정

 

사용 방법은 다음과 같다.

class Rectangle {
    constructor(height, width) {
      this.height = height;
      this.width = width;
    }
  
    // width를 위한 getter
    get width() {
      return this.width;
    }
  
    // width를 위한 setter
    set width(value) {
        // 검증 1 : value가 음수이면 오류!
        if (value <= 0) {
          //
          console.log("[오류] 가로길이는 0보다 커야 합니다!");
          return;
        } else if (typeof value !== "number") {
          console.log("[오류] 가로길이로 입력된 값이 숫자타입이 아닙니다!");
          return;
        }
        this.width = value;
    }
    
    // ... 중략 ...
}

const AAA = new Rectangle(10, 10);
! 주의 !
그런데 위 코드를 동작시켜보면, 오류가 발생한다!

오류

 

해당 오류는 콜 스택의 크기가 최대치를 초과했다는 뜻이다.

왜 그런가 하면, 먼저 다음 함수를 보자.

    set width(value) {
        // 검증 1 : value가 음수이면 오류!
        if (value <= 0) {
          //
          console.log("[오류] 가로길이는 0보다 커야 합니다!");
          return;
        } else if (typeof value !== "number") {
          console.log("[오류] 가로길이로 입력된 값이 숫자타입이 아닙니다!");
          return;
        }
        this.width = value;
    }

 

해당 함수는 value를 클래스 내부 속성인 width에 저장하기 위한 함수다.

하지만 그래서 this.width = value; 부분에서 문제가 발생하는데, this.width 에 set하기 위해서는 다시 width() 함수로 올라가야 하고, 다시 this.width 에 set하기 위해 또 width() 함수로 올라간다. 즉, 나도 모르는 새에 무한 루프에 빠지는 것이다.

 

따라서 이를 방지하기 위해 this로 접근하는 속성은 앞에 ' _ ' 를 붙여야 한다.

  • 수정된 코드
더보기
// 수정된 코드: this가 붙는 속성들은 앞에 ' _ '를 붙였다.
class Rectangle {
    constructor(height, width) {
      this._height = height;
      this._width = width;
    }
  
    // width를 위한 getter
    get width() {
      return this._width;
    }
  
    // width를 위한 setter
    set width(value) {
        // 검증 1 : value가 음수이면 오류!
        if (value <= 0) {
          //
          console.log("[오류] 가로길이는 0보다 커야 합니다!");
          return;
        } else if (typeof value !== "number") {
          console.log("[오류] 가로길이로 입력된 값이 숫자타입이 아닙니다!");
          return;
        }
        this._width = value;
    }
    
    // ... 중략 ...
}

const AAA = new Rectangle(10, 10);
underscore( _ ) 은 private 라는 의미를 가진다.

private은 은밀하고 뭔가 감춰야 할 때 사용된다.
JS에 getter와 setter가 들어오면서 변수 이름을 똑같이 써버리면 겹치게 되는 현상이 생기게 되어 언더스코어( _ )로 분리시켜야 할 필요셩이 생겼다.
따라서, this._width 은 인스턴스 내에서만 쓰이기 위한 변수로서 분리하겠다는 뜻이기도 하다.

- 2024/08/23 추가
JS에서 언더바는 단지 관례에 불과하며, 실제로는 private 접근 제어를 제공하지 않는다.
자세한 내용은 아래 게시물을 참고 바란다.

2024.08.23 - [프로젝트] - 24/08/23 - [개인] 로그 라이크 게임(2): JS 클래스의 _와 private

 

  • 연습
더보기
class Car {
    constructor(modelName, modelYear, type, price) {
        this._modelName = modelName;
        this._modelYear = modelYear;
        this._type = type;
        this._price = price;
    }

    makeNoise() {
        console.log(this._modelName + ": 빵~~");
    }

    set modelName(value) {
        if(value.length <= 0 || typeof value !== 'string') {
            console.log("[오류] 모델명에 오류 발생");
            return;
        }
        this._modelName = value;
    }
    set modelYear(value) {
        if(4 !== value.length || typeof value !== 'string') {
            console.log("[오류] 년도에 오류 발생");
            return;
        }
        this._modelYear = value;
    }
    set type(value) {
        if(value.length <= 0) {
            console.log("[오류] 타입이 입력X");
            return;
        }
        else if(value !== "g" && value !== "d" && value !== "e") {
            console.log("[오류] 타입을 잘못 입력")
            return;
        }
        this._type = value;
    }
    set price(value) {
        if(typeof value !== 'number') {
            console.log("[오류] 숫자가 아님");
            return;
        }
        else if(value < '1000000') {
            console.log("[오류] 가격이 100만원 미만.")
        }
        this._price = value;
    }

    get modelName() {
        return this._modelName;
    }
    get modelYear() {
        return this._modelYear;
    }
    get type() {
        return this._type;
    }
    get price() {
        return this._price;
    }
}

const car1 = new Car("Sorento", "2023", "e", 5000);
const car2 = new Car("SM5", "1999", "g", 3000);
const car3 = new Car("QM6", "2010", "g", 4500);

console.log(car1.modelName);

car1.modelName = "aaa";
console.log(car1.modelName);
car1.modelName = 1;
console.log(car1.modelName);