SOLID ( 객체 지향 설계 원칙 )

뾰족머리삼돌이·2024년 2월 12일
0

JAVA

목록 보기
8/8

S.O.L.I.D

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

위키피디아에서 설명하듯이 SOLID란, 유지보수와 확장이 쉬운 시스템을 만들고자 할 때의 소프트웨어 개발 원칙을 뜻한다

Single responsibility principle : 단일 책임 원칙
Open/closed principle : 개방-폐쇄 원칙
Liskov substitution principle : 리스코프 치환 원칙
Interface segregation principle : 인터페이스 분리 원칙
Dependency inversion principle : 의존관계 역전 원칙

줄여서 각각 SRP, OCP, LSP, ISP, DIP라고 표현된다

해당 원칙들은 OOP의 4대 특징 ( 캡슐화, 상속, 추상화, 다형성 ) 과 함께 객체지향의 핵심이라고도 생각되는 유지보수와 확장성이 뛰어난 시스템을 설계하기 위해서는 반드시 알고 있어야한다

흔히 사용되는 디자인 패턴 또한 객체 지향 설계의 일반적인 문제들에 대한 일반적인 해결책 으로, SOLID 원칙을 따르고있다

SRP, 단일 책임 원칙

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

다르게 말하면 하나의 클래스에 너무 많은 기능을 넣으면 안된다는 의미이다

왜 그런가?를 생각하기 위해서는 반대행위를 했을 때 일어나는 일을 생각해봐야한다


하나의 클래스에 많은 기능을 넣는다면?

먼저 클래스에 문제가 생긴다면 전체 시스템에 에러가 생길 가능성이 높다

하나의 클래스에 너무 많은 기능을 넣게되면 시스템 내에서 해당 클래스의 비중이 올라가게 된다
다르게 말하면 시스템 자체가 해당 클래스에 대한 높은 의존도를 가진다는 뜻이기 때문에, 클래스에 문제가 생기면 전체 시스템의 문제가 된다

또한 수정사항이 발생한 경우, 해당 위치를 찾고 연관관계를 파악하는데 많은 시간이 소모된다

클래스에 많은 기능이 들어간다면 원하는 기능이 동작하는 위치를 찾는게 쉽지않다
게다가 기능들 사이에 연관관계가 존재한다면 각 위치를 파악하고 수정하는데 많은 시간이 소모된다

팀 프로젝트를 감안했을 때, 팀 내의 인원들이 각자의 작업을 위해 하나의 클래스를 수정한다면 어느 것이 최신본인지 파악하는 것에도 어려움이 있을 것이다


SRP를 준수하게되면 코드 가독성과 유지보수의 측면에서 이점을 가지게된다
이를 더욱 효과적으로 하기 위해서는 몇 가지 사항을 유의해야한다

먼저 클래스에 유의미한 이름을 부여하는게 중요하다
즉, 클래스의 명칭을 통해 어떤 작업을 수행할지 알 수 있게 해야한다

또한 기능에 대해 한 클래스내에서 파악할 수 있도록 구성하는게 좋다
특정 클래스가 담당하는 기능이 어떤 것인지를 파악하기 위해 가능한 해당 클래스만 살펴보게 해야한다
이는 결합도, 응집도와 관련이 있는 내용으로, 기능을 위해 외부조건이나 다른 클래스를 확인하지 않아도 되도록 설계하는게 좋다

OCP, 개방-폐쇄 원칙

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

직접적인 코드는 수정하지 않되, 다양한 상황에 대처할 수 있도록 설계하는 것을 의미한다

이는 추상화와 밀접한 관계가 있다
추상화는 객체 또는 클래스의 핵심적인 특징을 파악하고 분리하여 관리하는 것을 의미한다
추상클래스나 인터페이스를 통해 구현되며, 직접적인 수정을 하지않고 항상 동일한 방법을 통해 동작하는 것을 보장한다

따라서 개발자는 상속을 통하여 기존 코드를 수정하지않고 기능을 확장시키는게 가능해진다

예를들어 LinkedListArrayList는 서로 내부구현이 다르지만 동일한 List를 상속하여 구현했다
외부에서는 동일한 사용방법을 통해서 사용할 수 있으며, 만일 새로운 구성의 List를 추가하더라도 기존 코드를 수정할 필요가 없으므로 OCP를 만족한다고 볼 수 있다

LSP, 리스코프 치환 원칙

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다

