우테코에 참여하면서, 클래스 관련 문법을 자주 사용하다보니 자바스크립트에서 객체 지향 프로그래밍이 무엇인지에 대해서 자세히 알아보고 싶었습니다.🥲 추가적으로 면접 스터디를 위해서도 오늘의 글을 포스팅해봅니다.
먼저 객체 지향 프로그래밍이 무엇이냐? 에 대해 단도직입적으로 이야기해보자면 객체 지향 프로그래밍이란 프로그램의 덩치가 커져가면서 생기는 문제점을 해결하기 위해 나온 하나의 방법론입니다.
객체를 설계하고 찍어낼 수 있는 구조를 클래스라고 하고, 클래스에 만들어지는 인스턴스를 Object라고 하여 프로그래밍의 모든 것들을 이러한 객체로 간주, 객체 간의 상호작용을 중심으로 설계하는 프로그래밍 개념이 바로 Object-Oriented Programming(OOP) = 객체 지향 프로그래밍 입니다.
쉽게 이해하기 위해서 객체 지향 이전의 이야기를 알아볼까합니다.
객체 지향 이전의 이야기
완전 초창기 프로그래밍에서는 순차적 프로그래밍
이 있었습니다. 이 말은 즉슨, 순차적으로 프로그램이 실행되었습니다. 그러다보니 프로그래밍을 하다보면서 그전에 만들어 두었던 것들과 비슷하게 반복적인 동작이 필요하다는 것을 알게 됩니다. 이때 당시에는 함수라는 개념이 없었기 때문에 특정 위치로 실행 순서를 강제로 변경하는 goto
문을 만들어내게 됩니다. 이렇게 강제로 실행 순서를 바꾸다보니 코드가 커져가면서 코드의 흐름을 제어하기가 쉽지 않았습니다. 그래서 다른 방법을 찾아나서게 됩니다.
var hp = 100
var mp = 100
gameloop:
...
if(key === 'A'){
goto magic
}
...
goto gameloop
magic:
mp -= 10
...
goto gameloop
절차적 프로그래밍
이후 실행 순서를 강제로 바꾸는 것이 아니라, 일정하게 반복되는 코드를 만들고 그에 해당하는 코드를 호출하고 다시 원래 자리로 돌아오는 방식의 프로시저(함수)를 통해 개발하는 절차적 프로그래밍 패러다임이 탄생하면서 지금의 함수와 같은 개념이 생겼습니다.
즉, 절차적 프로그래밍은 데이터와 데이터를 처리하는 동작을 함수 단위로 코드를 분리하고 재사용하는 형태로 프로그래밍하는 방식이 됩니다. (함수형 프로그래밍과는 다른 것입니다.) 절차적 프로그래밍은 우리에게 아주 익숙한 방식이라고 합니다.
코드의 덩치가 커지면서 발생한 문제
패러다임의 한계는 프로그램의 덩치가 커지면서 발생하게 되는데요, 이러한 방식으로 코드가 커지게 되면 다음과 같은 문제가 발생하기 시작했습니다. 기본적으로 프로그래밍은 전역 변수의 형태로 만들었습니다. (예전에는 let, const가 존재하지 않았습니다.) 그러다 보니 프로그래밍의 덩치가 커지면 커질수록 변수에 같은 이름을 쓸 수 없게 됩니다. 그러다보니 변수명 관리가 매우 복잡해지고 힘들다보니 foo_x
, foo_y
이런 prefix
만 늘어나게 됩니다. 따라서 이를 해결하고자 하나의 파일단위, 모듈단위에서 prefix
를 부여해 관리하자는 네임스페이스 방식이 등장하게 됩니다.
데이터를 묶어서 관리해보자! = 구조체
하지만 네임스페이스만으로는 비슷한 형태의 데이터들을 쉽게 다룰 수 없었습니다. 만약 게임을 만든다고 가정해보면, 캐릭터 하나하나씩 이름, hp, mp, item 등 구조의 형태를 가지는 변수를 만들기 위해 여전히 prefix를 붙여서 만들어야했습니다.
var character1_name = 'daeun'
var character1_mp = 300
var character1_hp = 500
function character1_useSkill(){
...
character1_mp -= 100 // 변수를 직접 수정하게 됨
...
}
character1_do_something()
따라서 100개의 캐릭터가 있다고 가정한다면, character100
까지 prefix가 붙게됩니다..😂
위와 같은 방식으로 프로그래밍을 하게 될 경우 캐릭터 2개, 3개만 되어도 중복되는 코드가 많아지게 됩니다. 따라서 이렇게 서로 연관이 있는 데이터들을 하나로 묶어서 네임스페이스처럼 관리하여 해당 변수에 접근을 할 수 있는 구조체
라는 형식을 생각하게 됩니다.
var character = {
name: 'daeun',
hp: 300,
mp: 500,
}
function character1_useSkill(){
...
character.mp -= 100 // 변수를 직접 수정하게 됨
...
}
do_something();
의미있는 단위로 변수들을 하나로 묶어줌으로써 변수 명의 중복을 줄이고, 함수나 배열 등에서도 하나의 변수처럼 활용할 수 있게 되었습니다. 이렇게 코드가 덩치가 되어도 일관성을 유지하면서 코드를 짤 수 있게 됩니다. 이처한 개념으로 만들어진 언어 중 가장 유명한 것이 C언어
입니다.
객체 지향 프로그래밍의 등장
구조체가 생기게 되면서 데이터들을 의미있는 데이터로 구조화시켜서 프로그래밍하기 보다 데이터 중심으로 코딩하게 되면 코드의 덩치가 커져도 일관성을 유지하기 좋다는 것을 깨닫게 됩니다. 그러면서 코드를 한 곳에 모으다 보니 같은 패턴이 자주 만들어지게 된다는 것을 알게 됩니다.
// struct
var character = {
name: 'daeun',
hp: 300,
mp: 500,
}
function character_attack(character){...}
function character_useSkill(character){...}
function character_moveTo(character, toX, toY){...}
위와 같이 특정 구조체만 가지고 동작하는 함수 군들이 만들어진다는 것을 알게 되었고, 함수 역시 전역 네임스페이스를 쓰고 있다보니 character_
와 같은 prefix
를 달아야한다는 것을 알게 됩니다.
구조체에 항상 쓰이는 함수들도 하나도 합치면 어떨까? = class
그래서 구조체와 항상 쓰이는 함수들도 하나로 묶어 구조체와 함께 함수까지 포함하는 개념을 만들게 되고, 이를 class
라고 부르게 됩니다.
class Character {
name = 'daeun'
hp = 300
mp = 500
attack(){}
useSkill(){}
moveTo(){}
}
var character = new Character();
character.attack();
character.useSkill();
character.moveTo();
이렇게 만들고 보니 기존의 데이터 처리방법을 분리하여 개발하던 절차적 프로그래밍과 달리 데이터 처리 방식이 하나의 모듈로 관리되면서 마치 작은 프로그램들이 독립적으로 돌아가는 형태를 띄게 되어 덩치가 큰 프로그래밍을 작성하더라도 작은 부품들을 만들고 이를 조합하고 결합하는 방식으로 개발할 수 있다는 것을 알게 됩니다.
기존의 구조체와 함수를 합쳐서 만드는 것을 class
라고 부르기로 했고 class
를 통해 만들어진 결과물을 값과 동작을 함께 가지고 있는 것이 주변 사물과 비슷하다고 하여 Object
라고 부르게 됩니다. 이런식으로 작은 문제를 해결하는 것들을 모아서 하나의 문제를 해결하는 프로그램을 개발하는 방식을 Bottom-Up 방식이라고 합니다. 작은 문제를 해결하는 독립된 객체를 먼저 만들고 조립하자는 개발방식은 다음과 같이 개념이 확장이 됩니다.
프로그램은 모두 객체로 이뤄져있고 객체들 간의 메시지를 주고받는 상호작용으로 이뤄진다.
이렇게 프로그램을 객체로 바라보는 관점으로 프로그래밍 하는 것을 객체 지향 프로그래밍이라고 합니다.
독립된 객체를 조립해서 사용하는 방식은 레고와 같이 재사용이 가능한 객체를 많이 만들어 놓는 것이 중요하다는 것을 알게 되고, 객체의 재사용을 높이기 위해서 OOP는 아래와 같은 개념들이 추가되었습니다.
외부에서 알 필요가 없는 것들은 숨겨놓자! = 캡슐화
작은 문제를 해결하는 독립된 객체를 사용하게 되면서 객체의 모든 데이터들에 접근해야할 필요가 없다는 것을 알게 됩니다. 내부의 데이터는 알아서 조작할 수 있도록 하고 외부에서는 필요한 내용만 만들어두는 편이 프로그래밍의 안정성과 사용적인 측면에서 낫다는 것을 알게 됩니다. 만약 캡슐화를 하지 않는다면, 외부에서도 데이터들을 쉽게 가변시킬 수 있겠죠?
그래서 꼭 외부로 노출되어야하는 값과 내부에서 사용해야하는 값을 구분하는 기능을 추가하도록 합니다. 내부 데이터에 직접 접근하지 못하게 하고, 필요한 메서드만 열어두는 것을 캡슐화
라고 합니다. 객체 지향 프로그래밍에서는 이러한 캡슐화를 통해 객체의 안정성을 높이고 필요한 메서드만 열어둠으로써 객체의 재사용성을 높일 수 있습니다.
// class
class Character {
name = 'daeun'
#hp = 300
#mp = 500
attack(){...}
useSkill(){...this.#mp -= 50; ...}
moveTo(toX, toY)
}
//object
var character = new Character();
character.name = '다은' // public한 필드는 외부에서 수정가능한 위험이 있다!
// private을 이용하면 mp를 외부에서 함부로 수정할 수 없게 된다.
character.mp = 3000 // 에러 발생
객체의 일부부만 재사용은 어떻게 해야할까? = 상속
객체가 중심이 되어 그것을 재사용하는 것은 좋은데, 객체에 여러개의 변수와 함수가 섞여있거 그 중 일부만 재사용하고 일부는 다르게 사용하고 싶은 경우가 종종 있게 됩니다. 그래서 객체의 일부분만 재사용하는 방법이 필요하게 됩니다.
만약 스타크래프트를 예시로 들어 저글링을 클래스를 만들어 봅니다.
class Zergling {
name = '저글링'
hp = 35
die(){...}
attack(){...}
moveTo(toX, toY){...}
}
이제 히드라를 만드려고 합니다. 그런데 히드라를 만들다 보니 저글링과 동일한 로직이 너무 많습니다. hp가 0이면 죽고 땅으로 이동하는 알고리즘도 동일합니다. 하지만 모션도 다르고 hp도 다릅니다. 그래서 객체에서 공통된 부분만 따로 만들어서 그 코드를 같이 상속
받아서 활용을 하고 나머지 달라지는 것들만 각자 코드로 작성하는 방식으로 만들면 어떨까? 라는 생각을 하게 됩니다.
class Unit(){
name = ''
hp = 0
die(){...}
attack(){...}
moveTo(toX, toY){...}
}
class Zergling extends Unit {
name = '저글링'
hp = 35
attack (){...}
}
class Hydra extends Unit {
name = '히드라'
hp = 70
attack (){...}
}
저글링과 히드라처럼 공통적인 부분을 모아서 상위의 개념으로 새롭게 이름을 붙이는 것을 추상화
라고 합니다. 이렇게 상속과 추상화를 통해서 객체의 일부분을 재사용하는 방법을 찾게 되었습니다.
우리는 모두 움직이지만 각자의 방식으로 움직여요 = 다양성
아까 예시에서 히드라, 저글링과 다르게 다른 캐릭터들이 유닛들을 선택한 다음에 어디로 이동하라는 내용을 구현한다고 가정해본다면 공중으로 이동하는 캐릭터도 있고, 점프를 하면서 이동하는 캐릭터도 있을 수 있습니다. 이처럼 각자 이동하는 속도나 방향은 다를지라도 이동한다는 다같은 공통점이 있기 때문에 유닛으로 취급하여 같은 타입으로 취급할 수 있게 됩니다.
따라서 추상화된 유닛이란 타입은 저글링, 히드라, 등 다른 여러 하위 타입인 여러가지 타입으로 참조할 수 있는 개념이 바로 다양성입니다.