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

[혼공스] 09 - 1 '클래스의 기본 기능' 정리

by jaeheon0520 2024. 2. 13.

 

INTRO

C를 제외한 모든 프로그래밍 언어는 객체 지향(Object Oriented)이라는 패러다임을 기반으로 만들어진 프로그래밍 언어이다. 객체 지향 패러다임이란 객체를 우선적으로 생각해서 프로그램을 만든다는 방법론이다. 객체 지향 프로그래밍 언어들은 클래스라는 문법으로 객체를 효율적이고 안전하게 만들어 객체 지향 패러다임을 쉽게 프로그래밍에 적용할 수 있도록 도와준다.

 

 

추상화

필요한 요소만 사용해서 객체를 표현하는 것을 추상화(abstraction)라고 부른다. 좀 더 포괄적인 사전적 의미로는 복잡한 자료, 묘듈, 시스템 등으로부터 핵심적인 개념과 기능을 간단하게 추려내는 것을 추상화라고 한다.

 

같은 형태의 객체 만들기

학생 성적 관리 프로그램을 만든다고 생각해보자. 학생이라는 객체가 필요하고, 그러한 학생들로부터 성적 관리에 필요한 공통사항을 추출하는데, 이를 추상화라고 한다.

 

학생들이 여러 명이므로 추출한 요소는 배열을 이용해 관리한다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <script>
        // 객체를 선언한다
        const students = []
        students.push({이름: '구름', 국어: 87, 영어: 98, 수학: 88, 과학: 90})
        students.push({이름: '별이', 국어: 92, 영어: 98, 수학: 96, 과학: 88})
        students.push({이름: '겨울', 국어: 76, 영어: 96, 수학: 94, 과학: 86})
        students.push({이름: '바다', 국어: 98, 영어: 52, 수학: 98, 과학: 92})

        // 객체를 출력한다.
        console.log(JSON.stringify(students, null, 2)) // 객체를 json 문자열로 변화할 때 사용하는 메소드
    </script>
</head>
<body>
   
</body>
</html>

 

객체와 배열 조합하기

 

여기에서 각각의 객체에 학생들의 성적 총합과 평균을 구하는 기능을 추가해 보자.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <script>
        // 객체를 선언한다
        const students = []
        students.push({이름: '구름', 국어: 87, 영어: 98, 수학: 88, 과학: 90})
        students.push({이름: '별이', 국어: 92, 영어: 98, 수학: 96, 과학: 88})
        students.push({이름: '겨울', 국어: 76, 영어: 96, 수학: 94, 과학: 86})
        students.push({이름: '바다', 국어: 98, 영어: 52, 수학: 98, 과학: 92})

        // 객체를 출력한다.
        let output = '이름\t총점\t평균\n'
        for (const s of students) {
            const sum = s.국어 + s.영어 + s.수학 + s.과학
            const average = sum/4
            output += `${s.이름}\t${sum}\t${average}\n`
        }
        console.log(output)
    </script>
</head>
<body>
   
</body>
</html>

 

객체 활용하기

 

 

객체를 처리하는 함수

성적 총합을 구하는 기능과 평균을 구하는 기능은 여러 프로그램에서 활용될 수 있다. 따라서 단순하게 계산하는 것보다 함수를 만들어 놓으면 확장성을 고려했을 때 좋은 방법이다.

 

