본문 바로가기
혼공학습단 11기(完)

[혼공스] 09 - 2 '클래스의 고급 기능' 정리 (2)

by jaeheon0520 2024. 2. 16.

 

Getter와 Setter

방금 살펴보았던 private 속성을 사용하면 외부에서는 #length 속성에 아예 접근할 수 없는 문제가 발생한다. 현재 square 객체의 length 속성이 몇인지 확인할 수도 없고, length 속성을 변경하고 싶어도 변경할 수 없다.

 

그래서 프레임워크 개발자들은 상황에 따라서 속성을 읽고 쓸 수 있는 메소드를 만들어서 제공한다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <script>
        // 정사각형 클래스
        class Square {
            #length

            constructor (length) {
                this.setLength(length)
            }

            setLength (value) { // 함수를 사용하므로, 내부에서 예외 처리 등을 할 수 있다.
                if (value <= 0) {
                    throw '길이는 0보다 커야 합니다.'
                }
                this.#length = value
            }

            getLength (value) {
                return this.#length
            }

            getPerimeter () { return 4 * this.#length}
            getArea () { return this.#length * this.#length}
        }

        // 클래스 사용하기
        const square = new Square (10)
        console.log(`한 변의 길이는 ${square.getLength()}입니다.`)

        // 예외 발생시키기
        square.setLength(-10)
    </script>
</head>
<body>
   
</body>
</html>

 

getter 와 setter 메소드

 

코드를 보면 getLength() 메소드와 setLength() 메소드가 추가된 것을 볼 수 있다. 

 

이때 getLength() 메소드처럼 속성 값을 확인할 때 사용하는 메소드를 게터(getter)라고 부르며, setLength() 메소드처럼 속성에 값을 지정할 때 사용하는 메소드를 세터(setter)라고 부른다.

 

처음 getter와 setter를 배우면 모든 private 속성에 getter 와 setter를 붙이려고 하는 경우가 있다. getter와 setter는 필요한 경우에만 사용한다. 만약 사용자가 값을 읽는 것을 거부하겠다면 getter를 만들지 않아도 된다. 또한 사용자가 값을 지정하는 것을 거부하겠다면 setter를 만들지 않아도 된다. 아예 속성에 접근하지 못하게 둘 다 막을 수도 있다.

 

이러한 형태의 코드를 수많은 프레임워크 개발자들이 사용하기 시작하니, 프로그래밍 언어 개발자들은 프레임워크 개발자들이 코드를 더 쉽게 작성하고 사용할 수 있도록 다음과 같은 get 키워드와 set 키워드 문법을 제공한다. 

 

class 클래스 이름 {
    get 이름 () { return 값 }
    set 이름 (value) {}
}

 

사실 이렇게만 보면 getOO() 형태와 setOO() 형태의 메소드를 만들어서 사용하는 것이 더 쉬워보일 수도 있다. 하지만 이 문법을 활용하면 애플리케이션 개발자쪽의 코드가 훨씬 간단해진다. 어떤 형태로 간단해지는지 실제 코드를 살펴보자.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <script>
        // 정사각형 클래스
        class Square {
            #length

            constructor (length) {
                this.length = length // this.length에 값을 지정하면, set 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)
        console.log(`한 변의 길이: ${squareA.length}`)
        console.log(`둘레: ${squareA.perimeter}`)
        console.log(`넓이: ${squareA.area}`)

        // 예외 발생시키기
        const squareB = new Square(-10)
    </script>
</head>
<body>
   
</body>
</html>

 

get 키워드와 set 키워드 조합하기

 

Square 클래스가 갖고 있던 모든 getOO()과 setOO() .형태의 코드에서 get set 뒤에 띄어쓰기를 넣었습니다. 클래스쪽은 큰 변경이 없는 것 같지만, 클래스를 활용하는 쪽에서는 단순하게 속성을 사용하는 형태처럼 getter setter를 사용할 수 있게 되었다.

 

이렇게 코드를 작성하면 코드를 사용하는 쪽에서 getter와 setter를 훨씬 더 쉽게 사용할 수 있다.

 

static 속성과 메소드

지금까지 살펴본 내용들을 기반으로 보면 이제 프레임워크 개발자들은 안전하게 프레임워크를 개발 할 수 있게 되었다. 프레임워크 개발자들은 더 효율적으로 프레임워크 개발할 수 있게 다양한 패턴을 고안한다. 이러한 패턴을 디자인 패턴(Design pattern)이라고 부른다. 

 

원래 자바스크립트에는 클래스라는 기능이 없었다. 하지만 여러 디자인 패턴을 활용하기 위해서 클래스 문법들이 계속해서 추가된 것이라 할 수 있다. 비교적 최근 추가된 문법으로 static 속성 static 메소드가 있다. static을 정적이라는 한국어로 불러서 정적 속성, 정적 메소드라고 부르기도 한다.

 

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

 

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

 

클래스 이름.속성
클래스 이름.메소드()

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <script>
        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
            }
        }

        // static 속성 사용하기
        const squareA = new Square(10)
        const squareB = new Square(20)
        const squareC = new Square(30)
        console.log(`지금까지 생성된 Square 인스턴스는 ${Square.counter}개 입니다.`)
        // static 메소드 사용하기
        console.log(`한 변의 길이가 20인 정사각형의 둘레는 ${Square.perimeterOf(20)}입니다.`)
        console.log(`한 변의 길이가 30인 정사각형의 넓이는 ${Square.areaOf(30)}입니다.`)
    </script>
</head>
<body>
   
</body>
</html>

 

static 키워드 사용하기

 

#counter라는 이름의 static 속성과 counter()라는 이름의 static 메소드(getter)를 만들었다. #counter라는 속성은 Square 객체의 생성자가 호출될 때마다 1씩 증가하도록 했다. 이를 활용하면 현재까지 Square 객체가 몇 개 생성되었는지 확인할 수 있다.

 

또한 perimeterOf() 메소드와 areaOf() 메소드를 추가했다. 이 메소드들은 Square 객체를 생성하지 않고도 둘레와 넓이을 간단하게 구할 수 있게 해주는 메소드이다.

 

위의 코드를 보고 나면 외부에 변수와 함수를 선언해도 되겠다라는 생각을 할 수 있다. 그러나 이렇게 변수와 함수를 클래스 내부에 작성하면 다음과 같은 장점이 있다.

 

  • 어떤 속성과 함수가 클래스 내부에 귀속되어 있다는 것을 명시적으로 나타낼 수 있다
  • private 특성과 getter, setter를 부여해서 조금 더 안전한 변수와 함수로 사용할 수 있다.

 

오버라이드

부모가 갖고 있는 함수를 자식에서 다시 선언해서 덮어쓰는 것을 오버라이드(override)라고 부른다. 프레임워크를 다룰 때 반드시 활용하는 개념이다. 물론 오버라이드라는 개념을 몰라도 코드를 작성할 수는 있지만, 알고나면 "내부적으로 어떤 코드가 있길래 이렇게 동작하는지"를 알 수 있다.

 

다음 코드는 LifeCycle이라는 간단한 클래스를 선언하고 사용하는 예이다. LifeCycle 클래스에는 a(), b(), c()라는 이름의 메소드가 있고, call이라는 이름의 메소드에서 이를 호출하고 있다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <script>
        // 클래스를 선언한다
        class LifeCycle {
            call () {
                this.a()
                this.b()
                this.c()
            }

            a () { console.log('a() 메소드를 호출합니다.')}
            b () { console.log('b() 메소드를 호출합니다.')}
            c () { console.log('c() 메소드를 호출합니다.')}
        }

        // 인스턴스를 생성한다
        new LifeCycle().call()
    </script>
</head>
<body>
   
</body>
</html>

 

메소드에서 순서대로 메소드 호출하기

 

이어서 다음 코드를 살펴보자. 다음 코드에서는 LifeCycle 클래스를 상속받는 Child라는 이름의 클래스를 선언했다. 그리고 내부에서 부모에 있던 a()라는 이름의 메소드를 만들었다. 이를 "오버라이드했다"고 표현한다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <script>
        // 클래스를 선언한다
        class LifeCycle {
            call () {
                this.a()
                this.b()
                this.c()
            }

            a () { console.log('a() 메소드를 호출합니다.')}
            b () { console.log('b() 메소드를 호출합니다.')}
            c () { console.log('c() 메소드를 호출합니다.')}
        }

        class Child extends LifeCycle {
            a () { // 오버라이드 하는 부분
                console.log('자식의 a() 메소드입니다.')
            }
        }

        // 인스턴스를 생성한다
        new Child().call()
    </script>
</head>
<body>
   
</body>
</html>

 

오버라이드

 

코드를 실행하면 원래 a() 메소드에 있던 출력이 바뀌는 것을 볼 수 있다. call() 메소드에서 a() 메소드를 실행하는데, a() 메소드가 덮어 쓰여졌으니 새로운 a() 메소드의 내용을 출력하는 것이 전부이다.

 

만약 부모에 있던 메소드의 내용도 사용하고 싶다면 다음과 같이 super.메소드() 형태의 코드를 사용한다. super.a()는 부모의 a() 메소드를 실행하는 코드이다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <script>
        // 클래스를 선언한다
        class LifeCycle {
            call () {
                this.a()
                this.b()
                this.c()
            }

            a () { console.log('a() 메소드를 호출합니다.')}
            b () { console.log('b() 메소드를 호출합니다.')}
            c () { console.log('c() 메소드를 호출합니다.')}
        }

        class Child extends LifeCycle {
            a () { // 오버라이드 하는 부분
                super.a()
                console.log('자식의 a() 메소드입니다.')
            }
        }

        // 인스턴스를 생성한다
        new Child().call()
    </script>
</head>
<body>
   
</body>
</html>

 

부모에 있던 내용 가져오기

 

오버라이드는 그냥 이름만 덮어 쓰는 것이라 이렇게 간단한 기능에 굳이 이름까지 붙여야 하는지 의문이 생길 수 있다. 하지만 오버리이드는 정말 많은 곳에 활용된다. 이어서 오버라이드의 예를 살펴보자.

 

오버라이드 예

지금까지 어떤 객체를 문자열로 만드는 메소드는 toString() 메소드라는 이름으로 만들었다. 이 이름은 단순하게 아무렇게나 붙여진 것이 아니다.

 

자바스크립트의 모든 객체는 toString()이라는 메소드를 갖는다. 숫자, 문자열, 불, 배열, 함수, 클래스, 클래스의 인스턴스 모두 toString()이라는 메소드가 있다. 이는 자바스크립트가 Object라는 최상위 클래스를 가지며, 이떤 클래스를 만들어도 자동으로 Object 클래스를 상속받게 되어서 발생하는 현상이다. 따라서 toString()이라는 이름으로 메소드를 만들면 Object 클래스에 있던 toString() 메소드를 오버라이드하는 것이 된다.

 

자바스크립트는 내부적으로 어떤 객체를 문자열로 만들 때 toString() 메소드를 호출한다. 따라서 toString() 메소드를 오버라이드하면 내부적으로 문자열로 변환되는 형태를 바꿀 수 있다.

 

다음 코드를 살펴보자.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <script>
        class Pet {
            constructor (name, age) {
                this.name = name
                this.age = age
            }

            toString() {
                return `이름: ${this.name}\n나이: ${this.age}`
            }
        }

        const pet = new Pet('구름', 6)
        alert(pet)
        console.log(pet + '')
    </script>
</head>
<body>
   
</body>
</html>

 

toString() 메소드 오버라이드하기

 

자바스크립트의 alert() 함수는 매개변수로 받은 자료를 문자열로 바꾼 뒤에 출력한다. toString() 메소드를 오버라이드 했으므로 우리가 바꾼 형태로 출력되는 것을 볼 수 있다. 

 

또한 문자열과 다른 자료형을 결합할 때도 내부적으로 다른 자료형을 문자열로 변환한 뒤 결합한다. 따라서 문자열 결합 연산자를 호출할 때도 우리가 오버라이드한 toString() 메소드의 리턴값이 나오는 것을 확인할 수 있다.

 

이렇게 '혼자 공부하는 자바스크립트'의 내용은 마무리된다.

 

다음 혼공학습단과 혼공스를 마무리 하는 글로 지난 한달을 정리하면 될 것 같다.

 

오늘 하루도 쌓였다.