OOP에서 가장 중요한 원리 중 하나는 내부 인터페이스
와 외부 인터페이스
를 구분짓는 것이다.
실생활에 빗대어 내부 인터페이스와 외부 인터페이스 구분이 무엇을 의미하는지 알아보자
커피 머신의 커버는 심플하다. 하지만 내부는 다양한 세부 요소들이 기계를 작동시키고 있다.
커버는 심플하지만 기계 내부 요소들이 잘 작동하게끔 보호해주고 있다. 즉 신뢰를 준다는 말.
만약 외형이 없다면 버튼이 없기에 사용법이 훨씬 복잡해지고, 어디를 눌러야 할지 모르기 때문에 다칠 위험성도 존재한다.
다시 프로그래밍으로 돌아오자면, 객체는 커피 머신과 같다. 프로그래밍에서는 커버를 사용하는 대신 특별한 문법과 컨벤션
을 사용해 안쪽 세부 사항을 숨긴다.
객체 지향 프로그래밍에서 프로퍼티와 메서드는 두 그룹으로 분류된다.
내부 인터페이스
: 동일한 클래스 내의 다른 메서드에서는 접근할 수 있지만, 클래스 밖에선 접근할 수 없는 프로퍼티와 메서드외부 인터페이스
: 클래스 밖에서도 접근 가능한 프로퍼티와 메서드내부 인터페이스(커피 머신의 내부 발열 장치)의 세부 사항들은 서로의 정보를 이용하여 객체를 동작시킨다. 이 내부 인터페이스의 기능들은 외부 인터페이스(커버의 버튼)를 통해야만 사용할 수 있다.
자바스크립트에는 아래와 같은 두 가지 타입의 프로퍼티와 메서드가 있다.
private
: 클래스 내부에서만 접근할 수 있으며 내부 인터페이스를 구성한다.public
: 어디서든지 접근할 수 있으며 외부 인터페이스를 구성한다.protected
: 클래스 자신과 자손 클래스에서만 접근을 허용한다.자바스크립트는 protected 필드를 문법적으로 지원하지 않지만, 언더스코어로 시작하여 protected 필드를 나타낼 때 사용한다.
먼저 간단한 커피 머신 클래스를 만들어보자.
class CoffeeMachine {
waterAmount = 0; // 물통에 차 있는 물의 양
constructor(power) {
this.power = power;
console.log(`전력량이 ${power}인 커피머신을 만듭니다.`);
}
}
// 커피 머신 생성
const coffeeMachine = new CoffeeMachine(100);
// 물 추가
coffeeMachine.waterAmount = 200;
현재 프로퍼티 waterAmount
와 power
는 public이다. 손쉽게 읽고 원하는 값으로 변경하기 쉬운 상태라는 뜻이다.
이제 waterAmount
에 언더스코어를 붙여 protected로 바꿔서 통제해보자.
class CoffeeMachine {
_waterAmount = 0; // 물통에 차 있는 물의 양
set waterAmount(value) {
if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다.");
this._waterAmount = value;
}
get waterAmount() {
return this._waterAmount;
}
constructor(power) {
this._power = power;
}
}
// 커피 머신 생성
const coffeeMachine = new CoffeeMachine(100);
// 물 추가
coffeeMachine.waterAmount = -10; // Error: 물의 양은 음수가 될 수 없습니다.
power
프로퍼티를 읽기만 가능하도록 만들어보자. 프로퍼티를 생성할 때만 값을 할당할 수 있고, 그 이후에는 값을 절대 수정하지 말아야 하는 상황이 종종 있는데, 이럴 때 읽기 전용 프로퍼티를 활용할 수 있다.
커피 머신의 경우에는 전력이 이에 해당한다.
읽기 전용 프로퍼티를 만들려면 setter는 만들지 않고 getter만 만들어야 한다.
class CoffeeMachine {
_waterAmount = 0; // 물통에 차 있는 물의 양
setWaterAmount(value) {
if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다.");
this._waterAmount = value;
}
getWaterAmount() {
return this._waterAmount;
}
constructor(power) {
this._power = power;
}
get power() {
return this._power;
}
}
// 커피 머신 생성
const coffeeMachine = new CoffeeMachine(100);
console.log(`전력량이 ${coffeeMachine.power}인 커피머신을 만듭니다.`); // 전력량이 100인 커피머신을 만듭니다.
coffeeMachine.power = 25; // // Error (setter 없음)
private 프로퍼티는 #
으로 시작한다. #
이 붙으면 클래스 안에서만 접근할 수 있다.
물 용량의 한도를 나타내는 private 프로퍼티 #waterLimit
과 남아있는 물의 양을 확인해주는 private 메서드 #checkWater
를 구현해보자
class CoffeeMachine {
// ...
#waterLimit = 200;
#checkWater(value) {
if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다.");
if (value > this.#waterLimit) throw new Error("물이 용량을 초과합니다.");
}
// ...
}
// 커피 머신 생성
const coffeeMachine = new CoffeeMachine(100);
// 클래스 외부에서 private에 접근할 수 없다.
coffeeMachine.#checkWater(); // Private field '#checkWater' must be declared in an enclosing class
coffeeMachine.#waterLimit = 1000; // Private field '#waterLimit' must be declared in an enclosing class
객체 지향 프로그래밍에선 내부 인터페이스와 외부 인터페이스를 구분하는 것을 캡슐화(encapsulation)
이라 한다.
캡슐화의 이점은 다음과 같다.
사용자가 자신의 발등을 찍지 않도록 보호
: 보호 커버가 없는 커피 머신. 사용자가 내부를 멋대로 만진다면 고장이 날 수 밖에 없다. 프로그래밍 또한 외부에서 의도치 않게 클래스를 조작하게 되면 그 결과를 예측할 수 없게 된다.유지보수의 용이성
: 끊임없이 일어나는 유지보수 상황에 내부 인터페이스가 엄격하게 구분되어 있다면, 그 어떤 외부 코드도 내부 private 메서드에 이존하고 있지 않기 때문에 private 메서드를 안전하게 바꿀 수 있고, 매개변수를 변경하거나 없앨 수도 있다.복잡성 은닉
: 구현 세부 사항이 숨겨져 있으면 간단하고 편리해진다. 외부 인터페이스에 대한 설명도 문서화하기 쉬워진다. 내부 인터페이스를 숨기려면 protected나 private 프로퍼티를 사용하면 된다.Reference