하위타입의 인스턴스는 상위타입의 인스턴스로써 동작할 수 있어야한다는 의미다
상속을 통해 확장을 진행한 경우, 하위타입은 상위타입을 포함하게된다
따라서 하위타입은 상위타입의 동작을 그대로 수행할 수 있어야한다

재정의를 통해 수정된 내부동작이 기존 동작과 동떨어지면 안된다

관련된 예시로는 흔히 사용되는 ArrayList가 있다
List<Integer> list = new ArrayList<>();와 같이 선언되어 부모 타입의 인스턴스로써 동작하는데 문제가 없는 것을 볼 수 있다


유의사항

LSP는 필연적으로 상속이 연관되어있다
상속은 OOP 4대 특징에 들어갈만큼 중요한 특징으로, 그 핵심은 코드의 재사용에 있다

하지만 무분별한 상속의 사용은 클래스간의 결합도를 높히기 때문에 연쇄적인 효과가 발생할 가능성이 늘어난다
하나의 상위클래스를 수정하게되면 하위클래스가 모두 영향을 받기 때문이다

따라서 상속은 클래스간에 IS-A 또는 IS-A-KIND-OF 의 관계가 성립할 때만 사용하는게 좋다
이 외의 상황에서는 필드로 이용하거나 ( 컴포지션 ), BE-ABLE-TO의 관계일 경우 인터페이스를 구현하는 것이 권장된다


ISP, 인터페이스 분리 원칙

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

인터페이스판 SRP라고 볼 수 있다
하나의 인터페이스에 너무 많은 기능을 집어넣지 말라는 말이다

인터페이스는 클래스와 다르게 다중상속이 가능하다
그렇기 때문에 하나의 인터페이스에 다양한 기능을 넣기보다는 각 기능별로 구현해놓는게 관리에 용이하다

SRP와 동일하게 특정 기능에 문제가 생겼을 때, 이를 파악하고 대처하기 쉽기 때문이다


유의사항

ISP는 클라이언트가 원하는 기능만 구현할 수 있도록 기능에 따라 인터페이스를 분리하라는 내용이다
이는 SRP와 매우 유사한데, 마냥 같은 내용은 아니다

상황에 따라서 책임에 맞게 분리하였지만, 클라이언트에게는 불필요한 내용이 포함되는 경우가 있을 수 있기 때문이다
해당 예제는 이곳에서 확인할 수 있다

또한 다양한 곳에서 사용됨을 상정하여 구현해야하기 때문에, 한번 설계를 했다면 다시 인터페이스를 분리하지 말아야한다


DIP, 의존 역전 원칙

추상화에 의존해야지, 구체화에 의존하면 안된다

외부 의존성을 가지는 경우 인터페이스 타입을 이용해라는 의미이다
즉, 클래스가 구현하고있는 인터페이스 또는 상위타입이 존재한다면 해당 인터페이스 타입을 이용하라는 말이다

역시 클래스타입을 이용하는 경우의 문제점을 생각해봐야한다


클래스타입을 이용하는 경우

정확하게 특정 클래스를 참조하는 경우, 해당 클래스에 문제가 발생하면 현재 클래스에도 문제가 발생한다
문제는 하위타입의 클래스는 자주 변경되는 경우가 많다는 것이다

상황에 따라서 메서드의 동작이 변경될 수도 있고, 또는 클래스 자체가 더이상 사용되지 않을수도 있다
해당 클래스를 직접 참조하게 되면 클래스와 운명을 같이하게 된다


상위타입을 이용하여 참조를 하게되면 클래스에 문제가 생긴경우, 상위 타입을 구현하는 다른 클래스로 손쉽게 변경할 수 있다
또한, 상위타입은 수정이 자주 일어나지 않기때문에 안정적이기도 하다

이는 앞서 살펴본 OCPLSP와도 밀접한 관계가 있다
OCP는 추상화가 핵심이었고, LSP는 상속이 핵심이었다

OCP는 추상화를 이용하여 기존 코드를 수정하지않고 기능을 확장하는 것이 목표였으며
LSP는 상속을 이용하여 구현된 하위 타입의 인스턴스가 상위타입으로써 동작할 수 있어야함을 의미했다

참고 사이트

객체 지향 설계의 5가지 원칙 - S.O.L.I.D
리팩토링 구루
객체의 결합도와 응집도

0개의 댓글