1. 객체란?
- 객체란 현실 세계에서 실재하는 모든 "대상"을 "변수"(상태/속성)와 "함수"(행동)로 추상화 시킨 개념이다.
* 여기서 추상화는 대상의 본질적인 특징을 추출하여 표현한 것이라고 이해하고 있다.
2. OOP란?
- OOP란 Object Oriented Programming의 약자이다. 말 그대로, 객체 지향 프로그래밍이다.
- 즉, 하나의 소프트웨어가 동작하는 원리를 그것을 구성하는 여러 객체 간의 상호작용으로 정의하고, 이에 따라, 객체를 중심으로 소프트웨어를 설계/개발해야한다는 프로그래밍 패러다임인 것이다.
3. OOP의 4가지 특징
- 이러한 OOP는 크게 1) 추상화, 2) 상속, 3) 다형성, 4) 캡슐화라는 특징에 기반한 프로그래밍 패러다임이다.
- 요컨대, 위 4가지 특징에 기반하여 객체를 중심으로 설계된 프로그램은 개발 생산성이 뛰어날 것으로 기대하는 것이다.
- 각 특징을 하나씩 살펴보겠다.
1) 추상화
- 추상화란 위에서 언급한 것처럼 어떤 대상/집단의 공통적이고 본질적인 특징을 추출하여 정의한 것을 의미한다.
- 자연스레, OOP의 특징 중 하나인 추상화란 어떤 대상을 구현할 때, 그 대상의 본질적인 특징을 정의하고, 이것에 기반하여 대상을 객체로 구현하는 것을 의미한다.
- 이 떄, 대상의 본질적인 특징을 정의하는 데 프로그래밍적으로 활용되는 개념이 abstract class와 interface이다.
* abstract class와 interface의 차이는 다음 글에서 다룬다.
- 예시 코드는 다음과 같다. 스포츠팀을 추상화하여, 야구팀과 스포츠팀에 대한 class를 구현한다.
1. 스포츠 팀에 대한 추상화
- 팀에는 선수와 감독이 있다.
- 선수를 추가하고, 감독/선수를 조회할 수 있다.
abstract class SportsTeam {
protected manager: string;
protected players: string[] = [];
constructor(manager: string) {
this.manager = manager;
}
}
interface Managable {
getPlayers(): string[];
createPlayers(player: string): string;
getManager(): string;
}
interface SoccerIntroduction {
getTeamSubject(): "soccer";
}
interface BaseballIntroduction {
getTeamSubject(): "baseball";
}
2. 추상화를 기반으로 객체 생성자 구현
class BaseballTeam extends SportsTeam implements Managable, BaseballIntroduction {
constructor(manager: string) {
super(manager);
}
getPlayers() {
return this.players;
}
getManager(): string {
return this.manager;
}
createPlayers(player: string) {
this.players.push(player);
return "create player success";
}
getTeamSubject(): "baseball" {
return "baseball";
}
}
2. 추상화를 기반으로 객체 생성자 구현
class SoccerTeam extends SportsTeam implements Managable, SoccerIntroduction {
constructor(manager: string) {
super(manager);
}
getPlayers() {
return this.players;
}
getManager(): string {
return this.manager;
}
createPlayers(player: string) {
this.players.push(player);
return "create player success";
}
getTeamSubject(): "soccer" {
return "soccer";
}
}
2) 상속
- OOP의 주요 특징인 상속은 대상을 객체로 추상화/구현할 때, 기존에 구현한 클래스를 재활용하여 구현할 수 있는 것을 의미한다. (이 때, 재활용한 클래스는 상위 클래스 / 기존 클래스를 재활용하여 구현한 클래스는 하위 클래스가 된다.)
- 구체적으로, 대상을 추상화하여 객체로 구현할 때, 대상의 특징을 일부분 구현한 객체가 있을 경우, 이를 확장하여 대상을 추상화/구현할 수 있다는 것이다.
- 이러한 상속을 통해, 코드 재사용성을 높일 수 있다.
- 예시 코드는 다음과 같다. 위와 동일하게 스포츠 팀과 야구 팀의 예시다.
1. 스포츠 팀에 대해 구현한 SportsTeam class
class SportsTeam {
protected manager: string;
protected players: string[] = [];
constructor(manager: string) {
this.manager = manager;
}
}
2. 스포츠 팀을 상속하여 구현한 BaseballTeam class
class BaseballTeam extends SportsTeam {
constructor(manager: string) {
super(manager);
}
getPlayers() {
return this.players;
}
getManager(): string {
return this.manager;
}
createPlayers(player: string) {
this.players.push(player);
return "create player success";
}
}
3) 다형성
- OOP의 특징으로서 다형성이란 어떤 객체의 속성이나 기능이 상황에 따라 여러 형태로 변할 수 있다는 것을 의미한다.
- OOP에서 다형성을 구현하는 예시로는 상속/구현 상황에서 메서드 오버라이딩/오버로딩이 있다.
- 다형성을 통해 개발 유연성, 코드 재사용성을 제고시킬 수 있다.
- 다형성이 구현된 구조에서는 상위 객체의 타입으로 하위 객체를 참조할 수 있기 때문이다.
- 다음은 예시 코드다.
1) 스포츠 팀을 상속하여 구현된 야구팀과 축구팀에서 메소드 오버라이딩을 이용해 getTeamIntroduction()를 변형시켰다.
interface Managable {
getPlayers(): string[];
createPlayers(player: string): void;
getManager(): string;
}
1. 스포츠 팀 추상화
abstract class SportsTeam implements Managable {
protected manager: string;
protected players: string[] = [];
protected name: string;
protected abstract subject: string;
constructor(name: string, manager: string) {
this.manager = manager;
this.name = name;
}
createPlayers(player: string) {
this.players.push(player);
}
abstract getPlayers(): string[];
abstract getManager(): string;
abstract getTeamIntroduction(): string;
}
2. 추상화를 기반으로 야구팀 객체 생성자 구현
class BaseballTeam extends SportsTeam {
subject: string = "baseball";
constructor(name: string, manager: string) {
super(name, manager);
}
getPlayers() {
return this.players;
}
getManager(): string {
return this.manager;
}
createPlayers(player: string) {
this.players.push(player);
return "create player success";
}
3. 메소드 오버라이딩으로 팀 소개 조회 메소드 구현
getTeamIntroduction(): string {
return `${this.name} play ${this.subject} we need pitcher`;
}
}
2. 추상화를 기반으로 축구팀 객체 생성자 구현
class SoccerTeam extends SportsTeam implements Managable {
subject = "soccer";
constructor(name: string, manager: string) {
super(name, manager);
}
getPlayers() {
return this.players;
}
getManager(): string {
return this.manager;
}
createPlayers(player: string) {
this.players.push(player);
return "create player success";
}
3. 메소드 오버라이등으로 팀 소개 조회 메소드 구현
getTeamIntroduction(): string {
return `${this.name} play ${this.subject}. Join the best soccer team`;
}
}
2) 선수 객체에서 객체의 다형성을 이용해, 간결한 코드로 선수가 속한 팀과 그 팀의 종목을 조회하는 method를 구현했다.
class Player {
private name: string;
private clubList: SportsTeam[] = [];
constructor(name: string) {
this.name = name;
}
- 스포츠 팀의 다형성을 이용해 getTeamIntroduction()을 간결하게 구현.
playWhichSubject(SportsTeam: SportsTeam) {
return SportsTeam.getTeamIntroduction();
}
- 스포츠 팀의 다형성을 이용해 joinSprotsTeam()을 간결하게 구현.
joinSportsTeam(SportsTeam: SportsTeam) {
this.clubList.push(SportsTeam);
SportsTeam.createPlayers(this.name);
}
getBelongedTeams() {
return this.clubList;
}
}
const eagles = new BaseballTeam("eagles", "yu hyeonjun");
const liverpool = new SoccerTeam("soccer", "klopp");
const user = new Player("yu");
ealges, liverpool은 SportsTeam을 상속받았기 때문에, 코드 재사용성이 높아진다.
user.joinSportsTeam(eagles);
user.joinSportsTeam(liverpool);
const clubList = user.getBelongedTeams();
for (let i = 0; i < clubList.length; i++) {
console.log("소속 팀과 종목 조회", clubList[i].getTeamIntroduction());
// 소속 팀과 종목 조회 eagles play baseball | 소속 팀과 종목 조회 liverpool play soccer
}
- 만약 BaseballTeam, SoccerTeam이 SportsTeam을 상속받지 않았다면, 다형성을 활용할 수 없어, SportsTeam이 아니라 BaseballTeam, SoccerTeam를 매개변수로 받는 각각의 메소드가 필요했을 것이다.
- SportsTeam의 다형성을 이용해 코드를 재사용한 사례라고 할 수 있겠다.
4) 캡슐화
- 캡슐화란 클래스 내의 연관된 속성(property)이나 함수(method)를 하나의 캡슐로 묶어 외부로부터 클래스로의 접근을 최소화하는 것을 의미한다.
- 캡슐화는 다음과 같이 두 가지 장점이 있다.
- 1) 외부로부터 클래스의 변수, 함수를 보호
- 2) 외부에는 필요한 요소만 노출하고, 내부의 상세한 동작은 은닉
- 클래스의 캡슐화는 public, static, private, protected와 같은 접근제한자를 통해 구현할 수 있다.
- public: 하위 클래스와 인스턴스에서 접근 가능.
- static: 클래스와 외부에서 접근 가능. 인스턴스에서는 접근 불가
- private: 클래스 본인만 접근 가능, 하위 클래스, 인스턴스 접근 불가
- protected: 클래스 본인, 하위 클래스에서 접근 가능, 인스턴스 접근 불가
- 캡슐화는 객체 간의 결합도를 감소시키고, 응집도를 강화하는데 기여합니다. 즉, 유지보수의 용이성이 좋아지는 효과를 기대할 수 있다.
- 객체 간 결합도: 객체 간에 의존하는 정도를 의미합니다.
- 객체의 응집도: 한 객체의 자율성, 특정 역할에 대한 독립적인 책임을 의미합니다.
- 예시 코드는 다음과 같습니다. 아래 예시를 통해 1) 캡슐화가 어떻게 응집도 개선에 기여하는지, 2) 응집도 개선이 유지보수의 용이성에 기여하는지를 확인할 수 있다.
1) 캡슐화 전
class Pharmacist {
private medicine1 = "for covid";
private medicine2 = "no pill";
private medicine3 = "for new covid";
greetToClinet() {
return `약국입니다. 어서오세요.`;
}
examineClient(condition: string) {
if (condition === "very hard") {
return "new covid";
} else if (condition === "hard") {
return "covid";
} else {
return "no covid";
}
}
makePill(disease: string) {
if (disease === "new covid") {
return this.medicine3;
} else if (disease === "covid") {
return this.medicine1;
} else {
return this.medicine2;
}
}
givePillToClient(pill: string) {
if (pill === this.medicine1) {
return `${this.medicine1} 복용법입니다.`;
} else if (pill === this.medicine2) {
return `${this.medicine2}. 쉬세요`;
} else {
return `${this.medicine3} 복용법입니다.`;
}
}
}
class Patient {
private name: string;
private condition: string;
disease = "?";
constructor(name: string, condition: string) {
this.name = name;
this.condition = condition;
}
goToPharmacy() {
const pharmacy = new Pharmacist();
pharmacy.greetToClinet();
this.disease = pharmacy.examineClient(this.condition);
const pill = pharmacy.makePill(this.disease);
return pharmacy.givePillToClient(pill);
}
}
const patient = new Patient("yu", "hard");
console.log(patient.goToPharmacy()); // for covid 복용법입니다.
- Pharmacist class의 method들이 모두 public으로 설정되어 있다.
- 그래서 Patient class의 goToPharmacy() 메소드에서 생성된 Pharmacist 인스턴스에서 Pharmacist의 모든 속성과 기능에 접근할 수 있는 것을 확인할 수 있다.
- 만약, Pharmacist의 속성과 기능에 변경사항이 생기면, goToPharmacy도 모두 변경해줘야하는 번거로움이 남아있다.
2) 캡슐화 후
class Pharmacist2 {
private medicine1 = "for covid";
private medicine2 = "no pill";
private medicine3 = "for new covid";
private greetToClinet() {
return `약국입니다. 어서오세요.`;
}
private examineClient(condition: string) {
if (condition === "very hard") {
return "new covid";
} else if (condition === "hard") {
return "covid";
} else {
return "no covid";
}
}
private makePill(disease: string) {
if (disease === "new covid") {
return this.medicine3;
} else if (disease === "covid") {
return this.medicine1;
} else {
return this.medicine2;
}
}
private givePillToClient(pill: string) {
if (pill === this.medicine1) {
return `${this.medicine1} 복용법입니다.`;
} else if (pill === this.medicine2) {
return `${this.medicine2}. 쉬세요`;
} else {
return `${this.medicine3} 복용법입니다.`;
}
}
operate(condition: string) {
this.greetToClinet();
return this.givePillToClient(this.makePill(this.examineClient(condition)));
}
}
class Patient2 {
private name: string;
private condition: string;
disease = "?";
constructor(name: string, condition: string) {
this.name = name;
this.condition = condition;
}
goToPharmacy() {
const pharmacy = new Pharmacist2();
return pharmacy.operate(this.condition);
}
}
const patient2 = new Patient("yu", "hard");
console.log(patient.goToPharmacy()); // for covid 복용법입니다.
- public이었던 Pharmacist의 메소드들을 모두 private으로 변경했다. 그리고 이들을 모두 실행하는 public 메소드 operate를 새로 생성했다.
- Patient에서는 Pharmacist의 opreate 메소드만 접근 가능하다. 하지만, opreate에서 Pharmacist에서 접근해야하는 기능들은 모두 접근할 수 있다.
- 즉, Pharmacist 측면에서는 Patient에 operate 메소드만 노출시키고, 그 외 메소드들은 감춘 것이다. 즉, 데이터 은닉과 데이터 보호를 실현한 것이다.
- 아울러, 코드적으로도 Pharmacist의 변경사항이 생기면, 해당 Class 변경해주면 된다. 유지보수의 용이성이 개선된 것이다.
4. 요약
- OOP는 객체 지향 프로그래밍의 줄임말이다.
- 객체란 현실 세계에서 실재하는 모든 대상을 추상화하여 변수와 함수로 구성한 프로그래밍적 개념이다.
- OOP는 4가지 특징을 가지고 있고, 이 특징들로 말미암아, OOP를 통해 개발 생산성을 제고할 수 있다.
- 추상화: 대상의 본질적인 특성을 추출하여 객체로 표현하는 것.
- 상속: 기존에 있는 클래스를 활용하여 객체를 추상화 및 구현, 코드 재사용성 증가
- 다형성: 메서드 오버라이딩 및 오버로딩으로 상황에 따라 여러 기능 구현 / 하위 클래스는 상위 클래스 타입으로 참조 가능 / 코드 재사용성 증가
- 캡슐화: 외부로부터 데이터 보호, 은닉 / 유지보수의 용이성 증가
5. 참고자료
'디자인패턴, 프로그래밍 패러다임, 아키텍처 > OOP' 카테고리의 다른 글
SOLID 원칙 (0) | 2023.03.06 |
---|