객체 지향 프로그래밍(OOP)의 등장
객체 지향 프로그래밍이라는 패러다임이 등장하기 전에는 절차 지향 프로그래밍이 있었다. 모든 것을 절차적으로 생각했으며 절차적 언어는 순차적인 명령의 조합이기에 기껏해야 함수로 이동하는 것이 전부였다. 초기의 C, 포트란 같은 언어들은 객체 지향의 개념이 없는 절차적 언어였다.
그러나, 객체 지향 프로그래밍이라는 패러다임이 등장하면서, 단순히 별개의 변수와 함수로 순차적으로 작동하는 것을 넘어, 데이터의 접근과, 데이터의 처리 과정에 대한 모형을 만들어 내는 방식을 고안해냈다. 따라서, 데이터와 기능이 별개로 취급되지 않고, 한 번에 묶여서 처리할 수 있게 되었다. 객체 지향 언어 "클래스"라고 부르는 데이터 모델의 청사진을 사용해 코드를 작성하는 것이다. 이러한 객체 지향의 특징은 빠르게 현대 언어에 적용이 되었고, 현대의 언어들은 대부분 객체 지향의 특징을 갖고 있다. (대표적으로 Java, C++, C# 등)
자바스크립트는 엄밀히 말해 객체 지향 언어는 아니지만, 객체 지향 패턴으로 작성할 수 있다.
객체 지향 프로그래밍(Object-oriented programming)
메모리상에서 반환되기 전까지의 의미는 가비지컬렉터가 객체를 메모리에서 해제하는 순간을 말한다.
1. 객체 생성후 해당 객체를 더이상 사용하지않음
2. 해당 객체를 다른 변수 혹은 다른 객체가 더이상 참조하지않음
위의 두 조건을 만족할경우 메모리상에서 가비지컬렉터에 의해서 제거가 된다.
OOP의 4가지 주요 개념을 통해 재사용성을 얻을 수 있다.
클래스는, 세부 사항(속성)이 들어가지 않은 청사진이다. 세부 사항만 넣는다면, 객체가 되는 것이다. 클래스는 객체를 만들기 위한 생성자(constructor) 함수를 포함한다.
클래스를 통해 만들어진 객체를 특별히 인스턴스 객체, 줄여서 인스턴스라고 부른다. 생성자를 통해 세부 사항(속성)을 넣어준다.
- 클래스는 일종의 원형(original form)으로, 객체를 생성하기 위한 아이디어나 청사진이다.
- 인스턴스는 클래스의 사례(instance object)이다.
Four fundamental concepts of Object-oriented programming
애플리케이션을 만들 때 좋은 설계를 하기 위해서는, 기본적으로 이 객체지향을 이해하고 응용하는 것이 중요하다. 객체 지향 프로그래밍에는 크게 네 가지 기본적인 개념이 있다.
Encapsulation(캡슐화)
데이터와 기능을 하나의 단위로 묶는 것
캡슐화란 데이터(속성)와 기능(메서드)을 따로 정의하는 것이 아닌, 하나의 객체 안에 넣어서 묶는 것이다.
느슨한 결합(Loose Coupling)에 유리: 언제든 구현을 수정할 수 있음
데이터(속성)와 기능(메서드)들이 느슨하게 결합되는 것이다. 느슨한 결합은 코드 실행 순서에 따라 절차적으로 코드를 작성하는 것이 아니라, 코드가 상징하는 실제 모습과 닮게 코드를 모아 결합하는 것을 의미한다. 마우스 구동을 위한 코드 작성을 예로 들면 스위치가 눌리고, 전기 신호가 생겨서, 전선을 타고 흐르고.. 와 같은 전 과정을 이곳저곳에 나누어 작성하는 것이 아니라, 마우스의 상태를 속성(property)으로 정하고 클릭, 이동을 메서드(method)로 정해서 코드만 보고도 인스턴스 객체의 기능을 상상할 수 있게 작성하는 것이 느슨한 결합을 추구하는 코드 작성법이다.
은닉(hiding): 구현은 숨기고, 동작은 노출시킴
캡슐화라는 개념은 "은닉화"의 특징도 포함하고 있는데, 은닉화는 내부 데이터나 내부 구현이 외부로 노출되지 않도록 만드는 것이다. 따라서, 디테일한 구현이나 데이터는 숨기고, 객체 외부에서 필요한 동작(메서드)만 노출시켜야 한다. 은닉화의 특징을 살려서 코드를 작성하면 객체 내 메서드의 구현만 수정하고, 노출된 메서드를 사용하는 코드 흐름은 바뀌지 않도록 만들 수 있다.
절차적 코드의 경우 데이터의 형태가 바뀔 때에 코드의 흐름에 큰 영향을 미치게 되어 유지 보수가 어렵다. 그래서 더 엄격한 클래스는 속성의 직접적인 접근을 막고, 설정하는 함수(setter), 불러오는 함수(getter)를 철저하게 나누기도 한다.
Abstraction(추상화)
추상화는 내부 구현은 아주 복잡한데, 실제로 노출되는 부분은 단순하게 만든다는 개념이다.
예를 들어 전화라는 객체가 있다면, 그 안에는 스피커와 마이크가 존재하고, 서킷 보드 등이 존재하는 등 내부 구현이 되어 있을 것이다. 그러나 실제로 우리가 사용할 때에는, 이러한 존재에 대해서는 생각하지 않고 단순히 수화기를 들고 버튼을 눌러서 해결하는 것으로(추상화) 인터페이스(interface)를 단순화할 수 있다.
이는 너무 많은 기능들이 노출되지 않은 덕분에 예기치 못한 사용상의 변화가 일어나지 않도록 만들 수 있다.
- 캡슐화 : 코드나 데이터의 은닉에 포커스
- 추상화 : 클래스를 사용하는 사람이 필요하지 않은 메서드 등을 노출시키지 않고, 단순한 이름으로 정의하는 것에 포커스
클래스 정의 시, 메서드와 속성만 정의한 것을 인터페이스라고 부르며 이것이 추상화의 본질이다.
Inheritance(상속)
상속은 부모 클래스의 특징을 자식 클래스가 물려받는 것이다.
부모/자식으로 이야기하기도 하지만, 보다 그 특징을 자세하게 설명하는 용어는 "기본 클래스(base class)의 특징을 파생 클래스(derived class)가 상속받는다"로 표현하는 것이 적합하다. (그러나, 더욱 많이 쓰이고 있는 부모/자식이라는 용어를 사용한다.)
예를 들어, 사람(Human)이라는 클래스가 있다고 가정했을 때 사람은 기본적으로 이름과 성별, 나이와 같은 속성, 그리고 먹다, 자다 등과 같은 메서드가 있다고 볼 수 있다. 추가적으로 학생(Student)이라는 클래스를 작성할 때 앞서 구현했던 사람(Human) 클래스의 속성과 메서드를 다시 구현한다면 비효율적이다. 학생의 본질은 결국 사람이므로, 상속을 이용하여 학생(Student) 클래스는 사람(Human) 클래스를 상속받을 수 있다. 학생은 추가적으로 학습 내용, 공부하다 와 같은 속성/메서드를 추가한다.
Polymorphism(다형성)
Polymorphism이라는 단어의 poly는 "많은", 그리고 morph는 "형태"라는 뜻을 가지고 있다. 즉 "다양한 형태"를 가질 수 있다는 의미이다. "말하다"라는 동작의 본질은 "입으로 소리를 내다"를 의미한다. 그러나, 각기 다른 동물들이 "말할 때" 제각각의 소리를 내는 것처럼, 객체 역시 똑같은 메서드라 하더라도, 다른 방식으로 구현될 수 있다.
예로 들어 DOM에서 Textarea(TextBox), Select, 그리고 Checkbox 등의 모든 요소를 전부 HTML Element라고 부른다. 이
모든 엘리먼트들은 전부 객체이므로, 내부적으로 모양을 그리고 화면에 뿌리는 메서드가 존재할 것이다. 이 메서드가 render라는 이름을 갖고 있다고 가정해 본다면 이 경우에는 TextBox, Select, Checkbox의 공통의 부모인 HTML Element라는 클래스에 render라는 메서드를 만들고 상속을 받게 만들 수 있다.
다형성의 핵심은 이 같은 이름의 render라는 메서드가 조금씩 다르게 작동한다는 데 있다. TextBox는 가로로 긴 네모 상자와 커서가 있는 형태일 것이고, Select 박스는 눌렀을 때 선택지가 나오도록 화면에 그려야 할 것이다. 이처럼 같은 이름을 가진 메서드라도 조금씩 다르게 작동하는 것이 바로 다형성이다.
만일 언어 자체에서 다형성을 제공하지 않는다면, 기본(부모) 클래스에 종류별로 분기를 시켜서 하나하나 다르게 만들어야 할 것이다. 또는 각각의 자식 클래스에 별도의 각기 다른 render 함수를 만들 수도 있겠지만, 엘리먼트라는 클래스의 본질상 "화면에 뿌린다"(render)는 개념은 부모가 갖고 있는 것이 합리적이다.
정리
마지막으로 OOP의 주요 개념에 대한 장점을 정리해보면 다음과 같다.
- 캡슐화 : 코드가 복잡하지 않게 만들고, 재사용성을 높인다.
- 추상화 : 코드가 복잡하지 않게 만들고, 단순화된 사용으로 변화에 대한 영향을 최소화한다.
- 상속 : 불필요한 코드를 줄여 재사용성을 높인다.
- 다형성 : 동일한 메서드에 대해 if/else if와 같은 조건문 대신 객체의 특성에 맞게 달리 작성하는 것이 가능해진다.
OOP의 의미
객체 지향 프로그래밍 패러다임을 따라해 보면, 사람이 세계를 보고 이해하는 방법과 매우 흡사하다고 느끼게 될 것이다. 코드 상에서, 혹은 화면에 보이는 하나의 요소를 객체 단위로 구분시켜서 생각하면, 보다 이해하기 쉬운 코드를 작성할 수 있게 된다. OOP의 특성을 이해하고 잘 사용하면 좋은 설계를 할 수 있다.
References
자바스크립트의 한계
JavaScript에 더 많은 기능을 붙인 새로운 언어인, TypeScript는
보다 객체 지향적으로 디자인되었으며,
아래에서 언급하는 기능이 존재한다.
은닉화(private 키워드)의 한계
Java나 TypeScript라는 프로그래밍 언어는 클래스 내부에서만 쓰이는 속성 및 메서드를 구분시키기 위해 private이라는 키워드를 제공한다. 아래는 TypeScript의 예제로, name이라는 속성이 존재하지만 private 키워드가 붙어 있어서, 클래스 내부에서만 사용 가능하다.
이러한 은닉화를 도와주는 기능이 JavaScript에서는 널리 쓰이지 않는다. 정확히는 지원하는 브라우저가 적다.
- JavaScript에서는 은닉화를 돕기 위해서 일반적으로 클로저 모듈 패턴을 사용한다.
- 클래스/인스턴스 형태로 만들 때에는 ES2019부터 #이라는 키워드가 도입되었다.
// TypeScript 문법입니다.
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
new Animal("Cat").name; // 사용 불가
// Property 'name' is private and only accessible within class 'Animal'.
추상화(interface 키워드) 기능의 부재
객체 지향 프로그래밍의 주요 키워드 중 하나인 추상화는, 속성과 메서드의 이름만 노출시켜서 사용을 단순화한다는 의미로 즉, 인터페이스(interface)의 단순화를 의미한다. Java나 TypeScript 언어는 언어의 주요 기능으로 interface를 구현해 놓았다. 아래의 코드를 보면 인터페이스와 구현이 따로 정의되어 있다.
- 이러한 부분은 JavaScript에는 존재하지 않는 기능이다.
// TypeScript 문법입니다.
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}
ㄴ 인터페이스의 이점
인터페이스의 이점은, 인터페이스가 일종의 규약처럼 간주되어, 인터페이스를 클래스로 구현하는 사람들이 이에 맞게 작성할 수 있게 돕는다. (인터페이스는 다양한 구현이 있을 수 있다.)
- 클래스를 이용하는 입장에서 노출된 인터페이스를 통해 "이 클래스는 메서드 이름이 의도한 바대로 작동할 것이다"라는 것을 명백히 드러나게 해 준다.
- 실질적인 구현 방법을 공개하지 않고, 사용법을 노출시키기에도 유리하다.
어떤 클래스가 외부 공개용으로 모듈처럼 작동할 때에 인터페이스는 빛을 발하는데, 이러한 인터페이스 사용의 대표적인 예가 API(Application Programming Interface)이다.
typescript로 interface 구현
interface ICar {
brand: string // brand는 문자열이어야 한다.
name: string
color: string
speed: number // speed는 number여야 한다.
drive():void //drive 메서드는 매개변수가 없고 반환 값이 없어야 한다.
setSpeed(speed:number):void // setSpeed 메서드는 매개변수는 number 형태여야 하고 반환값은 없다.
decreaseSpeed(speed:number):void
}
// Car 클래스는 위의 인터페이스에 맞추어 설계하며 메서드의 내부 로직은 위의 규약과 맞기만 하면
// 어떤 논리 구조를 형성하든지 문제가 없다.
class Car implements ICar {
constructor(
public brand: string,
public name: string,
public color: string,
public speed:number = 0
){}
}
const mini = new Car('BMW', 'mini', 'white')
console.log(mini)
해당 코드를 실행하면 다음과 같은 에러가 발생한다.
"Class 'Car' incorrectly implements interface 'ICar'. Type 'Car' is missing the following properties from type 'ICar': drive, setSpeed, decreaseSpeed"
Car 클래스가 interface ICar의 프로퍼티를 모두 구현하지 못했기에 코드 실행 이전에 타입스크립트에서 다음과 같은 애러를 보여준다. 모든 프로퍼티를 다 구현하면 정상적으로 출력이 된다.
class Car implements ICar {
constructor(
public brand: string,
public name: string,
public color: string,
public speed:number = 0
){}
drive(){
console.log(`${this.name}가 주행을 시작합니다.`)
}
setSpeed(speed:number){
this.speed = speed
}
decreaseSpeed(speed:number){
this.speed -= speed
}
}
const mini = new Car('BMW', 'mini', 'white')
mini.setSpeed(20)
mini.decreaseSpeed(10)
console.log(mini)
/*
Car: {
"brand": "BMW",
"name": "mini",
"color": "white",
"speed": 10
}
*/
'FE > JavaScript' 카테고리의 다른 글
[JS] 비동기 흐름 : 배열 메서드 구현해보기 (Underbar) - 하 (0) | 2023.03.19 |
---|---|
[JS] 프로토타입 체인 (0) | 2023.03.15 |
[JS] 객체 지향 프로그래밍 - 클래스와 인스턴스 (0) | 2023.03.15 |
[JS] 추상화 ** (0) | 2023.03.14 |
[JS] 내장 고차 함수 (filter, map, reduce) (0) | 2023.03.14 |