05AM

SOLID 원칙 본문

1 week 1 conquer/CS

SOLID 원칙

_05AM 2023. 4. 21. 18:04

정의

[위키백과 - SOLID 원칙]

컴퓨터 프로그래밍에서 SOLID란 로버트 마틴이 2000년대 초반에 명명한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더스가 두문자어 기억술로 소개한 것이다. 프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용할 수 있다.

SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스 코드가 읽기 쉽고 확장하기 쉽게 될 때까지 소프트웨어 소스 코드를 리팩터링하여 코드 냄새(code smell)를 제거하기 위해 적용할 수 있는 지침이다. 이 원칙들은 애자일 소프트웨어 개발과 적응적 소프트웨어 개발의 전반적 전략의 일부다.

 

※ 아래 내용은 넥스트리소프트의 게시물을 읽고, 그 중 일부를 발췌하여 정리하였습니다.

1. 단일 책임의 원칙 : SRP (Single responsibility principle)

There should never be more than one reason for a class to change.
: 클래스가 변경되는 이유가 하나 이상 있어서는 안 됩니다.

한 클래스는 하나의 책임만 가져야 한다.

 

작성된 클래스는 하나의 기능만 가지며 클래스가 제공하는 모든 서비스는 그 하나의 책임을 수행하는데 집중되어 있어야 한다는 원칙이다.

 

이는 어떤 변화에 의해 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 함을 의미한다. SPR원리를 적용하면 무엇보다도 책임 영역이 확실해지기 때문에 한 책임의 변경에서 다른 책임의 변경으로 연쇄작용에서 자유로울 수 있다. 뿐만 아니라 책임을 적절히 분배함으로써 코드의 가독성 향상, 유지보수 용이라는 이점까지 누릴 수 있으며 다른 원리들을 적용하는 기초가 된다.

 

적용 방법

리팩토링에서 소개하는 해결방법은 SPR 원리와 관련이 있다. 그 근본정신이 항상 객체들의 책임을 최상의 상태로 분배한다는 것에서 비롯되기 때문이다.

 

여러 원인에 의한 변경으로 혼재된 각 책임을 각각의 개별 클래스로 분할하여 클래스당 하나의 책임만을 맡도록 하는 것이다. 여기서의 관건은 책임만 분리하는 것이 아니라 분리된 두 클래스 간의 관계 복잡도를 줄이도록 설계하는 것이다. 각각의 클래스들이 유사하고 비슷한 책임을 중복해서 갖고 있다면 공유되는 요소를 부모 클래스로 정의하여 위임한다.

 

따라서 각각의 개별 클래스들의 유사한 책임들은 부모에게 위임하고 다른 책임들은 각자에게 정의할 수 있다.

2. 개방-폐쇄 원칙 : OCP (Open/closed principle)

You should be able to extend a classes behaviour, without modifying it.
: 클래스 동작을 수정하지 않고 확장할 수 있어야 합니다.

소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

 

이것은 변경을 위한 비용은 가능한 줄이고 확장을 위한 비용은 가능한 극대화해야 한다는 의미로, 요구사항의 변경이나 추가사항이 발생하더라도 기존 구성요소는 수정이 일어나지 말아야 하며, 기존 구성요소를 쉽게 확장해서 재사용할 수 있어야 한다는 뜻이다.

 

OCP는 관리가능하고 재사용 가능한 코드를 만드는 기반이며, OCP를 가능케하는 주요 메커니즘은 추상화와 다형성이라고 말한다.

 

적용 방법

1. 변경(확장)될 것과 변하지 않을 것을 엄격히 구분한다.

2. 이 두 모듈이 만나는 지점에 인터페이스를 정의한다.

3. 구현에 의존하기보다 정의한 인터페이스에 의존하도록 코드를 작성한다.

 

3. 리스코프 치환 원칙 : LSP (Liskov substitution principle)

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
: 기본 클래스에 대한 포인터나 참조를 사용하는 함수는 파생 클래스의 객체를 알지 못하고 사용할 수 있어야 합니다.

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다. 계약에 의한 설계를 참고하라.

 

서브 타입은 언제나 기반 타입과 호환될 수 있어야 한다. 달리 말하면 서브 타입은 기반 타입이 약속한 규약(public interface, 메소드가 던지는 예외 등)을 지켜야 한다.

 

상속은 구현 상속(extends)이든 인터페이스 상속(implements)이든 궁극적으로 다형성을 통한 확장성 획득을 목표로 한다. LSP 원리도 역시 서브 클래스가 확장에 대한 인터페이스를 준수해야 한다는 것을 의미한다. 다형성과 확장성을 극대화하려면 하위 클래스를 사용하는 것보다는 상위의 클래스(인터페이스)를 사용하는 것이 더 좋다. 일반적으로 선언은 기반 클래스로, 생석은 구체 클래스를 대입하는 방법을 사용한다.

상속을 통한 재사용은 기반 클래스와 서브 클래스 사이에 is-a관계가 있을 경우로만 제한되어야 한다. 그 외의 경우에는 합성을 이용한 재사용을 해야 한다.

 

