본문 바로가기

내일배움캠프_개발일지/Node.js 기초

Node.js 심화 - 4

__________________________________________________

 

1-11  객체 지향 프로그래밍 (Object-Oriented Programming)

 

=>  < 3.3객체지향 02 >

3.3 객체지향 Goal : 객체지향이란 무엇인지 이해하고 SOLID 원칙을 이용하여 코드를 작성할 수 있다.

 

 

객체 지향 프로그래밍, 즉 OOP에 대하여.

 

 

 

 

 

__________________________

 

— 프로그래밍 패러다임 —

=> 프로그래밍 패러다임은 무엇을 해야 할지를 말하기보다는 무엇을 해서는 안 되는지를 말해준다.

 

 

프로그래밍 패러다임에는 대표적으로 3가지가 존재한다.

 

1.

구조적 프로그래밍 (Structured Programming) == 절차적 프로그래밍.

=> 

제어 흐름직접적인 전환에 대한 규칙을 제시.

기능을 중심적으로 개발을 진행.

프로그래밍이라는 기술이 시작되면서 가장 처음으로 적용된 패러다임

 

 

2.

객체 지향 프로그래밍 (Object-Oriented Programming, OOP)

=>

제어흐름간접적인 전환에 대한 규칙을 제시

프로그램의 처리단위가 객체인 프로그래밍 방법

현실 세계를 모델링”하는 대표적인 프로그래밍 패러다임

 

 

3.

함수형 프로그래밍 (Functional Programming)

=> 

할당문에 대한 규칙을 제시

함수를 중심적으로 개발을 진행

3가지의 패러다임 중 가장 처음 만들어졌지만 최근들어 겨우 도입되기 시작하는 패러다임

 

 

 

 

 

__________________________

 

— 객체 지향 프로그래밍 (Object-Oriented Programming, OOP) —

 

=> 데이터프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식을 

객체지향 프로그래밍(Object-Oriented Programming)이라 부른다.

 

여러가지 모듈을 하나하나 합쳐서 큰 하나의 프로젝트를 만든다.

 

 

1.

객체 지향 프로그래밍코드를 추상화해 직관적으로 사고할 수 있기 때문에, 대표적인 프로그래밍 방법론으로 적용되고 있어.

 

2.

자동차, 동물, 사람 등과 같은 현실 세계의 객체를 유연하게 표현할 수 있어.

 

3.

어떠한 특성을 가지고 있으며 특정 기능을 수행할 수 있어.

 

4.

자동차는 객체이고 출발, 정지, 운행 및 제동과 같은 기능을 수행할 수 있어.

 

 

 

 

__________________________

 

— 왜 객체 지향 프로그래밍을 해야 하는가 —

 

1.

API 를 만들 때마다 복붙으로 갯수를 늘려나간다?

만약 수정해야 할 일이 생긴다면? 전부 다 하나 하나씩 수정해야 겠지.

 

2.

프로그래밍을 하면서 효율적으로 시간을 관리할 수 있어야 해.

따라서 우리는 코드를 깔끔하고 가독성 좋게 짜야 할 뿐만 아니라, 나중에 유지 보수를 해야 할 때에도 쉽게 할 수 있도록 해야 해.

 

3.

발생한 문제 상황을 빠르게 인지하고, 어떤 코드에서 오류가 발생했는지 빠르게 찾아보며, 

오류 사항을 빠르게 고쳐 개발에 사용하는 시간을 최대한으로 줄이는것목표로 삼아야

 

4.

객체지향 프로그래밍데이터와 프로세스를 하나의 단위로 처리하는 특성을 가지고 있기 때문에 

코드를 수정해야할 때 어떤 코드에서 문제가 발생했는지 개발자들이 직관적으로 인지할 수 있으며,

여러곳에 분산된 모든 코드를 수정해야하는 것이 아닌 해당 로직을 수행하는 코드만 수정 하더라도 문제가 해결될 수 있어.

 

 

 

 

