[Design Pattern] SOLID 원칙 이란?

배석재·2021년 3월 20일
0

안녕하세요! 이번 포스팅은 객체지향 개발의 5대 원칙인 SOLID에 대하여 포스팅 해보려고 합니다!
이전에 포스팅에 OOP에 대하여 다뤄보긴 했습니다만, 이런 OOP를 어떤 식으로 활용을 해야 될지에 대해서는 감이 잡히지 않은 상태였습니다!
따라서 이번엔 SOLID 원칙이라는 디자인 패턴에 대하여 포스팅 해보려 합니다 ㅎㅎ


OOP 란?

먼저 해당 원칙에 대하여 설명하기 전에 객체 지향(OOP) 방법론에 대하여 간략하게 다시 정리하고 시작하겠습니다!

OOP는 프로그램을 어떻게 설계해야 하는지에 대한 개념이자 방법론이다.

  • 프로그램을 단순히 데이터와 처리 방법으로 나누는 것이 아니라,
    프로그램을 수많은 '객체'라는 기본 단위로 나누고, 객체 간 상호작용으로 서술한다.

  • 객체를 데이터의 묶음으로만 착각하기 쉬운데, 그보다는 하나의 '역할'을 수행하는 메소드와 데이터의 묶음으로 봐야 한다.

  • 프로그래밍 방식은 절차적 -> 구조적 -> 객체지향 방식으로 발전해왔다.

    • 절차적 프로그래밍
      • 입력을 받아 명시된 순서대로 처리한 다음, 그 결과를 내는 것
      • 어떤 논리를 어떤 순서대로 써나가는 것인가로 간주되었다.
      • 즉, 프로그램 자체가 가지는 기능에 대해서만 신경을 썼지, 이 프로그램이 대체 어떤 데이터를 취급하는 것인가에는 그다지 관심이 없었던 것이다.

    • 구조적 프로그래밍
      • 절차적 프로그래밍 방식을 개선하기 위해 나온 방식
      • 프로그램을 함수(Procedure) 단위로 나누고 Peocedure끼리 호출을 하는 것
      • 프로그램이라는 큰 문제를 해결하기 위해 그것을 몇개의 작은 문제들로 나누어 해결하기 때문에 하향식(Top-down) 방식이라고도 한다.

    • 객체지향 프로그래밍
      • 구조적 프로그래밍 방식을 개선하기 위해 나온 방식
      • 큰 문제를 작게 쪼개는 것이 아니라, 먼저 작은 문제들을 해결할 수 있는 객체들을 만든 뒤, 이 객체들을 조합해서 큰 문제를 해결하는 상향식(Bottom-up) 해결법을 도입한 것이다.
      • 이 객체란 것을 일단 한번 독립성/신뢰성이 높게 만들어 놓기만 하면 그 이후엔 그 객체를 수정 없이 재사용할 수 있으므로 개발 기간과 비용이 대폭 줄어들게 된다.

따라서 OOP를 사용하면 코드의 중복을 어느 정도 줄일 수 있고 입력 코드, 계산 코드와 결과 출력 코드 등 코드의 역할 분담을 좀 더 확실하게 할 수 있어서 가독성이 높아질 수 있습니다!


SOLID 원칙 이란?

따라서 위와 같은 방법론으로 구현하고 개발하기 위한 원칙으로 SOLID 원칙이 나오게 되었다고 합니다!
그렇다면 각 원칙에 대하여 설명해보도록 하겠습니다!

SRP(Single Responsibility Principle) : 단일 책임 원칙

객체 지향 프로그래밍에서 단일 책임 원칙(single responsibility principle)이란 모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 함을 일컫는다. 클래스가 제공하는 모든 기능은 이 책임과 주의 깊게 부합해야 한다.

출처) 위키백과

위의 내용대로, <하나의 클래스 혹은 객체는 하나의 기능만 가져야 한다.> 입니다!
아래의 예시를 보며 살펴보겠습니다!

해당 클래스는 serivalNumber라는 고유 정보가 있습니다!
또한 price, maker, type, model, backWood, topWood 등 변화가 일어날 수 있는 변화 요소로 파악되는 것 들이 있네요!
이러한 변화 요소에 변화가 발생하게 된다면 해당 클래스의 내용을 전부 수정해야할 뿐만 아니라, 이와 연관된 모든 내용이 다 변경 되어야 될테니 SRP의 적용 대상이 된다고 할 수 있습니다!

따라서 변화 요소만을 가지고 있는 새로운 클래스를 만들고 따로 분리해 놓았습니다!
이렇게 된다면 해당 요소가 변경될 때 GuitarSpec 클래스만 변경하면 되고, 가독성이 더 높아진다는 장점을 가지게 됩니다!

따라서 클래스는 자신의 이름이 나타내는 일을 해야하고, 올바른 클래스의 이름은 자신의 책임을 나타낼 수 있는 좋은 방법이 된다고 합니다!


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

개방-폐쇄 원칙(OCP, Open-Closed Principle)은 '소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다'는 프로그래밍 원칙이다.

