JavaScript SOLID 예제

🪐 C:on·2022년 1월 27일
1

자바스크립트

목록 보기
3/4
post-custom-banner

🔎 SOLID

solid는 객체지향 프로그래밍의 설계 원칙을 다섯가지로 정의한 것이다. 유지보수와 확장에 유연한 프로그래밍을 하기위해서 SOLID원칙을 적용한다.


Srp : 단일책임원칙

클래스를 수정할 땐 수정할 이유가 2개 이상 생기면 잘못 설계된 것으로 본다.

2개 이상의 이유는 너무 많은 기능들을 한 클래스가 수행한다는 의미가 되기 때문이다.

잘못된 예

class cafeOwner {
  constructor(coffeebeans) {
    this.coffeebeans = coffeebeans;
  }
  
  manageShop(time){
  	console.log(`managing coffee shop at ${time}`)
  }

  makeCoffee(coffeebeans) {
    console.log(`making cofffe with ${coffeebeans}`)
  }

  serveCoffee(guest) {
    console.log(`serving coffee to ${guest}`)
  }
}

카페사장이 가게도 관리하고 커피도 만들고 서빙도하고 있다.

커피가 너무 쓴 것 같다는 컴플레인을 받아서 커피를 만들 때 원두양을 1/2로 줄이려한다.

따라서 makeCoffee() 메서드를 수정할것이다.
수정으로 인해 다른 곳에 영향을 끼치지는 않는 지 확인이 필요하다.

그런데 원두의 양을 변경하는 것과 전혀 관련이 없는 manageShop()메서드까지 확인을 해주려니 너무 비효율적이라는 생각이 든다.

올바른 예

class coffemaker {
  makeCoffee(coffeebeans) {
    console.log(`making cofffe with ${coffeebeans}`)
  }
}

class coffeeServer{
  serveCoffee(guest) {
    console.log(`serving coffee to ${guest}`)
  }
}

class cafeOwner {
  coffee;
  constructor(coffeebeans) {
    this.coffeebeans = coffeebeans;
  }

  manageShop(time){
  	console.log(`managing coffee shop at ${time}`)
  }
}

이렇게 책임을 나누면 클래스를 체크해야 하는 범위가 줄어들어 관리가 쉬워질 것이다.


Ocp : 개방 폐쇠 원칙

확장에는 개방적이며, 수정에는 폐쇠적이어야 한다는 원칙이다.

  • 기능 추가가 필요할 때 기존 코드의 수정이 일어나지 않도록 한다.
  • 내부 매커니즘이 변경되어도 외부에는 코드변화가 없어야 한다.

이 두가지를 만족시켜야 하는 원칙이다.

잘못된 예

class LatteMaker {
  coffee = "Latte";
}

class TeaMaker {
  coffee = "Blacktea"
}

class cafeOwner {
  constructor(maker, server) {
    this.maker = maker
  }

  makeCoffee(){
    if(this.maker.coffee==="Latte"){
      brewingLatte()
    }else if(this.maker.coffee==="Tea"){
      brewingBlacktea()
    }
  }
}

function brewingLatte(){
  console.log(`making coffee with Milk`)
}

function brewingBlacktea(){
  console.log(`making coffee without Milk`)
}

현재 카페사장은 라떼머신이 무슨 커피가 담겨있는지 확인한 뒤 라떼를 만들 수 있고, 홍차머신이 무슨 커피가 담겨있는지 확인한 뒤 홍차를 만들 수 있다.

분명 두 머신은 구분되어있는데 이렇게 일일히 확인하고 커피를 내리는 것은 정말 비효율적인 작업이다.
실제로 이렇게 커피를 내린다면 답답한 손님들은 더 이상 이 가게를 찾지 않을 것이다.

안에 들어있는게 무엇인지 확인할 필요없이 머신이 알아서 커피를 내려주도록 수정해보자.

올바른 예