__________________________

 

— 객체지향 프로그래밍의 장점 —

 

1.

체지향 프로그래밍의존성을 효율적으로 통제할 수 있는 다양한 방법을 제공함으로써 

요구사항 변경에 좀 더 수월하게 대응할 수 있는 가능성을 높여준다.

 

2.

동작을 기준으로 프로그래밍을 진행하는 것보다 데이터를 중심으로 프로그래밍을 하게되면 

코드의 덩치가 커지더라도 일관성을 유지하기 좋아.

 

3.

객체지향 코드는 자신의 문제를 스스로 처리해야 한다는 우리의 예상을 만족시켜주기 때문에 이해하기 쉽고, 

객체 내부의 변경이 객체 외부에 파급되지 않도록 제어할 수 있기 때문에 변경하기 수월하다.

 

4.

데이터프로세스하나의 단위로 통합해 놓는 방식으로 표현하기도

 

5.

데이터와 데이터를 사용하는 프로세스동일한 객체 안에 위치한다면 객체지향 프로그래밍 방식을 따르고 있을 확률이 높다.

 

 

 

 

__________________________

 

— 객체 지향 설계 —

 

1.

좋은 설계요구하는 기능온전히 수행하면서 추후의 변경을 매끄럽게 수용할 수 있는 설계

 

2.

변경 가능한 코드이해하기 쉬운 코드

 

3.

변경하기 쉬운 설계는 한 번에 하나의 클래스만 변경할 수 있는 설계

 

4.

훌륭한 객체지향 설계의 핵심은 캡슐화를 이용해 의존성을 적절히 관리함으로써 객체 사이의 결합도를 낮추는 것.

 

 

 

 

 

 

 

 

 

 

 

__________________________________________________

 

1-12  객체 지향 설계 5원칙 (SOLID)

 

=>  < 3.3객체지향 03 >

3.3 객체지향 Goal : 객체지향이란 무엇인지 이해하고 SOLID 원칙을 이용하여 코드를 작성할 수 있다.

 

 

객체 지향 프로그래밍, 즉 OOP 를 설계하기 위한 5원칙에 대해서.

 

 

객체 지향 프로그래밍 설계의 다섯가지 기본원칙을 SOLID라는 것으로 불리고 있어.

SOLID는 프로그래머가 시간이 지나도 유지 보수확장이 쉬운 시스템을 만들고자 할 때 사용하는 원칙들의 집합.

 

 

SOLID의 종류

  • 단일 책임의 원칙 (Single Responsibility Principle, SRP)
  • 개방-폐쇄 원칙 (Open-Closed Principle, OCP)
  • 리스코프 치환 원칙 (Liskov substitution principle, LSP)
  • 인터페이스 분리 원칙 (Interface segregation principle, ISP)
  • 의존성 역전 원칙 (Dependency Inversion Principle, DIP)

 

 

 

 

 

 

1.

< 단일 책임의 원칙 (Single Responsibility Principle, SRP) >

 

하나의 객체는 단 하나의 책임을 가져야 한다.

=> 즉, 클래스나 모듈을 변경할 이유가 단 하나 뿐이어야 한다는 원칙.

 

SRP는 책임이라는 개념을 정의하며 적절한 클래스의 크기를 제시한다.

 

SRP는 객체 지향설계에서 중요한 개념이고 이해하고 따르기 쉬운 개념이지만, 프로그래머가 가장 무시하는 규칙. 

일반적인 프로그래머는 “깨끗하고 우아하게 작성된 소프트웨어"보다 “동작하기만 하는 소프트웨어"에 초점을 맞추기 때문.

=> 왜? 당장은 동작하면 되니까…..

 

 

— 예시 —

——

/** SRP After **/

class UserAuth {

  constructor(user) { // UserAuth 클래스 생성자

    this.user = user;

  }

 

