객체지향

2022-05-21

출처 자바스크립트에서 객체 지향을 하는 게 맞나요? 테오의 프론트엔드

오늘은 객체지향 프로그래밍에 대해 이야기를 해보려고 합니다. 그리고 자바스크립트의 객체지향은 일반적인 객체지향 프로그래밍과는 어떻게 다른지 그리고 Javascript에서는 객체지향 프로그래밍을 어떻게 해야 하는 게 좋을지 한번 이야기해보고자 합니다.

프롤로그

프로그래밍 세계에서 무언가를 학습하려고 할 때에는 그 이론 자체를 이해하기보다는 먼저 그 기술이 나오게 된 배경을 이해하는 것이 굉장히 중요합니다. 그래야 새로운 변환점을 맞이했을 때 맥락에 맞게 내 것으로 만드는게 쉬워지기 때문입니다.

프로그램뿐만 아니라 기술의 발전은 대략 다음과 사이클로 돌아가게 됩니다.

첫 번째 시기: 문제 인식

프로그램을 하다 보면 누구나 한 번쯤 겪을 법한 문제들이 쌓이기 시작하는 단계입니다. 그러면 이러한 문제를 해결하기 위해서 저마다의 여러 가지 해법들을 찾아보게 됩니다. 문제는 인식하여 공감대는 형성되었지만 이 문제를 해결하기 위한 최선의 방법은 아직 등장하지 않는 시기입니다.

두 번째 시기: 명명된 새로운 기술의 등장

여러 가지 해법들이 쌓여가다보면 모두가 합의할만한 이론이나 기술이 등장하고 이러한 문제 인식과 해법에 대해 이름이 붙여지는 시기입니다. 그리고 이로 인해 대부분의 사람들이 명확하게 이 문제와 해결방법에 대해서 인식을 하게 됩니다. 여기서 중요한 사실은 이미 대부분의 컨텍스트와 해결 방법은 나온 상태에서 이것들에 대해 정리된 이론이나 개념들은 나중에 이름 붙여진다는 사실입니다. 이때 보통 패러다임이 전환되었다고들 말하죠.

세 번째 시기: 기술의 부흥

이러한 패러다임의 전환으로 인해, 이론과 개념들 그리고 문제 해결 방법이 정립이 되고 나면 이후 발생하는 모든 문제들은 새로운 시각인 이 방법을 가지고 해결해보려는 시도들이 발생하게 됩니다. 그러면서 개념과 기술이 폭발적으로 성장하는 시기를 겪게 됩니다. 기존의 문제들이 세련된 형태로 대부분 해결이 되며 이 기술을 당연한 것으로 배우게 되는 시기입니다.

네 번째 시기: 안정기(혹은 정체기)

패러다임 전환으로 폭발적인 변화의 시기를 통해 성장을 하고 나면 새로운 한계점을 마주치게 됩니다. 기존의 방식으로 만들어진 문제점들이 그 패러다임으로는 해결이 되지 않는 문제가 발생하며 기존 방식으로 이를 해결하려다 보면 굉장히 복잡해진다는 사실을 알게 됩니다. 이러면서 문제 인식을 가지고 다시 첫 번째 시기로 돌아가 기술 발전의 사이클이 형성되죠.

위 내용을 React에 대입해도 좋고 컴퓨터나 스마트폰에 대입해도 좋습니다. 그렇게 알고 있는 기술에 빗대어 한번 곱씹어 보길 바랍니다. 대부분의 기술 발전은 위와 같은 사이클을 가지게 되어 있습니다. 우리가 프로그래밍을 하다 보면 만들어지는 개념들이나 배워야 하는 것들은 대부분 세 번째 시기 때에 만들어지는 응용법들을 배우고 학습하게 됩니다. 이러한 맥락과 해결방법은 자연 발생한 결과물인데, 이러한 결과물에 이름을 붙이고 이론을 다듬는 과정에서 만들어진 생소한 용어와 정의 그리고 개념은 그 맥락 없이 이해하려면 상당히 어렵습니다.

결국 기술이란 지난 문제들을 해결하기 위해서 만들어졌기 때문에, 왜 이러한 기술이 필요했고 어떠한 발전 과정을 거쳤는지 파악한다면 새로운 용어에 대한 정의나 개념의 이해가 더 쉽게 될 것입니다.

이번에 적어 볼 이야기는 객체 지향 프로그래밍입니다. 위에서 언급한 흐름에 맞춰 설명을 하려고 하니 감안하여 읽어주세요. 그럼, 객체지향 프로그래밍에 대한 간단한 역사와 내용 그리고 자바스크립트에서는 이러한 전통적인 객체지향과는 어떠한 차이점이 있는지에 대해서 한번 알아봅시다.

주의:  앞으로 객체지향의 용어와 개념을 자연스럽게 설명하기 위해서 객체지향 프로그래밍의 발전과정이 마치 선형적으로 진행된 것처럼 풀어나갈 것이나 실제로는 그렇지 않다는  미리 알려드립니다.

