-
SOLID원칙 이란 러이 2000년대 초반 로버트 마틴이 명명한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더스가 두문자어 기억술로 소개한 것이다. 프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용할 수 있다.[3] SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스 코드가 읽기 쉽고 확장하기 쉽게 될 때까지 소프트웨어 소스 코드를 리팩터링하여 코드 냄새를 제거하기 위해 적용할 수 있는 지침이다. 이 원칙들은 애자일 소프트웨어 개발과 적응적 소프트웨어 개발의 전반적 전략의 일부다. 출처 : 위키백과
1. 단일책임 원칙(Single Responsiblity Principle)
하나의 객체는 하나의 책임만 가져야한다.
모든 클래스는 하나의 책임을 행하는데 집중되여야한다. 이것은 어떤 변화 때문에 클래스를 변경하는 이유는 하나 여야만한다. 책임이 많은 클래스는 각각 책임이 변경 될 때 마다 변화가 생기므로 클래스는 하나의 책임을 가지게 설계를 해야한다.
객체지향 원칙의 기초적인 원칙이며 잘 적용시켜 하나의 책임으로 적절히 분배하면 클래스의 결합도는 낮아지고 응집도는 높아져 클래스간의 영향도 줄어들고 코드의 가독성, 유지보수에서도 이점이 생긴다.
> 결합도 : 객체 간의 의존관계의 정도를 말한다. 의존관계가 높으면 결합도가 높다고 말한다.
> 응집도 : 객체가 가진 기능들이 같은 책임(역할)을 행하는 정도를 말한다. 같은 책임을 행할 수록 응집도 높다고 말한다.
2. 개방폐쇄 원칙(Open - Closed Principle)
소프트웨어의 요소(모듈,클래스,함수 등)들은 변경에는 닫혀있어야하고 확장에는열려있어야 한다.
소프트웨어 개체의 행위는 확장할 수 있어야 하지만, 이때 개체를 변경해서는 안 된다
소프트웨어에 변경이나 추가가 될 때 기존요소들에 변경이 없어야 하고 재사용하여 확장 가능 해야한다.
소프트웨어를 확장하기 쉬운 동시에 변경으로 인해 소프트웨어에 많은 영향을 받지 않는 것을 목표로 한다.
이에 기능을 추가하거나 변경 할 때 기존 코드변경을 하지 않고 새로운 코드를 추가되어 이뤄저야 하고 이것은 관리가 용이하고 재사용 가능한 코드를 만드는 기반이 되는 매커니즘이다.
이 원칙은 추상화(Abstraction)와 다형성(Polymorphism)을 통해 이루어지고 객체지향을 가능케하는 아주 중요한 원리이다.
3. 리스코프 치환 원칙(Liskov Substitution Principle)
프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다
(기본 클래스에 대한 포인터 또는 참조를 사용하는 함수는 파생 클래스의 개체를 알지 못하는 상태에서 사용할 수 있어야 한다)
즉 상위타입의 객체를 사용하는 프로그램은 상위타입을 상속받은 하위타입으로 치환하여도 문제없이 사용가능하여야한다. 사용가능하다는 말은 자식클래스는 부모클래스의 역할을 수행 할줄 아는 것을 얘기한다.이 원칙은 개방폐쇄 원칙에서 얘기하는 다향성을 설명한다.
LSP 를 위반하는 문제
LSP 위반을 설명 할 때 가장 많이 나오는 예시는 직사각형을 상속하는 정사각형의 문제이다.
직사각형을 상속해 구현하는 정사각형 기본적으로 정사각형은 높이와 넓이가 둘다 같아야 하는 제약조건이 있다. 이때 User가 사용하는 Reactangle 의 넓이를 수정하는 로직을 수행한다고 할 때 LSP를 따르려면 Reactangle을 상속받은 Square 으로 치환하여 로직수행(넓이만 변경)이 가능해야 하지만 정사각형의 제약조건 때문에 User가 기대하는 값을 얻을수 없기 때문에 이는 LSP를 위반한다. 이문제를 해결할려면 넓이 수정을 할 때 마다 사각형의 타입을 확인하는 로직을 추가해야 하는데 이는 개방폐쇄 원칙을 위반하게 된다(타입이 늘어날수록 User 의 변경이 불가피하다, LSP를 위반시 같이 위반할 가능성이 높다)
리스코프 치환 원칙은 처음 등장했을 때는 상속에 대한 개념을 정하는 원칙이였지만 지금은 코드단에서 뿐만아니라 아키텍쳐를 설계할때도
적용되는 원칙으로 바뀌었다. LSP를 지킨다는 의미는 현재사용하는 모듈에서 새로운 모듈로 변경해도 아무 문제없이 사용이 가능하고 이는 확장에 대해서 OCP를 보장한다고 말할 수 있다.
4. 인터페이스 분리 원칙(Interface Segregation Principle)
클래스는 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.
인터페이스 크기가 크다는건 곧 지킬 책임이 많다는걸 의미한다.때문에 클래스는 자신이 사용하지 않은 기능의 인터페이스는 구현하지 말아야한다. 그래서 하나의 일반적인 인터페이스 보단 쓰임에 맞게 구체적인 인터페이스들로 나눠서 필요한 책임을 덜어줘야한다. 단일책임원칙은 클래스의 책임에 대해 얘기한다면 인터페이스 분리원칙은 인터페이스의 책임에 대해서 말한다.
예시
clean architecture OPS를 사용하는 User1, User2, User3 이 있다. 이때 각 유저들이 하나의 op만 사용한다고 가정한다.
User1 은 op2 와 op3 를 사용하지 않지만 의존되어 해당 기능을 구현해야한다. 또한 User2 의 필요에 따라 op2 파라미터를 변경하면 나머지 OPS를 의존하는 유저들도 코드를 변경하여 다시 컴파일 하여야한다.
적용
clean architecture 인터페이스를 두고 op 를 유저의 필요에 맞춰 분리 했다. 이렇게 되면 User1 은 U1:Ops::op1 을 의존하여 기능을 사용하지만 OPS를 의존하지 않는다.이렇기 때문에 나머지 op2, op3 에 변화가 생겨도 User1은 영향을 받지 않게 된다.
5. 의존관계 역전 원칙(Dependency Inversion Principle)
고수준(상위) 모듈이 저수준(하위)모듈을 의존하는 것이 아닌 고수준 모듈이 정의한 추상타입을 저수준모듈이 의존해야한다.
의존성에 관한 원칙으로 변화가 자주 일어 나고 바뀌는 것(저수준 모듈)에 의존하는 것 보다 변화가 없는(고수준 모듈) 것 에 의존해야 한다.
고수준 모듈과 저수준 모듈의 차이
고수준 모듈
- 의미 있는 단일 기능 제공
- 상위수준의 정책 구현
저수준 모듈
- 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현
고수준 모듈이 저수준 모듈에 의존할 때 발생하는 문제
기능 요구사항 : 할인정책에 따라 가격할인을 받을 수 있고 할인정책은 하나만 적용가능하다.
고수준 모듈 : 할인정책에 따라 할인된 가격을 계산하다.
저수준 모듈 : 비율 할인 , 정액할인 등 다양한 할인으로 할인 정책을 정해서 가격계산에 반영한다.
Calculate 클래스가 균일요금 할인 정책을 의존하여 할인금액을 계산한다.(정책 조건의 만족여부는 생략)
public class Calculate { RateDiscountPolicy rateDiscountPolicy; //균일요금 할인 정책 public int calc(int price,Condition... conditions) { if(conditions) return price - rateDiscountPolicy.getDiscountPrice(price); else return price; } }
현재 코드는 서비스가 요구하는 대로 문제없이 작동되지만 만약 할인정책이 추가가 되면 문제가 생긴다.
어느날 회사에서 균일요금 할인 정책이 아닌 10%의 할인을 제공해주는 할인정책을 추가로 구현해돌라고 요청이 왔다.
그러면 기존 균일요금 할인 정책만 의존 하고 있던 Calculate 클래스는 새로운 정책이 생김에 따라 코드의 수정이 생긴다.
public class Calculate { RateDiscountPolicy rateDiscountPolicy; //균일요금 할인 정책 PercentDiscountPolicy percentDiscountPolicy //퍼센트 할인정책 클래스를 새롭게 의존 하게된다. public int calc(int price,Condition... conditions) { if(conditions) return price - rateDiscountPolicy.getDiscountPrice(price); else if(...) return price - percentDiscountPolicy.getDiscountPice(price); } }
Calculate 클래스는 PercentDiscountPolicy 클래스가 추가 되었기 때문에 코드를 수정할 수 밖에 없게된다.
변화가 생긴 쪽은 저수준모듈인 할인정책의 추가 인데 고수준모듈인 계산클래스도 같이 변경이 된다.
이처럼 변화와 변경이 잦은 저수준 모듈 을 의존하게 되면 그걸 의존하는 모듈도 추가변경이 불가피 해지며 이는 결코 좋은 것이 아니다.
수정된 코드
그럼 고수준 모듈이 필요한 할인정책을 추상화한 타입을 저수준 모듈이 의존하게 만들어보자.
//고수준 모듈인 계산클래스에서 필요한 할인정책을 추상화 하였다. public interface DiscountPolicy { public int getDiscountPolicy(int price); }
//기존 할인 정책은 고수준모듈에서 정의한 DiscountPolicy를 의존(구현)하여 재작성 하였다 public class RateDiscountPolicy implements DiscountPolicy{ ... } public class PercentDiscountPolicy implements DiscountPolicy{ ... }
계산 클래스의 변화
public class Calculate { private DiscountPolicy discountPolicy; public int calc(int price,Condition... conditions) { if(conditions) return price - discountPolicy.getDiscountPrice(price); } }
이렇게 코드를 변경하게 되면 달리 새로운 정책이 추가되어도 DiscountPolicy를 의존하여 구현하기 때문에 고수준모듈은 추가 변경이 필요없게 된다. 그리고 의존관계를 구현하는 방법중에는 의존성주입이존재한다.
UML 새로운 정책이 생김에도 기존에 있던 모듈을 수정하지 않는 것은 곧 개방폐쇄 원칙에서 얘기하는 추상화를 설명한다.
고수준 패키지, 저수준 패키지
고수준 패키지에는 고수준 모듈과 고수준 모듈이 정의한 추상타입이 포함된다. 저수준 패키지는 추상타입을 구현한 저수준모듈이 저수준 패키지에 포함된다.
패키지 저수준과 고수준 모듈을 정확하게 분리하면 패키지도 명확하게 분리가 가능해진다.
'정리' 카테고리의 다른 글
IntelliJ 내가 자주사용하는 단축키 정리 (0) 2022.03.30