01. 객체 지향 (Object-Oriented)
- 1) 좋은 설계란 무엇일까요?
💡 **좋은 설계**란 요구하는 기능을 **정확하게 수행**하면서, 추후의 **변경을 매끄럽게 수용**할 수 있는 설계입니다.
코드의 변경이 필요할 때는 해당 코드가 **이해하기 쉬워야** 변경이 간편해집니다. 따라서, **변경하기 쉬운 코드**는 그 자체로도 **이해하기 쉬운 코드**를 의미합니다.
저희는 이러한 코드를 지향하며 코드를 작성하게 됩니다. 이를 위해 다양한 **프로그래밍 패러다임**과 **아키텍처 패턴**을 프로젝트에 도입하여, **체계적이고 깔끔한 코드 구조**를 구현할 수 있게 되는 것이죠. 😉
- 2) 객체 (Object) 란?
💡 **객체(Object)**는 현실 세계의 물체나 개념을 소프트웨어 세계로 옮긴 것입니다. 예를 들면, ‘**자동차**’나 ‘**사람**’처럼 생각하면 됩니다. 여기서, 객체는 여러 **속성**과 **행동[메서드]**으로 구성됩니다.
- 객체는 **정보[데이터]**와 그 정보를 처리하는 **행동[함수 또는 메서드]**을 가지고 있습니다.
- 객체들은 서로 메서드 호출을 통해 메시지를 주고 받아 협력합니다.
- 여러 객체가 함께 협력한다면, 보다 어려운 작업도 수월하게 수행할 수 있겠죠?
→ 레고 블록을 하나씩 조립해서 **큰 레고 구조물**을 만드는 것이라고 생각해보세요. 비슷한 개념이랍니다. ☺️
- 3) 객체 지향 (Object-Oriented)이란?
💡 **객체 지향**은 소프트웨어 개발에서 주요 구성 요소를 **기능(Function)**이 아닌 **객체(Object)**로 삼으며 “**어떤 객체가 어떤 일을 할 것인가**”에 초점을 맞춥니다.
즉, **객체를 도출**하고 각각의 **역할을 명확하게 정의**하는 것에 **초점**을 맞추는 방법론입니다.
- **객체 지향**은 **책임**과 **권한**을 가진 **객체**들이 서로 메시지를 주고받아 **협력**하여 필요한 **기능을 수행**하는 방법론입니다.
- 이 방법은 크고 복잡한 시스템도 **효과적으로 분해**하고 **구성**하며, 개발자 **손쉽게 이해**하고 **효율적으로 관리**할 수 있게 도와줍니다.
- 4) 객체 지향적인 소프트웨어는 어떻게 구분할 수 있을까요?
💡 **절차 지향**과 **객체 지향** 소프트웨어를 구분하는 방법은 아래의 기준을 만족할 경우 **객체 지향**, 만족하지 않으면 **절차 지향**적인 성격을 가집니다.
1. **캡슐화**, **다형성**, **상속**을 지원하는가?
→ 여기서 **클래스(Class)**는 **객체를 생성**할 때 사용하는 틀이며, `new` 키워드를 통해 **객체를 생성**할 수 있습니다. 😉
2. **데이터 접근 제한(Access modifier)**이 가능한가?
**→ 데이터 접근 제한**은 **특정 데이터에 대한 외부 접근을 제한**하여, 데이터의 안정성과 보안을 높여줍니다.
👉 **캡슐화, 다형성, 상속**등의 개념은 다음 섹션에서 더 자세히 살펴보도록 하겠습니다! 😊
02. 객체 지향 프로그래밍 (Object-Oriented Programming, OOP)
- 1) 프로그래밍 패러다임
💡 **프로그래밍 패러다임(Programming Paradigm)**은 프로그래밍의 **방식**이나 **관점**을 바탕으로 **효율적이고 명확한 코드를 작성하는 방법**을 나타냅니다.
📌 프로그래밍의 세계에서는 가장 대표적인 세 가지의 **프로그래밍 패러다임**이 존재합니다.
1. **구조적 프로그래밍 (Structured Programming)**
2. **객체 지향 프로그래밍 (Object-Oriented Programming, OOP)**
3. **함수형 프로그래밍 (Functional Programming)**
**구조적 프로그래밍**은 **기능** 중심적인 개발을 진행합니다.
- **구조적 프로그래밍**은 프로그래밍이라는 기술이 시작되면서 **가장 처음으로 적용**된 패러다임입니다.
**객체 지향 프로그래밍**은 프로그램의 처리단위가 **객체**인 프로그래밍 방법입니다.
- **객체 지향 프로그래밍**은 “**현실 세계를 프로그램으로 모델링**”하는 가장 대표적인 프로그래밍 패러다임입니다.
**함수형 프로그래밍**은 **함수**를 중심적으로 개발을 진행합니다.
- **함수형 프로그래밍**은 세가지의 패러다임 중 가장 초기에 만들어졌지만, 최근들어 주목받기 시작한 패러다임 입니다.
💪 저희는 이러한 프로그래밍 패러다임 중 가장 많은 사랑을 받고 있는 **객체 지향 프로그래밍 (Object-Oriented Programming, OOP)**을 배울 예정입니다!
- 2) 객체 지향 프로그래밍 (Object-Oriented Programming, OOP)
객체 지향 프로그래밍(OOP), 출처: https://www.orientsoftware.com
💡 **객체지향 프로그래밍(Object-Oriented Programming)**이란 **상태(데이터)**와 그 데이터를 조작하는 **프로세스(메서드)**가 같은 모듈 **내부**에 배치되는 프로그래밍 방식을 의미합니다.
- **객체 지향 프로그래밍**은 **코드를 추상화**하여 개발자가 더욱 직관적으로 사고할 수 있게 하는 **대표적인 프로그래밍 방법론**으로 적용되고 있습니다.
- **객체 지향 프로그래밍**에서는 자동차, 동물, 사람 등과 같은 **현실 세계의 객체**를 유연하게 **표현**할 수 있습니다.
- **객체**는 고유한 **특성**을 가지고 있으며, 특정 **기능을 수행**할 수 있습니다.
- 예를 들어, 자동차라는 **객체**는 출발, 정지, 운행 및 제동과 같은 **기능**을 수행할 수 있습니다.
- 3) 무엇을 목표로 코드를 작성해야 할까요?
📌 코드는 **가독성**이 좋아야 하며, **재사용성**이 높고, **유지보수**가 쉬워야 합니다.
만약, API를 만들 때 마다 **복사 → 붙여넣기** 방식으로 **동일한 코드를 여러군데 분산** 시킨다면, 해당 로직을 **수정**해야할 때 복사한 **모든 코드를 일일히 찾아가며 수정**을 해야하는 상황이 발생합니다.
저희는 프로그래밍을 하면서 **효율적으로 시간을 관리**할 수 있어야합니다. 그러기 위해선 코드를 얼마나 깔끔하게 짜느냐도 중요하지만, **코드 변경점**이 발생하더라도 최대한 코드를 **적게 수정**하여 **더욱 많은 시간**을 만들 수 있어야합니다.
발생한 문제 상황을 **빠르게 인지**하고, 어떤 코드에서 **오류**가 발생했는지 **빠르게 찾아보며**, 오류 사항을 **빠르게 고쳐** 개발에 사용하는 **시간을 최대한으로 줄이는것**을 **목표**로 삼아야합니다.
- 4) 왜 객체 지향 프로그래밍을 사용해야할까요?
💡 **객체 지향 프로그래밍(OOP)**은 프로그램을 객체들의 집합으로 볼 수 있는 설계 원칙을 제공합니다. 이 원칙에 따라, 각 객체는 특정 **데이터**와 그 데이터를 처리하는 **함수(메서드)**를 함께 갖게 됩니다.
**객체 지향 프로그래밍**의 방식은 **데이터**와 **기능**이 밀접하게 연결되어 있기 때문에, 코드의 **구조와 동작을 직관적으로 파악**할 수 있습니다.
예를 들면, ‘**자동차**’라는 객체가 있다고 가정할 때, 이 객체는 ‘**색상**’, ‘**속도**’와 같은 **데이터**와 ‘**출발**’, ‘**정지**’와 같은 **기능(메서드)**을 가지게 됩니다. 따라서, 만약 문제가 발생한다면 ‘**자동차**’라는 **객체의 내부만 살펴보면 되는것** 입니다.
또한, 객체 지향의 특성으로 인해 하나의 객체에 정의된 기능이나 데이터 구조는 다른 객체에서도 쉽게 재사용할 수 있습니다. 이로 인해 코드의 **재사용성과 확장성이 향상**되고, 결과적으로 개발 시간을 효율적으로 관리할 수 있게 되는 것이죠. 🙂
03. 객체 지향 프로그래밍의 핵심 원칙
- 1) 캡슐화 (Encapsulation)
💡 **객체 내부의 세부적인 사항을 감추는 것**, 즉 중요한 정보를 **외부로 노출시키지 않도록** 만드는 것을 **캡슐화**(**Encapsulation**)라고 합니다.
**집의 현관문**을 생각해본다면, 저희는 집 안을 외부에게 노출시키지 않기 위해 현관문을 잠글 수 있습니다. 여기서 **현관문**은 저희가 **데이터에 접근할 수 있는 방법을 제한**하는 메서드와 같아요.
`Javascript` 는 완벽한 캡슐화를 지원하지 않습니다. 그러나, 개발자들은 변수 앞에 언더바(`_`)를 붙여 내부의 변수를 숨긴것 “**처럼**” 나타내는 [규칙](https://stackoverflow.com/questions/4484424/is-the-underscore-prefix-for-property-and-method-names-merely-a-convention)을 따르곤 합니다.
완벽한 캡슐화를 위해 이번 예제는 `TypeScript`로 확인해보도록 하겠습니다.
```tsx
/** Encapsulation **/
class User {
private name: string; // name 변수를 외부에서 접근을 할 수 없게 만듭니다.
private age: number; // age 변수를 외부에서 접근을 할 수 없게 만듭니다.
setName(name: string) { // Private 속성을 가진 name 변수의 값을 변경합니다.
this.name = name;
}
getName() { // Private 속성을 가진 name 변수의 값을 조회합니다.
return this.name;
}
setAge(age: number) { // Private 속성을 가진 age 변수의 값을 변경합니다.
this.age = age;
}
getAge() { // Private 속성을 가진 age 변수의 값을 조회합니다.
return this.age;
}
}
const user = new User(); // user 인스턴스 생성
user.setName('이용우');
user.setAge(30);
console.log(user.getName()); // 이용우
console.log(user.getAge()); // 30
console.log(user.name); // Error: User 클래스의 name 변수는 private로 설정되어 있어 바로 접근할 수 없습니다.
```
`User` 클래스를 선언하고 내부에는 `name`, `age` 멤버 변수를 초기화 하였습니다.
여기서는 특별하게 `private`라는 **접근 제한자(Access modifier)**를 사용하고 있는데요, **인스턴스 내부에서만 해당 변수에 접근이 가능**하도록 제한하는 문법 입니다.
기존 `Javascript`에서는 존재하지 않았지만 `Typescript`에서 제공하는 문법입니다.
**→ [접근 제한자에 대해 자세히 알고 싶다면 여기를 클릭하세요!](https://www.howdy-mj.me/typescript/access-modifiers/)**
따라서, `User` 클래스의 `name`, `age` 멤버 변수는 클래스 외부에서는 어떠한 방법으로도 **직접 접근**을 할 수 없습니다. 오로지 **setter**만 변수를 변경할 수 있고, **getter**만 변수를 조회할 수 있게 되었습니다.
→ **getter**는 변수의 값을 가져오는 (`getName`, `getAge`)를 나타내고, **setter**는 변수의 값을 설정하는 (`setName`, `setAge`)를 나타냅니다.
🔥 이로인해, 저희는 `User` 클래스의 **중요한 정보를 외부로 노출시키지 않도록** 만드는 **캡슐화**(**Encapsulation**)를 따르는 코드를 작성하게 되었습니다.
- 2) 상속 (Inheritance)
💡 **상속(Inheritance)**은 하나의 클래스가 가진 **특징(함수, 변수 및 데이터)**을 다른 클래스가 그대로 **물려 받는 것**을 말합니다.
이미 정의된 **상위** 클래스의 특징을 **하위** 클래스에서 물려받아 **코드의 중복을 제거**하고 **코드 재사용성을 증대**시킵니다.
- 개별 클래스를 **상속 관계**로 묶음으로써 클래스 간의 **체계화된 구조를 쉽게 파악**할 수 있게 됩니다.
- 상위 클래스의 데이터와 메서드를 변경함으로써 전체 코드에 대한 **일관성을 유지**할 수 있습니다.
**상속(Inheritance)**은 기존에 작성된 클래스를 재활용하여 사용할 수 있습니다.
그럼, 상속을 구현하기 위한 예제를 한번 확인해볼까요?
```jsx
/** Inheritance **/
class Mother { // Mother 부모 클래스
constructor(name, age, tech) { // 부모 클래스 생성자
this.name = name;
this.age = age;
this.tech = tech;
}
getTech(){ return this.tech; } // 부모 클래스 getTech 메서드
}
class Child extends Mother{ // Mother 클래스를 상속받은 Child 자식 클래스
constructor(name, age, tech) { // 자식 클래스 생성자
super(name, age, tech); // 부모 클래스의 생성자를 호출
}
}
const child = new Child("이용우", "28", "Node.js");
console.log(child.name); // 이용우
console.log(child.age); // 28
console.log(child.getTech()); // 부모 클래스의 getTech 메서드 호출: Node.js
```
`Mother` 부모 클래스를 상속받은 `Child` 자식 클래스에서 `name`, `age` 멤버 변수를 직접 접근하여 호출하고, `Mother` 부모 클래스에서 정의된 `getTech()` 메소드를 호출할 수 있게 되었습니다.
이처럼, 상속의 이러한 특성 덕분에 코드를 재사용하기 수월해지고, 중복을 줄일 수 있게되는 장점이 있는것이죠. 😉
🔥 **상속(Inheritance)**을 활용하여 부모 클래스의 코드를 수정하면 자식 클래스도 해당 변경을 반영할 수 있게 되었습니다. 이를 통해 클래스 전체의 코드 일관성을 유지할 수 있게 되었습니다!
- 3) 추상화 (Abstraction)
💡 객체에서 **공통된 부분을 모아 상위 개념으로 새롭게 정의하는 것**을 **추상화(Abstraction)**라고 합니다. 즉, **불필요한 세부 사항을 생략**하고, 중요한 특징만을 강조함으로써 코드를 더욱 **간결하고 관리하기 쉽게** 만드는 원칙입니다.
- **추상화**를 통해 객체들의 **불 필요한 특성을 제거**함으로써, **공통적인 특성을 더욱 명확하게 파악**할 수 있게 됩니다.
- 이를 통해 전체 시스템의 **구조를 명확하게 이해**하게 되고, 테스트를 더욱 쉽게 작성할 수 있게 됩니다.
클래스를 설계할 때, 공통적으로 묶일 수 있는 기능을 **추상화(Abstraction) → 추상 클래스(Abstract Class) → 인터페이스(Interface)** 순으로 정리한다면, 여러 클래스 간의 일관성을 유지하면서, 다양한 형태로 확장될 수 있는 코드, 즉 **다형성(Polymorphism)**이 가능해집니다.
→ 여기서 **인터페이스(`Interface`)**란, 클래스 정의할 때 **메소드**와 **속성**만 **정의**하여 인터페이스에 선언된 프로퍼티 또는 메소드의 **구현을 강제**하여 코드의 **일관성을 유지**하게 합니다.
```tsx
/** Abstraction **/
interface Human {
name: string;
setName(name);
getName();
}
// 인터페이스에서 상속받은 프로퍼티와 메소드는 구현하지 않을 경우 에러가 발생합니다.
class Employee implements Human {
constructor(public name: string) { }
// Human 인터페이스에서 상속받은 메소드
setName(name) { this.name = name; }
// Human 인터페이스에서 상속받은 메소드
getName() { return this.name; }
}
const employee = new Employee("");
employee.setName("이용우"); // Employee 클래스의 name을 변경하는 setter
console.log(employee.getName()); // Employee 클래스의 name을 조회하는 getter
```
`Employee` 클래스는 `Human` 인터페이스에서 정의한 `name` 프로퍼티와 `setName`, `getName` 메서드를 강제로 구현하게 되었습니다.
따라서, **동일한 인터페이스**인 `Human` 인터페이스를 구현하는 **모든 클래스**는 해당 인터페이스에 선언된 **프로퍼티**와 **메서드**를 구현해야 함을 보장하게 되었습니다. 이로 인해 **코드의 일관성을 유지**할 수 있게된 것이죠. 😉
- 4) 다형성 (Polymorphism)
💡 **다형성(Polymorphism)**은 하나의 **객체(클래스)**가 **다양한 형태로 동작**하는것을 의미합니다. 이는 객체가 가진 특성에 따라 **같은 기능이 다르게 재구성되는 것**을 의미합니다.
즉, **동일한 메서드나 함수 명**을 사용하더라도, 클래스마다 **그 메서드가 다르게 동작**하는 것이 다형성의 핵심입니다.
**다형성**은 **역할(인터페이스)**과 **구현**을 분리하게 해줍니다. 따라서, **오버라이딩**을 통해 특정 서비스의 기능을 **유연하게 변경하거나 확장**할 수 있게 합니다.
**Java**의 **오버로딩(Overloading)**, **오버라이딩(Overriding)**은 대표적인 다형성의 예시입니다.
→ [오버로딩, 오버라이딩에 대해 자세히 알고 싶다면 여기를 클릭하세요!](http://www.tcpschool.com/java/java_inheritance_overriding)
```jsx
/** Polymorphism **/
class Person {
constructor(name) { this.name = name; }
buy() {}
}
class Employee extends Person {
buy() { console.log(`${this.constructor.name} 클래스의 ${this.name}님이 물건을 구매하였습니다.`); }
}
class User extends Person {
buy() { console.log(`${this.constructor.name} 클래스의 ${this.name}님이 물건을 구매하였습니다.`); }
}
const employee1 = new Employee("이용우");
const employee2 = new Employee("김창환");
const user1 = new User("이태강");
const user2 = new User("김민수");
const personsArray = [employee1, employee2, user1, user2];
// personsArray에 저장되어 있는 Employee, User 인스턴스들의 buy 메소드를 호출합니다.
personsArray.forEach((person) => person.buy());
// Employee 클래스의 이용우님이 물건을 구매하였습니다.
// Employee 클래스의 김창환님이 물건을 구매하였습니다.
// User 클래스의 이태강님이 물건을 구매하였습니다.
// User 클래스의 김민수님이 물건을 구매하였습니다.
```
위의 `personsArray.forEach()` 예제에서 `person` 변수는 `**Person**` 클래스를 상속받은 `**Employee**` 또는 `**User**` 클래스의 인스턴스를 참조합니다.
여기서, 각 인스턴스의 `buy` **메서드를 호출하는 것은 동일**하지만, `**Employee**`와 `**User**` 클래스의 `buy` **메서드는 서로 다른 행위**를 수행하고 있는 것을 확인할 수 있습니다. 이것이 바로 **다형성(Polymorphism)**의 특징입니다! 😊