class LatteMaker {
  coffee = "Latte";
  brewingCoffee(){
    console.log(`making coffee with Milk`)
  }
}

class BlackteaMaker {
  coffee = "Blacktea"
  brewingCoffee(){
    console.log(`making coffee without Milk`)
  }
}

class cafeOwner {
  constructor(maker, server) {
    this.maker = maker
  }

  makeCoffee(){
    this.maker.brewingCoffee()
  }
}

이제 카페사장은 필요한 커피 머신을 받아서 makeCoffee()버튼만 누르면 머신이 알아서 커피를 내릴 수 있게 되었다.


Liskov : 리스코프 치환 원칙

부모클래스와 자식클래스가 있다면 부모는 자식으로 교체되어도 프로그램이 정상 동작해야한다는 원칙이다.

올바르지 못한 복제는 오류를 야기한다.
따라서 상속을 할때는 리스코프 치환원칙을 지키도록 설계해야한다.

잘못된 예

class TeaMaker {
  coffee = ""

  makeCoffee(){
    this.coffee += "🍵"
  }

  serveCoffee(){
    console.log(`${this.coffee}서빙 완료`)
  }
}

class LatteMaker extends TeaMaker{}

const coffeeMaker = [new TeaMaker(), new TeaMaker(), new LatteMaker()]

coffeeMaker.forEach((maker)=>{
  try{
    maker.makeCoffee()
    maker.serveCoffee() 
  }catch(err){
    console.log(err)
  }
})

찻집 가게에는 teaMaker가 2대있었다.
teaMaker는 차를 내리고 서빙까지 해주는 첨단 로봇으로 전자공학부 출신인 가게 사장이 만들었다.

사장은 teaMaker를 복제하여 LatteMaker를 만들어 두고 알바한테 잘 사용하라고 편지를 남긴 뒤 휴가를 떠났다.

손님이 와서 녹차 2잔과 라떼 1잔을 주문했다.

알바는 녹차 2잔은 teaMaker에게, 라떼 1잔은 LatteMaker에게 차를 내린 후 서빙하라고 명령했다.

서빙이 완료되고 손님은 왜 녹차가 3잔이 왔냐며 알바를 구박했다.

사장이 복제를 잘못해서 발생한 실수이다.

LatteMaker는 teaMaker가 고장났을 때 대신 차를 내려주지 못한다. 즉, 치환되지 못한다.
그러므로 사장은 복제를 하면 안되는 것이었다.

올바른 예

class coffeeMaker {
  coffee = ""

  serveCoffee(){
    console.log(`${this.coffee}서빙 완료`)
  }
}

class TeaMaker extends coffeeMaker {
  makeCoffee(){
    this.coffee += "🍵"
  }
}

class LatteMaker extends coffeeMaker{
  makeCoffee(){
    this.coffee += "☕"
  }
}

복제를 하려했다면 위 코드와 같이 서빙기능을 수행하는 로봇을 만들어서 그것을 복제하여 다양한 커피머신을 만들었어야 했다.

TeaMaker, LatteMaker 모두 coffeeMaker가 하는 일들을 모두 수행할 수 있다. 따라서 Lsp원칙을 만족하므로 복제를 해도 괜찮은 것이다.


자바스크립트는 인터페이스 기능이 없다.
ISPDIP는 이해를 위해 자바스크립트와 유사한 문법으로 인터페이스를 구현할 수 있는 타입스크립트를 사용하여 예시를 작성했다.


Isp : 인터페이스 분리 원칙

사용자는 자신이 이용하지 않는 메소드에 의존할 필요가 없어야 한다는 원칙이다.

큰 덩어리의 인터페이스들을 더 작은 단위로 분리시킴으로써 사용자는 꼭 필요한 메서드들만 이용할 수 있게 된다.

앞에서 말했듯 자바스크립트에서는 인터페이스기능이 없다. 인터페이스를 사용하려면 타입스크립트를 활용해야 한다.

