SOLID 원칙이란

지인호·2022년 2월 16일
2

TIL

목록 보기
25/28
post-thumbnail

객체지향 프로그래밍 및 설계에 대한 다섯가지 기본원칙

SOLID 가 나온 배경

powered by Clean Architecture

1990년대이전 개발자는 하드웨어로인한 소프트웨어의 제약사항 때문에, “유지보수성이 높은 코드” 보다는 “기기 입장에서 효율적인 코드”를 더욱 선호하는 추세였다.

하지만, 무어의 법칙 이 나올 정도로, 하드웨어가 끊임없이 발전하고 프로그래머가 소프트웨어를 키우는 속도 보다 하드웨어의 발전속도가 더 빨라지게 되자, 개발자는 하드웨어로 인한 제약사항에서 어느정도 벗어나게 되었다.

즉, 이제는 기기 입장에서 효율적인 코드가 아닌, 유지보수성 / 재가용성이 높은 코드 등에 관련된 패러다임이나 원칙 또한 끊임없이 나오게 되었고, OOP 에 관련된 유용한원칙중 2000년대까지 살아남은 원칙들을 로버트 C 마틴 이라는분이 모아서 명명한 원칙이다.

SOLID 원칙이란

객체 지향 프로그래밍 및 객체 지향 설계에 대한 5가지 기본원칙을 뜻한다.

소프트웨어의 유지보수성과 재가용성에 초점을 둔 원칙이며, 기존 로버트 C 마틴이 명명한 원칙을 두문자어 기억술로 소개한 것 이다.

SRP 단일 책임 원칙

밥아저씨왈 가장 오해를 많이하는(?) 법칙

위키피디아를 보면, 단일책임원칙이란 “한 클래스는 하나의 책임만 가져야한다” 라고 소개되고 있다. 하지만 이는 SRP 에 대한 완전한 설명이 아니다

건방진 소리라고 들릴수도 있겠지만, 이 말은 내가한것이 아닌 SOLID 를 명명하신 로버트 C 마틴 엔지니어분께서 Clean Architecture 라는 저서를 통해 직접 말씀하신 이야기이다 (어느정도 포맷팅 하긴 하였지만)

진짜 SRP 원칙은 아래와 같다.

단일 모듈은 변경의 이유가 하나, 오직 하나뿐이어야 한다.

모든 소프트웨어의 변경 이유는 “사용자와 이해관계자를 만족시키기 위함” 하나이다.
판교장터(현 당근마켓)이 일반인들에게 제공되는 서비스로 변경된 이유는 판교 개발자분들의 지인/배우자 분들의 요청에 의해서이고, 배민에서 MSA 를 적용한 이유는 장애에 유연하게 대처하여 사용자의 불편을 해소하기위함이듯이, 모든 소프트웨어에 대해, 변경의 이유란 사용자, 이해 당사자를 만족시키기 위함 이다.

이러한 관점에서 위의 원칙은 다음과 같이 바뀔 수 있다.

하나의 모듈은 오직 하나의 사용자 또는 이해관계자에 대해서만 책임져야한다.

그런데, 하나의 모듈에 대하여 동일한 방식으로 변경을 원하는 사람이 과연 한명만 있을까? 아까 말했던 배민은 서비스를 사용하는 대부분의 사용자 그룹 및 조직내 이해관계자 을 만족시키기 위해 MSA 를 도입하였다. 그렇다면 사용자 또는 이해관계자 라는 표현은 부적절하다.

그러니 이러한 사용자/이해관계자 개인 혹은 그룹을 액터 라는 단어로 칭하기로 해보자

이렇게 되서 현재 우리가 알고있는 SRP 원칙과 비슷한 원칙이 나오게된다.

하나의 모듈은 하나의 액터에 대해서만 책임져야한다.

이 정의조차 완전하지 않다고 느낄수도 있다. 모듈이란, 소스파일의 집합을 뜻하기 떄문인데, 로버트 C 마틴이 말하는 모듈은 단순히 함수와 데이터 구조로 응집된 집합 이다.

여기서 나온 응집된이라는 단어가 SRP 의 핵심이다. 하나의 액터를 책임지는 코드를 “응집시켜주는 힘” 이 바로 응집성이다.

분명, 위의 문장은 위키피디아에서의 SRP 에 대한 설명과 거의 일치하는 문장이다. 하지만 여러분들이 해석한 이 문장의 의미는 이 글을 읽기 전과는 달라졌을 것 이다. 아마도

OCP 개방 폐쇄 원칙

소프트웨어 개체는 확장에 열려있어야 하고, 변경에는 닫혀있어야한다

즉, 소프트웨어에서 개체의 행위는 확장 가능하여야하지만, 이때 기존 개체를 변경해서는 안된다.

이는 소프트웨어를 확장하기 쉬울 뿐더러, 어떠한 변경으로 인하여 기존 시스템이 받는 영향을 최소화하기 위함이다.

대부분 이러한 OCP 를 코드/클래스 scope 내에서 적용하지만, 사실 OCP 원칙은 컴포넌트 수준에서 적용을 하였을때, 더욱 가치있는 원칙이다.

LSP 리스코프 치환 원칙

T타입 객체 o1 각각에 대응하는 S타입 객체 o2 가 있고, S 타입을 이용해서 정의한 모든 프로그램 P 에서의 o2 를 o1 으로 치환하더라도 P 의 행위가 변하지 않을경우 T 는 S의 하위타입이다.

