앞서 클래스란 무엇인지, 그리고 클래스 내부의 생성자와 메소드는 어떻게 정의하는지 알아보았다.
클래스는 객체를 안전하고 효율적으로 생성하기 위함이라고 하였는데,
앞서 배운 내용만으로는 단순히 객체를 생성하는 함수에 그치지 않는다고 생각될 것이다.
이번 글에서 클래스가 어떻게 객체를 안전하고 효율적으로 생성하는지 알아보자.
상속
상속이란 클래스의 선언 코드를 중복해서 작성하지 않도록 함으로써 코드의 생산 효율을 올리는 문법으로,
어떤 클래스가 가지고 있는 속성과 메소드를 다른 클래스에게 물려줄 수 있도록 한다.
이때 속성과 메소드를 물려주는 클래스를 부모 클래스, 물려받는 클래스를 자식 클래스라고 한다.
class 자식 클래스 이름 extends 부모 클래스 이름 {
}
자식 클래스의 생성자는 필요할 경우 부모 클래스의 생성자를 나타내는 super 메소드를 사용할 수 있다.
<script>
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 rectangle = new Rectangle(10, 20)
console.log(`직사각형의 둘레: ${rectangle.getPerimeter()}`)
console.log(`직사각형의 넓이: ${rectangle.getArea()}`)
const square = new Square(10)
console.log(`정사각형의 둘레: ${square.getPerimeter()}`)
console.log(`정사각형의 넓이: ${square.getArea()}`)
</script>
직사각형의 둘레: 60
직사각형의 넓이: 200
정사각형의 둘레: 40
정사각형의 넓이: 100
private 속성과 메소드
그러나 new Square(-10) 또는 square.length=-10 같이 클래스를 의도한 바와 다르게 사용하는 사용자가 있을 수도 있는데,
이를 방지하기 위해 private 속성과 메소드를 사용한다.
특정 속성과 메소드를 private으로 선언하려면 해당 속성과 메소드 앞에 #을 붙이면 되고
특히 속성의 경우 반드시 사용하기 전에 미리 private으로 선언해주어야 한다.
class 클래스 이름 {
#속성 이름
#메소드 이름 () {}
}
<script>
class Square {
#length
constructor (length) {
if (length <= 0) {
throw '길이는 0보다 커야 합니다.'
}
this.#length = length
}
getPerimeter () { return 4 * this.#length }
getArea () { return this.#length * this.#length }
}
const square = new Square(10)
square.length = -10
console.log(`정사각형의 둘레: ${square.getPerimeter()}`)
console.log(`정사각형의 넓이: ${square.getArea()}`)
</script>
정사각형의 둘레: 40
정사각형의 넓이: 100
생성자 내 조건문만으로는 new Square(-10) 과 같은 경우는 방지할 수 있지만
square.length=-10과 같이 객체 생성 이후 클래스 외부에서 속성에 직접 접근하는 경우는 방지할 수 없다.
이때 length 속성을 클래스 외부에서 접근하지 못 하도록 하는 private으로 선언해준다면
square.length=-10이 작동하지 못 해 square의 둘레와 넓이를 정상적으로 출력하게 된다.
게터와 세터
앞서 private 속성은 클래스 외부에서 접근할 수 없다고 하였다.
이는 사용자가 클래스를 의도한 바와 다르게 사용하는 것을 방지할 수 있지만
동시에 현재 length 속성의 값을 확인하거나 변경할 수 없도록 한다.
이 경우 게터와 세터를 사용하면 필요에 따라 private 속성 값을 확인하거나 지정할 수 있다.
class 클래스 이름 {
get 속성 이름 () { return 값 }
set 속성 이름 (value) { }
}
<script>
class Square {
#length
constructor (length) {
this.length = length
}
get length () {
return this.#length
}
get perimeter () { return 4 * this.#length }
get area () { return this.#length * this.#length }
set length (length) {
if (length <= 0) {
throw '길이는 0보다 커야 합니다.'
}
this.#length = length
}
}
const square = new Square(10)
square.length = 20
console.log(`한 변의 길이: ${square.length}`)
console.log(`정사각형의 둘레: ${square.perimeter}`)
console.log(`정사각형의 넓이: ${square.area}`)
square.length = -10
</script>
한 변의 길이: 20
정사각형의 둘레: 80
정사각형의 넓이: 400
Uncaught Error 길이는 0보다 커야 합니다.
at set length (c:\Users\minha\Desktop\test.html:30:9)
at <anonymous> (c:\Users\minha\Desktop\test.html:41:17)
세터를 이용해 length 속성 값을 20으로 변경하고 게터를 이용해 length, perimeter, area 속성 값을 확인했다.
이후 세터를 이용해 length 속성 값을 -10으로 변경하니 에러가 발생했다.
static 속성과 메소드
앞서 알아본 private 속성과 메소드, 게터와 세터 모두 인스턴스 생성 후 해당 인스턴스를 대상으로 사용하였으나
static 속성과 메소드는 인스턴스를 생성할 필요 없이 클래스를 대상으로 사용할 수 있다.
class 클래스 이름 {
static 속성 이름 = 값
static 메소드 이름 () { }
}
즉 static 속성과 메소드는 클래스 이름.속성, 클래스 이름.메소드() 와 같이 사용할 수 있다.
<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
}
}
const squareA = new Square(10)
const squareB = new Square(20)
const squareC = new Square(30)
console.log(`Square 인스턴스 개수: ${Square.counter}`)
console.log(`한 변이 20인 정사각형의 둘레: ${Square.perimeterOf(20)}`)
console.log(`한 변이 20인 정사각형의 넓이: ${Square.areaOf(20)}`)
</script>
Square 인스턴스 개수: 3
한 변이 20인 정사각형의 둘레: 80
한 변이 20인 정사각형의 넓이: 400
클래스 내 일반적인 메소드와 달리, static 메소드는 인스턴스가 아닌 클래스 자체에 접근하므로
counter 게터의 경우 this.#counter 가 아닌 Square.#counter로 작성하였다.
오버라이드
앞서 상속을 이용해 부모 클래스의 속성과 메소드를 자식 클래스에서 그대로 사용하였다.
그러나 상황에 따라 부모 클래스의 메소드를 수정하여 사용해야 하는 경우도 있을 수 있는데,
이와 같이 부모 클래스의 메소드를 자식 클래스에서 다시 선언하여 덮어쓰는 것을 오버라이드라고 한다.
<script>
class Parent {
call () {
this.a()
this.b()
}
a () { console.log('부모 a() 메소드 호출') }
b () { console.log('부모 b() 메소드 호출') }
}
class Child extends Parent {
a () { console.log('자식 a() 메소드 호출') }
}
new Child().call()
</script>
자식 a() 메소드 호출
부모 b() 메소드 호출
Parent 클래스를 상속한 Child 클래스의 call() 메소드를 호출하였고
오버라이드한 a() 메소드는 Child 클래스에서 정의된 대로,
오버라이드하지 않은 b() 메소드는 Parent 클래스에서 정의된 대로 출력되고 있다.
만약 Parent 클래스에서 정의된 대로 출력함과 동시에 오버라이드를 하고 싶다면 super를 사용할 수 있다.
<script>
class Parent {
call () {
this.a()
this.b()
}
a () { console.log('부모 a() 메소드 호출') }
b () { console.log('부모 b() 메소드 호출') }
}
class Child extends Parent {
a () {
super.a()
console.log('자식 a() 메소드 호출')
}
}
new Child().call()
</script>
부모 a() 메소드 호출
자식 a() 메소드 호출
부모 b() 메소드 호출
가장 자주 사용되는 오버라이드의 예시로는 toString() 메소드가 있다.
JavaScript는 Object라는 최상위 클래스를 가지고 모든 클래스는 Object 클래스를 상속받는다.
Object 클래스는 객체를 문자열로 변환하는 toString() 메소드를 가지므로 모든 클래스 역시 toString() 메소드를 가진다.
이때 toString() 메소드를 오버라이드하면 객체를 우리가 원하는 형태의 문자열로 변환할 수 있다.
<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)
</script>
이름: 구름
나이: 6
alert() 함수는 매개변수로 받은 자료를 문자열로 변환해 출력한다.
pet을 문자열로 변환하는 과정에서 오버라이드한 toString() 메소드가 사용되므로
toString() 메소드에서 정의한 형태를 가지는 문자열이 출력되고 있음을 확인할 수 있다.
이 글은 혼자 공부하는 자바스크립트 (윤인성 저)를 바탕으로 공부한 내용을 작성한 글입니다.
'Frontend > JavaScript' 카테고리의 다른 글
MODULE_NOT_FOUND 에러 해결하기 (1) | 2024.10.28 |
---|---|
클래스 ① 클래스 기본 (4) | 2024.10.28 |
예외 처리 ② 예외 처리 고급 (0) | 2024.10.27 |
예외 처리 ① 구문 오류와 예외 (0) | 2024.10.27 |
문서 객체 모델 ② 이벤트 활용 (8) | 2024.10.26 |