그 유명한 SOLID. 넌 누구냐?

YEONGHUN KO·2023년 12월 26일
0

CS

목록 보기
6/6
post-thumbnail
post-custom-banner

⚖️ 단일 책임 원칙 (Single Responsibility Principle)

같은 이유로 변경될 코드들은 모으고. 다른 이유로 변경될 코드들은 흩어라.

온갖 잡동사니를 다 섞지 마라.

같은 방향을 바라보는 것 끼리 묶어라. 훨씬 눈이, 특히 정신이 편해진다.

내 개인적인 생각으로는, SRP가 SOLID의 대 전제이자 가장 기본 바탕이 아닐까 싶다.

응집된 분리 가 핵심이다.

⚖️ 개방-폐쇄 원칙 (Open-Closed Principle)

모듈은 확장에 열려있고, 변경에는 닫혀있어야 한다.

유연해야한다. 후술할 DIP와 비슷하다고 할 수 있으려나.

함수나 클래스를 선언하는 쪽에서 자유롭게 인자를 통해 제어가능하도록 확장지향의 코드를 짜야한다.

// option을 받는게 아니라 fn을 받아보자.
// 이제 새로운 array를 만든다는 매커니즘은 닫혀있으나 방식에 대해서는 열려있다.
function map(array, fn) {
  const result = []
  for (let i = 0; i < array.length; i++) {
    result[i] = fn(array[i], i, array) // 내부 값을 외부로 전달하고 결과를 받아서 사용한다.
  }
  return result
}

// 얼마든지 새로운 기능을 만들어도 map코드에는 영향이 없다.
const getDoubledArray = (array) => map(array, (x) => x * 2)
const getTripledArray = (array) => map(array, (x) => x * 3)
const getHalfArray = (array) => map(array, (x) => x / 2)

⚖️ 리스코프 치환 원칙 (Liskov Substitution Principle)

어떤 인터페이스를 사용하는 프로그램은 그 인터페이스의 구현체를 위반한 하여, 동작이 오락가락하면 안된다.

"모든 사용자들(인터페이스를 사용하는 객체)은 이런 의미를 모두가 동일하게 인식하고 있어야 한다. 즉 의미에 동의해야 한다는 말입니다"

문제상황을 가정하자.

Animal이라는 부모 클래스의 인터페이스 제대로 숙지하지 못하여 주니어가 Fish라는 클래스를 만들어 예외처리 하였다고 하자.

//  구현부
abstract class Animal {
    void speak() {}
}

class Cat extends Animal {
    void speak() {
        System.out.println("냐옹");
    }
}

class Dog extends Animal {
    void speak() {
        System.out.println("멍멍");
    }
}

