SOLID 원칙

sith-call.dev·2022년 2월 4일
0

객체지향

목록 보기
3/3

SOLID 원칙

컴퓨터 프로그래밍에서 SOLID란 로버트 마틴이 2000년대 초반에 명명한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더스가 두문자어 기억술로 소개한 것이다. (위키백과)

즉 이전까지는 객체와 클래스가 무엇이며, 객체지향이란 패러다임 그 자체에 집중했다. 그러나 이번에는 이 객체지향 패러다임을 이용한 설계에 집중해본다. SOLID 원칙이란 바로 이 설계를 할 때 지켜야하는 원칙이자 BP이다.

전제

소프트웨어는 항상 변경을 전제한다. 따라서 변경에 민감하게 반응하지 못하는 설계는 프로젝트를 힘들게 한다.

이렇듯 소프트웨어 세계는 언제나 불안정하다. (Every day is chaos...)
그래서 이런 변화에 대응하면서 객체지향 패러다임으로 소프트웨어를 설계하기 위한 원칙들이 바로 SOLID라고 할 수 있다.

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

SRP는 하나의 클래스에 한 가지 책임을 가르치는 원칙이다. 우리는 설계 관점에서 우리가 인식하지 못하는 SRP 위반을 자주 하게 된다. 이 위반을 경계하기 위해 깊은 통찰력이 필요하지도 않다. 단지 머리에 ‘책임’이란 단어를 상기하는 습관이면 된다.

하나의 책임을 하나의 클래스에 부여한다. 아주 간단한 법칙이라고 생각할 수 있다. 어찌보면 이러한 접근은 분할 정복이다. 왜냐하면 큰 문제를 여러 개의 작은 문제로 분할한 뒤에 원래의 문제를 해결해 나가고자 하기 때문이다.

손자가 말하였다. 적은 병력을 통치하듯이 대규모의 병력을 통치하려면 병력수를 분리하여야 한다. 대규모의 병력이 전투를 하려면 군대의 효율적인 진형과 정확한 의사소통이 중요하다. - 손자의 손자병법

그러나 나는 위의 손자병법 구절이 제일 먼저 떠올랐다. 그때 당시 나는 이 구절을 읽으면서 병력을 분리하는 것이 매우 어려울 것이라 생각했다. 이는 소프트웨어도 마찬가지라 생각한다. 하나의 클래스에 하나의 책임만 주기 위해선, 먼저 주어진 일에 필요한 책임들을 나열할 수 있어야 한다. 이때 중요한 점은 적절하게 책임 분리되어야 한다는 것이다. 주어진 일에 필요하지만 최소한의 책임만을 추출하는 것은 마치 그 일의 본질적인 원소를 추출해내는 것과 같다고 생각한다.

그래서 나는 반대로 SRP를 지키지 않았을 때 생기는 문제들을 피하는 방식으로 SRP를 지키는게 더 쉬운 방법이라고 생각한다.

SRP를 따르지 않았을 때 생기는 문제점

왕따가 발생

난 A라는 메소드만 필요한데, 쓸모없는 B메소드까지 한 클래스에 있다면 B 클래스는 항상 사용되지 않아서 왕따가 된다.

불필요한 변경 임팩트 전달

만약 B 메소드가 변경되었다면, A 메소드만 사용하던 클래스는 사용하지도 않는 클래스 때문에 다시 컴파일 되어야만 한다. 이런 식의 임팩트는 한 클래스를 수정했을 시에 여러 클래스가 재컴파일되고 수정되어야 하는 상황까지 확장될 수 있다.(이러한 현상을 '산탄총 수술'이라고 비유한다.)

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

이 개방-폐쇄 원칙을 잘 정의한 버틀란트 메이어(Bertrand Meyer)는 소프트웨어 구성 요소(컴포넌트, 클래스, 모듈, 함수)는 확장에 대해서는 개방돼야 하지만 변경에 대해서는 폐쇄되어야 한다고 말한다. 변경을 위한 비용은 가능한 줄이고 확장을 위한 비용은 가능한 극대화해야 한다는 의미다.

