자바스크립트는 프로토타입의 객체지향언어입니다.

따라서 자바스크립트를 사용해 프로그래밍하는 사람들은 대부분 면접에서 OOP에 대한 질문을 받습니다.

 

OOP, 객체지향프로그래밍(Object Oriented Programming)에 대해 알아보도록 하겠습니다.

개념에 들어가기 전에 객체지향프로그래밍 패러다임에 대해 알아보도록 하겠습니다.

 

객체지향 프로그래밍은 구조적 프로그래밍보다 2년 앞서 등장했다고 합니다.

함수 호출이 반환된 이후 함수에서 선언된 지역변수가 오랫동안 유지되는 것을 발견한 것은 클래스의 생성자가 되었고 지역변수는 인스턴스의 변수가 되었고 중첩함수는 메서드가 되었다고 합니다.

 

함수 포인터를 특정 규칙에 따라 사용하는 과정을 통해 다형성이 등장했다고 하는데,

다형성은 아래 객체지향의 3요소에서 설명하듯이 객체지향의 최대 장점이라고 합니다.

 

 

 

객체지향프로그래밍(OOP : Object Oriented Programming)

프로그래밍 패러다임 중 하나로 객체 중심적인 사고를 하고 있으며

데이터를 추상화시켜 상태와 행위를 가진 객체를 만들고 객체 간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 방법입니다.

 

객체지향을 검색하면 붕어빵과 붕어빵 틀에 대한 예제를 많이 접하게 되는데,

잘못된 코드 비유라는 것을 설명해주고 있습니다.

클래스 = 붕어빵 틀

객체 = 붕어빵

 

"

붕어빵틀 붕어빵 = new 붕어빵틀(); 를 클래스에 대입해보면 아래와 같은데,

클래스 객체변수명 = new 클래스(); 말이 되지 않는 예제라고 합니다.

붕어빵틀은 붕어빵을 만드는 Factory로 이해해야 하며 클래스와 객체관계로 이해하면 안된다고 합니다.

"

-출처:https://sjh836.tistory.com/158 

 

그렇습니다.

클래스는 추상화적이어야 하며 붕어빵틀과 같이 구체화하고 실체면 안 됩니다.

또한, 클래스는 개념일 뿐 객체가 될 수 없기 때문입니다.

 

또 다른 예시가 잘되있는 블로그글을 가져와보겠습니다.

블로그 방문해서 읽어보시는 것을 추천합니다.

 

"

예를 들어 "바나나"라는 클래스가 있다고 하면 우리는 개념적으로 바나나가 갖고 있는 특징들을 떠올릴 수 있습니다. 껍질은 노란색이고 속은 하얗고 맛은 달고 길쭉한 모양이라는 특징들을요. 

하지만 실제로 바나나를 먹기 위해서는 마트에 가서 진열대에 놓여있는 실제의 "바나나(object)" 사와야 합니다.  

마트 직원에게 "바나나 어디 있나요?" 라고 물어볼 때 마트 직원이 안내를 해 줄 수 있는 것은 나와 마트 직원이 모두 바나나라는 추상적인 개념을 알고 있기 때문에 가능한 것입니다. 

진열대에는 바나나가 여러 개 있을 수 있죠. 그 중에 내가 어떤 바나나를 사서 먹었다면 그 바나나는 다른 바나나들 구별되는 유일한 실체입니다.

 

위 예문에서 글씨색으로 클래스와 객체를 구분해놓았는데 차이를 아시겠지요?

개(dog)라는 클래스가 있다면 우리집 강아지 로이, 옆집 강아지 초코는 객체인 것이죠.

"

출처: https://gracefulprograming.tistory.com/130 [Peter의 우아한 프로그래밍]

 

객체지향의 개념은 명확히 정의할 수가 없고 특성을 통해 이해하는 것이 최고의 방법이라고 합니다.

객체지향의 개념을 알아보기 위한 3요소와 5원칙에 대해 알아보겠습니다.

 

3요소

  • 캡슐화 (Encapsulation) = 정보 은닉(data hiding)
  • 다형성(Polymorphism) = 사용편의
  • 상속(Inheritance) = 재사용 + 확장

캡슐화 (Encapsulation) = 정보 은닉(data hiding)

객체의 속성(data fields)과 행위(methods)를 하나의 클래스라는 캡슐에 묶는 것을 캡슐화라고 합니다.