class Fish extends Animal {
    void speak() {
        try {
            throw new Exception("물고기는 말할 수 없음");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// 사용부

List<Animal> list = new ArrayList<>();
list.add(new Cat());
list.add(new Dog());
list.add(new Fish());

for(Animal a : list) {
    a.speak(); // Error!!!
}

다른 시니어 개발자: 아니 speak메소드는 Animal에서 에러 발생안한다고 해서 사용했는데 갑자기 왠 에러??

예외처리한다고 인터페이스를 무시한채 마음대로, 부모의 규칙을 위반하거나 오버라이딩하여 돌발행동을 하면 안됨!

무시한다면, 그 인터페이스를 따른 다른 개발자들에게 엄청난 혼란을 가져오게됨.

그리고 버그도 반드시 발생!

리스코프 치환 원칙은 1988년 바바라 리스코프(Barbara Liskov)가 올바른 상속 관계의 특징을 정의하기 위해 발표한 것으로, 서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다는 것을 뜻한다.

교체할 수 있다는 말은, 자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행이 보장되어야 한다는 의미이다.

즉, 부모 클래스의 인스턴스를 사용하는 위치에 자식 클래스의 인스턴스를 대신 사용했을 때 코드가 원래 의도대로 작동해야 한다는 의미이다.
이것을 부모 클래스와 자식 클래스 사이의 행위가 일관성이 있다고 말한다.

리스코프 치환 원칙의 핵심은 부모 클래스의 행동 규약을 자식 클래스가 위반하면 안 된다는 것이다.

행동 규약을 위반한다는 것은 자식 클래스가 오버라이딩을 할 때, 잘못되게 재정의하면 리스코프 치환 원칙을 위배할 수 있다는 의미이다.

⚖️ 인터페이스 분리 원칙 (Interface Segregation Principle)

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

특정 인터페이스가 특정한 경우만 사용될 것 같다고 생각될 경우 분리하라.

역시 관심사를 분리하는 것이다.

Bad

interface SmartPrinter {
  print();
  fax();
  scan();
}

class AllInOnePrinter implements SmartPrinter {
  print() {
    // ...
  }  
  
  fax() {
    // ...
  }

  scan() {
    // ...
  }
}

class EconomicPrinter implements SmartPrinter {
  print() {
    // ...
  }  
  
  fax() {
    throw new Error('Fax not supported.');
  }

  scan() {
    throw new Error('Scan not supported.');
  }
}

Good


interface Printer {
  print();
}

interface Fax {
  fax();
}

interface Scanner {
  scan();
}

class AllInOnePrinter implements Printer, Fax, Scanner {
  print() {
    // ...
  }  
  
  fax() {
    // ...
  }

  scan() {
    // ...
  }
}

class EconomicPrinter implements Printer {
  print() {
    // ...
  }
}

⚖️ 의존 역전 원칙 (Dependency Inversion Principle)

추상화하는 방향으로 의존하라. 상위 레벨 모듈이 하위 레벨 세부 사항에 의존해서는 안된다.

로마에 오면 로마의 법과 로마의 언어를 알아야한다.

로마(한 국가, => 상위레벨)가 입국하는 개개인(하위 레벨)에 맞춰 언어를 변경하거나 헌법을 변경할 수 없다.

interface Toy {
 toString:() => string
}

public class Kid {
    private Toy toy;

    public void setToy(Toy toy) {
        this.toy = toy;
    }

    public void play() {
        System.out.println(toy.toString());
    }
}
  
public class Robot extends Toy {
    public String toString() {
        return "Robot";
    }
}

public class Lego extends Toy {
    public String toString() {
        return "Lego";
    }
}

public class Main{
    public static void main(String[] args) {
        Toy lego = new Lego();
        Kid k = new Kid();
        k.setToy(lego);
        k.play();
    }
}

아이가 갖고 노는 장난감이 Kid에 맞춰야하는 거다.

즉, Kid의 play라는 api에서는 Toy가 toString api를 가지고 있다고 전제한다.

그럼 이제 Toy는 toString이라는 api를 갖게끔 설계하면 된다.

선 설계 후 구현 이라고 해도 될려나..

Main에서(상위 컴포넌트)에서 쓸때는 하위컴포넌트가 뭔지 상관할 필요 없다.

Toy라는 클래스로 추상화되어 들어올 뿐이다.

220볼트 플러그가 어떤 전자제품이 들어오는지 일일이 구분해서 신경쓸 필요 없듯이.(이 경우 마찬가지로 전자제품이 220볼트를 받아들 일 수 있는 코드가 부착되어 출시되어야 한다.)

저수준 모듈(하위)이 고수준 모듈의 추상화에 의존해야한다(맞춰야한다.)

"이 방식을 포워딩(forwarding)이라고 하며 필드의 인스턴스를 참조해 사용하는 메소드를 포워딩 메소드(forwarding method) 라고 부른다."

출처

  1. 의존성 역전 원칙 (DIP)

  2. 객체지향 5원칙 (SOLID)은 구시대의 유물 ?

  3. 테오의 SOLID 원칙

  4. 상속을 자제하고 합성을 이용하자

  5. 완벽하게 이해하는 LSP

profile
'과연 이게 최선일까?' 끊임없이 생각하기
post-custom-banner

0개의 댓글