출처) 위키백과

위의 내용대로, <확장에 대해서는 개방적이고 수정, 변경에 대해서는 폐쇄적이여야 한다.> 입니다!
바로 예시를 통해 알아보겠습니다!

위의 클래스는 SRP가 적용된 클래스의 모습입니다!
이미 SRP를 적용해서 변화가 발생할 수 있는 부분에 대해 최소화를 했지만 변경이 발생할 수 있는 부분이 더 있습니다!
만약 Guitar 이외에 첼로, 바이올린, 피아노 등 새로운 악기가 추가 된다면 어떻게 될까요?

위와 같이 매번 해당 악기의 클래스를 새로 만들고, 악기의 스펙에 관련된 클래스도 새로 만들어야 될 것 입니다!
이렇게 되면, 작업 효율도 굉장히 떨어질 것 입니다!

따라서 위와 같이 추상화 작업을 통해, 악기들의 공통 특성을 담고 있는 StringInstrument라는 인터페이스를 생성하게 됩니다!
이렇게 되면 악기가 새로 추가 되면서 변경이 발생하는 부분을 분리하여, 코드의 수정은 최소화 하고 응집성은 높이는 효과를 볼 수 있습니다!


LSP(Liskov Substitution Principle) : 리스코프 치환 원칙

치환성(영어: substitutability)은 객체 지향 프로그래밍 원칙이다. 컴퓨터 프로그램에서 자료형 S가 자료형 T의 하위형이라면 필요한 프로그램의 속성(정확성, 수행하는 업무 등)의 변경 없이 자료형 T의 객체를 자료형 S의 객체로 교체(치환)할 수 있어야 한다는 원칙이다.

출처) 위키백과

위의 내용을 좀 더 이해하기 쉽게 설명하자면, <자식 클래스는 언제나 자신의 부모 클래스를 대체할 수 있다는 원칙> 이라고 설명할 수 있을것 같습니다!
즉 부모 클래스가 들어갈 자리에 자식 클래스를 넣어도 계획대로 잘 작동해야 한다는 것이고, 이러한 점은 상속의 본질이고 이를 지키지 않으면 부모 클래스 본래의 의미가 변해서 다형성을 지킬 수 없게 되기 때문입니다!

해당 부분을 예를 들어 설명해보자면

위의 코드는 Rectangle을 부모 클래스로 정의하고, Square을 하위 클래스로 정의를 하였습니다.
이를 사용하는 곳에서 높이와 길이를 주고 넓이를 계산하는 간단한 코드입니다.
여기서 리스코프 치환 원칙을 위배한 부분은 직사각형을 정사각형의 하위 클래스로 정의를 한 부분입니다.
정사각형의 경우 길이이든 높이이든 하나의 속성만 있으면 되는데 높이와 길이를 세팅하게 되어 불필요한 로직이 수행이 되었고, 결과적으로 잘못된 값으로 인해 오류가 발생하였습니다.
정의된 Rectange을 사용하는 클래스의 경우 이 하위의 속성이 어떤것이 있든지 동일한 처리를 해야하는데, setWidth와 setHeight을 함으로써 하위 클래스에서 올바른 결과가 나오지 못하게 된 것입니다!

따라서 위의 내용처럼 상위 클래스는 하위 클래스에서 공통적으로 가지고 있거나 추상화해야하는 기능만을 가지고 있고, 하위 클래스는 자기만의 독작적인 성격을 가지고 있는게 수정을 해야합니다!


ISP(Interface Segregation Principle) : 인터페이스 분리 원칙

인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다. 인터페이스 분리 원칙은 큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시킴으로써 클라이언트들이 꼭 필요한 메서드들만 이용할 수 있게 한다. 이와 같은 작은 단위들을 역할 인터페이스라고도 부른다. 인터페이스 분리 원칙을 통해 시스템의 내부 의존성을 약화시켜 리팩토링, 수정, 재배포를 쉽게 할 수 있다.

출처) 위키백과

위의 내용대로, <한 클래스는 자신이 사용하지 않는 인터페이스, 메소드는 구현되지 않아야 한다> 입니다!

interface Animal {
  eat(): void;
  sleep(): void;
  cry(): void;
  fly(): void;
}

class Bird implements Animal {
  eat(): void {
    console.log('새가 음식을 먹었어요!');
  }
  sleep(): void {
    console.log('Zzzzzz....');
  }
  cry(): void {
    console.log('짹짹!!');
  }
  fly(): void {
    console.log('새가 하늘을 날았어요!');
  }
}

class Human implements Animal {
  eat(): void {
    console.log('아 배부르다');
  }
  sleep(): void {
    console.log('Zzzzzz....');
  }
  cry(): void {
    console.log('ㅠㅠㅠ');
  }
  fly(): void {
    // ????
  }
}

위와 같이 Animal이란 인터페이스를 상속 받는 Bird와 Human 클래스가 있습니다!
그런데 Human 클래스에서 Fly라는 메소드를 구현하려고 하니 사람은 날 수 없는데.. 라는 생각과 함께 구현이 될 수 없는 부분이 생기게 되었습니다!

