디자인 패턴에 대한 글을 쓰다가 "SOLID원칙" 이라는 단어를 알게 되어 정리해보려 합니다
SOLID 원칙이란?
객체지향 설계에서 지켜줘야할 5개의 원칙(SRP, OCP, LSP, DIP, ISP)를 말합니다.
하지만 개념을 알아도 이를 적용하여 개발하는것이 어려운 원칙들입니다.
설계원칙을 알아야 하는 이유
- 시스템에 예상하지 못한 변경사항이 발생하더라도 유연하게 대처하기 위해
- 이후에 확장성있는 시스템 구조를 설계하기 위해.
좋은 설계란?
- 시스템에 새로운 요구사항이나 변경사항이 있을 때, 영향을 받는 범위가 적은 구조를 말한다
- 여러 디자인 패턴들은 SOLID원칙에 입각해 만들어진 것이다
1. SRP (Single Responsibility Principle), 단일 책임 원칙
객체는 단 하나의 책임만 가져야 하며, 클래스는 그 책임을 완전히 캡슐화해야한다
객체지향적으로 설계할 때 응집도를 높게, 결합도는 낮게 설계하는 것이 좋다.
- 응집도 : 한 프로그램의 요소가 얼마나 뭉쳐있는지, 즉 구성요소들 사이의 응집력을 말한다
- 결합도 : 프로그램 구성 요소들 사이가 얼마나 의존적인지 말한다
- SRP에 따른 설계 시 응집도 높게, 결합도 낮게 설계 가능
- 책임은 변경하려는 이유로 정의하고, 어떤 클래스나 모듈은 변경하려는 단 하나 이유만 가져야한다는 결론을 내림
예시
- 보고서 편집, 출력 모듈
- 변경 사유 : 1) 보고서 내용 - 실질적 2) 보고서 형식 - 꾸미기
- 단일 책임 원칙에 의하면 두 책임을 분리된 클래스나 모듈로 나누어야 한다.
- 두가지를 묶는 것은 나쁜 설계일 수 있다
- 편집 과정에 변경이 일어나면 같은 클래스의 일부로 있는 출력 코드가 망가질 위험이 있다
요즘 버전의 새로운 정의 : 각각의 [module]은 단 한가지 일만 해야하며, 그 일을 잘해야한다.
이 원칙은 [높은 응집]과 연관되어있다. 특히 코드에서 여러가지 역할 혹은 목적을 함부로 섞으면 안된다.
아래는 [Javascript]를 사용한 FP(Functional Programming)버전의 예시다
const saveUserDetails = (user) => { ... }
const performOrder = (order) => { ...}
const shipItem = (item, address) => { ... }
export { saveUserDetails, performOrder, shipItem };
// calling code
import { saveUserDetails, performOrder, shipItem } from "allActions";
이는 [microservice] 디자인에도 적용가능하다. 만약 위 세 함수 역할을 하는 하나의 서비스가 있다면, 이는 과한 행동을 하는 함수이다,
2. OCP (Open-Closed Principle), 개방-폐쇄 원칙
기존의 코드를 변경하지 않으면서(closed), 기능을 추가할 수 있도록(open) 설계해야한다
즉, 확장에 대해서는 개방적이고 수정에 대해서는 폐쇄적이어야 한다
개방-폐쇄 원칙이 잘 적용되면, 기능을 추가하거나 변경해야할 때 원래 코드를 변경하지 않아도, 기존 코드에 새로운 코드를 추가함으로써 기능의 추가나 변경이 가능하다!
- "확장에 개방" 이유는, [class]작성자의 의존도를 제한하기 위해서다.
- [class]를 변경하려면 원본 작성자에게 수정을 요청하거나 내가 직접 deep-dive해 수정을 진행할것이다. 이렇게 변경된 코드는 문제가 생기기 시작하게 되고 이는 [단일 책임 원칙]에 위배된다
- "수정에 폐쇄" 이유는, 다운스트림 소비자를 전부 신뢰할 수 없고, 미숙한 개발자로부터 보호되어야 하기 때문이다.
예시
만족하는 설계가 되려면 캡슐화를 통해 여러 객체에서 사용하는 같은 기능을 인터페이스에 정의하는 방법이 있다.
Animal 인터페이스를 구현한 각 클래스들은 울음소리 crying()함수를 재정의 한다.
울음소리를 호출하는 클라이언트는 다음과 같이 인터페이스에서 정의한 crying()함수만 호출하면 된다
public class Client {
public static void main(String args[]){
Animal cat = new Cat();
Animal dog = new Dog();
cat.crying();
dog.crying();
}
}
이렇게 캡슐화를 하면 동물이 추가되었을 때 crying()함수를 호출하는 부분은 건드릴 필요가 없으면서 쉽게 확장할 수 있게 된다.
요즘 버전의 새로운 정의 : 재작성하는 것 대신 [module]을 사용하거나 추가할 수 있어야한다.
이는 OOP (Object Oriented Programming)에서는 쉽게 와닿는다. 반면 FP (Functional Programming)세계에서는 수정을 허용하기 위해서는 외부의 Hook points를 선언해주어야만 한다.
다음 함수는 추가적인 함수를 전달해 before, after [hook]뿐 아니라 기본동작까지 override할 수 있는 예시다.
// library code
const saveRecord = (record, save, beforeSave, afterSave) => {
const defaultSave = (record) => {
// default save functionality
}
if (beforeSave) beforeSave(record);
if (save) {
save(record);
}
else {
defaultSave(record);
}
if (afterSave) afterSave(record);
}
// calling code
const customSave = (record) => { ... }
saveRecord(myRecord, customSave);
3. LSP (Liskov Substitution Principle), 리스코프 치환 원칙
자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야한다.
즉, 자식 클래스는 언제나 부모 클래스의 역할을 대체할 수 있어야 한다는 것을 말하며, 부모 클래스와 자식 클래스의 행위가 일관됨을 의미
자식클래스가 부모 클래스를 대체하기 위해서는 부모의 기능에 대해 ˚오버라이드 되지 않도록 하면 된다.
즉, 자식 클래스는 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행하도록 해야 LSP를 만족하기 된다.
˚오버라이드 : 조상 클래스로부터 상속받은 메소드의 내용을 상속받는 클래스에 맞게 변경하는 것
LSP에 따르면 객체지향적으로 설계하기 위해서는 오버라이드는 가급적 피하는것이 좋다고 한다.
예시
Vehicle의 확장을 통해 Bicycle도 동일하게 작동할 것을 인지할 수 있다.
- 이는 OOP의 기본 속성이다. subclass를 부모 class 대신에 사용이 가능해야한다는 것을 의미한다,
- T타입을 지닌 모든 객체가 T처럼 행동할것이란 확신을 갖고 안적하게 의존할 수 있게 된다.
class Vehicle {
public int getNumberOfWheels() {
return 4;
}
}
class Bicycle extends Vehicle {
public int getNumberOfWheels() {
return 2;
}
}
// calling code
public static int COST_PER_TIRE = 50;
public int tireCost(Vehicle vehicle) {
return COST_PER_TIRE * vehicle.getNumberOfWheels();
}
Bicycle bicycle = new Bicycle();
System.out.println(tireCost(bicycle)); // 100
4. ISP (Interface Segregation Principle), 인터페이스 분리 원칙
자신이 사용하지 않는 인터페이스는 구현하지 말아야한다는 설계 원칙
하나의 거대한 인터페이스 보다는 여러개의 구체적인 인터페이스가 낫다는걸 의미
SRP는 객체의 단일 책임을 뜻한다면, ISP는 인터페이스의 단일 책임을 의미한다
예시
핸드폰에는 전화, 문자, 알람, 계산기 등의 기능이 있다.
3G폰과 스마트폰에 들어가는 기능들은 Phone의 기능을 사용하므로 call, sms, alarm, calculator기능이 정의된 Phone 인터페이스를 정의하려한다,
ISP를 만족하기 위해서는 Phone 인터페이스에 call(), sms(), alarm(), calculator()함수를 모두 정의하는 것보다,
Call, Sms, Alarm, Calculator 인터페이스를 각각 정의하여 3G폰과 스마트폰 클래스에서 4개의 인터페이스를 구현하도록 설계해야한다.
이렇게 설계하면 각 인터페이스의 메서드들이 서로 영향을 미치지 않게 된다
즉, 자신이 사용하지 않는 메서드에 대해 영향력이 줄어들게 된다
5. DIP (Dependency Inversion Principle), 의존 역전 원칙
객체들이 서로 정보를 주고 받을 때 의존관계가 형성되는데, 이 때 객체들은 나름대로의 원칙을 갖고 정보를 주고 받아야한다는 원칙
나름대로 원칙이란, 추상성이 낮은 클래스보다 추상성이 높은 클래스와 의존 관계를 맺어야한다는 것을 의미한다.
일반적으로 인터페이스를 활용하면 이 원칙을 준수할 수 있게 된다 (캡슐화)
Client 객체는 Cat, Dog, Gird의 crying()메서드에 직접 접근하지 않고, Animal 인터페이스의 crying()메서드를 호출함으로써 DIP를 만족할 수 있다
solid원칙에 대해 정리해봤습니다. 다음엔 solid원칙에 기반한 다른 디자인 패턴들에 대해서도 정의해보도록 하겠습니다
출처에 있는 블로그를 참고했습니다 정리가 잘 되어있으니 참고해보세요 :)
출처
- https://ko.wikipedia.org/wiki/SOLID_(%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%EC%84%A4%EA%B3%84)
- https://morohaji.tistory.com/73?category=938495
- https://victorydntmd.tistory.com/291