방법은 우선 변하는(확장되는) 것과 변하지 않는 것을 엄격히 구분해야 한다. 변하는 것은 가능한 변하기 쉽게, 변하지 않는 것은(폐쇄돼야 하는 것은) 변하는 것에 영향을 받지 않게 설계하는 것이다. 다음으로 이 두 모듈이 만나는 지점에 인터페이스를 정의해야 한다. 인터페이스는 변하는 것과 변하지 않는 모듈의 교차점으로 서로를 보호하는 방죽 역할을 한다.

출처 : https://zdnet.co.kr/view/?no=00000039134727

나는 이 원칙을 상수함수로 이해했다.
마치 변화하는 것(열려 있는 것)은 미지수 X이고 이 미지수 X를 항상 상수 K(닫혀 있는 것)에 대응시키는 함수 f가 있다. 이때 이 f가 위에서 말하는 인터페이스 역할을 해주는 것과 마찬가지이다.

K = f(X)

그러나 위의 설명은 너무나 추상적이다. 언젠가 다시 읽을 나를 위해서 구체적으로 더 작성해보자. 인터페이스로 인해서 프로그램 내부에서 변할 가능성이 있는 부분과 변해선 안될 부분이 구분된다. 그리고 인터페이스는 변할 가능성에 있는 부분에 변화가 생겨도 이것을 변해선 안될 부분이 전과 같은 방식으로 이해할 수 있도록 해준다. 그래서 변할 가능성이 있는 부분은 자기 자신의 가능성을 마음껏 펼칠 수 있다. 또한 이런 방식이 항상 변화의 위험이 도사리고 있는 소프트웨어 세계에 더 적합하다.

이때 변할 수 있음을 열려 있다고 표현하고, 변할 수 없음을 닫혀 있다고 표현한다.

L - LSP(Liskov Substitusion Principle) : 리스코프 치환 법칙

서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다.

즉 서브 타입은 언제나 기반 타입과 호환될 수 있어야 한다. 달리 말하면 서브 타입은 기반 타입이 약속한 규약(public 인터페이스, 물론 메쏘드가 던지는 예외까지 포함된다)을 지켜야 한다는 것이다.

상속은 구현 상속(extends 관계)이든 인터페이스 상속(implements 관계)이든 궁극적으로는 다형성을 통한 확장성 획득을 목표로 한다. LSP 원칙도 역시 서브 클래스가 확장에 대한 인터페이스를 준수해야 함을 의미한다. 다형성과 확장성을 극대화하려면 하위 클래스를 사용하는 것보다 상위의 클래스(인터페이스)를 이용하는 것이 좋다.

일반적으로 선언은 기반 클래스로 생성은 구체 클래스로 대입하는 방법을 사용한다.

나는 이것을 어떤 논리적 연산의 피연산자를 원소가 아닌 집합으로 사용하려는 의도라고 생각한다. 왜냐하면 앞에서도 미리 말했듯이 소프트웨어 세계는 언제 변화를 요구 받을지 모른다. 그렇다면 원소보단 집합의 맥락으로 논리를 작성해두는 편이 낫다. 집합이 원소보다 더 범용적이기 때문이다. 예를 들어 <'사과'를 먹는다.>라는 규칙은 사과 대신 오렌지를 먹어야할 경우 글자를 바꿔야 한다. 그러나 <'과일'을 먹는다.>라고 규칙을 세워두면, 사과를 먹든, 오렌지를 먹든 규칙을 변경할 필요가 없다. 그만큼 집합적인 표현은 범용성을 띠는 것이다. 이런 맥락이 반영된 부분이 나는 리스코프 치환 원칙이라 생각한다. 왜냐하면 집합적으로 논리를 표현하기 위해선 반드시 원소는 자신의 집합으로서 취급 받을 수 있어야 하기 때문이다. 반대로 원소가 자신의 집합으로서 취급 받지 못한다면, 그 원소는 자신의 집합이라 칭한 그 집합의 원소가 아니게 되기 때문이다.

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

“사람은 다른 사람과 말을 할 때 듣는 사람의 경험에 맞추어 말해야만 한다. 예를 들어 목수와 이야기할 때는 목수가 사용하는 언어를 사용해야 한다.” - 플라톤의 파에톤(Phaethon)