interface Animal {
  eat(): void;
  sleep(): void;
  cry(): void;
}

interface FlyableAnimal extends Animal {
  fly(): void;
}

class Bird implements FlyableAnimal {
  eat(): void {
    console.log('새가 음식을 먹었어요!');
  }
  sleep(): void {
    console.log('Zzzzzz....');
  }
  cry(): void {
    console.log('짹짹!!');
  }
  fly(): void {
    console.log('새가 하늘을 날았어요!');
  }
}

class Human implements Animal {
  eat(): void {
    console.log('아 배부르다');
  }
  sleep(): void {
    console.log('Zzzzzz....');
  }
  cry(): void {
    console.log('ㅠㅠㅠ');
  }
  // fly() 메서드를 구현하지 않는다. ISP 준수!
}

따라서 인터페이스를 상속하는 인터페이스를 통해 해결을 하였고, 위와 같이 구성함으로써 인터페이스의 단일 책임을 강화할 수 있게 됩니다!

interface Animal {
  eat(): void;
  sleep(): void;
  cry(): void;
}

interface Flyable {
  fly(): void;
}

class Bird implements Animal, Flyable {
  eat(): void {
    console.log('새가 음식을 먹었어요!');
  }
  sleep(): void {
    console.log('Zzzzzz....');
  }
  cry(): void {
    console.log('짹짹!!');
  }
  fly(): void {
    console.log('새가 하늘을 날았어요!');
  }
}

class Human implements Animal {
  eat(): void {
    console.log('아 배부르다');
  }
  sleep(): void {
    console.log('Zzzzzz....');
  }
  cry(): void {
    console.log('ㅠㅠㅠ');
  }
  // 마찬가지로 fly() 메서드를 구현하지 않는다. ISP 준수!
}

다른 방법으로는, 여래 개의 인터페이스를 상속하여 해결할 수도 있습니다!
Flyable 인터페이스를 만들고 Bird 클래스가 Flyable과 Animal 인터페이스를 둘 다 상속함으로써 마찬가지로 인터페이스의 단일 책임을 강화하게 됩니다!


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

객체 지향 프로그래밍에서 의존관계 역전 원칙은 소프트웨어 모듈들을 분리하는 특정 형식을 지칭한다. 이 원칙을 따르면, 상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존관계를 반전(역전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다. 이 원칙은 다음과 같은 내용을 담고 있다.

  • 첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
  • 둘째, 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.

출처) 위키백과

위의 내용을 정리하자면, <클래스를 참조할 일이 있을 때 그 클래스를 직접 참조하지 말고 그 대상의 추상 클래스를 만들어서 참조해야 한다> 라고 할 수 있을 것 같습니다!
아래의 예시로 보자면,

class Keyboard {
  getString(): string {
    return '사랑해요 여러분';
  }
}

class Console {
  writeString(str: string): void {
    console.log(`writed ${str} to console.`);
  }
}

class Task {
  static copy(keyboard: Keyboard, console: Console) {
    console.writeString(keyboard.getString());
  }
}

const k = new Keyboard();
const c = new Console();
Task.copy(k, c);

위와 같은 기능에 “파일도 입력할 수 있게 만들자.” 라는 새로운 요구사항이 발생하게 된다고 가정해보겠습니다!
그런데 copy 메서드는 오직 Keyboard 클래스만을 매개변수로 받고 있습니다.

interface Input {
  getString(): string;
}

interface Output {
  writeString(str: string): void;
}

class Task {
  static copy(input: Input, output: Output) {
    output.writeString(input.getString());
  }
}

먼저 Input, Output 인터페이스를 만들고

class Keyboard implements Input {
  getString(): string {
    return 'Keyboard Input';
  }
}

class Console implements Output {
  writeString(str: string): void {
    console.log(`writed ${str} to console.`);
  }
}

const k = new Keyboard();
const c = new Console();
Task.copy(k, c);

위와 같이 구성하게되면 "파일도 입력할 수 있게 만들자.” 라는 요구사항을 지킬 수 있게 되었습니다!
또한 콘솔 말고도 모니터에서 출력되게도 만들 수 있습니다.

class File implements Input {
  getString(): string {
    return 'File Input';
  }
}

class Monitor implements Output {
  writeString(str: string): void {
    console.log(`writed ${str} to monitor.`);
  }
}

const f = new File();
const m = new Monitor();
Task.copy(f, m);

따라서, 추상성이 높고 안정적인 고수준의 클래스는 구체적이고 불안정한 저수준의 클래스에 의존해서는 안된다는 원칙인 DIP를 지키게 될 수 있게 되었습니다!


참고 자료)
https://www.nextree.co.kr/p6960/
https://shlee0882.tistory.com/187
https://medium.com/humanscape-tech/solid-%EB%B2%95%EC%B9%99-%E4%B8%AD-lid-fb9b89e383ef

profile
"personality begins Where Comparison ends"

0개의 댓글