티스토리 뷰

객체지향 프로그래밍(OOP, Object-Oriented Programming)은 절차지향 프로그래밍(Procedural Programming)과 대비되는 프로그래밍 방법론이다.

절차지향 프로그래밍 방식 언어로는 대표적으로 C언어가 있다. 일반적으로 C코드는 특정 기능을 수행하는 함수들로 구성되어 있다. C 프로그래머는 프로그램의 각 기능을 구현하고, 이 기능들이 어떤 절차로 수행되는가를 중심으로 개발한다. 객체지향 프로그래밍 방식 언어는 Java가 대표적이다. Java 프로그래머는 프로그램에 필요한 각 객체들의 속성과 동작을 구현하고, 이 객체들이 어떻게 상호작용하는가를 중심으로 개발한다. (절차지향 프로그래밍은 명령을 순차적으로 수행하고, 객체지향은 그렇지 않다는 의미는 아니다. C든 Java든 명령은 순차적으로 수행된다.)

C와 Java를 예시로 들었는데, 절차지향과 객체지향은 방법론이지 언어의 속성은 아니다. Java로도 절차지향 프로그래밍을 할 수 있고, C언어로 객체지향 프로그래밍을 할 수 있다. 다만 C언어는 문법 자체로 객체지향을 지원하지 않기 때문에 매우 비효율적이다. 반면 Java는 언어가 차체적으로 객체지향 프로그래밍을 위한 다양한 문법을 제공하고 있다.

객체지향의 가장 큰 장점은 유지보수가 쉽다는 것이다. 특히 다른 사람과 함께 개발해야 할 때 발생하는 혼란을 줄여준다. 다른 사람들과 절차지향 프로그래밍 방식으로 프로젝트를 해봤다면 알겠지만, 내가 작업하는 코드에 다른 사람이 구현한 함수를 가져다 써야하는 경우가 있다. 반대로 내가 구현한 함수를 다른 사람이 가져다 쓰는 경우도 있다. 여기서 문제가 발생한다. 내가 작성한 함수를 다른 부분에도 사용하기 위해 내용을 고치면 내 함수를 쓰던 다른 사람의 코드에 문제가 생길 수 있다. 따라서 다른 사람이 작성한 코드를 매번 해석해야 하며, 내 함수가 어디서 어떻게 사용되는지 항상 신경쓰고 있어야 한다.

객체지향 프로그래밍 방식으로 개발을 한다면 이런 문제를 해결할 수 있다. 각 기능을 독립적인 모듈로 관리할 수 있으며, 다른 사람이 내 코드의 내용을 직접 수정하지 않고 데이터에 접근하게 만들 수 있다. 따라서 코드 재사용성을 높이고 의존성을 관리하기 쉬워진다. 대신 코드 설계를 잘해야 한다. 객체 사이의 관계를 생각하지 않고 무작정 코드를 작성하기 시작하면 모든 것이 꼬여버릴 수 있다.

how to build a horse with java

처음 객체지향 프로그래밍 방식으로 개발을 하면 굉장히 번거롭다고 느껴진다. 특히 바로 결과물이 나오지 않고 설계도를 그려야 한다는 점이 답답하다. 말 한마리 만드는데 말 공장을 만들어야 한다. 그래도 계속 하다보면 공장짓는 것도 익숙해진다 (...) 객체지향 방식은 현실 세계를 표현하기 적합하고, 또 나름 직관적이기도 하다. 미래를 생각한다면 객체지향 프로그래밍을 하자!

자바스크립트로도 객체지향 프로그래밍을 할 수 있을까? 한때 자바스크립트로 객체지향 프로그래밍을 한다고 하면 "자바스크립트의 객체지향은 진정한 객체지향이 아니다"라고 하는 사람들이 있었다.