객체지향프로그래밍에서는 쉽고 효과적으로 데이터와 함수들을 캡슐화할 수 있습니다.

캡슐화는 외부에서 객체의 내부데이터로 직접 접근하지 못하게 통제하여 정보를 은닉합니다.

은닉된 정보로의 접근은 접근지정자를 통해서만 조작할 수 있도록 합니다.

 

JAVA에서는 접근지정자를 사용하는데, 접근지정자로는 private, protected, public이 있습니다.

 

private : 자기 클래스 내부의 메서드만 접근 허용

protected : 자기 클래스, 상속받은 자식 클래스에서의 접근을 허용

public : 모든 접근을 허용

 

Javascript에서는 키워드를 제공하지 않기 때문에 외부에서 접근이 제한된 데이터에 접근하려고 할 때 외부에서 접근할 수 있는 메서드를 제공하여 객체의 데이터에 접근하는 방식을 구현하여 사용하고는 합니다.

 

예제

var Person = function(name){

    // private
    var name = name;
   
    // public
    return {
        getName : function(){
            return name;
        },

        setName : function(newName){
            name = newName;
        }
    }
};

var yoon = new Person('Yoon');
console.log(yoon.name); // undefined

console.log(yoon.getName()); // yoon

yoon.setName('yoonhee');
console.log(yoon.getName()); // yoonhee

yoon.name으로 접근하면 캡슐화(정보은닉)로 인해 undefined가 반환되는 것을 확인할 수 있습니다.

이렇듯 캡슐화는 원본 데이터를 유지하며 보존, 보호하기 위해 존재합니다.

의미적인 이유로는 사용자가 굳이 알 필요가 없는 정보를 은닉함으로 최소한의 정보를 가지고 사용자가 프로그램을 사용할 수 있게 합니다.

 

다형성(Polymorphism) = 사용편의

하나의 객체가 여러 가지 형태를 가질 수 있는 것을 의미합니다.

쉽게 예를 들면 스마트폰이라는 객체가 전화도 하고 문자도 하고 게임도 할 수 있는,

즉 여러 가지 기능을 수행할 수 있는 것을 말합니다.

하나의 객체를 여러 개의 타입으로 보거나 혹은 하나의 타입을 여러 개의 객체로 해석할 수 있는 개념입니다. 다형성의 개념을 사용하는 방법으로는 오버라이딩(Overriding)과 오버로딩(Overloading)이 있습니다.

 

오버라이딩(Overriding)

 

부모 메서드로부터 상속받은 자식 메서드는 부모와 같은 이름, 인자, 반환 값을 가지게 됩니다.

이때 상속받은 메소드를 자식객체에서 재정의하는 것을 오버라이딩 이라고합니다.

 

예제

function Person(){
    this.name = '아무개';  
}

Person.prototype.getName = function(){
    console.log('내 이름은 : ' + this.name);
};

function Parents() {
    console.log('Parents');
}

Parents.prototype = new Person(); // 생성자
Parents.prototype.constructor = Parents;

// 오버라이딩
Parents.prototype.getName = function(){
    console.log('내 아이의 이름은 : ' + this.name + ' 입니다.');
};

var Child  = new Parents();
Child.getName(); // 내 아이의 이름은 아무개 입니다.

 

오버로딩(Overloading)

C#, C++, 자바 등의 다양한 프로그래밍 언어에서 사용되는 함수의 특징으로, 같은 함수 이름을 가지고 있으나 매개변수, 리턴타입 등의 특징은 다른 여러개의 서브프로그램 생성을 가능하게 하는 것을 말합니다.

Javascript에서는, 함수를 변수로 취급하고 모든 변수는 전역객체의 속성(객체의 키가 겹칠 수 없다)으로 취급되기 때문에 기본적으로 없는 개념입니다. 인자 값을 이용해서 오버로딩 기능을 구현할 수는 있습니다.

 

예제

Java의 경우

void overload(){
  System.out.println("매개변수 0개");
}

void overload(int i, int j){
  System.out.println("매개변수 "+ i + " 그리고 " + j);
}

void overload(double d){
  System.out.println("매개변수 " + d);
}

Javascript에서의 구현

function overload(a, b, c) {
  if (typeof c === 'function') { // 문자열 두 개와 콜백
    c(a, b);
  } else if (typeof b === 'function') { // 옵션 객체와 콜백
    b(a);
  } else { // 콜백 하나
    a();
  }
}