객체지향 이전의 이야기

객체지향 패러다임이 존재하기 전으로 가봅시다. 바로 상상하기는 쉽지 않을 테니 이 글을 읽는 사람은 최소한 자바스크립트를 알고 있다는 가정 하에 Javascript에서 Object를 쓰지 않고 boolean, string, number와 if, for, while 만으로 개발을 한다고 한번 상상해 보세요.

*예시 코드는 그 시절 언어보다는 이해가 편하도록 Javascript로 통일하였습니다.

순차적 프로그래밍과 goto

완전 초창기 프로그래밍에서는 프로그램은 순서대로 실행되었습니다. 지금도 프로그래밍을 하다 보면 그전에 만들어 둔 것과 같은 반복적인 동작이 필요하다는 것을 알게 됩니다. for나 while가 같은 반복문이 존재했지만 일부 반복의 범위가 코드를 다시 실행을 해야 할 경우가 발생을 했고 함수라는 개념이 없던 시절에는 특정 위치로 실행 순서를 강제로 변경하는 goto문을 만들어내게 됩니다. 이렇게 강제로 실행 순서를 바꾸다 보니 코드가 커져가면서 이 코드의 흐름을 제어하기가 힘들어지고 무언가 방법을 찾게 됩니다.

var hp = 100
var mp = 100

gameloop:
...
if (key == 'A') {
  goto magic
}
...
goto gameloop

magic:
mp -= 10
...
goto gameloop

절차적(구조적) 프로그래밍

이후 실행 순서를 강제로 바꾸는 것이 아니라 일정하게 반복되는 코드를 따로 만들어두고, 그에 해당하는 코드를 호출하고 나서 다시 원래 자리로 돌아오는 방식의 프로시저(함수)를 통해 개발하는 절차적 프로그래밍 패러다임이 탄생하면서 지금의 함수와 같은 개념이 생겼습니다.

즉, 절차적 프로그래밍은 데이터와 데이터를 처리하는 동작을 함수 단위로 코드를 분리하고 재사용하는 형태로 프로그래밍하는 방식이 됩니다. (함수라는 용어를 쓰기는 하지만 함수형 프로그래밍하고는 다릅니다.) 절차적 프로그래밍은 우리에게 아주 익숙한 방식입니다. 현재에도 간단한 코드들을 작성을 할 때에는 이러한 방식으로 프로그래밍을 하고 있습니다.

언제나 문제는 코드의 덩치가 커질 때 발생한다.

패러다임의 한계는 프로그램의 덩치가 커져야 알 수가 있습니다. 이러한 방식으로 코드가 커지게 되면 다음과 같은 문제가 발생했습니다. 기본적으로 절차적 프로그래밍은 전역 변수의 형태로 만들었습니다. 그러다 보니 프로그램의 덩치가 커지면 커질수록 변수에 같은 이름을 쓸 수가 없게 됩니다. 그러다 보니 변수명 관리가 굉장히 복잡해지게 됩니다. 이름 앞에 foo_x, foo_y, bar_something과 같이 prefix가 늘어만 가고 매번 이렇게 prefix를 붙이지 않고 하나의 파일단위 혹은 모듈단위로 prefix를 부여해서 관리하는 namespace(네임 스페이스)라는 방식이 등장을 합니다.

데이터를 묶어서 관리해보자! = 구조체

그러나 namespace만으로는 비슷한 형태의 데이터들을 쉽게 다룰 수는 없었습니다. 가령 게임을 만든다고 가정해보면 하나의 캐릭터에 속해있는 이름, hp, mp, item 등 구조의 형태를 가지는 변수를 만들기 위해서 여전히 prefix를 붙여서 만들어야 했습니다.

var character1_name = "teo.yu"
var character1_hp = 300
var character1_mp = 500

function character1_useSkill() {
    ...
    character1_mp -= 100 // 변수를 직접 수정하게 됨.
}

character1_do_somthing()

위와 같은 식으로 프로그래밍을 하게 될 경우 캐릭터가 2개, 3개만 되어도 만들어야 할 코드와 중복될 내용이 눈에 선하죠. 그래서 이렇게 서로 연관이 있는 데이터들을 하나로 묶어 namespace처럼 관리하여 해당 변수에 접근을 할 수 있는 구조체라는 형식을 생각하게 됩니다.

// struct
// (사실 구조체는 엄밀히 말해 하나의 Type인데 js라서 그냥 이렇게 쓰겠습니다. 찰떡같이 이해해주세요.)
var character = {
    name: "teo.yu"
    hp: 300
    mp: 500
}

function useSkill(character) {
    ...
    character.mp -= 100 // 변수를 직접 수정하게 됨.
}

do_somthing(character)

