NEW 기획 디자인 개발 프로덕트 아웃소싱 프리랜싱

개발

자바스크립트에서 객체 지향을 하는 게 맞나요?

이번 글에서는 객체지향 프로그래밍에 대해 이야기를 해보려고 합니다. 그리고 자바스크립트의 객체지향은 일반적인 객체지향 프로그래밍과는 어떻게 다른지 그리고 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();                               

 

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

 

Class Objects
(출처: @programmer__meme)

 

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

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

 

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

 

Object oriented programming
(출처: memegenerartor.net)

 

독립된 객체를 조립해서 사용하는 방식은 레고와 같이 재사용이 가능한 객체들을 많이 만들어 놓는 것이 중요하다는 것을 알게 되고 객체의 재사용을 높이기 위해 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이라고 부르면 좋을 것 같네요. 우리가 사과바나나를 한데 모아 과일이라고 부르듯이 공통적인 부분을 모아서 상위의 개념으로 새롭게 이름을 붙이는 것을 추상화라고 합니다. 이렇게 상속과 추상화를 통해서 객체의 일부분을 재사용하는 방법을 찾게 되었습니다.

 

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