  verifyCredentials() { // 사용자의 인증을 검증하는 메소드

    //...

  }

}

 

class UserSettings {

  constructor(user) { // UserSettings 클래스 생성자

    this.userAuth = new UserAuth(user); // UserAuth를 새로운 객체로 정의한다.

  }

 

  changeSettings (userSettings) { // 사용자의 설정을 변경하는 메소드

    if (this.userAuth.verifyCredentials()) { // 생성자에서 선언한 userAuth 객체의 메소드를 사용한다.

      //...

    }

  }

}

——

=> 

사용자의 설정변경하는 책임을 가진 UserSettings 클래스.

사용자의 인증검증하는 책임을 가진 UserAuth 클래스.

각각의 클래스는 1개의 책임 만을 가진다.

 

 

 

 

 

 

2.

< 개방-폐쇄 원칙 (Open-Closed Principle, OCP) >

 

소프트웨어 엔티티 또는 개체(클래스, 모듈, 함수 등)는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

=>

소프트웨어 개체의 행위는 확장될 수 있어야 하지만, 개체를 변경해서는 안된다.

기존 코드에 영향을 주지않고 소프트웨어에 새로운 기능이나 구성 요소를 추가할 수 있어야 한다는 것.

 

요구 사항을 확장하는데에 있어서 소프트웨어 수정이 많이 걸리나면, 결국 개발 코스트가 증가하는 것. 이를 억제하고자.

 

 

< 예제 >

——

/** OCP **/

 

function calculator(nums, callBackFunc) {

  let result = 0;

  for (const num of nums) {

    result = callBackFunc(result, num); 

  }

  return result;

}

 

const add = (a, b) => a + b; // 함수 변수를 정의합니다.

const sub = (a, b) => a - b;

const mul = (a, b) => a * b;

const div = (a, b) => a / b;

console.log(calculator([2, 3, 5], add)); // add 함수 변수를 Callback 함수로 전달합니다.

console.log(calculator([5, 2, 1], sub)); // sub 함수 변수를 Callback 함수로 전달합니다.

——

=> 계산기에 어떠한 기능을 추가 하더라도 calculator함수 내부의 코드를 수정하지 않을 수 있게 되었어.

*** reduce 마냥 result 가 축적되기 때문에 0으로 초기화가 안되는거야.

 

 

 

 

3.

< 리스코프 치환 원칙 (Liskov substitution principle, LSP) >

 

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

=> 

만약 부모 클래스자식 클래스가 있는 경우 서로를 바꾸더라도 해당 프로그램에서 잘못된 결과를 도출하지 않는 것.

 

 

정사각형(Sqaure)직사각형(Rectagle) 문제를 이용해서 LSP를 적용해보도록 하자.

 

 

< 예제 >

——

/** LSP After **/

class Shape { // Rectangle과 Square의 부모 클래스를 정의합니다.

  getArea() { // getArea는 빈 메소드로 정의

  }

}

 

class Rectangle extends Shape { // Rectangle은 Shape를 상속받습니다.

  constructor(width = 0, height = 0) { // 직사각형의 생성자

    super();

    this.width = width;

    this.height = height;

  }

 

  getArea() { // 직사각형의 높이와 너비의 결과값을 조회하는 메소드

    return this.width * this.height;

  }

}

 

class Square extends Shape { // Square는 Shape를 상속받습니다.

  constructor(length = 0) { // 정사각형의 생성자

    super();

    this.length = length; // 정사각형은 너비와 높이가 같이 깨문에 width와 height 대신 length를 사용합니다.

  }

 

  getArea() { // 정사각형의 높이와 너비의 결과값을 조회하는 메소드

    return this.length * this.length;

  }

}

 

const rectangleArea = new Rectangle(7, 7) // 49

  .getArea(); // 7 * 7 = 49

const squareArea = new Square(7) // 49

  .getArea(); // 7 * 7 = 49