function callback(a, b) {
  if (b) {
    console.log('문자열', a, b);
  } else if (a) {
    console.log('옵션 객체', a);
  }  else {
    console.log('매개변수 없음');
  }
}

overload('zero', 'babo', callback); // 문자열 zero babo
overload({ name: 'zero', value: 'babo' }, callback); // 옵션 객체 { name: 'zero', value: 'babo' }
overload(callback); // 매개변수 없음

- 위 코드는 제로초님의 코드입니다! --> 제로초님 블로그 바로가기!

 

상속(Inheritance) = 재사용 + 확장

상속은 상위 클래스의 특성을 하위 클래스에서 물려받는 것을 말하고 하위 클래스에서는 더 필요한 속성을 확장해서 사용할 수 있습니다.

하위 클래스는 상속받은 상위 클래스의 속성(변수) 및 기능(메소드,함수)을 물려받습니다.

메모리 관점에서는 하위클래스의 인스턴스가 생성될 때 상위클래스의 인스턴스도 같이 생성된다고 합니다.

Javascript에서 상속은 Class 키워드를 사용하거나 프로토타입을 통해 할 수 있습니다.

 

예제

function Person(){
    this.type = '사람';
}

var Yoons = new Person();
console.log(Yoons); 

상속

상위 클래스의 속성을 가져온 것을 확인할 수 있으며 상속으로 인해 속성이나 기능이 재사용 가능합니다.

그리고 추가로 하나의 중요한 요소 추상화가 있습니다.

 

추상화(Abstraction) = 모델링

객체지향에서 추상화는 모델링이며 구체적인 것을 상세히 하지 않고 필요성에 의한 특성만을 가지고 구성하는 것을 말합니다.

코드상에서는 동작의 구현을 제외한 선언하는 부분에 해당하며 설계하는 부분을 말합니다.

 

Javascript는 추상클래스와 인터페이스를 제공하지 않아서 리터럴 방식, 함수 활용방식, 프로토타입 방식, 클래스로 구현할 수 있습니다.

하지만 자바의 인터페이스는 제공하지 않기 때문에 인터페이스의 기능을 사용할 수는 없습니다.

(인터페이스는 클래스의 필수 메서드 규약을 할 수 있습니다)

 

추상화는 객체를 만들기 전에 필요하며 추상화 클래스를 생성한 후 상태와 행동을 추가하여 객체를 생성합니다. 이때 추상화의 개념이 잘 안 섰는데, https://itewbm.tistory.com/24 이 블로거의 글이 도움되었습니다.

 

추상화 단계가 없는 경우를 가정해보겠습니다.

동물이라는 객체를 추상화클래스 없이 생성합니다.

동물로부터 상속하여 코끼리와 독수리 객체를 생성하려 할 때 객체는 구체화가 되어야 하기 때문에 구체화 된 기능을 추가해보겠습니다.

동물은 (코끼리) 살아있다.
동물은 (코끼리) 코가 길다.
동물은 (코끼리) 아프리카에 서식한다.
동물은 (코끼리) 4족 보행을 한다.

동물은 (독수리) 살아있다.
동물은 (독수리) 하늘을 난다.
동물은 (독수리) 아프리카에 서식한다.
동물은 (독수리) 잡식성을 가졌다.
동물은 (독수리) 날개가 있다.

 

구체화하여 선언된 것을 봤을 때, 같은 기능도 있으나 같지 않은 기능도 있습니다.

그리고 결론적으로 동물이라는 객체를 봤을 때 동물 객체는 이미 동물이 아니라는 것입니다.

동물은 살아있다. 코가 길다. 하늘을 난다. 아프리카에 서식한다. 4족 보행을 한다. 잡식성을 가졌다. 날개가 있다. 어느 동물이 위와 같은 객체가 될까요?

 

쉽게 생각하자면 위와 같은 예제를 생각할 수 있다는 것입니다.

추상화 단계를 거쳐 동물이라는 객체를 구현하게 된다면 동물이라는 객체를 만들 수 있으므로 추상화하는 단계의 설계는 중요합니다.

 

추상화

동물은 살아있다.
동물은 세포를 가지고 있다.
동물은 눈이 있다.

이렇게 구체적이지 않은 추상화 설계가 필요하며

이를 통해 코끼리라는 구체화한 객체를 생성하는 것입니다.

(어느 동물에 대입해도 가능한 경우처럼 말이죠)

 

