[TIL] 객체 지향 설계 5원칙 23.07.11

이상훈·2023년 7월 13일
0

[내일배움캠프]

목록 보기
44/68

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

  • 하나의 객체는 단 하나의 책임을 가져야 한다
  • 클래스나 모듈을 변경하는 이유는 단 하나여야 함
  • 이를 지키지 않으면 한 책임의 변경에 의해 다른 책임과 관련된 코드에 영향을 미칠 수 있음

    응집도는 높고 결합도는 낮은 프로그램을 설계하는 것이 객체지향 설계의 핵심이다.
    새로운 요구사항이나 프로그램 변경에 의해 클래스 내부의 동작들이 연쇄적으로 변경되어야 할 수도 있다. 이는 유지보수가 비효율적이므로, 책임을 잘게 쪼개어 분리시킬 필요가 있다.

SRP 적용 전

class UserSettings {
  constructor(user) { // UserSettings 클래스 생성자
    this.user = user;
  }

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

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

SRP 적용 후

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 객체의 메소드를 사용
      //...
    }
  }
}

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

  • 소프트웨어 엔티티 또는 개체(클래스, 모듈, 함수 등)는 확장에는 열려 있으나 변경에는 닫혀 있어야 함
    -> 소프트웨어 개체의 행위는 확장될 수 있어야 하지만, 개체를 변경해서는 안된다. 기존 코드에 영향을 주지 않고 소프트웨어에 새로운 기능이나 구성 요소를 추가할 수 있어야 함
  • 이를 지키지 않으면 instanceof와 같은 연산자를 사용하거나, 다운 캐스팅 발생

자주 변화하는 부분을 추상화함으로써 기존 코드를 수정하지 않고도 기능을 확장할 수 있도록 함으로써 유연함을 높이는 것이 핵심이다.

OCP 적용 전

function calculator(nums, option) {
  let result = 0;
  for (const num of nums) {
    if (option === "add") result += num; // option이 add일 경우 덧셈 연산
    else if (option === "sub") result -= num; // option이 sub일 경우 뺄셈 연산
    // 새로운 연산(기능)을 추가 하기 위해서는 함수 내부에서 코드 수정이 필요
  }
  return result;
}

console.log(calculator([2, 3, 5], "add")); // 10
console.log(calculator([5, 2, 1], "sub")); // -8

OCP 적용 후

function calculator(nums, callBackFunc) {
  // option을 CallbackFunc로 변경
  let result = 0;
  for (const num of nums) {
    result = callBackFunc(result, num); // option으로 분기하지 않고, Callback함수를 실행하도록 변경
  }
  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 함수로 전달

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

  • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 함
  • 자식 클래스는 언제나 자신의 부모 클래스를 대체할 수 있음
    -> 부모 클래스가 들어갈 자리에 자식 클래스를 넣어도 계획대로 잘 작동해야 함
  • 상속관계에서는 일반화 관계 (IS-A)가 성립해야 한다는 의미
    -> 상속관계가 아닌 클래스를 상속관계로 설정하면, 이 원칙이 위배됨

리스코프 치환 원칙을 지키지 않으면 OCP 원칙을 위반하게 되는 것. 따라서 상속 관계를 잘 정의하여 LSP 원칙이 위배되지 않도록 설계

LSP 적용 전

class Rectangle {
  constructor(width = 0, height = 0) { // 직사각형의 생성자
    this.width = width;
    this.height = height;
  }

  setWidth(width) { // 직사각형은 높이와 너비를 독립적으로 정의
    this.width = width;
    return this;
  }

  setHeight(height) { // 직사각형은 높이와 너비를 독립적으로 정의
    this.height = height;
    return this;
  }

  getArea() { // 사각형의 높이와 너비의 결과값을 조회하는 메소드
    return this.width * this.height;
  }
}

class Square extends Rectangle { // 정사각형은 직사각형을 상속받음
  setWidth(width) { // 정사각형은 높이와 너비가 동일하게 정의
    this.width = width;
    this.height = width;
    return this;
  }

  setHeight(height) { // 정사각형은 높이와 너비가 동일하게 정의
    this.width = height;
    this.height = height;
    return this;
  }
}

const rectangleArea = new Rectangle() // 35
  .setWidth(5) // 너비 5
  .setHeight(7) // 높이 7
  .getArea(); // 5 * 7 = 35
const squareArea = new Square() // 49
  .setWidth(5) // 너비 5
  .setHeight(7) // 높이를 7로 정의하였지만, 정사각형은 높이와 너비를 동일하게 정의
  .getArea(); // 7 * 7 = 49

LSP 적용 후

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

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

  • 클라이언트가 필요하지 않는 기능을 가진 인터페이스에 의존해서는 안 되고, 최대한 인터페이스를 작게 유지해야 함
  • 하나의 일반적인 인터페이스보다 여러개의 구체적인 인터페이스가 나음

각 클라이언트가 필요로 하는 인터페이스들을 분리함으로써, 클라이언트가 사용하지 않는 인터페이스에 변경이 발생하더라도 영향을 받지 않도록 함

ISP 적용 전

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

  fax();

  scan();
}

