IT,프로그래밍/better code

SOLID원칙 - 1. SOLID란? SRP(단일책임원칙), OCP(열림-닫힘원칙)

SOLID

참고 링크

원문링크

 

객체지향 프로그래밍에서 유지보수가 어렵고 코드가 혼란스러워 지는것을 방지하기 위햐어 SOLID라는 다섯가지 원칙을 만들었습니다

  • S: Single Responsibility Principle (단일책임원칙)
  • O: Open-Closed Principle (열린-닫힌 원칙)
  • L: Liskov Substitution Principle (리스코프 치환 원칙)
  • I: Interface Segregation Principle (인터페이스 분리 원칙)
  • D: Dependency Inversion Principle (의존성 역전 원칙)

SOLID원칙은 모듈화, 캡슐화, 확장용이성, 구성용이한 컴포넌트등을 고려한 소프트웨어의 구축을 위한 설계입니다.

 

Single-responsibility principle(SRP : 단일 책임 원칙)

하나의 module, class 혹은 function이 단 하나의 기능을 꼭 책임져야한다는 개념이다.

export class Movie {
  id: number;
  title: string;
  year: number;
  genres: string[];
}

여기서 interface 나 type이 아닌 class를 사용한 이유는 컴파일을 거치게 되면 어차피 class가 되기 때문에 바로 class로 써서 사용한것이다.

 

그렇다면 안좋은 예제를 살펴보자

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
    saveAnimal(a: Animal) { }
}

위의 예제는 SRP를 위반하였다.

 

아까도 말했지만, 클래스는 하나의 책임을 가져야 한다고 했다.

 

하지만 위의 class를 보게된다면

  1. Animal 데이터 베이스관리
  2. Animal property의 관리

두가지 역할을 한번에 다룹니다.

 

즉, saveAnimal이 DB의 Animal storage를 관리하는동안, 생성자와 getAnimalName은 Animal property를 관리합니다.

이렇게 된다면 나중에 생길수 있는 이슈를 생각해 볼수 있습니다.

 

추후에 DB관리 기능에 영향을 주도록 변경하게 된다면,

변경사항에 맞춰서 Animal property의 사용을 만드는 클래스는 반드시 수정하게되며 새로 컴팔일 해야합니다.

 

즉, 시스템이 유연하지 않으며 도미노 효과로 보이고, 파급효과를 주는것이 보입니다.

 

그러면 어떻게 리팩토링을 해야할까요 ?

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
}
class AnimalDB {
    getAnimal(a: Animal) { }
    saveAnimal(a: Animal) { }
}

위와 같이 class를 역할에 맞게 분리를 하였습니다.

Animal class에는 property만을 다루는 역할을 가지고 있습니다.

그렇기에 생성자와 getter만이 있습니다.

 

AnimalDB class에는 Animal이라는 정보를 DB에 insert하거나 read하는 역할만을 가지고 있습니다.

이렇듯이 각각의 클래스를 역할에 맞게 단일 책임을 부여해 주는것을 SRP라고 합니다.

클래스들이 같은 이유로 매번 변화하는 변화경향이 있다면, 클래스를 설계할때 연관된 기능들을 함께 모으는 것을 목표로 해야한다. 우리는 기능을 분리하노록 노력하고, 기능들은 서로 다른 이유로 변경되어야 한다. - Steve Fenton

 

Open-Closed Principle (OCP:열림-닫힘 원칙)

소프트웨어 엔티티(클래스,모듈,함수)는 확장을 위해 열려있고, 수정되서는 안된다.

위에서 다룬 Animal class를 다시한번 가져와 보겠습니다.

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
}

 

우리는 Animal 리스트를 반복하고, 각 Animal의 울음소리를 반복하였습니다.

//...
const animals: Array<Animal> = [
    new Animal('lion'),
    new Animal('mouse')
];
function AnimalSound(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        if(a[i].name == 'lion')
            return 'roar';
        if(a[i].name == 'mouse')
            return 'squeak';
    }
}
AnimalSound(animals);

함수 AnimalSound()는 OCP를 따르지 않고 있습니다.

 

왜냐하면 새로운 종의 Animal에 대해서 닫혀있지 않기 때문이죠 .

 

무슨 말이냐면 만일에 Snake라는 동물을 추가한다고 가정한다면 AnimalSound 는 다음과 같아 잘것이기 때문입니다.

function AnimalSound(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        if(a[i].name == 'lion')
            return 'roar';
        if(a[i].name == 'mouse')
            return 'squeak';
        if(a[i].name == 'snake')
            return 'hiss';
    }
}

단지 snake라는 동물을 추가하고 싶은것 뿐인데, 로직을 변경해야 하는 상황이 오게 됩니다.

 

위의 예제는 정말 간단한 경우 지만, 실제로는 if조건문이 계속해서 붙는다면 상당히 복잡해 질것입니다.

 

그러면 OCP를 준수하도록 리팩토링 해보겠습니다

class Animal {
        makeSound();
        //...
}
class Lion extends Animal {
    makeSound() {
        return 'roar';
    }
}
class Squirrel extends Animal {
    makeSound() {
        return 'squeak';
    }
}
class Snake extends Animal {
    makeSound() {
        return 'hiss';
    }
}
function AnimalSound(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        a[i].makeSound();
    }
}
AnimalSound(animals);

여기서 가장 큰 특징은 모든 동물 class들이 class Animal 이라는

기본 class에서 각각 상속을 받아서 Class내에서 각자의 울음소리를 구현한다는 점입니다

 

또한 AnimalSound 함수에서도, 이전과 같이 if로 조건을 걸어서 매번 추가할때마다 로직을 변경하는것이 아닌, 이미 각각의 동물 class들의 내부에 makeSound 함수가 울음소리를 가지고 있기에 출력만 해주면 된다는 점이 다릅니다.

 

이제, 새로운 동물을 추가할때 if 로직을 변경하는것이아닌 그저, class를 추가하고, animal배열에 추가만 하면 되도록 바뀌었습니다.

다른예제를 한번 살펴보겠습니다.

 

여러분이 좋아하는 고객에게 20% 할인해주고자 할때, 클래스는 아래와 같을겁니다.

class Discount {
    giveDiscount() {
        return this.price * 0.2
    }
}

추후에 VIP 고객에게는 20%를 추가로 할인해주기로 결정했을때, 코드는 아래와 같을것입니다.

 

class Discount {
    giveDiscount() {
        if(this.customer == 'fav') {
            return this.price * 0.2;
        }
        if(this.customer == 'vip') {
            return this.price * 0.4;
        }
    }
}

위 코드는 OCP 원칙을 지키지 못했습니다.

 

만약에 위와 같이 다른이유로 신규 할인률을 다른 고객에게 적용하려고 한다면, 새로운 로직이 추가되는 것을 보게 될 것입니다.

OCP 원칙을 준수하며 만드는 방법은 Discount를 확장하여 새로운 클래스를 추가하는 것입니다.

 

추가된 신규 클래스에서 우리는 신규 행위를 구현 할 수 있을 것입니다.

class VIPDiscount: Discount {
    getDiscount() {
        return super.getDiscount() * 2;
    }
}

만약, 80%의 할인율을 슈퍼 VIP 고객에게 적용하려면 아래와 같습니다.

class SuperVIPDiscount: VIPDiscount {
    getDiscount() {
        return super.getDiscount() * 2;
    }
}

이제, 우리는 ‘수정’과는 별개로 ‘확장’ 된 모습을 볼 수 있습니다.