——

=> 

Rectangle클래스와 Square클래스에서 상위 타입의 getArea 메소드를 호출하더라도 문제없이 원하는 결과값을 도출할 수 있어.

 

 

 

 

 

4.

< 인터페이스 분리 원칙 (Interface segregation principle, ISP) >

 

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

=>

즉, 클라이언트가 필요하지 않는 기능을 가진 인터페이스에 의존해서는 안 되고, 최대한 인터페이스를 작게 유지해야 한다.

사용자가 필요하지 않은 것들에 의존하지 않도록, 인터페이스를 작게 유지해야 한다는 것.

 

< 예시 >

——

/** ISP Before **/

interface SmartPrinter { // SmartPrinter가 사용할 수 있는 기능들을 정의한 인터페이스 

  print();

 

  fax();

 

  scan();

}

——

=> 위와 같은 인터페이스를 상속받는 클래스가 있다고 치자.

해당 클래스는 bring, fax, scan 이라는 3가지의 기능들을 모두 다 사용할 수 있어. 여기 까지는 문제가 없어.

헌데 새로운 클래스가 생겼어. 문제가 발생해. 왜? 해당 클래스는 print 와 scan 기능만 수행하는 클래스거든.

 

즉 인터페이스가 너무 큰 나머지, 여러 개의 클래스들이 이 인터페이스를 상속받을 때 상속받는 클래스 중 몇 개는

인터페이스의 3가지 기능들을 모두 다 수행하지는 않을 것이란 이야기지.

 

 

 

SmartPrinter 인터페이스에 정의된 기능들을 Printer, Fax, Scanner 인터페이스로 분리하여 

ISP 원칙에서 “클라이언트가 필요하지 않는 기능을 가진 인터페이스에 의존해서는 안 되고, 최대한 인터페이스를 작게 유지해야한다.”에 

해당하는 원칙을 수행하는 코드로 개선을 해보자.

 

 

< 예시 >

——

/** ISP After **/

interface Printer { // print 기능을 하는 Printer 인터페이스

  print();

}

 

interface Fax { // fax 기능을 하는 Fax 인터페이스

  fax();

}

 

interface Scanner { // scan 기능을 하는 Scanner 인터페이스

  scan();

}

 

 

// AllInOnePrinter클래스는 print, fax, scan 기능을 지원하는 Printer, Fax, Scanner 인터페이스를 상속받았다.

class AllInOnePrinter implements Printer, Fax, Scanner {

  print() { // Printer 인터페이스를 상속받아 print 기능을 지원한다.

    // ...

  }

 

  fax() { // Fax 인터페이스를 상속받아 fax 기능을 지원한다.

    // ...

  }

 

  scan() { // Scanner 인터페이스를 상속받아 scan 기능을 지원한다.

    // ...

  }

}

 

// EconomicPrinter클래스는 print 기능을 지원하는 Printer 인터페이스를 상속받았다.

class EconomicPrinter implements Printer {

  print() { // EconomicPrinter 클래스는 print 기능만 지원한다.

    // ...

  }

}

 

// FacsimilePrinter클래스는 print, fax 기능을 지원하는 Printer, Fax 인터페이스를 상속받았다.

class FacsimilePrinter implements Printer, Fax {

  print() { // FacsimilePrinter 클래스는 print, fax 기능을 지원한다.

    // ...

  }

 

  fax() { // FacsimilePrinter 클래스는 print, fax 기능을 지원한다.

    // ...

  }

}

——

=> 인터페이스를 기능별로, 단위별로 잘게 쪼개었어.

그리고 각각의 자식 클래스들 마다 자신에게만 필요한 인터페이스들을 상속 받고 있지.

이렇게 되면 확장성이 좋아져.

 

필요없는 인터페이스를 분리하여 ISP원칙을 수행하는 코드를 구현할 수 있게 되었어.