코끼리는 살아있다.
코끼리는 세포를 가지고 있다.
코끼리는 눈이 있다.

독수리는 살아있다.
독수리는 세포를 가지고 있다.
독수리는 눈이 있다.

 

 

객체지향의 기초가 되는 5원칙

SOLID

  • SRP (Single responsibility principle)  단일 책임 원칙
  • OCP (Open-closed principle)  개방 폐쇄 원칙
  • LSP (Liskov substitution principle)  리스코브 치환 원칙
  • ISP (Interface segregation principle)  인터페이스 분리 원칙
  • DIP (Dependency inversion principle)  의존 역전 원칙

 

단일 책임 원칙 (Single Responsibility Principle)

단 한 개의 기능을 가져야 한다는 원칙입니다.

하나의 기능을 가지고 있으므로 클래스의 변경에서도 이유는 한 개가 되어야 합니다.

(클래스를 설계할 때 하나의 기능만을 수행하도록 해야 하며 그 기능에 집중해야 한다는 것을 의미합니다.)

 

단일책임원칙을 통해 개발하면 기능은 하나이기 때문에 책임영역이 확실하며 변경이 필요한 경우도 하나의 이유이기 때문에 코드의 가독성 및 유지보수에 유리합니다.

 

만약 단일책임 원칙을 위배하였을 경우 위와는 반대로 책임영역이 확실하지 않기 때문에 로직을 파악하는 시간이 오래 걸리며 버그 발생확률 또한 높아져 유지보수가 어려워집니다.

따라서 단일책임원칙을 위배하면 나쁜 설계가 될 수 있습니다.

 

산탄총 수술이라는 재미있는 말이 있는데,

하나의 책임이 여러 개의 클래스로 분산되었을 경우 단일 책임 원칙에 맞게 변경하는 경우를 말합니다.

산탄총은 하나의 총알에 여러 탄이 들어가 있고 산탄총을 맞게 된다면 여러 개의 탄을 하나하나 찾아서 치료해야 하기 때문이라고 합니다.

 

개방폐쇄의 원칙 (Open Close Principle)

소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 열려(Open)있어야 하고 수정에는 닫혀(Close)있어야 한다는 원칙입니다. 기능의 확장은 가능하지만, 수정은 하지 않아야 한다는 원칙입니다.

 

확장의 비용을 극대화 시키고 수정의 비용은 최소화한다는 의미로 요구사항의 변경이나 추가 요청사항이 발생했을 때 구성요소의 수정은 없어야 하며 기존요소를 확장하여 재사용할 수 있는 설계를 하는 것을 의미합니다.

Open
모듈의 기능을 확장할 수 있습니다.

Close
모듈의 소스코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경할 수 있어야 합니다.

재사용 코드를 만드는데 기반이 되며 개방 폐쇄원칙을 가능하게 하는 메커니즘으로는 추상화와 다형성이 있습니다.

 

리스코프 치환의 원칙 (The Liskov Substitution)

부모 클래스와 자식 클래스 사이의 기능이 일관성이 있어야 한다는 의미입니다.

일관성이 있다는 의미는 부모 클래스로 동작하던 프로그램이 속성의 변경 없이 자식클래스로 치환되었을 때 정상적으로 동작해야 한다는 것을 말합니다. 그러므로 상위 클래스는 공통속성이나 추상화된 기능만을 가지고 있어야 합니다.

 

이해가 잘 안 가서 찾던 중에 쉬운 예제를 작성한 블로거를 찾았습니다.

https://programmingfbf7290.tistory.com/entry/SOLID-%EC%9B%90%EC%B9%993-%EB%A6%AC%EC%8A%A4%EC%BD%94%ED%94%84-%EC%B9%98%ED%99%98-%EC%9B%90%EC%B9%99LSP

 

예제 

부모클래스는 스마트폰, 자식클래스는 갤럭시폰

갤럭시 폰은 스마트폰이다.

 

스마트폰의 속성

전화, 메시지가 가능하다.
데이터, 와이파이를 이용해 인터넷을 사용할 수 있다.
앱 마켓을 통해 앱을 다운받는다.

 

스마트폰의 속성을 가진 프로그램이 갤럭시 폰으로 치환되면

갤럭시 폰은 전화, 메시지가 가능하다.
데이터, 와이파이를 이용해 인터넷을 사용할 수 있다.
앱 마켓을 통해 앱을 다운받는다.

