객체 지향 ( Object - Oriented )
객체 ( Object )는 현실 세계의 물체나 개념을 소프트웨어 세계로 옮긴 것이다.
예를 들면, '자동차'나 '사람'처럼 생각하면 된다. 여기서, 객체는 여러 속성과 행동(메서드)으로 구성된다.
- 객체는 정보(데이터)와 그 정보를 처리하는 행동(함수 또는 메서드)을 가지고 있다.
- 객체들은 서로 메서드 호출을 통해 메세지를 주고 받아 협력한다.
객체 지향은 소프트웨어 개발에서 주요 구성 요소를 기능(Function)이 아닌 객체(Object)로 삼으며, "어떤 객체가 어떤일을 할 것인가"에 초점을 맞춘다. 즉, 객체를 도출하고 각각의 역할을 명확하게 정의하는 것에 초점을 맞추는 방법론이다.
- 객체 지향은 책임과 권한을 가진 객체들이 서로 메시지를 주고받아 협력하여 필요한 기능을 수행하는 방법론이다.
- 이 방법은 크고 복잡한 시스템도 효과적으로 분해하고 구성하며, 효율적으로 관리할 수 있게 도와준다.
객체 지향 프로그래밍 ( Object - Oriented Programming, OOP )
프로그래밍 패러다임
- 프로그래밍 패러다임( Programming Paradigm )은 프로그래밍의 방식이나 관점을 바탕으로 효율적이고 명확한 코드를 작성하는 방법을 나타낸다.
프로그래밍에서는 가장 대표적인 세 가지의 프로그래밍 패러다임이 존재한다.
- 구조적 프로그래밍 ( Structured Programming )
- 객체 지향 프로그래밍 ( Object-Oriented Programming, OOP )
- 함수형 프로그래밍 ( Functional Programming )
구조적 프로그래밍은 기능 중심적인 개발을 진행한다.
- 구조적 프로그래밍은 프로그래밍이라는 기술이 시작되면서 가장 처음으로 적용된 패러다임이다.
객체 지향 프로그래밍은 프로그램의 처리단위가 객체인 프로그래밍 방법이다.
- 객체 지향 프로그래밍은 '현실 세계를 프로그램으로 모델링'하는 가장 대표적인 프로그래밍 패러다임이다.
함수 프로그래밍은 함수를 중심적으로 개발을 진행한다.
- 함수형 프로그래밍은 세가지의 패러다임 중 가장 초기에 만들어졌으나, 최근들어 주목받기 시작한 패러다임이다.
객체 지향 프로그래밍 ( Object - Oriented Programming )이란 상태 ( 데이터 )와 그 데이터를 조작하는 프로세스 ( 메서드 )가 같은 모듈 내부에 배치되는 프로그래밍 방식을 의미한다.
- 객체 지향 프로그래밍은 코드를 추상화해 개발자가 더욱 직관적으로 사고할 수 있게 하는 대표적인 프로그래밍 방법론이다.
- 객제 치향 프로그래밍에서는 자동차, 동물, 사람 등과 같은 현실 세계의 객체를 유연하게 표현할 수 있다.
- 객체는 고유한 특성을 가지고 있고, 특정 기능을 수행할 수 있다.
객체 지향 프로그래밍을 사용해야하는 이유
객체 지향 프로그래밍( OOP )은 프로그램을 객체들의 집합으로 볼 수 있는 설계 원칙을 제공한다.
이 원칙에 따라, 각 객체는 특정 데이터와 그 데이터를 처리하는 함수 ( 메서드 )를 함께 갖게 된다.
객체 지향 프로그래밍의 방식은 데이터와 기능이 밀접하게 연결되어 있기 때문에, 코드의 구조와 동작을 직관적으로 파악할 수 있다. 예를 들어, '자동차'라는 객체가 있다고 가정할 때, 이 객체는 '색상', '속도'와 같은 데이터와 '출발', '정지'와 같은 기능( 메서드 )을 가지게 된다. 따라서, 만약 문제가 발생한다면 '자동차'라는 객체의 내부만 살펴보면 된다.
또한, 객체 지향의 특성으로 하나의 객체에 정의된 기능이나 데이터 구조는 다른 객체에서도 쉽게 재사용할 수 있다.
이로 인해 코드의 재사용성과 확장성이 향상되고, 결과적으로 개발 시간을 효율적으로 관리할 수 있게 된다.
객체 지향 프로그래밍의 핵심 원칙
1. 캡슐화 ( Encapsulation )
객체 내부의 세부적인 사항을 감추는 것, 즉 중요한 정보를 외부로 노출시키지 않도록 만드는 것을 캡슐화 ( Encapsulation )라고 한다.
Javascript는 완벽한 캡슐화를 지원하지 않는다. 개발자들은 변수 앞에 언더바 ( _ )를 붙여 내부의 변수를 숨긴것 처럼 나타내는 규칙을 따른다.
완벽한 캡슐화를 위해, TypeScript로 확인해 보자.
/** 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에서는 제공하는 문법이다.
따라서, User 클래스의 name, age 멤버 변수는 클래스 외부에서 어떠한 방법으로도 직접 접근을 할 수 없다. 오로지 setter만 변수를 변경할 수 있고, getter만 변수를 조회할 수 있게 되었다.
getter는 변수의 값을 가져오는 ( getName, getAge )를 나타내고, setter는 변수의 값을 설정하는 ( setName, sestAge )를 나타낸다. 이를 통해 User 클래스의 중요한 정보를 외부로 노출시키지 않도록 만드는 캡슐화( Encapsulation )를 지키는 코드를 작성했다.
2. 상속 ( Inheritance )
상속 ( Inheritance )은 하나의 클래스가 가진 특징 ( 함수, 변수 및 데이터 )을 다른 클래스가 그대로 물려 받는 것을 말한다.
이미 정의된 상위 클래스의 특징을 하위 클래스에서 물려받아 코도의 중복을 제거하고 코드 재사용성을 증대시킨다.
- 개별 클래스를 상속 관계로 묶음으로써 클래스 간의 체계화된 구조를 쉽게 파악할 수 있게 된다.
- 상위 클래스의 데이터와 메서드를 변경함으로써 전체 코드에 대한 일관성을 유지할 수 있다.
상속은 기존에 작성된 클래스를 재활용해 사용할 수 있다.
상속을 구현하기 위한 예제를 확인해보자.
/** 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() 메소드를 호출할 수 있게 되었다.
이처럼, 상속의 이러한 특성 덕분에 코드를 재사용하기 수월해지고, 중복을 줄일 수 있게되는 장점이 있다.
상속을 활용해 부모 클래스의 코드를 수정하면 자식 클래스도 해당 변경을 반영할 수 있게 되었다. 이를 통해 클래스 전체의 코드 일관성을 유지할 수 있게 된다.
3. 추상화 ( Abstraction )
객체에서 공통된 부분을 모아 상위 개념으로 새롭게 정의하는 것을 추상화 ( Abstraction )라고 한다. 즉, 불필요한 세부 사항을 생략하고, 중요한 특징만을 강조함으로써 코드를 더욱 간결하고 관리하기 쉽게 만드는 원칙이다.
- 추상화를 통해 객체들의 불필요한 특성을 제거함으로써, 공통적인 특성을 더욱 명확하게 파악할 수 있게 된다.
- 이를 통해 전체 시스템의 구조를 명확하게 이해하게 되고, 테스트를 더욱 쉽게 작성할 수 있게 된다.
클래스를 설계할 때, 공통적으로 묶일 수 있는 기능을 추상화 ( Abstraction ) ▶ 추상 클래스 ( Abstract Class ) ▶ 인터페이스 ( Interface ) 순으로 정리한다면, 여러 클래스 간의 일관성을 유지하면서, 다양한 형태로 확장될 수 있는 콛, 즉 다형성 ( Polymorphism )이 가능해진다.
▶ 여기서 인터페이스 ( Interface )란, 클래스 정의할 때 메소드와 속성만 정의해 인터페이스에 선언된 프로퍼티 또는 메소드의 구현을 강제해 코드의 일관성을 유지하게 한다.
/** 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 )은 하나의 객체( 클래스 )가 다양한 형태로 동작하는 것을 의미한다.
이는 객체가 가진 특성에 따라 같은 기능이 다르게 재구성되는 것을 의미한다.
즉, 동일한 메서드나 함수 명을 사용하더라도, 클래스마다 그 메서드가 다르게 동작하는 것이 다형성의 핵심이다.
다형성은 역할 ( 인터페이스 )과 구현을 분리하게 해준다. 따라서, 오버라이딩을 통해 특정 서비스의 기능을 유연하게
변경하거나 확장할 수 있게 한다.
▶ 오버로딩, 오버라이딩 자세한 정보
http://www.tcpschool.com/java/java_inheritance_overriding
/** 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 )의 특징이다.
'IT' 카테고리의 다른 글
[IT] 아키텍처 패턴 ( Architecture Pattern ) (1) | 2024.09.27 |
---|---|
[IT] 객체 지향 설계 5 원칙 ( SOLID ) (1) | 2024.09.27 |
[IT] Visual Studio Code - 디버그 (0) | 2024.09.19 |
[IT] 정규표현식 (1) | 2024.09.10 |
[IT] Access Token, Refresh Token (1) | 2024.09.09 |