// SmartPrinter 인터페이스를 상속받은 AllInOnePrinter 클래스
class AllInOnePrinter implements SmartPrinter {
  print() { // AllInOnePrinter 클래스는 print, fax, scan 기능을 지원
    // ...
  }

  fax() { // AllInOnePrinter 클래스는 print, fax, scan 기능을 지원
    // ...
  }

  scan() { // AllInOnePrinter 클래스는 print, fax, scan 기능을 지원
    // ...
  }
}

// SmartPrinter 인터페이스를 상속받은 EconomicPrinter 클래스
class EconomicPrinter implements SmartPrinter {
  print() { // EconomicPrinter 클래스는 print 기능만 지원
    // ...
  }

  fax() { // EconomicPrinter 클래스는 fax 기능을 지원하지 않음
    throw new Error('팩스 기능을 지원하지 않습니다.');
  }

  scan() { // EconomicPrinter 클래스는 scan 기능을 지원하지 않음
    throw new Error('Scan 기능을 지원하지 않습니다.');
  }
}

ISP 적용 후

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 기능을 지원
    // ...
  }
}

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

  • 고수준 계층의 모듈(도메인)은 저수준 계층의 모듈(하부구조)에 의존해서는 안되며, 둘 다 추상화에 의존해야 함
  • 추상화는 세부사항에 의존해서는 안되며, 세부 사항은 추상화에 의존해야 함
  • 저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적

구체적인 클래스보다 인터페이스나 추상클래스에 의존해야 함

DIP 적용 전

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

class XmlFormatter {
  parseXml(content) {
    // Xml 파일을 String 형식으로 변환
  }
}

class JsonFormatter {
  parseJson(content) {
    // JSON 파일을 String 형식으로 변환
  }
}

class ReportReader {

  async read(path) {
    const fileExtension = path.split('.').pop(); // 파일 확장자

    if (fileExtension === 'xml') {
      const formatter = new XmlFormatter(); // xml 파일 확장자일 경우 XmlFormatter를 사용

      const text = await readFile(path, (err, data) => data);
      return formatter.parseXml(text); // xmlFormatter클래스로 파싱을 할 때 parseXml 메소드를 사용

    } else if (fileExtension === 'json') {
      const formatter = new JsonFormatter(); // json 파일 확장자일 경우 JsonFormatter를 사용

      const text = await readFile(path, (err, data) => data);
      return formatter.parseJson(text); // JsonFormatter클래스로 파싱을 할 때 parseJson 메소드를 사용
    }
  }
}

const reader = new ReportReader();
const report = await reader.read('report.xml');
// or
// const report = await reader.read('report.json');

DIP 적용 후

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');

참고블로그 : https://velog.io/@haero_kim
참고자료 : Sparta

profile
코린이

0개의 댓글