이런 LSP를 바탕으로 OCP는 확장하는 부분에 다형성을 제공해 변화에 열려있는 프로그램을 만들 수 있도록 한다.

 

적용 방법

1. 만약 두 개체가 똑같은 일을 한다면 둘을 하나의 클래스로 표현하고 이들을 구분할 수 있는 필드를 둔다.

2. 똑같은 연산을 제공하지만, 이들은 약간씩 다르게 한다면 공통의 인터페이스를 만들고 둘이 이를 구현 한다.(인터페이스 상속)

3. 공통된 연산이 없다면 완전 별개인 2개의 클래스를 만든다.

4. 만약 두 개체가 하는 일에 추가적으로 무언가를 더 한다면 구현 상속을 사용한다.

 

4. 인터페이스 분리 원칙 : ISP (Interface segregation principle)

Clients should not be forced to depend upon interfaces that they do not use.
: 클라이언트가 사용하지 않는 인터페이스에 의존하도록 강요해서는 안 됩니다.

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

 

클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원리이다. 즉, 어떤 클래스가 다른 클래스에 종속될 때에는 가능한 최소한의 인터페이스만을 사용해야 한다.

만약 어떤 클래스를 이용하는 클라이언트가 여러 개고 이들이 해당 클래스의 특정 부분집합만을 이용한다면, 이들을 따로 인터페이스로 빼내어 클라이언트가 기대하는 메시지만을 전달할 수 있도록 한다.

 

SRP가 클래스의 단일책임을 강조한다면, ISP는 인터페이스의 단일책임을 강조한다. 하지만 ISP는 어떤 클래스 혹은 인터페이스가 여러 책임 혹은 역할을 갖는 것을 인정한다. 이러한 경우 ISP가 사용되는데 SRP가 클래스 분리를 통해 변화에의 적응성을 획득하는 반면, ISP에서는 인터페이스 분리를 통해 같은 목표에 도달한다.

 

적용 방법

1. 클래스 인터페이스를 통한 분리

 

클래스의 상속을 이용하여 인터페이스를 나눌 수 있다.

이와 같은 구조는 클라이언트에게 변화를 주지 않을 뿐 아니라 인터페이스를 분리하는 효과를 갖는다. 하지만 거의 모든 객체지향 언어에서는 상속을 이용한 확장은 상속받는 클래스의 성격을 디자인 시점에 규정해버린다. 따라서 인터페이스를 상속받는 순간 인터페이스에 예속되어 제공하는 서비스의 성격이 제한된다.

 

2. 객체 인터페이스를 통한 분리

 

위임을 이용하여 인터페이스를 나눌 수 있다.

위임이란, 특정 일의 책임을 다른 클래스나 메소드에 맡기는 것이다. 만약 다른 클래스의 기능을 사용해야 하지만 그 기능을 변경하고 싶지 않다면, 상속 대신 위임을 사용한다.

5. 의존관계 역전 원칙 : DIP (Dependency inversion principle)

High level modules should not depend upon low level modules. Both should depend upon abstractions.
: 상위 레벨 모듈은 하위 레벨 모듈에 의존해서는 안 됩니다. 둘 다 추상화에 의존해야 합니다.

Avstractions should not depend upon details. Details should depend upon abstractions.
: 추상화는 세부 사항에 의존해서는 안 됩니다. 세부 사항은 추상화에 따라 달라져야 합니다.

프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

 

의존 관계의 역전(Dependency Inversion)이란 구조적 다자인에서 발생하던 하위레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하는 위계관계를 끊는 의미의 역전이다. 실제 사용 관계는 바뀌지 않으며, 추상을 매개로 메시지를 주고 받음으로써 관계를 최대한 느슨하게 만드는 원칙이다.

 

DIP의 키워드는 'IOC', '훅 메소드', '확장성' 이다. 이 세 가지 요소가 조합되어 복잡한 컴포넌트들의 관계를 단순화하고 컴포넌트 간의 커뮤니케이션을 효율적이게 한다.

 

DIP는 복잡하고 지난한 컴포넌트 간의 커뮤니케이션 관계를 단순화하기 위한 원칙이다.

적용 방법

- Layering

 

Grady Booch는 잘 구조화된 객체지향 아키텍처들은 각 레이어마다 잘 정의되고 통제되는 인터페이스를 통한 긴밀한 서비스들의 집합을 제공하는 레이어들로 구성되어 있다고 하였다. 이것은 단순히 레이어를 통한 구조화만을 뜻하는 것이 아니라 Transitive Dependency가 발생했을 때 상위 레벨의 레이어가 하위 레벨의 레이어를 바로 의존하게 하는 것이 아니라 이 둘 사이에 존재하는 추상레벨을 통해 의존해야할 것을 말하고 있다.

이를 통해서 상위레벨의 모듈은 하위레벨의 모듈로의 의존성에서 벗어나 그 자체로 재사용되고 확장성도 보장 받을 수 있다. 아래는 이를 도식화한 그림이다.

 

[출처] 객체지향 개발 5대 원리: SOLID (nextree.co.kr)

참고

객체 지향적 관점에서의 has-a와 is-a 차이점 (tistory.com)

객체지향 개발 5대 원리: SOLID (nextree.co.kr)

Comments