의미 있는 단위로 변수들을 하나로 묶음으로써 변수 명의 중복을 줄이고 함수나 배열 등에서도 하나의 변수처럼 활용할 수 있게 됩니다. 이렇게 코드가 덩치가 커져도 일관성을 유지하면서 코드를 짤 수 있게 되었습니다. 이러한 개념으로 만들어진 언어 중에서 가장 유명한 것이 바로 C언어입니다.

객체 지향 프로그래밍의 등장

구조체가 생기면서 산재해 있는 데이터들을 의미 있는 데이터로 구조화시켜서 프로그래밍하니 동작보다는 데이터를 중심으로 코딩하게 되면 코드의 덩치가 커져도 일관성을 유지하기 좋다는 것을 깨닫게 됩니다. 그러면서 코드를 한데 모으다 보니 다음과 같은 패턴이 자주 만들어진다는 것을 알게 됩니다.

// struct
var character = {
    name: "teo.yu"
    hp: 300
    mp: 500
}

function character_attck(character) {...}
function character_useSkill(character) {...}
function character_moveTo(character, toX, toY) {...}

위와 같이 특정 구조체만 가지고 동작을 하는 함수 군들이 만들어진다는 것을 알게 되었고 함수 역시 전역 네임스페이스를 쓰고 있다 보니 character_와 같은 prefix를 달아야 한다는 것을 알게 되었습니다.

구조체에 항상 쓰이는 함수들도 하나로 합치는 것은 어떨까? = class

그래서 구조체와 항상 쓰이는 함수들을 하나로 묶어서 구조체와 함께 함수까지 포함하는 개념을 만들게 되고 이를 class라고 불렀습니다.

struct + function(struct, ...) = class
(구조체 + 구조체를 항상 인자로 가지는 함수 = 클래스!)
// class
class Character {
    name = "teo.yu"
    hp = 300
    mp = 500
    
    attck() {...}
    useSkill() {...}
    moveTo(toX, toY) {...}
}

// object
var character = new Character();
character.attck();
character.useSkill()
character.jump();

이렇게 만들고 보니 기존의 데이터와 처리방법을 분리해서 개발하던 절차식 프로그래밍과 달리 데이터와 처리방식이 하나의 모듈로 관리되면서 마치 작은 프로그램들이 독립적으로 돌아가는 형태를 띄게 되어 덩치가 큰 프로그래밍을 작성하더라도 작은 부품들을 미리 만들어두고 이를 조립하고 결합하는 방식으로 개발할 수 있다는 것을 알게 됩니다. 그래서 이러한 부품을 만드는 설계도를 만들어두고 공장에서 찍어내듯 부품을 만들고 이것들을 조립하는 것과 같은 개념으로 ClassObject가 등장을 합니다.

기존의 구조체와 함수를 합쳐서 선언하는 것을 Class라고 부르기로 했고 Class를 통해 만들어진 결과물을 값과 동작을 함께 가지고 있는 것이 주위 사물과 유사하다고 하여 Object라고 부르기로 했습니다. 이런 식으로 작은 문제를 해결하는 것들을 모아서 하나의 문제를 해결하는 프로그램으로 개발하는 방식을 Bottom-up 방식이라고 합니다. 작은 문제를 해결하는 독립된 객체를 먼저 만들고 조립하자는 개발방식은 다음과 같이 개념이 확장됩니다.

프로그램은 모두 객체로 만들어져 있고 객체들 간의 메시지를 주고받는 상호작용으로 이루어진다.

이렇게 프로그램을 객체로 바라보는 관점으로 프로그래밍을 하는 것을 Object-Oriented Programming (OOP) = 객체지향 프로그래밍이라고 부르게 되었습니다.

독립된 객체를 조립해서 사용하는 방식은 레고와 같이 재사용이 가능한 객체들을 많이 만들어 놓는 것이 중요하다는 것을 알게 되고 객체의 재사용을 높이기 위해 OOP에는 아래와 같은 여러 가지 개념들이 추가되었습니다.

외부에서 알 필요 없는 것들은 숨겨놓자! - 캡슐화

작은 문제를 해결하는 독립된 객체를 사용하게 되면서 객체의 모든 데이터에 접근을 해야 할 필요가 없다는 것을 알게 되었습니다. 내부의 데이터는 내부에서 알아서 조작을 할 수 있도록 하고 외부에서는 필요한 내용만 만들어 두는 편이 프로그램의 안정성과 사용성 측면에서 낫다는 것을 알게 됩니다.

그래서 꼭 외부로 노출해야 하는 값과 내부에서만 사용하는 값을 구분하는 기능을 추가하도록 합니다. 이를 데이터를 보호해주는 캡슐과 같은 것으로 보고 내부 데이터에 바로 접근을 하지 못하게 하고 필요한 메소드만 열어두는 특성을 캡슐화라고 부릅니다. 객체지향 프로그래밍에서는 이러한 캡슐화를 통해 객체의 안정성을 높이고 필요한 메소드만 열어둠으로써 객체의 재사용성을 높일 수 있도록 하였습니다.

// class
class Character {
    name = "teo.yu"
    #hp = 300
    #mp = 500
    