원래 자바스크립트는 prototype을 기반으로 객체지향 프로그래밍을 해야했다. 하지만 ES6에 여러 객체지향 문법이 추가되면서 자바나 C++같은 다른 객체지향 언어들과 비슷한 방식으로 객체지향 프로그래밍을 할 수 있게 되었고, 보다 간결하게 객체지향 프로그래밍을 할 수 있게 되었다. (prototype 기반 OOP는 MDN의 객체지향 자바스크립트 소개를 참고하자.)

ES6를 사용하려면 Babel과 같은 트랜스파일러를 사용해야 한다. 이에 대해 잘 모른다면 21세기 프론트엔드 기본 튜토리얼을 통해 먼저 모던 프론트엔드 개발 환경을 구축해야 한다.

Class

// Animal.js
class Animal {

}

클래스는 객체의 설계도다. 클래스를 바탕으로 객체를 찍어낸다. 자바스크립트에서 클래스 선언은 아주 간단하다. 참고로 클래스 정의는 hoisting되지 않는다는 점을 유의해야 한다.

// index.js
import Animal from './Animal';
let anim = new Animal();

다른 파일에서 Animal 클래스에 접근하려면 우선 Animal 클래스를 import해야 한다. anim 변수를 만들고 new 키워드를 통해 Animal 객체를 생성할 수 있다. 여기서 anim은 Animal 클래스를 가리키는 레퍼런스 변수(Reference variable)이며, 인스턴스(Instance)라고 부른다. 그리고 이것이 바로 클래스의 객체를 생성하는 과정이다.

Constructor

// Animal.js
class Animal {
    constructor(name) {

    }
}

클래스는 constructor를 가질 수 있다. constructor는 new Animal(); 명령을 통해 실행되며, constructor에 name처럼 매개변수를 둘 수도 있다. 만약 constructor를 명시하지 않는다면 비어있는 default constructor가 만들어진다. 굳이 빈 constructor를 만들 필요는 없다.

Instance variable

// Animal.js
class Animal {
    constructor(name) {
        this.name = name;
    }
}

클래스의 인스턴스 변수는 constructor 안에 선언한다. 정확히는 variable이 아니라 property다.

// index.js
import Animal from './Animal';
let anim = new Animal('Jake');

객체를 생성할 때 매개변수를 넘겨줄 수 있다. anim의 인스턴스 변수 name의 값은 'Jake'다.

Information hiding

자바스크립트에는 은닉된 정보라는 개념이 없다. 자바에는 private, protected, public과 같은 접근제어자가 있어서 외부에서 인스턴스에 접근하는 것을 통제할 수 있지만, 자바스크립트는 모든 것이 public이다. 종종 인스턴스 변수 이름 앞에 언더스코어를 붙이는 방식(this._name)으로 private한 변수임을 표현하는 경우도 있는데, 오히려 오해를 불러일으킨다. Airbnb JavaScript 스타일 가이드를 참고.

Method

// Animal.js
class Animal {
    constructor(name) {
        this.name = name;
    }

    getName() {
        return this.name;
    }
}

메소는 객체의 동작을 정의한다. getName() 메소드는 Animal 객체의 인스턴스 변수인 this.name을 반환한다. 메소드는 함수와 비슷하다.

// index.js
import Animal from './Animal';
let anim = new Animal('Jake');
console.log(anim.getName()); // 'Jake'

호출 역시 직관적이다.

static method

// Animal.js
class Animal {
    constructor(name) {
        this.name = name;
    }

    getName() {
        return this.name;
    }

    static sleep() {
        console.log('Zzz');
    }
}

메소드 앞에 static 키워드를 붙여주면 따로 인스턴스를 생성하지 않고 메소드를 호출할 수 있다.

// index.js
import Animal from './Animal';
let anim = new Animal('Jake');
Animal.sleep(); // 'Zzz'
anim.sleep(); // Uncaught TypeError: anim.sleep is not a function

인스턴스를 통해 static 메소드를 호출하면 TypeError가 발생한다.

Inheritance & Polymorphism

// Animal.js
class Animal {
    constructor(name) {
        this.name = name;
    }

    getName() {
        return this.name;
    }
}