치환되어도 정상적으로 동작하므로 리스코프 치환의 원칙을 잘 적용한 케이스입니다.

 

인터페이스 분리 원칙 (Interface Segregation Principle)

클래스가 사용하지 않는 인터페이스는 구현하지 않아야 하며 의존하지 않아야 한다는 원칙입니다.

(인터페이스는 클래스의 필수 메서드 규약을 할 수 있습니다)

또한, 클래스가 다른 클래스에 종속적일 때에는 가능한 최소한의 인터페이스만을 사용해야 합니다.

큰 단위의 인터페이스는 구체적이면서 작은 단위의 역할 인터페이스로 분리해서 클래스에서 필요한 메서드만을 사용할 수 있도록 해야 합니다.

그리고 이렇게 인터페이스를 분리하는 것을 통해 시스템의 내부 의존성을 약화해 리팩토링 및 수정, 재배포를 쉽게 할 수 있게 됩니다.

 

의존성 역전의 원칙 (Dependency Inversion Principle)

상위 레벨 모듈이 하위 레벨 모듈의 구현에 의존해서는 안 되며 하위 레벨 모듈이 상위 레벨 모듈에서 정의한 추상 타입에 의존해야 하는 것을 말합니다.

 

전통적인 구조적 디자인에서는 상위 레벨 모듈에서 하위레벨 모듈을 의존하게 합니다.

이렇게 되면 하위레벨모듈에서 변경이 있으면 상위레벨 모듈도 변경될 수 있습니다.

따라서 의존성 역전은 모듈 간의 직접적인 의존을 끊고 상위 레벨 모듈에서 정의한 추상을 하위레벨 모듈이 구현하게 하는 원칙입니다.

결과적으로 상위 레벨 모듈과 하위 레벨 모두 추상클래스에 의존하게 되며 변화에 쉽게 영향받지 않게 됩니다.

즉, 외부에서 의존성을 주입함으로써 의존성을 줄이는 것이며 모듈 간의 관계를 최대한 느슨하게 만드는 것이 원칙입니다.

 

 

끝맺음

실제로 객체지향을 공부하면서 객체지향의 정의란? 하면 나오는 요소들은 실제로 객체지향만 적용되는 부분이 아니었습니다. 캡슐화, 상속 같은 경우 절차지향 프로그래밍에서도 완벽히 구현할 수 있으며 `클린아키텍쳐 소프트웨어 구조와 설계의 원칙`책을 보면 예제와 함께 실제 프로그래머들도 오래전부터 사용해왔음을 알려줍니다.

 

캡슐화에 대한 글을 인용하자면 아래와 같이 나와 있습니다.

"

객체지향이 강력한 캡슐화에 의존한다는 정의는 받아들이기 힘들다.

실제로 많은 객체지향언어가 캡슐화를 거의 강제하지 않는다.

"

 

상속 같은 경우에도 캡슐화와 마찬가지로 새로운 개념이 아니며 절차지향에서도 구현할 수 있지만, 객체지향에서는 조금 더 편리한 방식으로 사용할 수 있다는 것을 말합니다.

 

객체지향에서 가장 중요한 요소는 다형성입니다.

다형성도 물론 절차지향언어에서 구현해서 사용할 수 있으나 안전하지 않으며 위험합니다.

반대로 객체지향에서는 편리하고 안전하게 구현할 수 있으며 대수롭지 않게 사용할 만큼 중요한 요소입니다.

객체지향의 최대 장점은 다형성이며 다형성을 통해 구현하는 원칙들에 있습니다. (의존성 역전)

다형성과 의존성 역전을 통해 소스코드의 의존성 방향 제어의 흐름을 결정할 수 있으며 이는 객체지향이 지향하는 방식입니다.

 

"

객체지향을 사용하면 아키텍트는 플러그인 아키텍처를 구성할 수 있고,

이를 통해 고수준의 정책을 포함하는 모듈은 저수준의 세부사항을 포함하는 모듈에 대해

독립성을 보장할 수 있다.

저수준의 세부사항은 중요도가 낮은 플러그인 모듈로 만들 수 있고,

고수준의 정책을 포함하는 모듈과는 독립적으로 개발하고 배포할 수 있다.

"

- 클린아키텍쳐 소프트웨어 구조와 설계의 원칙, 로버트 C.마틴 지음

 

 

 


 

참고 및 출처, 인용

+ Recent posts