ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 혼자 공부하는 자바스크립트 9장 - 클래스
    자바스크립트/혼자 공부하는 자바스크립트 2021. 8. 16. 19:31
    클래스의 기본 기능
    C를 제외한 다양한 프로그래밍 언어는, 객체를 우선적으로 생각해서 프로그램을 만드는 객체 지향 프로그래밍 언어가 많다.

    클래스라는 문법은 객체를 효율적이고 안전하게 만들어 객체 지향 패러다임을 쉽게 프로그래밍에 적용할 수 있도록 돕는다.

     

    추상화

    복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념과 기능을 간추려내는 것

    대부분의 프로그램은 "우리가 어떤 데이터를 활욯하는가?" 라는 생각으로 부터 시작된다. 현실의 객체는 데이터가 될 수 있는 수많은 속성을 갖고 있기에, 현실의 모든 정보를 컴퓨터 내부에 입력하는 것은 불가능에 가깝다. 프로그램을 만들 땐 필요한 요소만 사용해 객체를 표현하는데, 이를 추상화라고 한다.

    학생 성적 관리 프로그램을 만든다고 치자. 우선 학생이라는 객체가 필요하고, 학생으로부터 성적 관리에 필요한 공통사항을 추출하는 것이 추상화이다.

     

    같은 형태의 객체 만들기

    const students = [];
    
    students.push({ 이름: "구름", 국어: 87, 영어: 98, 수학: 88, 과학: 90 });
    students.push({ 이름: "별이", 국어: 92, 영어: 92, 수학: 96, 과학: 88 });
    students.push({ 이름: "겨울", 국어: 76, 영어: 96, 수학: 94, 과학: 86 });
    students.push({ 이름: "바다", 국어: 98, 영어: 52, 수학: 98, 과학: 92 });

    위 학생 객체의 배열에 성적 총합, 평균을 구하는 기능을 함수로 만들어 놓으면 여러 프로그램에서 활용될 수 있으므로 좋을 것이다.

    function getSumOf (student) {
      return student.국어 + student.영어 + student.수학 + student.과학;
    }
    
    function getAverageOf (student) {
      return getSumOf(student) / 4;
    }
    
    let output = "이름\t총점\t평균\n";
    for (const s of students) {
      output += `${s.이름}\t${getSumOf(s)}점\t${getAverageOf(s)}점\n`
    }
    
    console.log(output);
    //        이름    총점    평균
    //        구름    363점    90.75점
    //        별이    368점    92점
    //        겨울    352점    88점
    //        바다    340점    85점

    이렇게 객체 속성 선언, 객체 기능 선언, 객체 활용 부분을 나누면 기능을 추가하거나 할 때 쉽게 유지보수 할 수 있다.

     

    객체의 기능을 메소드로 추가하기

    현재는 객체가 학생 객체 하나여서 위처럼 따로 함수를 선언해도 문제가 없지만, 객체의 종류와 수가 늘어나면 함수의 이름이 겹치거나, 함수 매개변수가 혼동되는 등의 우려가 있다. 이를 해결하기 위해 함수를 메소드로 객체 내부에 넣어서 활용할 수 있다.

    for (const student of students) {
      student.getSum = function () {
        return this.국어 + this.영어 + this.수학 + this.과학;
      }
    
      student.getAverage = function () {
        return this.getSum() / 4;
      }
    }

    함수를 활용해 객체를 생성할 수도 있다.

    function createStudent (이름, 국어, 영어, 수학, 과학) {
      return {
        이름: 이름,
        국어: 국어,
        영어: 영어,
        수학: 수학,
        과학: 과학,
    
        getSum() {
           return this.국어 + this.영어 + this.수학 + this.과학;
        },
    
        getAverage() {
          return this.getSum() / 4;
        },
    
            toString() {
          return `${this.이름}\t${this.getSum()}점\t${this.getAverage()}점\n`
        }
      }
    }
    
    const students = [];
    students.push(createStudent("구름", 87, 98, 88, 90));
    students.push(createStudent("별이", 92, 98, 96, 88));
    students.push(createStudent("겨울", 76, 96, 94, 86));
    students.push(createStudent("바다", 98, 52, 98, 92));
    
    let output = `이름\t총점\t평균\n`;
    for (const s of students) {
      output += s.toString();
    }
    
    console.log(output);

    이렇게 함수를 활용해 객체를 생성하면 오탈자의 위험이 줄고, 코드를 입력하는 양이 줄고, 속성과 메소드를 한 함수 내부에서 관리함으로써 객체를 더 손쉽게 유지보수 할 수 있다.

    하지만 이 방식은 객체별로 getSum(), getAverage(), toString() 메소드를 생성하면 기본 자료형인 함수보다 무거운 자료형이 여러번 생성되는 문제가 있다.

     

    클래스 선언하기

    객체를 효율적으로 만들 수 있는 방법은 크게 클래스, 프로토타입의 2가지가 있다. 클래스는 객체를 만들 때 많은 지원을 하는 대신 많은 제한을 거는 문법이고, 프로토타입은 제한을 많이 하진 않지만 지원도 별로 하지 않는 문법이다. 자바스크립트는 초기에 프로토타입 문법을 제공했지만, 최신 자바스크립트는 클래스 문법을 제공하기 시작했다.

    class 클래스 이름 {
    
    }

    클래스를 기반으로 만든 객체는 전문 용어로 인스턴스라고 부른다. 그냥 객체라고 부르는 경우도 많다. 인스턴스를 생성할 때는 다음과 같이 생성한다.

    new 클래스 이름 ();

    즉, 클래스는 객체를 만드는 함수(와 비슷한 것)이고, 인스턴스는 함수로 만든 객체(와 비슷한 것)이다.

     

    class Student {
    
    }
    
    const student = new Student();
    
    const students = [
      new Student(),
      new Student(),
      new Student(),
      new Student()
    ]

    클래스 이름의 첫 글자는 대문자로 지정하는 것이 관습이다. 식별자만 보고도 클래스라는 것을 이해할 수 있도록 하기 위함이다.

     

    생성자

    new Student()의 코드에서 괄호는 생성자라는 이름의 함수이다. 생성자는 인스턴스를 생성할 때 처음 호출되는 메소드로, 생성자에서는 속성을 추가하는 등 객체의 초기화 처리를 한다. 생성자는 다음과 같이 만든다.

    class 클래스이름 {
      constructor () {
        // 생성자 코드
      }
    }
    class Student {
      constructor (이름, 국어, 영어, 수학, 과학) {
        this.이름 = 이름;
        this.국어 = 국어;
        this.영어 = 영어;
        this.수학 = 수학;
        this.과학 = 과학;
      }
    }
    
    const students = [];
    students.push(new Student("구름", 87, 98, 88, 90));
    students.push(new Student("별이", 92, 98, 96, 88));
    students.push(new Student("겨울", 76, 96, 94, 86));
    students.push(new Student("바다", 98, 52, 98, 92));

     

    메소드

    메소드는 다음과 같이 만든다.

    class Student {
      constructor (이름, 국어, 영어, 수학, 과학) {
        this.이름 = 이름;
        this.국어 = 국어;
        this.영어 = 영어;
        this.수학 = 수학;
        this.과학 = 과학;
      }
      
      getSum() {
        return this.국어 + this.영어 + this.수학 + this.과학;
      }
      
      getAverage() {
        return this.getSum() / 4;
      }
      
      toString() {
        return `${this.이름}\t${this.getSum()}점\t${this.getAverage()}점\n`;
      }
    }

    객체에 메소드를 넣는 것처럼 쉼표를 기입하지 않는 것에 유의한다.

     

    클래스의 고급 기능

    상속

    class 클래스이름 extends 부모클래스 {
      
    }
    class Rectangle {
      constructor (width, height) {
        this.width = width;
        this.height = height;
      }
      
      getPerimeter () {
        return 2 * (this.width + this.height);
      }
      
      getArea () {
        return this.width * this.height;
      }
    }
    
    class Square extends Rectangle {
      constructor (length) {
        super(length, length);
      }
    }
    
    const square = new Square(10);
    console.log(`둘레: ${square.getPerimeter()}`);
    console.log(`넓이: ${square.getArea()}`);

    Rectangle 클래스는 직사각형, Square 클래스는 정사각형을 나타낸다. 둘 다 사각형의 범주여서 Rectangle 클래스가 갖고 있는 속성과 메소드를 상속을 통해 Square 클래스에서 사용할 수 있다. 이때 상속을 해주는 클래스를 부모 클래스, 상속을 받는 클래스를 자식 클래스라고 부른다. Square 클래스를 보면, 부모 클래스인 Rectangle 클래스에서 상속받았기 때문에 getPerimeter(), getArea() 메소드를 선언하지 않은 것을 볼 수 있다.

    Square 클래스의 생성자 코드인 super(length, length) 코드는 부모의 생성자를 나타내는 함수로, Rectangle 클래스의 constructor (width,height)를 부르는 코드다.

     

    private 속성과 메소드

    위의 예제처럼 Square 클래스를 선언했을 때, 한 변의 길이를 음수로 지정하는 것을 막을 수 없다.

    class Square {
      constructor (length) {
        if (length <= 0) {
          throw "길이는 0보다 커야 합니다.";
        }
        this.length = length;
      }
    }
    
    const square = new Square();
    square.length = -10;

    조건문으로 강제 에러를 띄우더라도, 생성자로 객체를 생성한 이후 속성을 변경하는 것은 막을 수 없다.

    클래스 사용자가 클래스 속성, 메소드 등을 의도하지 않은 방향으로 사용하는 것을 막아 클래스의 안정성을 확보하기 위해 나온 문법이 private 속성과 메소드이다.

    class 클래스이름 {
      #속성 이름
      #메소드 이름 () {
      
    	}
    }
    class Square {
      #length
      
      constructor (length) {
        if (length <= 0) {
          throw "길이는 0보다 커야 합니다.";
        }
        this.#length = length;
      }
    
    	getArea () { return this.#length * this.#length }
    }
    
    const square = new Square(10);
    square.length = -10;
    console.log(square.getArea());		// 100
    
    square.#length = -10;
    	// VM1305:17 Uncaught SyntaxError: Private field '#length' must be declared in an enclosing class

    # 이 붙어있는 속성과 메소드는 모두 private 속성과 메소드가 된다. private 속성은 사용하기 전에 미리 외부에 어떤 속성을 private 속성으로 사용하겠다고 선언해줘야 한다는 점을 유의해야 한다.

    length 속성을 외부에서 바꿔도 클래스 내부에서 사용하고 있는 속성은 #length 여서 메소드 실행 결과에는 영향을 끼치지 않는다. #length 속성을 외부에서 바꾸려 시도할 경우 에러가 뜨게 된다.

     

    게터와 세터

    private 속성을 사용하면 외부에서 아예 접근할 수 없게되는 문제가 있다. 속성의 값도 확인할 수 없고, 변경할 수도 없다. 이를 위해 프레임워크는 상황에 따라 속성을 읽고 쓸 수 있는 메소드를 제공한다.

    class Square {
      #length
      
      
      constructor (length) {
        this.setLength(length);
      }
    
    	setLength (value) {
        if (value <= 0) {
          throw "길일는 0보다 커야 합니다.";
        }
        this.#length = value;
      }
    
    	getLength (value) {
        return this.#length;
      }	
    }
    
    const square = new Square(10);
    console.log(square.getLength());		// 10
    
    square.setLength(-10);		// Uncaught 길이는 0보다 커야 합니다.

    getXX() 메소드처럼 속성 값을 확인할 때 사용하는 메소드를 게터, setXX() 메소드 처럼 속성 값을 지정할 때 사용하는 메소드를 세터라고 부른다. 게터와 세터는 필요한 경우에만 만들어 사용한다. 사용자가 값을 굳이 지정할 필요가 없거나, 값을 읽는 것을 원하지 않는다면 만들 필요가 없다.

     

    프레임워크는 게터와 세터를 쉽게 만들 수 있는 getset 키워드 문법을 제공한다.

    class 클래스이름 {
      get 이름 () { return 값 }
      set 이름 (value) { }
    }
    class Square {
      #length
      
      constructor (length) {
        this.length = length
      }
    
    	get length() {
        return this.#length
      }
    
    	get perimeter() {
        return this.#length * 4
      }
    
    	get area () {
        return this.#length * this.#length
      }
    
    	set length(length) {
        if(length <= 0) {
          throw "길이는 0보다 커야 합니다."
        }
        this.#length = length
      }
    }
    
    const square = new Square(10);
    console.log(`길이: ${square.length}`);		// 길이: 10
    console.log(`둘레: ${square.perimeter}`);		// 둘레: 40
    console.log(`넓이: ${square.area}`);		// 넓이: 100

    이와 같이 인스턴스를 생성하면 단순하게 속성을 사용하는 형태처럼 게터와 세터를 사용할 수 있다. 생성자 메소드에서 this.length에 값을 지정하면 set length(length) 메소드가 호출된다.

     

     

    static 속성과 메소드

    효율적으로 프레임워크를 개발할 수 있는 다양한 패턴을 디자인 패턴이라고 한다. 자바스크립트는 여러 디자인 패턴을 활요하기 위해서 클래스 문법들이 계속해서 추가되었는데, 최근 추가된 문법으로 static 속성(정적 속성)과 static 메소드(정적 메소드)가 있다.

    class 클래스이름 {
      static 속성 = 값
    	static 메소드 () {
        
      }
    }

    static 속성과 메소드는 인스턴스를 만들지 않고 사용할 수 있는 속성과 메소드이다. 일반적인 변수와 함수처럼 사용 가능하며, 클래스 이름 뒤에 점을 찍고 사용한다.

    class Square {
      #length
      static #counter = 0
      static get counter () {
        return Square.#counter
      }
    
    	constructor (length) {
        this.length = length
        Square.#counter += 1
      }
    
    	static perimeterOf (length) {
        return length  * 4
      }
    
    	static areaOf (length) {
        return length * length
      }
    
    	get length () { return this.#length }
      get perimeter () { return this.#length * 4 }
      get area () { return this.#length * this.#length }
      
      set length(length) {
        if (length <= 0) {
          throw "길이는 0보다 커야 합니다."
        }
        this.#length = length
      }
    }
    
    const squareA = new Square(10);
    const squareB = new Square(20);
    const squareC = new Square(30);
                        
    console.log(`생성된 Square 인스턴스 수: ${Square.counter}개`)
    		// 생성된 Square 인스턴스 수: 3개
    console.log(`한 변 길이가 20인 정사각형의 둘레: ${Square.perimeterOf(20)}`)
    		// 한 변 길이가 20인 정사각형의 둘레: 80
    console.log(`한 변 길이가 30인 정사각형의 넓이: ${Square.areaOf(30)}`)
    		// 한 변 길이가 30인 정사각형의 넓이: 900

    #counter static 속성과 counter() static 게터 메소드를 만들었다. #counter 메소드는 Square 클래스의 생성자가 호출될 때마다 1씩 증가해, 이를 활용하면 생성된 인스턴스의 수를 알 수 있다.

    perimeterOf() 메소드와 areaOf() 메소드를 활용하면 새 객체를 생성하지 않고도 둘레와 넓이를 알 수 있다.

    댓글

Designed by Tistory.