이는 바바라 리스코프라는 사람이 정의한 하위타입에 대한 정의이다.

보통은 해당 문장을 뒤집어서 T가 S의 하위타입이면 T 가 사용된 모든 부분이 S로 치환가능하여야 한다 혹은 A에 대한 하위타입 B 와 C에 대해 A 가 사용된 부분은 B나 C 둘 중 하나로 치환이 가능하여야 한다 라고 사용하는데, 이것이 바로 리스코프 치환원칙이다.

즉, 리스코프 치환원칙은 다음과 같다

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위타입의 인스턴스로 치환가능하여야한다.

어떻게보면 당연한 이야기처럼 들리겠지만, 이러한 리스코프 치환원칙을 대차게 부숴버리는 처음 예제를 본 내 머리도 같이 부숴버렸던 정사각형/직사각형 문제가 있다

이 예제에서는 Square 를 Rectangle 의 하위타입으로 명명하고있다. 하지만 예제를 조금 더 자세히 보면, 적어도 리스코프가 정의한 하위타입은 아니라는것을 알 수 있다.

setH 와 setW 를 통해 높이와 너비를 독립적으로 설정할 수 있는 직사각형과 달리, 직사각형의 하위타입으로 정의된 정사각형은 높이와 너비를 각각 독립적으로 설정할 수 없다

Rectangle r = ...
r.setW(4);
r.setH(2);
assert(r.area() == 8);

다음 코드를 본다면 더욱 확실하게 다가올것이다. 참조변수 r이 new Rectangle(Rectangle 의 인스턴스) 이었다면 해당 assert 문을 통과하겠지만, r이 Square 의 인스턴스였다면 area 가 4가 되므로(2*2) assert 문을 통과할 수 없다. 즉 치환한다면 정확성을 깨뜨린다

보통 이러한 문제는 area 라는 함수를 정의한 Shape 를 상위타입으로 둔 Rectangle 과 Square 하위타입을 설계한다. 즉, Rectangle 과 Square 는 서로 직접적인 상/하위 관계를 맺지 않는다.

LSP 를 지켜야하는 이유는 위의 예제를 보면 바로 알 수 있을것이다. 만약 해당 코드가 국토 측량시에 사용되었다면?그럴일은 없겠지만 아마 국가적인 손실이 발생할 것이라고 조심스럽게 예측해본다.

ISP 인터페이스 분리 원칙

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다

정확히는 각구현체에 필요한 메서드가 정의된 인터페이스여러개가 각구현체에 불필요한 메서드가 정의된 인터페이스 하나보다 낫다. 라고 바꾸어말할 수 있겠다.

예를들자면, Vehicle 이라는 인터페이스에 fly(), run(), move() 라는 메서드가 명시되어있고, Car, Airplane, Train 이라는 Vehicle 에 대한 구현체가 있다고 가정해보자.

Airplane 은 fly() 메서드를 사용하겠지만, Car 와 Train 은 해당 메서드가 불필요할 것 이다. (적어도 2020년대에는)

만약 기존에 정수형 변수 speed 를 인자로 받던 fly 메서드가 실수형 변수인 detailSpeed 를 받도록 변경되었다면 어떻게 될까? fly 메서드를 사용하지 않는 Car 와 Train 또한 이러한 변경사항이 “소스코드 내에서” 이루어져야 할 것 이다. (물론 동적언어의 경우위와같은 문제가 발생하지 않을 수 있다)

이러한 문제를 방지하기 위해서 사용되는 원칙이 바로 ISP 이다.

의존성 역전원칙

해당 원칙은 유연한 소프트웨어를 만들어야한다는 원칙이다.

그렇다면 유연한 소프트웨어는 무엇일까?

의존성 역전 원칙에서 말하는 이상적인 소프트웨어 즉 유연성이 극대화된 소프트웨어는 소스코드의 의존성이 추상에 의존하며, 구현체에 의존하지 않는것을 뜻한다.

조금더 쉽게 설명하자면 import 문을 통해 의존하는개체는 오직 추상클래스나 인터페이스같은 추상적인 선언만이어야 한다는 것 이다.

그런데 당장 우리가 자주쓰는 String 만 해도 구현체이다. 그럼 굳이 이 String 을 감싼 추상 클래스나 인터페이스를 만들어야할까?

이는 의존성 역전원칙의 이유를 보면 알 수 있다.

우선 String 클래스는 매우 안정적이다. String 클래스의 변경은 거의 없고, 있더라도 엄격하게 통제되는 상황에서 발생한다. 즉 변경으로 인해 기존 시스템에 영향을 끼칠것이라고 염려하지 않아도 된다.

의존성 역전 원칙이 구현체에 대한 의존관계를 피하려 하는 이유는 구현체가 변동성이 크기떄문에 구현체에 의존하게되면 해당 구현체의 변동에 따라 구현체를 의존하는 다른 클래스/컴포넌트들이 지대한 영향을 받을 수 있기 떄문이다.

그렇기때문에, String 같이 안정적인 구현체를 사용한다고해서 SOLID 원칙을 심하게 위배하는것은 아니다(개발을 하다보니 오히려 String 보다 내가 작성한 Interface 의 수정이 더 많다는것을 느꼇다 ㅋㅋ)

profile
MSA 관련 기술에 관심이 많습니다

2개의 댓글

comment-user-thumbnail
2022년 5월 2일

도움이 많이 됐습니다. 이노스케님 ^_^

1개의 답글