    attck() {...}
    useSkill() {... this.#mp -= 50; ... }
    moveTo(toX, toY) {...}
}

// object
var character = new Character();
character.name = "테오" // public한 필드는 외부에서 수정이 가능한 잠재적 위험이 있다!

// private을 이용하면 mp를 외부에서 함부로(?) 수정할 수 없게 됩니다!
character.mp = 3000 // Error TS18013: Property '#mp' is not accessible outside class 'Human' because it has a private identifier.
은닉을 향한 자바스크립트의 여정

자바스크립트에서 OOP의 캡슐화는 어떻게 될까? 자세한 내용은  링크를 한번 읽어봐 주세요.

객체의 일부분만 재사용은 어떻게 해야 되지? - 상속!

객체가 중심이 되어 그것을 재사용하는 것은 좋은데 객체는 여러 개의 변수와 여러 개의 함수가 섞여있어, 일부는 재사용을 하고 일부는 달라져야 하는 경우가 자주 생긴다는 것을 알게 됩니다. 그래서 객체의 일부분만 재사용을 하는 방법이 필요하다고 느끼게 됩니다. 그래서 이러한 반복을 방지하기 위해 어떤 방법을 생각했을까요? 스타크래프트를 한번 만든다고 상상해 봅시다. 우리는 열심히 코딩을 해서 저글링 클래스를 만들었습니다.

// class
class Zergling {
    name = "저글링"
    hp = 35
    
    die() {...}
    attck() {...}
    moveTo(toX, toY) {...}
}

이제 히드라를 만들어보려고 합니다. 그런데 히드라를 만들다 보니 저글링과 동일한 로직이 너무 많습니다. hp가 0이면 죽고 땅으로 이동하는 알고리즘도 동일합니다. 하지만 모션도 다르고 공격 방식도 다르고 hp도 다르죠. 그래서 객체에서 공통된 부분만 따로 만들어서 그 코드를 같이 상속받아서 활용을 하고 나머지 달라지는 것들만 각자 코드를 작성하는 방식으로 만들면 어떨까 하는 생각을 하게 됩니다.

상속을 받을 수 있는 객체 이름을 따로 지어줘야겠다! - 추상화

그렇다면 객체에서 어떻게 공통된 부분의 코드를 만들고 이름을 붙여줘야 할까요?

class Unit {
    name = ""
    hp = 0
    
    die() {...}
    attck() {...}
    moveTo(toX, toY) {...}
}

class Zergling extends Unit {
    name = "저글링"
    hp = 35
    
    attck() {...}
}

class Hydralisk extends Unit {
    name = "히드라리스크"
    hp = 70
    