잘못된 예

interface robot{
  brew() : void;
  serve() : void;
}

class oldCoffeeRobot implements robot {
  brew(){
    console.log('커피 내리는 중')
  }

  serve(){
    console.log('서빙 완료!')
  }
}

class newCoffeRobot implements robot {
  brew(){
    console.log('커피 내리는 중')
  }

  serve(){
    console.log('서빙 완료!')
  }

  clean(){
    console.log('자체 청소 완료')
  }
}

최신 커피로봇은 청소기능이 추가되었다.
하지만 사용자는 이 기능을 사용할 수 없으며 있는지도 알 수 없다.

오래된 커피로봇의 규격서가 최신 커피로봇에 동일하게 적용되어있기 때문이다.

올바른 예

interface robot{
  brew() : void;
  serve() : void;
}

interface cleanableRobot extends robot{
  clean() : void;
}

class oldCoffeeRobot implements robot {
  brew(){
    console.log('커피 내리는 중')
  }

  serve(){
    console.log('서빙 완료!')
  }
}

class newCoffeRobot implements cleanableRobot {
  brew(){
    console.log('커피 내리는 중')
  }

  serve(){
    console.log('서빙 완료!')
  }

  clean(){
    console.log('자체 청소 완료')
  }
}

이렇게 인터페이스를 쓰임새에 맞게 작게 만들어 사용자가 필요한 메소드만 확인할 수 있도록 만드는 것이 ISP원칙이다.


Dip : 의존성 역전 원칙

  1. 상위 모듈은 하위 모듈에 종속되지 않고 추상화에 의존해야 한다
  2. 세부사항 역시 추상화에 의해 달라져야 한다.

잘못된 예

interface robot{
  brew() : void;
  serve() : void;
}

class fastCoffeeRobot implements robot {
  brew(){
    console.log('더 빠르게 아메리카노 내리는 중')
  }

  serve(){
    console.log('서빙 완료!')
  }
}

class CafeOwner {
  constructor(){
    const coffeeRobot = new fastCoffeeRobot()
    coffeeRobot.brew()
    coffeeRobot.serve()
  }
}

new CafeOwner()

새로 고용한 카페사장은 항상 fastCoffeeRobot만 사용할 수 있다.

최근 빠른 장비보다 안전한 장비가 개발되면서 safeCoffeRobot이 개발이 되었다.

이에 따라 카페의 특성에 맞게 어떤 카페에는 safeCoffeRobot를 사용하는 사장을, 어떤 카페에는 fastCoffeeRobot를 사용하는 사장을 배치시키고 싶다.

올바른 예

interface robot{
  brew() : void;
  serve() : void;
}

class fastCoffeeRobot implements robot {
  brew(){
    console.log('더 빠르게 아메리카노 내리는 중')
  }

  serve(){
    console.log('서빙 완료!')
  }
}

class safeCoffeRobot implements robot {
  brew(){
    console.log('아메리카노 내리기')
  }

  serve(){
    console.log('안전하게 서빙 완료!')
  }
}

class CafeOwner {
  constructor(myRobot : robot){
    myRobot.brew()
    myRobot.serve()
  }
}

const machine1 = new fastCoffeeRobot()
const cafeOwner1 = new CafeOwner(machine1)

const machine2 = new safeCoffeRobot()
const cafeOwner2 = new CafeOwner(machine2)

카페 사장은 특정 기계를 가지는 것이 아닌 mechine이라는 규격만 맞는다면 그게 어떤 기계이든 사용할 수 있다. 따라서 fastCoffeeRobotsafeCoffeRobot 중 어떤 것을 전달해주더라도 오류가 발생하지 않는다.
이러한 방식을 DI(의존성주입)이라한다.

의존성주입을 사용하면 규격에 맞는 부품을 유연하게 갈아끼워 줄 수 있기 때문에 리팩토링을 하기 수월해진다.

post-custom-banner

0개의 댓글