getSumOf()와 getAverageOf()라는 이름으로 함수를 만들고, 매개변수로 학생 객체를 받아 총합과 평균을 구하는 프로그램을 만들어보자.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <script>
        // 객체를 선언한다
        const students = []
        students.push({이름: '구름', 국어: 87, 영어: 98, 수학: 88, 과학: 90})
        students.push({이름: '별이', 국어: 92, 영어: 98, 수학: 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)
    </script>
</head>
<body>
   
</body>
</html>

 

객체를 만드는 부분과 객체를 활용하는 부분으로 나누었다. 이렇게 코드를 분할하면 현재 시점에는 쓸데없이 코드가 길어졌다고 생각할 수 있다. 하지만 객체에 더 많은 기능을 추가하게 되었을 때 객체를 쉽게 유지보수할 수 있으며, 객체를 활용할 때도 더 간단하게 코드를 작성할 수 있다.

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

현재 코드에서는 객체가 학생 객체 하나이므로 이렇게 코드를 작성해도 문제가 없지만, 객체의 수가 늘어나면 함수 이름 충돌이 발생할 수 있다. 또한 매개변수에 어떤 종류의 객체를 넣을지 몰라 함수를 사용하는 데 혼동이 있을 수 있다. 이러한 문제를 해결하기 위해 함수 이름을 getAverageOfStudent()처럼 의미를 알 수 있도록 길게 작성할 수도 있지만, 그러면 코드의 가독성이 떨어지는 문제가 생길 수 있다.

 

그래서 개발자들은 함수를 메소드로써 객체 내부에 넣어서 활용하는 방법을 사용하기 시작한다. 다음과 같이 반복문을 사용해 모든 객체에 getSumOf() 메소드와 getAverageOf() 메소드를 추가해보자.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <script>
        // 객체를 선언한다
        const students = []
        students.push({이름: '구름', 국어: 87, 영어: 98, 수학: 88, 과학: 90})
        students.push({이름: '별이', 국어: 92, 영어: 98, 수학: 96, 과학: 88})
        students.push({이름: '겨울', 국어: 76, 영어: 96, 수학: 94, 과학: 86})
        students.push({이름: '바다', 국어: 98, 영어: 52, 수학: 98, 과학: 92})

        // students 배열 내부의 객체 모두에 메소드를 추가한다
        for (const student of students) {
            student.getSum = function () {
                return this.국어 + this.영어 + this.수학 + this.과학
            }

            student.getAverage = function () {
                return this.getSum() / 4
            }
        }

        // 객체를 출력한다.
        let output = '이름\t총점\t평균\n'
        for (const s of students) {
            output += `${s.이름}\t${s.getSum()}\t${s.getAverage()}\n`
        }
        console.log(output)
    </script>
</head>
<body>
   
</body>
</html>

 

이렇게 코드를 작성하면 함수 이름 충돌도 발생하지 않고, 함수를 잘못 사용하는 경우도 줄일 수 있다.

 

 

지금까지의 코드는 객체의 키와 값을 하나하나 모두 입력해서 생성했다. 만약 함수를 사용해서 객체를 찍어내면 어떨까? 함수만 만들면 객체를 좀 더 쉽게 생성할 수 있다. 다음 코드를 살펴보자.

 

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

 

createStudent() 함수를 만들고, 여기에 객체를 만들어 리턴하게 만들었다.

 

이렇게 함수를 만들면 여러 가지 이득이 발생한다.

객체를 하나하나 만들 때와 비교해서

  • 오탈자의 위험이 줄어든다
  • 코드를 입력하는 양이 크게 줄어든다
  • 속성과 메소드를 한 함수 내부에서 관리할 수 있으므로 객체를 더 손쉽게 유지보수 할 수 있다

그런데 현재 코드에는 눈에 보이지 않는 문제가 있다. 객체별로 getSum(), getAverage(), toString() 메소드를 생성하므로 함수라는 기본 자료형보다 무거운 자료형이 여러 번 생성된다.

 

클래스 선언하기

객체들을 정의하고 그러한 객체를 활용해서 프로그램을 만드는 것을 객체 지향 프로그래밍(Object Oriented Programming)이라고 한다. 이 패턴을 수많은 개발자들이 활용하자 프로그래밍 언어 개발자들이 프로그래밍 언어에 객체를 더 효율적으로 만들 수 있는 문법을 추가하기 시작했다.

 

프로그래밍 언어 개발자들은 크게 클래스(class)프로토타입(prototype)이라는 2가지 문법으로 객체를 효율적으로 만들 수 있게 했다. 간단하게 구분하면 클래스는 객체를 만들 때 수많은 지원을 하는 대신 많은 제한을 거는 문법이다. 반면 프로토타입은 제한을 많이 하지 않지만, 대신 지원도 별로 하지 않는 문법이다.

 

현재 사용되는 대부분의 객체 지향 프로그래밍 언어는 클래스 문법을 제공한다. 자바스크립트는 초기에 프로토타입 문법을 제공했다. 하지만 시대의 모든 흐름이 클래스 문법의 승리로 이어지자, 최신 자바스크립트는 클래스 문법을 제공하기 시작했다. 

 

클래스는 다음과 같은 형태로 생성된다

 

class 클래스이름 {

}

 

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

 

new 클래스 이름()

 

클래스와 인스턴스라는 새로운 용어가 등장해서 어려워 보일 수도 있지만, 다음과 같이 생각하면 쉽다.

 

  • 클래스: 이전에 살펴보았던 객체를 만드는 함수와 비슷한 것
  • 인스턴스(객체): 이전에 만들었던 객체를 만드는 함수로 만든 객체와 비슷한 것

 

클래스와 인스턴스

 

학생을 나타내는 Student 클래스를 만들고, 인스턴스를 생성하는 코드를 작성해 실행해보자.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <script>
        // 클래스를 선언한다
        class Student {

        }

        // 학생을 선언한다.
        const student = new Student()

        // 학생 리스트를 선언한다.
        const students = [
            new Student(),
            new Student(),
            new Student(),
            new Student()
        ]
    </script>
</head>
<body>
   
</body>
</html>

 

참고로 클래스 이름은 첫 글자를 대문자로 정하는 것이 개발자들의 약속이다. 첫 번째 글자를 소문자로 지정해도 오류를 발생하지는 않지만, 식별자만 보고도 클래스라는 것을 바로 이해할 수 있도록 첫 글자를 대문자로 만드는 약속을 지키는 것이 좋다. 그래서 현재 코드에서도 Student 클래스는 첫 글자를 대문자로 지정한 것이다.

 

생성자

new Student()라는 코드를 보면 Student 뒤에 함수처럼 괄호를 열고 닫는 기호가 있다. 이는 객체가 생성될 때 호출되는 생성자(constructor)라는 이름의 함수이다. 생성자는 다음과 같은 형태로 만든다. 메소드의 이름을 constructor로 지정했지만, constructor라는 이름으로 사용하는 것이 아니라 new Student()처럼 클래스 이름으로 호출한다.

 

class 클래스 이름 {
	constructor () {
    	/* 생성자 코드 */
        }
    }

 

생성자는 클래스를 기반으로 인스턴스를 생성할 때 처음 호출되는 메소드이다. 따라서 생성자에서는 속성을 추가하는 등 객체의 초기화를 처리한다.

 

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

 

메소드

메소드는 다음과 같은 형태로 추가한다. 이렇게 메소드를 만들면 내부적으로 메소드가 중복되지 않고 하나만 생성되어 활용된다.

 

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

        // 객체를 선언한다
        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))
       
        // 출력한다
        let output = '이름\t총점\t평균\n'
        for (const s of students) {
            output += s.toString()
        }
        console.log(output)
    </script>
</head>
<body>
   
</body>
</html>

 

지금까지 클래스가 어떤 형태로 등장했는지 살펴보았다. 현재까지 살펴본 내용을 보면 함수를 사용해서 객체를 만드는 것과 큰 차이가 없다는 생각이 들 수 있는데 이어지는 포스팅에서 클래스라는 문법이 객체를 만들 때 어떤 지원을 해주고, 어떤 제한을 걸어주는지 이어지는 절에서 살펴보자.