객체지향 시스템은 메시지를 통해 커뮤니케이션하는 수많은 객체들로 구성된다. 그리고 이들 객체 간의 통신에도 앞에서 언급한 커뮤니케이션의 논리가 그대로 적용된다. 즉 상대방이 기대하고 있는 메시지를 ‘군더더기 없이’ 전달해야 하듯 서비스를 제공하는 객체는 자신을 이용하는 객체에게 해당 객체가 기대하는 서비스만을 제공해야 한다는 것이다.

일반적으로 인터페이스와 역할은 1:1 관계를 갖는다.

나는 이 원칙이 클래스 간의 소통에 대한 것이라 생각한다. 보통 SRP와 ISP를 많이 비교한다. 왜냐하면 뜻이 비슷한 것처럼 보이기 때문이다. 그러나 이것들의 차이점을 굳이 내가 스스로 추론해보자면 다음과 같다.

SRP : 클래스를 분할할 때의 원칙
ISP : 클래스끼리 소통할 때의 원칙

위와 같이 ISP는 클래스 간의 원활한 소통을 위한 원칙이다. 즉, 언제 어떤 클래스에게 연락이 올 지 모른다. 근데 그러한 연락을 한 클래스에게 모두 응답하도록 책임을 몰아준다면, A라는 클라이언트에게 응답할 메소드만 수정해야 하는데, B,C,D 클라이언트의 메소드까지 포함된 하나의 클래스가 재컴파일 된다. 이처럼 불필요한 영향력이 전파될 수 있다.

따라서 여러 클라이언트에게 요청을 받을 수 있는 클래스는 각각의 클라이언트에 맞게 분할하여 응답하는 것이 더 좋다. 하지만 이 또한 SRP와 마찬가지로 쉽지 않다. 그래서 동일하게 ISP가 적용되지 않았을 때 생기는 문제를 피하는 방식으로 ISP를 지키는 편이 낫다고 생각한다. 여기선 ISP를 지키지 않았을 때 생기는 문제점을 따로 언급하지 않겠다. (유명한 저서인 '리팩토링'을 참고할 것.)

D - DIP(Dependency Inversion Principle) : 의존성 역전 법칙

“전화하지마, 내가 전화할께(Don‘t call us, we’ll call you)" - 할리우드 원칙

IOC는 DIP의 중요한 골격이 된다.

서비스 요청자(Actor)는 서비스 제공자(프레임워크)에게 자신을 등록하고 서비스 제공자는 서비스를 마친 후 서비스 요청자에게 미리 정의해둔 인터페이스를 통해 결과를 알려준다. 여기서 ‘미리 정의해둔 인터페이스’를 흔히 훅(Hook) 메쏘드라고 부르며 훅 메쏘드는 ‘역전’을 위한 매개 포인트가 되는 것이다.

OCP와 DIP가 다른 점은 DIP는 IOC를 한다는 것이다.

이 원칙을 나는 발상의 전환이라고 하고 싶다. 이는 할리우드 원칙이 제시하는 예시에서 바로 느낄 수 있다. 단순하게 연락하는 방향을 바꿨을 뿐인데, 처리해야할 일이 많이 줄어들었다.

이로 인해서 ISP를 이용하여 확장성을 획득할 수도 있고, 비동기 처리도 구현할 수 있게 된다.

이때 발상의 전환은 구체적으로 프레임워크, 인터페이스(훅 메소드)를 통해서 구현된다. A라는 클래스가 B라는 클래스로 요청하는 방향성이 존재했다고 가정해보자. 이때 B 클래스에 A 클래스의 정보를 등록해두면 B 클래스가 A 클래스를 호출할 수 있게 되고, 요청의 방향성은 역전된다. 이것을 IOC(Inversion of Control)이라고 한다. 이때 A 클래스는 B 클래스에 등록되면서 자기 자신을 B 클래스가 호출할 수 있도록 인터페이스를 갖춘다. 즉, B 클래스가 A 클래스를 호출할 수 있는 수단을 갖추는 것이다. 그리고 이 인터페이스를 훅 메소드라고도 한다. 내가 생각하기엔 갈고리처럼 해당 클래스를 낚아채는 메소드이기 때문에 이렇게 이름을 지은 것 같다.

참고문서

https://zdnet.co.kr/view/?no=00000039134727
https://zdnet.co.kr/view/?no=00000039135552
https://zdnet.co.kr/view/?no=00000039139151
https://zdnet.co.kr/view/?no=00000039137043

profile
Try again, Fail again, Fail better

0개의 댓글