    attck() {...}
}

저글링도 히드라리스크도 함께 포함되고 있는 속성을 함께 가지고 있을 이름을 찾다 보니 Unit이라고 부르면 좋을 것 같네요. 우리가 사과, 바나나를 한데 모아 과일이라고 부르듯이 공통적인 부분을 모아서 상위의 개념으로 새롭게 이름을 붙이는 것을 추상화라고 합니다. 이렇게 상속추상화를 통해서 객체의 일부분을 재사용하는 방법을 찾게 되었습니다.

우리는 다 같이 움직이지만 각자의 방식으로 움직여요! - 다형성

이렇게 해서 상속과 추상화를 통해서 객체를 만들면 어떤 점이 좋아질까요?

스타크래프트에서 드래그해서 저글링과 히드라, 뮤탈리스크, 오버로드를 함께 섞어 유닛들을 선택한 다음에 어디로 이동을 하라고 하는 내용을 구현한다고 생각해보겠습니다. 저글링과 히드라는 걸어서 이동하고 뮤탈리스크와 오버로드는 공중으로 이동할 것입니다. 각자 이동하는 속도나 방법도 제각각이죠. 하지만 우리는 이 모든 것들이 하나의 이동이 가능한 유닛으로 취급하여 같은 타입으로 취급할 수 있게 됩니다.

let zergling1 = new Zergling()
let zergling2 = new Zergling()
let hydralisk1 = new Hydralisk()
let hydralisk2 = new Hydralisk()
let mutalisk = new Mutalisk()
let overload = new Overload()

let units = [zergling1, zergling2, hydralisk1, hydralisk2, mutalisk, overload]

// 모두 같은 이름의 moveTo 메소드를 호출하지만 각자의 방식대로 동작한다.
units.forEach(unit => unit.moveTo(300, 400))

이렇듯 추상화된 유닛이라는 타입은 저글링과 히드라, 뮤탈리스크, 오버로드 등 하위 타입인 여러 가지 타입으로 참조할 수 있다는 개념이 바로 다형성입니다.

상속추상화 그리고 같은 Unit의 메소드를 사용하지만 각자 정의된 방식이 있다면 각자의 방식대로 동작할 수 있도록 하는 다형성을 통해서 객체의 일부분만 재사용이 가능하도록 설계되었습니다.

객체지향 프로그래밍의 발전

이렇게 객체지향 프로그래밍의 개념들이 정립되고 다듬어져 C++과 같이 객체지향 프로그래밍을 할 수 있는 언어들이 등장하기 시작했습니다. 객체지향 프로그래밍은 완전 객체지향 패러다임을 지향하는 Java의 등장과 함께 프로그래밍 생태계에 주류 패러다임이 됩니다.

이제 프로그래밍을 할 때 객체 지향은 너무나도 당연한 것이 되었습니다. 모든 것이 객체이며, ‘객체 간의 관계’를 중요시하며 객체를 만들어내는 관점으로 프로그래밍을 바라보게 되었고 객체 단위로 만들어지는 재사용 라이브러리나 모듈들로 인해서 기존 절차적 프로그래밍에 비해 훨씬 덩치를 키우기가 용이해졌습니다. 독립적인 단위의 객체를 작게 만들고 조립한다는 개념은 엔터프라이즈 단위의 프로그래밍에서 훨씬 더 빛을 발하게 되었습니다.

그러나 객체지향은 언제나 복잡함이라는 것을 가지고 있는 개념이기에 그냥 class와 object를 쓴다고 나아지는 것이 아니라 객체지향을 잘하기 위해서는 객체들의 관계를 잘 설정해야 하고 이로 인해 초기 설계에 굉장히 노력이 많이 들어가기에 객체지향 그 이상의 설계를 하는 원칙이나 방법들이 발전하게 되었습니다.

은빛 총알은 없다. 객체지향도 잘해야지! - S.O.L.I.D

이제 기존 프로그래밍 개발의 한계를 벗어나 훨씬 더 덩치가 큰 단위로 개발을 하다 보니 객체지향을 쓴다는 것이 무조건 능사는 아니라는 것을 알게 되었습니다. 여기에서도 하면 안 되는 것들, 해야만 하는 것들을 발견하게 됩니다.

이러한 과정 속에서 S.O.L.I.D(Software development is not a Jenga game.)와 같은 객체지향을 잘하기 위한 원칙들 같은 것들이 나오면서 객체지향의 의미를 살릴 수 있고 문제점들을 해결할 수 있다는 것들을 알게 됩니다. 이렇게 객체지향은 조금씩 더 발전을 하기 시작합니다.

디자인 패턴의 등장

개발 패러다임은 완전히 객체지향 프로그래밍으로 넘어오게 되었습니다. 그리고 객체지향 패러다임으로 오랫동안 경험치가 쌓이다 보니 객체지향을 통해서 만들어지는 보편적인 설계 패턴들이 보이기 시작했습니다.

`소프트웨어 디자인 패턴(software design pattern)` 소프트웨어 공학의 소프트웨어 디자인에서 특정 문맥에서 공통적으로 발생하는 문제에 대해 재사용 가능한 해결책이다. 
소스나 기계 코드로 바로 전환될  있는 완성된 디자인은 아니며, 다른 상황에 맞게 사용될  있는 문제들을 해결하는 데에 쓰이는 서술이나 템플릿이다. 
디자인 패턴은 프로그래머가 애플리케이션이나 시스템을 디자인할  공통된 문제들을 해결하는 데에 쓰이는 형식화된 가장 좋은 관행이다. -  위키백과

늘 부딪히는 문제와 이를 해결하기 위한 객체지향 설계 그리고 예제 코드 등을 묶어서 하나의 패턴이라고 이름을 붙여놓고 문제를 해결하는 관행처럼 사용한다면 훨씬 더 객체지향에 적합한 코드를 만들 수 있을 것입니다. 이런 식으로 만들어진 패턴 중 유명한 것들은 대략 20여 개 정도가 존재합니다.

이러한 패턴들을 숙지하면서 적절한 문제에 적절한 설계구조를 만들게 된다면 다소 복잡할지언정 좋은 객체지향 프로그래밍 구조를 만들 수 있게 되었습니다. 우리가 보통 객체지향을 배운다고 하면 도달하는 종착지와 같은 곳입니다.

자바스크립트와 객체지향

제가요… OOP 디자인 패턴을 배웠거든요? 근데 Javascript에서는 어떻게 해야 할지 도저히 모르겠네요.

객체 지향 프로그래밍과 SOLID, 디자인 패턴 등을 배우고 나면 마치 모든 프로그래밍을 객체 지향으로 할 수 있을 것만 같고 아주 효과적인 디자인 패턴을 통해서 멋진 설계를 가진 프로그램을 할 수 있을 것 같다는 생각이 듭니다. 그렇게 자바스크립트를 만나게 되면 당황하게 됩니다. class는 어디 있지? (그래서 지금은 생겼습니다.) interface는 어디 있나요? (그래서 Typescript가 생겼습니다.) private, protected가 없으면 캡슐화는 어떻게 하죠? (그래서 지금 만들려고 하고 있죠.)

자바스크립트도 객체지향의 패러다임이 가장 핫한 시기에 만들어진 언어니 객체지향의 영향을 받지 않을 수 없었습니다. 하지만 자바스크립트를 맨 처음 설계한 사람은 객체 지향 프로그래밍에 회의적인 시각을 가지고 있었습니다. 그렇기에 당시 유행했던 객체지향의 개념은 그대로 가져오되 주류였던 Java와는 전혀 다른 방식으로 OOP를 풀어내게 됩니다.

Java와는 다르다! Java와는…

What's the difference between Javascript and Java?

Java and Javascript are similar like Car and Carpet are similar.
Javascript 탄생비화

Javascript를 창시한 Brendan Eich는 언어를 개발할 당시 유행하던 객체지향에 한계를 느끼고 
LISP, scheme  함수형 프로그래밍에 관심을 가지고 있었기에 함수형 프로그래밍의 형태로 언어를 만들고 싶어 했습니다. 
하지만 Netscape의 그의 상사는 당시 개발자들이 제일 많이 쓰던 Java와 같은 문법으로 만들기 요구했기 때문에 결국 둘의 혼종의 형태로 세상에 나오게 되었습니다.

자바스크립트는 설계자의 철학도 그랬지만 웹 브라우저에서는 처음부터 복잡한 언어를 만들 생각이 없었습니다. 그저 아주 간단하고 작은 형태의 스크립트 언어면 될 거라고 생각을 했어요. 그래서 복잡한 class나 설계, 추상화나 다형성 등을 만들고 싶지 않았습니다.

하지만 당시 가장 인기가 있었던 Java 개발자들을 끌어들이고 싶었기 때문에 Java의 문법과 패러다임은 최대한 유지를 해보고자 노력하게 되었습니다. 그래서 class가 없는 함수형 언어를 기반으로 하지만 객체 지향 프로그래밍 맛을 느낄 수 있는 언어가 탄생하게 된 것입니다.

Javascript 설계자에 빙의가 되어 봅시다!

‘OOP는 너무 복잡해! 특히 class 문법은 너무너무 복잡해. 최대한 간단하게 만들자… 그러면서도 객체 지향처럼 상속, 추상화, 다형성을 할 수만 있으면 되는 거 아닐까?’ 라는 목적을 가지고 Javascript 최초 설계를 어떻게 했을지 한번 상상해보았습니다.

class가 없는데 객체는 어떻게 만들게 할까? class 없이 바로 Object를 생성할 수 있게 하면 되겠다. JS는 no class 언어! 그러면 Java처럼 o.name, o.age 이렇게 객체 문법을 쓸 수 있겠지.

var cat = {
    name: "냥이",
    age: 1
}

console.log(o.name)
console.log(o.age)

타입이 없는데 객체가 가진 메소드는 어떻게 동작해야 될까? Java문법과 유사하게 객체에 함수를 정의하면 this를 넘겨주면 되지 않을까?

var cat = {
    name: "냥이",
    age: 1
    speak: function() { console.log("야옹~~", this.name, this.age) }
}

cat.speak()

그러면 상속이랑 추상화, 다형성은 어떻게 할 건데? 상속이란 게 결국 이미 만들어진 값들을 받아서 쓰는 건데 class가 아니라 object에서 받아오면 되지 않을까? 그래서 있으면 내 걸 쓰고 없으면 부모 값을 쓴다면 결과는 똑같은 거 같은데, 상속을 받는다는 게 아니라 없으면 찾아간다는 식으로 연결을 해보는 건 어떨까?

var animal = {
    name: "",
    age: 0
    sleep: function() { console.log("sleep... zzz", this.name) }
    speak: function() { console.log("...", this.name) }
}

var cat = {
    name: "냥이",
    age: 1
    speak: function() { console.log("야옹~~~", this.name, this.age) }
}

// 상속을 받는(척 하지만 prototype 연결)
car.__proto__ = animal

newObj.sleep() // cat에는 sleep없으니 연결된 animal를 찾아가서 sleep을 호출.. 이걸로 마치 상속 해결!
newObj.speak() // cat에 있는 속성이니 본인의 speak호출! 이걸로 다형성 해결!

그래도 class와 new를 이용해서 객체를 생성하는 방식은 필요하지 않을까? 그러면 … 그래도 class는 안 넣을 거니까 function으로 하면 되겠네! 그러면 prototype을 통해서 이렇게 정리하면 class 없이도 객체지향을 일단 할 수는 있겠지?

function Cat(name, age) {
    this.name = name
    this.age = age
}

Cat.prototype.speak = function() { console.log("야옹~~~", this.name, this.age) }

var cat = new Cat("냥이", 1)
cat.speak()

이렇게 해서 class가 없이 object에 prototype체인을 연결하는 방식을 통해 객체지향의 문법과 객체지향의 상속, 추상화, 다형성을 해결하였습니다. 그리고 이러한 객체를 바탕으로 Object, String, Number, Function, Array등에도 객체지향의 개념을 부여한 기본 라이브러리를 만들어서 class는 없지만 마치 객체지향의 언어를 사용하는 느낌으로 코드를 작성할 수 있는 언어가 탄생하였습니다.

아니! 그래도 class가 없이 어떻게 객체지향을 하나요?

당시에는 class가 없이 프로그래밍을 한다는 것은 상상도 하기 힘들었습니다. 그렇기에 Javascript 처음 출시가 되었을 때에는 수준이 낮은 언어로 폄하당했습니다. 브라우저의 성능의 한계도 있었고 언어도 엄청 단순하게 구성이 되었는데 당연히(?) 있어야 할 class마저 없었으니 말이죠.

당시에는 수도 없이 많은 class처럼 코딩을 하기 위한 라이브러리들이 엄청 많았고 그중에서 prototypeJS가 큰 인기를 끌었습니다. 홈페이지를 보면 class를 쓰기 위한 API가 맨 처음 소개되는 것을 확인할 수가 있습니다.

결국 제작자의 의도와 관계없이 가장 많이 요구하던 문법적인 기능이 바로 class였으며 결국(?) ES6에 와서는 class가 정식 문법이 되었습니다. 하지만 ES6의 class는 Javascript 객체지향의 근간인 prototype 방식을 문법적으로 class처럼 보이게 만들어준 도구에 불과합니다.

타입도 없고 Interface도 없네요?

오리처럼 걷고, 오리처럼 꽥꽥거리면 오리지…

// 타입이 뭣이 중한겨? 그냥 메소드를 가지고 있으면 실행하면 되지..
function test(duck) {
    if (typeof duck.walk === "function") {
        duck.walk()
    }
    
    if (typeof duck.quack === "function") {
    duck.quack()
    }
}

var duck1 = {...}
var duck2 = {...}

test(duck1)
test(duck2)

자바스크립트는 타입으로 인한 복잡성을 추구하지 않기 위해서 객체의 타입을 따지지 않습니다. 같은 이름의 변수나 메소드를 가지고 있다면 같은 타입의 객체라고 사용할 수 있습니다. 이를 통해서 복잡한 객체지향 프로그래밍의 설계를 복잡한 타입 정의와 상속을 통해 interface를 맞추지 않고 아주 간단하게(?) 만들어 줄 수가 있습니다. 물론 전통적으로 강력한 타입을 기반으로 하는 설계가 중요한 전통 언어의 시각으로 보기에는 어딘가 조잡해 보였겠지요. 그래서 Typescript가 생겨났다는 것을 기억합시다.

마! 우리는 함수가 1급 객체다!

논란이 많은 자바스크립트였지만 그래도 초기 설계 중에 가장 칭찬을 받는 점은 바로 함수가 1급 객체라는 점입니다. Javascript에서는 document.body.onclick = function(event) { ... } 와 같은 엄청 단순한 1줄짜리의 코드가 Java에서는 이러한 Listener의 구현을 위해 상당히 복잡한 패턴을 사용해야 했습니다.

함수를 값으로 넘길 수 있고 익명 함수와 클로저를 통해서 값을 보관하고 전달할 수 있다는 개념을 통해서, 기존의 객체지향에서 복잡하게 구현을 해야 했던 수많은 패턴들이 아주 아주 간단하게 해결되었습니다. 함수가 값이 될 수 있다는 점은 추후 많은 객체지향 언어에 새로운 영감을 주어, 지금은 객체지향만 고집하지 않고 함수형과 객체지향을 적절히 섞어 쓰는 방식으로 발전하고 있습니다.

결국, 객체지향의 시각으로 보았을 때 모자라던 언어가 이제는 객체지향의 패러다임에게 영향을 주고 있는 언어가 되었습니다.

그래서 Javascript에서는 객체지향을 어떻게 해야 하나요?

언제나 그렇듯 코딩에는 정답은 없습니다. 그리고 계속 트렌드는 바뀔 테니까요. 객체지향의 패러다임이 충만하던 시기에 Javascript는 class도 없는 열화판, 객체지향을 흉내 낸 언어였습니다. 물론 이 사이 지극정성으로 ES를 돌봐주었기에 지금과 같은 사람 같은 언어가 된 면도 있지만요. 결국 객체지향을 완벽히 지향하지 않았기에 현재의 자바스크립트가 생겨났고 이러한 개념이 다른 객체지향 언어에 영향을 줄 만큼 강력하다는 것을 보았습니다.

대부분의 객체지향 프로그래밍의 이론이나 설계 등은 Java의 최전성기 시절에 구축되었던 방법론들입니다. 모든 것이 객체이며 객체 간의 메시지의 흐름이 중요하고 그를 중심으로 설계를 해보려고 하니 타입과 인터페이스들로 복잡한 프로그램이 만들어졌고 이러한 패턴들을 또 모아서 학습을 하곤 했습니다. Javascript는 Java와는 태생이 다르기에 그 모든 것을 이해하더라도 결국은 Javascript스럽게 하는 것이 가장 좋다고 생각합니다. 극단적으로 객체지향을 배제할 필요도 없고 복잡하게 객체지향으로 설계를 해야 할 필요도 없습니다.

객체지향의 개념으로 데이터와 메소드를 통해 독립적인 작은 프로그램으로써 만들어 편리하게 재사용하는 장점을 취하되, 객체들의 결합이 높아져서 프로그램이 과도하게 복잡해지지 않도록 굳이 객체로 만들지 않아도 될 부분들은 함수형으로 만들어 활용하세요. 결국 간단하고 가독성 있고 덩치가 커져도 유지보수하기 좋으며 재사용이 편리한 코드를 만들어 내는 것이 Javascript를 잘하는 것입니다. Javascript는 객체지향과 함수형의 2가지 관점을 모두 가지고 있는 언어기에 이러한 밸런스를 잡는 것이 중요합니다. 물론 언제나 말은 쉽습니다. 실천이 어려운 것이죠.

끝으로

객체지향 프로그래밍은 프로그래밍 산업이 발전하면서 프로그램의 덩치가 커져가면서 생기는 문제점을 해결하기 위해서 나온 하나의 관점이자 방법론입니다. 기존의 방식으로는 변수들을 하나씩 관리하다 보니 변수명을 공유해서 써야 되는 문제들이 생기는 문제들이 발생했기 때문입니다. 연관 있는 이름들을 하나로 묶어주는 구조체라는 타입을 만드니 데이터를 중심으로 프로그래밍을 할 수 있게 되었습니다. 그렇다면 데이터와 함수를 한데 묶어서 관리하면 어떨까? 이렇게 하니 하나의 큰 프로그래밍을 작은 문제를 해결하는 독립적인 단위로 만들 수가 있게 되었습니다. 이렇게 작은 단위로 관리를 할 수 있게 되니 개발과 유지보수가 간편하게 된다는 장점을 알게 되었습니다.

이러한 객체를 설계하고 찍어낼 수 있는 구조를 클래스라고 하고, 클래스에 만들어진 인스턴스를 Object라고 하여 프로그래밍의 모든 것들을 이러한 객체로 간주, 객체 간의 상호작용을 중심으로 생각하고 설계하는 프로그래밍 개념이 바로 Object-Oriented Programming (OOP) = 객체지향 프로그래밍입니다.

객체라는 개념을 기반으로 만들어진 코드의 일부분들을 보다 효율적으로 재사용을 위해서 캡슐화, 상속, 추상화, 다형성이라는 추가적인 개념이 추가되면서 객체지향은 발전해 왔습니다. 그러던 중 Javascript라는 언어는 객체지향의 패러다임은 계승하면서도 단순하고 간단한 언어를 추구하다 보니 Java의 문법을 쓰지만 함수형을 기반으로 하는 class-free 프로토타입 기반 OOP라는 독특한 변화구를 가진 객체지향 맛 언어가 탄생했습니다.

Javascript가 쏘아 올린 작은 공으로 인해 객체지향 언어가 최고라는 패러다임은 적절히 함수형 프로그래밍과 섞으면 좋다 라는 것으로 변하게 되었습니다. Typescript가 대중화되면서 다시 Type과 Class, Interface가 있는 언어가 되었지만 패러다임은 이미 바뀌었습니다.

Javascript는 함수형 언어도 객체지향 언어도 아니지만 또 함수형 언어이기도 하고 객체지향 언어이기도 합니다. 많은 사람들의 손이 타다 보니 여러 취향을 반영할 수 있는 재미난 언어가 되어버렸습니다. 객체지향의 패러다임은 대부분은 Java가 전성기이던 시절에 완성이 되었습니다. 물론 지금이야 멀티패러다임 언어가 흔해졌기에 모두가 그렇지는 않지만 대부분의 객체지향 패러다임은 온전히 Javascript와는 맞는 옷은 아닙니다.

데이터와 메소드를 기반으로 작은 문제를 독립적으로 해결할 수 있는 작은 프로그램을 만들어 코드의 재사용을 높이고, 개발과 유지보수를 편하게 할 수 있다는 간결한 문법의 장점은 취하고, 객체를 연결하는 과정에서 과도하게 객체지향을 통해 만들어진 복잡한 구조를 만들지 않도록 지양하고, 적절한 선 안에서 함수형 프로그래밍이 가지고 있는 장점과 결합하는 것이, 현대 Javascript에서 객체지향 프로그래밍을 잘한다라고 볼 수 있는 것 같습니다.

그러기 위해서는 역시 많은 경험을 통해서 무엇이 좋고 무엇이 나쁜지 알아가서 본인만의 눈을 기르는 것이 중요하겠지요. 이 글이 무작정 경험을 쌓기보다는 조금 더 전략적으로 접근을 할 수 있도록 도움을 주는 길잡이가 될 수 있었기를 바랍니다.

results matching ""

    No results matching ""

    99 other / uml

    04 react / JSX