상속은 OOP의 중요한 개념 중 하나다. 상속은 말그대로 해당 클래스의 모든 내용을 다른 클래스에 그대로 복사한다는 의미다. 즉, Animal 클래스의 인스턴스 변수 this.name과 method getName()을 다른 클래스에 그대로 상속할 수 있다.

// Dog.js
import Animal from './Animal';
class Dog extends Animal {
    constructor(name) {
        super(name);
    }
}

extends 키워드를 사용해 Dog 클래스가 Animal 클래스를 상속했다. 이제 Animal 클래스는 Dog 클래스의 superclass가 되었고, Dog 클래스는 Animal 클래스의 subclass가 되었다. Dog 클래스는 Animal 클래스가 가지고 있는 this.namegetName()을 똑같이 갖는다.

subclass의 constructor에는 super()를 넣어 superclass의 constructor를 호출할 수도 있다. subclass에서 super()를 사용하지 않아도 되는 경우 에러가 발생하지는 않지만, 그래도 super()를 명시하길 권장한다.

// index.js
import Dog from './Dog';
let jake = new Dog('Jake');
console.log(jake.getName()); // 'Jake'

이런 식으로 사용한다. Dog 객체 jake가 Animal 클래스의 getName()을 호출한다.

Overriding

// Animal.js
class Animal {
    constructor(name) {
        this.name = name;
    }

    getName() {
        return this.name;
    }

    makeNoise() {
        console.log('It makes a noise');
    }
}

overriding은 subclass가 superclass의 메소드를 덮어쓰는 것을 말한다. 먼저 Animal 클래스에 makeNoise() 메소드를 추가했다.

// Dog.js
import Animal from './Animal';
class Dog extends Animal {
    constructor(name) {
        super(name);
    }

    // Override
    makeNoise() {
        console.log('Bark!');
    }
}

Dog 클래스에 같은 이름의 메소드 makeNoise()를 정의했다.

// index.js
import Dog from './Dog';
let jake = new Dog('Jake');
console.log(jake.getName()); // 'Jake'
jake.makeNoise(); // 'Bark!'

Animal 클래스의 makeNoise()가 Dog 클래스의 makeNoise()로 override된 것을 볼 수 있다.

Overloading

overloading은 같은 이름, 다른 매개변수를 가진 메소드가 여러 개 존재하는 것을 말한다. 매개변수가 다르면 다른 메소드임을 알 수 있기 때문에 가능한 기능인데, 자바스크립트에서는 불가능하다. 한 클래스 안에 같은 이름을 가진 메소드가 여러 개 존재할 수 없으며, constructor도 반드시 하나만 있어야 한다.

Abstract

Animal 클래스가 분명 존재하지만, 단순히 '동물'을 만든다는 것은 조금 이상한 일이다. 동물은 추상적인 개념이기 때문에 Animal 객체를 생성하는 일이 있어서는 안 된다. 이럴 때 추상화(Abstraction)를 통해 new Animal(...);과 같은 명령을 미연에 방지할 수 있다. Java의 경우 public abstract class Animal {...}과 같은 방식으로 추상 클래스를 만들 수 있다. 아쉽지만 자바스크립트에서는 추상 클래스나 메소드를 만들 수 없다. 다만 추상 메소드를 직접 구현하는 방법은 있다.

// Animal.js
class Animal {
    constructor(name) {
        this.name = name;
    }

    getName() {
        return this.name;
    }

    // Abstract
    makeNoise() {
        throw new Error('makeNoise() must be implement.');
    }
}

makeNoise()를 추상 메소드로 만들어 subclass에서 구현되지 않은 makeNoise()를 호출하면 에러를 발생시키도록 했다. 이 경우 추상 메소드는 반드시 subclass에서 override되어야 한다.

추상 클래스를 만드는 것을 조금 더 번거롭다. 직접 추상 클래스를 만들어 상속시키는 방식인데, 스택오버플로우의 Does ECMAScript 6 have a convention for abstract classes?를 참고해보자.

댓글
댓글쓰기 폼