본문 바로가기
Study/Design Pattern

[OOP] 객체지향 설계 5원칙 SOLID (SRP, OCP)

by _royJang 2022. 8. 13.

SOLID를 공부하는 이유

다른 개발자들은 몇 년의 경험이 쌓여야 잘 이뤄진 객체지향 설계에 대한 필요성을 느낀다 한다. 대부분의 개발자가 그렇게 말하는 것을 보았고 그 말은, 개발자 인생에서 반드시 익혀두어야 하는 개념임은 확실하다 생각했다. 그래서인지 공부를 해 보며 알 수 없는 부분들이 많았다. 굉장히 철학적이고 추상적으로 느껴졌다. 하지만 내용을 알면서 경험하는 것과 그렇지 않은 것은 큰 차이가 있다고 생각하기에 아직 걸음조차 떼지 못한 주니어 개발자이지만 앞으로의 경험을 대비해 지금이 SOLID를 공부하기에 효율적인 시기가 아닐까란 생각이 들었다.

SOLID의 탄생 배경

컴퓨터의 연산은 자료구조를 담당하는 memory와 연산을 담당하는 cpu의 합작이다. 초기 이 memory와 cpu의 성능이 좋지 못했을 때엔 하드웨어의 부족한 성능을 효율적으로 사용하는 것이 최우선이 되어야 했다. 하지만 무어의 법칙이 들어 맞으면서 이러한 하드웨어의 사용에 제약이 굉장히 줄어들었다. 그리고 이런 제약이 풀리자 다음으로 문제가 되는 것은 개발자의 시간이었다. 프로젝트의 부피가 커짐에 따라 객체의 연관 관계가 복잡해 졌고 잘 몰라도 이는 개발자의 퇴근을 지연시키는 가장 큰 원인이 됐을 것이다.

그렇기에 클린코드의 저자인 로버트 마틴은 이 SOLID의 개념을 만들었다. 프로젝트의 부피가 커짐에 따른 코드의 확장성유지관리를 위해.

SOLID란?

객체 지향언어에서 코드의 확장성유지관리를 위해 로버트 마틴이 제작한 5가지 설계 원칙이다.

구성 요소

  1. S - SRP(단일책임의 원칙 : Single Responsibility Principle)
  2. O - SCP(개방폐쇄의 원칙 : Open/Close Principle)
  3. L - LSP(리스코프 치환의 원칙 : Liskov Substitution Principle)
  4. I - ISP(인터페이스 분리의 원칙 : Interface Segregation Principle)
  5. D - DIP(의존성 역전의 원칙 : Dependency Inversion Principle)

❗개발자 모두의 경험에 따라 해석하는 방법이 다르다. 스스로의 해석본을 만들어 나가야 한다.
❗나는 코드를 Go를 예시로 든다. Go는 클래스라는 개념이 없다. Struct라는 개념과 Class의 개념을 비슷하게? 봐준다면 감사하겠다.

1) SRP

클래스는 하나의 기능만 가지며, 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
-로버트 마틴-

 

우리가 집중해야 할 부분은 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다는 부분이다. 만약 하나의 클래스가 여러 개의 기능을 가진다면 우린 클래스를 변경해야 하는 이유가 여러 개인 것이다.

 

SRP를 지키지 못한 코드 예시

type Calculator struct {
    //...
}

func (c *Calculator) Calculate() {
    //...
}

func (c *Calculator) Output() {
    //...
}

위 Calculator 구조체는 계산을 위한 Calculate() 메서드와 출력을 위한 Output() 메서드를 가지고 있다.

언뜻 보면 별 문제가 없을 거라는 생각이 든다. 하지만 누군가 계산에 사용되는 공식을 바꿔달라는 요청을 한다면? 누군가가 출력하는 형태를 추가해달라는 요청을 한다면?

우린 이 Calculator 구조체가 계산과 출력 두 가지의 기능을 가진다는 것을 인지할 수 있다.

 

개선시킨 코드

type Calculator struct {
    //...
}

func (c *Calculator) Calculate() {
    //...
}


type Printer interface{
    Output ()
}

type APrinter Printer
func (a APrinter) Output(){
    //...
}

type BPrinter Printer
func (b BPrinter) Output(){
    //...
}

Printer 인터페이스를 만들어 두가지 기능을 하던 Calculator 구조체에서 Output기능을 떼내었다.
이로써 우리가 얻을 수 있는 것들은

  1. 변경이 필요할 때 수정할 대상이 명확해진다.
  2. 테스트 코드를 작성하기 쉽다.
  3. 명확한 코드로 가독성을 향상시킨다.

가 있다.

객체지향 설계 5원칙의 기본이 되는 원칙인 SRP이다.

 

2) OCP

소프트웨어 개체는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.
-버틀란트 메이어-

 

여기서 개방은 새로운 동작과 기능을 추가해서 코드를 확장하거나 조정하는 것을 의미하고, 폐쇄는 버그 또는 다른 종류의 회귀를 초래할 수 있는 기존 코드의 변경을 피해야 하는 것을 의미한다.

 

OCP를 지키지 못한 코드 예시

func Printer(form string, input interface{}){
    switch form{
    case "csv":
        //...
    case "json":
        //...
    }
}

위와 같은 함수에서 xml이라는 form에 대한 기능을 추가한다면 어떻게 될까.

일딴 switch문에 새로운 케이스를 추가한다. 그리고 새로운 전처리 함수를 추가할 것이며 호출자가 xml을 부를 수 있게 수정하여야한다. 테스트 케이스의 추가는 당연하고.

 

개선시킨 코드

type Printer interface{
    Output ()
}

type CSVPrinter Printer
func (a APrinter) Output(){
    //...
}

type JSONPrinter Printer
func (b BPrinter) Output(){
    //...

interface를 사용하여 Printer에 대한 구현 코드를 추가시켰다. 이를 통해 우리는 좀더 축소된 개발 범위를 가지게 되며 사용자가 사용하는 주된 코드는 바꾸지 않을 수 있게 됐으며 정확한 버그의 위치를 예측할 수 있게 됐다.

OCP를 통해 얻을 수 있는 장점은

  • 기능의 확장에 대한 위험을 줄이는데 도움을 준다.
  • 기능을 변경시키는데 필요한 변경 사항을 최소화하는데 도움을 준다.
  • 버그가 발생할 수 있는 범위를 축소시킨다.
  • 깔끔하고 견고한 추상화를 가능케 한다.

과 같은 것이 있다.

 


참고
Go 프로그래밍 개발 환경에서 의존성 주입 실습 - 코리 스캇 지음.
https://mangkyu.tistory.com/194
https://www.youtube.com/watch?v=5UGjfLFEP9s&t=1s