결국 불 필요한 짐을 실은 인터페이스에 의존하게 된다면 예상치도 못한 문제에 빠질 수 있다는 것! 그것을 조심해야 해!

 

 

 

 

 

 

5.

< 의존성 역전 원칙 (Dependency Inversion Principle, DIP) >

 

프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.

, 높은 계층의 모듈(도메인)이 저수준의 모듈(하부구조)에 의존해서는 안된다.

=>

 

만약 추상화를 하지 않고 고수준 계층의 모듈저수준 계층의 모듈의존하고 있다면?

사소한 코드 변경에도 고수준 계층의 코드를 변경해야할 것이고, 소모되는 개발 코스트또한 엄청나게 증가할 것.

 

 

< 예시 >

——

/** DIP After **/

const readFile = require('fs').readFile;

 

class Formatter { // 인터페이스지만, Javascript로 구현하기 위해 클래스로 선언합니다.

  parse() {  }

}

 

class XmlFormatter extends Formatter {

  parse(content) {

    // Xml 파일을 String 형식으로 변환합니다.

  }

}

 

class JsonFormatter extends Formatter {

  parse(content) {

    // JSON 파일을 String 형식으로 변환합니다.

  }

}

 

class ReportReader {

  constructor(formatter) { // 생성자에서 Formatter 인터페이스를 상속받은 XmlFormatter, JsonFormatter를 전달받습니다.

    this.formatter = formatter;

  }

 

  async read(path) {

    const text = await readFile(path, (err, data) => data);

    return this.formatter.parse(text); // 추상화된 formatter로 데이터를 파싱합니다.

  }

}

 

const reader = new ReportReader(new XmlFormatter());

const report = await reader.read('report.xml');

// or

// const reader = new ReportReader(new JsonFormatter());

// const report = await reader.read('report.json’);

——

 

=> 

XmlFormatter 클래스와 JsonFormatter 클래스는 동일한 인터페이스 Formatter 라는 클래스를 가진다.

(자바스크립트에서는 그냥 클래스를 상속)

ReportReader 는 생성자 함수의 매개변수로 두 클래스 중 하나를 받고, 

this.formatter.parse 라는 간단한 실행구문 하나로 내가 원하는 형태로 데이터스를 파싱한다.

=> 즉 클래스의 인스턴스가 생성되며 또 다른 클래스의 인스턴스를 속성으로 받아.

 

어느 쪽의 파싱 클래스이던 똑같은 부모 메소드  parse() {  } 를 사용한다. 단, 내부 구현은 각 클래스 마다 다르다.

 

const reader = new ReportReader(new XmlFormatter());

const report = await reader.read('report.xml');

 

이렇게 내가 원하는 파싱 방식을 바깥에서 인스턴스를 만들 때 정해준다. 

XmlFormatter 클래스와 JsonFormatter 클래스는 동일한 메소드를 상속받았기에,

ReportReader 클래스 내부에서는 원하는 파싱 타입의 클래스 인스턴스를 인자로 받아 

this.formatter.parse(text) 이렇게 호출만 해주면 된다.

 

즉 해당 파싱이 어느 쪽의 파싱일 지는 바깥에서 const reader = new ReportReader(new XmlFormatter()); 로 정해준다.

마치 +, - 기능만 있던 클래스를 모든 사칙연산이 가능하도록 바꿔주고 해당 사칙연산이 어떤 연산인지를 바깥에서 정해준 것 처럼.

다형성의 이야기와도 같고, square 클래스와도 같은 이야기.

'내일배움캠프_개발일지 > Node.js 기초' 카테고리의 다른 글

Node.js 심화 - 6  (0) 2023.01.02
Node.js 심화 - 5  (0) 2023.01.02
Node.js 심화 - 3  (0) 2022.12.29
Node.js 심화 - 2  (1) 2022.12.27
Node.js 심화-1  (0) 2022.12.26