그 유명한 OOP(Object-Oriented Programming)!!!
어떠한 원칙들이 있는지, 그리고 이 원칙들을 코드에 어떻게 녹여 낼 수 있는지 알아보자.
우리는 여기서 헬스장을 OOP를 이용하여 만들어 볼 예정이다.
먼저, OOP없이 헬스장을 코딩했을 때와 OOP의 중요 4원칙을 적용했을 때 해당 코드가 어떻게 변하게 되는지 알아보자!
OOP 없이 함수 만들어 보기
TypeScript를 이용하여 운동을 하는 함수를 만들어보자.
운동을 하면 근육량과 피로도가 증가하게 된다.
type Muscle = {
muscles: number;
fatigability: number;
};
let muscles = 0;
let fatigability = 50;
let deltaPerHour = 10;
function workout(hours: number): Muscle {
if (fatigability >= 100) {
console.log('Too Tired to Workout! Need to Rest!');
}
muscles += hours * deltaPerHour;
fatigability += hours * deltaPerHour;
return {
muscles,
fatigability,
};
}
TS를 이용하여 간단하게 운동하는 함수 workout을 만들어 봤다.
흠... 나쁘지는 않아 보이지만, 함수와 관련된 변수들(muscles, fatigability, deltaPerHour)이 함수 밖에서 관리 해야 한다는 점이 조금 마음에 걸린다.
OOP 적용해 보기
객체지향프로그래밍(OOP)를 위 코드에 적용해보자.
아! 바로 코드를 적용하기 전에 OOP가 무엇인지 어떠한 원칙들이 있는지 정말 간략하게 한번 정리해보자!
What is OOP?
OOP(객체지향 프로그래밍)란, 프로그램을 단순히 데이터와 처리하는 방법으로 나누는 것이 아니라, 프로그램을 수많은 '객체(object)' 단위로 나누고 이들의 상호작용을 서술 하는 방식이다. ( 참고링크 )
여기에서 객체(object)란 단어 뜻 그대로 대상을 의미한다. 사람 한 명 한 명, 고양이 한 마리 한 마리를 객체라고 표현 할 수 있는 것이다.
사람이라는 같은 범주(class)에 속한다고 하더라도 각각의 개인은 독립적이고 다른 특성을 가지기 때문에 한 명 한 명이 객체가 되는 것이다.
고양이 역시 고양이라는 범주(class)에 속한다고 하더라도 생김새가 똑같다고 하더라도 각각 독립적이고 서로 독특한 특징을 가지기 때문에 한 마리 한마리 객체가 된다.
각 객체가 가지고 있는 공통적인 속성들을 묶어서 정의 한 것을 class 라고 한다. (추상화 - abstraction)
mark, bob, jobs라는 객체들은 다들 공통된 범주(class) '사람'에 속하고 있다. 그리고 이 사람이라는 클래스는 눈이 2개, 팔이 2개, 다리가 5개 와 같은 공통된 속성을 가질 수 있다.
클래스와 객체가 가지는 관계는 아래 그림과 같다.
Car라는 Class 범위 안에 각각 Ford, Toyota, Volkswagen이라는 객체들이 존재하게 되는 것이다.
이때 객체를 class의 instance라고 할 수 있다.
사실 이러한 개념은 너무나 유명해서 찾아보면 금방 나온다. 그래서 이걸 어떻게 실제 코드에 적용할 수 있을까!?
바로 직접 위의 예시에 적용해보자.
OOP의 4대 원칙
우선, 여기에서는 대략적인 느낌만 잡아 보고 아래에서 코드를 보며 위의 원칙들이 어떻게 구현되는지 알아보자.
Class를 이용
위에서 만든 함수를 클래스를 이용해 재설계 해보자.
아예 새로운 Gym이라는 클래스를 만들고 운동하는 함수를 해당 클래스의 메소드로 구현해보자!
아! 이번에는 Gym에서 운동을 하는 회원 한 명, 한 명을 객체로 만들 것이기 때문에 Muscle이라는 타입을 GymMember로 바꿔주자.
type GymMember = {
muscles: number;
fatigability: number;
};
class Gym {
muscles = 0;
fatigability: number;
deltaPerHour: number;
constructor(fatigability: number, deltaPerHour: number) {
this.fatigability = fatigability;
this.deltaPerHour = deltaPerHour;
}
get currentState(): GymMember {
return { muscles: this.muscles, fatigability: this.fatigability };
}
workout(hours: number): GymMember {
if (this.fatigability >= 100) {
console.log('Too Tired to Workout! Need to Rest!');
}
this.muscles += hours * this.deltaPerHour;
this.fatigability += hours * this.deltaPerHour;
return {
muscles: this.muscles,
fatigability: this.fatigability,
};
}
}
const mark = new Gym(30, 20);
const gadot = new Gym(80, 5);
console.log(mark.currentState);
mark.workout(2);
console.log(mark.currentState);
참고로, 헬스장에 오기전 모든 사람들의 muscle은 0이라고 고려했다. ( 여기에서라도 공평해지자 )
실행 결과를 살펴보자
단 2시간만 운동을 했는데, 근육량이 40이나 증가했다!
Encapsulation(캡슐화)
캡슐화란,
Class안의 Obejct가 그 속성을 내부에 private하게 관리하고, 밖에 노출하기로 결정한 속성만을 밖으로 노출하는 것을 의미한다.
이 원칙은 class의 속성에 private, public, protected와 같은 syntax를 사용하여 달성할 수 있다. ( 참고링크 )
위의 코드는 충분히 괜찮아 보인다. 하지만, OOP의 캡슐화 원칙을 사용하면 훨씬 더 멋진 코드를 작성 할 수 있다.
type GymMember = {
muscles: number;
fatigability: number;
};
class Gym {
//멤버 변수의 접근을 제한
private muscles = 0;
constructor(private fatigability: number, private deltaPerHour: number) {}
get currentState(): GymMember {
return { muscles: this.muscles, fatigability: this.fatigability };
}
workout(hours: number): GymMember {
if (this.fatigability >= 100) {
console.log('Too Tired to Workout! Need to Rest!');
}
this.muscles += hours * this.deltaPerHour;
this.fatigability += hours * this.deltaPerHour;
return {
muscles: this.muscles,
fatigability: this.fatigability,
};
}
}
const mark = new Gym(30, 20);
const gadot = new Gym(80, 5);
console.log(mark.currentState); //{ muscles: 0, fatigability: 30 }
mark.workout(2);
console.log(mark.currentState); //{ muscles: 40, fatigability: 70 }
이전 코드에서 우리는 기존의 멤버변수 muscles, fatigability, deltaPerHour에 누가 언제 접근이 가능한지에 대해서는 신경을 쓰지 않았다. 하지만, 위의 코드에서는 private 접근지정자를 사용하여 각 변수의 접근을 제한해 주었다.
왜 이러한 과정이 필요할까?
사실 mark라는 instance를 만든 시점 부터, mark는 deltaPerHour, fatigability, muscles에 직접 접근 할 필요가 없다.
하지만, 캡슐화를 진행하지 않는다면 아래처럼 직접 접근 및 수정이 가능하다. 이는 OOP의 원칙에 어긋나게 될 것이다.
더불어 복잡한 클래스에는 정말 수많은 멤버 변수들이 존재하게 되는데 모든 속성에 접근이 가능하다면, 사용자는 해당 클래스를 어떻게 이용해야 할지 혼란스럽게 된다. (이 개념은 Abstraction과도 연결이 되므로 아래에서 좀 더 자세히 살펴보자.)
Abstraction(추상화)
추상화란,
복잡성을 줄이기 위해 중요한 정보를 숨기는 것을 의미한다. 이를 통해 user가 selected methods 혹은 속성과만 interact 할 수 있도록 한다. 추상화는 유저에게 불필요한 과정을 숨김으로써 복잡성을 줄일 수 있다.
이러한 추상화 원칙은 private 혹은 interface를 통해 달성 할 수 있다.
위의 코드에서는 단순히 workout 메소드로 모든 운동을 단순하게 얘기했지만, 사실 운동은 그렇게 단순하지 않다.
workout 함수를 아래와 같이 수정해 봤다.
backWorkOut() {
console.log('working out back');
}
chestWorkOut() {
console.log('working out chest');
}
legsWorkOut() {
console.log('working out legs');
}
workout(hours: number): GymMember {
if (this.fatigability >= 100) {
console.log('Too Tired to Workout! Need to Rest!');
}
this.backWorkOut();
this.chestWorkOut();
this.legsWorkOut();
this.muscles += hours * this.deltaPerHour;
this.fatigability += hours * this.deltaPerHour;
return {
muscles: this.muscles,
fatigability: this.fatigability,
};
}
이제 해당 헬스장의 회원은 운동을 할 때 마다 트레이너가 짜준 스케쥴대로 등,가슴,하체를 모두 운동해야 한다. (근육량 0이었던 점을 생각하면 1분할이 그렇게 나쁜 선택은 아닌것 같다. )
그런데 이렇게 하면 user가 어떤 운동을 해야 할지 선택권이 생기게 된다. 아래 사진을 참조해 보자.
근육량이 0이었던 사람이 자신이 등 운동을 해야 할지, 가슴 운동을 해야 할 지, 다리 운동을 해야 할지 선택을 하면 안된다.
트레이너가 시키는 대로 그냥 운동(workout)을 하면 되는 것이다. 즉, 유저에게 복잡한 디테일을 알려줘서 복잡성을 증가시킬 필요가 없다.
위에서 말한 것 처럼 abstraction을 달성하는 방법은 private을 이용하는 방법과 interface를 이용하는 방법이 있다.
아래에서 좀 더 살펴 보자.
1. private
이 방법은 매우 간단하다. 보여주고 싶지 않은 메소드나 속성을 아래처럼 private syntax를 이용하여 숨겨 주면된다.
private backWorkOut() {
console.log('working out back');
}
private chestWorkOut() {
console.log('working out chest');
}
private legsWorkOut() {
console.log('working out legs');
}
그렇게 되면, 운동은 정상적으로 하면서도 복잡성은 덜어 낼 수 있다.
2. interface
interface는 계약서와 같은 역할을 한다. 즉, interface에 적혀져 있는 속성이나 메소드는 반드시 implements한 class에 구현이 되야 한다.
자, 위의 코드를 아래처럼 수정해보자. 참고로 위에서 붙였던 private syntax를 모두 제거하였다.
interface GymNewbie {
workout(hours: number): GymMember;
currentState: GymMember;
}
class Gym implements GymNewbie {
//...
}
const mark: GymNewbie = new Gym(30, 20);
const gadot = new Gym(80, 5);
console.log(mark.currentState);
mark.workout(2);
console.log(mark.currentState);
Gym이라는 클래스는 GymNewbie라는 interface를 구현(실행)하는 클래스라는 것을 의미한다.
이제, mark는 GymNewbie라는 interface를 이용하여 생성자함수를 실행 시켰고, gadot은 interface없이 실행 시켰다.
따라서 mark는 GymNewbie interface에 정의된 메소드 / 속성만 접근이 가능한 것에 반해
이에 반해, interface없이 생성한 gadot은 모든 메소드및 속성에 접근이 가능한 것을 볼 수 있다.
참고로 여러개의 interface를 하나의 클래스에 implements할 수 도 있다.
interface GymNewbie {
workout(hours: number): GymMember;
currentState: GymMember;
}
interface GymPro {
workout(hours: number): GymMember;
backWorkOut(): void;
chestWorkOut(): void;
legsWorkOut(): void;
}
class Gym implements GymNewbie, GymPro {
//...
}
Inheritance(상속)
상속이란,
말 그대로 하나의 entity가 다른 entity의 속성/메소드 들을 상속 받는 것을 의미한다.
좀 더 정확하게는 부모 클래스는 자기가 가지고 있는 속성이나 메소드를 자식 클래스에게 상속(extends)해줄 수 있다.
이러한 방식을 통해 클래스의 재사용성을 매우 높일 수 있게 된다.
지금까지 우리가 작성한 코드에서는 등 운동, 가슴 운동, 하체 운동만 가능했다.
런닝머신까지 있어서 유산소 운동까지 할 수 있는 luxury한 Gym클래스를 만들려면 어떻게 해야 할까?
기존에 작성한 코드를 다시 한번 더 작성해야 한다면 너무 막막할 것이다. 이럴때 기존에 만들어 두었던 Gym클래스를 상속함으로써 코드의 재사용성을 비약적으로 높일 수 있다.
아래 코드를 살펴보자.
class LuxuryGym extends Gym {
constructor(fatigability: number, deltaPerHour: number) {
super(fatigability, deltaPerHour);
}
run(mins: number) {
console.log(`running for ${mins}minutes`);
this.fatigability += this.deltaPerHour;
this.muscles += this.deltaPerHour;
}
workout(hours: number): GymMember {
super.workout(hours);
this.run(hours * 20);
return {
muscles: this.muscles,
fatigability: this.fatigability,
};
}
}
const mark = new LuxuryGym(20, 10);
console.log('===========================');
console.log(mark.currentState);
mark.workout(2);
console.log(mark.currentState);
자, 우리의 새로운 Luxury Gym이 열렸다.
LuxuryGym은 Gym Class를 상속받기 때문에, Gym 클래스의 생성자 함수가 인자로 받았던, fatigability와 deltaPerHour를 역시나 입력받아 super함수를 통해 전달해주자.
그리고 Gym클래스에는 없었던 run이라는 메소드를 추가해주자.
workout메소드에서, 우리는 기존에 GymClass에서 구현해 놓았던 함수를 그대로 쓰되, 무산소 운동이 끝난 후에 유산소 운동을 추가로 해주고 싶다.
이럴때 super를 통해 부모 클래스가 가지고 있던 메소드를 실행시켜 주고, 자식 클래스의 메소드를 실행시켜주자.
모든 무산소를 끝내고 유산소로 마무리 하는 모습을 볼 수 있다.
Polymorphism(다형성)
다형성이란,
하나의 클래스는 여러개의 모습을 가질 수 있다. 즉, 하나의 객체가 여러종류의 타입을 가질 수 있는 것을 의미한다.
overriding 이나 overloading을 통해, 같은 메소드도 다른 방식으로 실행이 가능해진다.
조금전 위에서 만든 Luxury Gym을 이용해서 어떻게 다형성이 구현되는지 직접 예시를 통해 알아보자.
const gymMembers: Gym[] = [
new Gym(80, 5),
new LuxuryGym(20, 10),
new Gym(70, 5),
new LuxuryGym(26, 13),
new Gym(60, 5),
new LuxuryGym(10, 12),
new Gym(30, 25),
new LuxuryGym(30, 11),
];
gymMembers.forEach((member) => {
console.log('===========================');
member.workout(2);
});
마무리
자, 여기까지 OOP가 가지고 있는 중요한 원칙에 대해 알아봤고,
실제로 어떤 식으로 코드를 작성해야 해당 원칙들을 지키면서 OOP를 할 수 있는지에 대해 알아봤다.
사실, composition에 대해서 더 알아볼 예정인데, 이 부분은 다음 글에서 소개하도록 하겠다.
'TypeScript' 카테고리의 다른 글
7. TypeScript - Generics! (0) | 2022.04.19 |
---|---|
6. TypeScript - composition (0) | 2022.04.14 |
4. TypeScript - Types (0) | 2022.04.11 |
3. TypeScript - 기본타입 (0) | 2022.04.11 |
2. TypeScript - 실행 (0) | 2022.04.11 |