SOLID 원칙이란?
시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들기 위해 추구해야하는 원칙이다. SOLID는
1. 단일 책임 원칙 (SRP - Single responsibility principle)
2. 개방-폐쇄 원칙 (OCP - Open/closed principle)
3. 리스코프 치환 원칙 (LSLiskov substitution principle)
4. 인터페이스 분리 원칙 (Interface segregation principle)
5. 의존관계 역전 원칙 (Dependency inversion principle)
위 5개의 원칙의 첫글자를 따서 만든 단어이다.
5개의 원칙들에 대해 알아보자!
1. 단일 책임 원칙 (SRP - Single responsibility principle)
단일 책임 원칙의 정의 : "한 클래스는 하나의 책임만 가져야 한다."
정의만 놓고 보면 클래스는 단 한개의 책임을 가져야 하며 클래스를 변경하는 이유는 단 하나여야 한다. 하지만 '책임'은 너무 추상적인 개념이다. 책임이 클 수도 있고 작을 수도 있으며 문맥과 상황에 따라 다를 수도 있기 때문이다.
이 상황을 쉽게 생각해보면 우리는 '변경의 파급력'에 초점을 맞춰서 생각해 볼 수 있다. 즉 코드의 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른것이다.
만약 A메서드에서 B메서드를 호출하고 B메서드에서 C메서드를 호출하는 로직이 있을 때 내가 A메서드를 수정하게 되면 상황에 따라 B메서드와 C메서드를 변경해야 할 가능성이 있다.(의존성이 높은 경우) 이 경우 유지 보수가 상당히 어려우며 SRP를 잘 지키지 못했다고 볼 수 있다.
2. 개방-폐쇄 원칙 (OCP - Open/Closed Principle)
개방 폐쇄 원칙의 정의 : "확장에는 열려있으나 변경에는 닫혀 있어야 한다."
이 부분도 글씨로만 들으면 에? 이게 뭔소리야? 할 텐데. 개발자가 코드를 확장하려면 코드를 어떻게 변경을 안해? 라고 생각이 들 것이다. 왜냐하면 내가 그랬으니까... 공부하다가 설명이 잘 되어있는 블로그를 발견했는데 예제는 SCB개발자이야기 이분의 블로그가 설명이 잘 되어있으니 참고하면 좋을 것 같다. 저 블로그의 예제와 내용을 요약해 보자면 OCP의 원칙에서
OPEN : 도형은 추가될 수 있다.
Closed : 도형이 추가되더라도 도형의 면적을 구하는 Calculator클래스는 변화하지 않아야 한다.
1. 도형의 면적은 도형의 종류에 따라 구하는 공식이 달라지기 때문에 도형의 면적을 구하는 메서드인 calculateArea라는 메서드를 shape인터페이스에 선언한다.
2. 각 도형 클래스는 calculateArea메서드를 상속받아 각 도형의 면적을 구하는 공식에 맞게 구현한다.
3. 면적을 구하는 클래스인 AreaCalculator는 도형의 타입을 파라미터로 받아 파라미터로 받은 도형클래스에서 구현한 면적을 구하는 메서드를 호출한다.
4. 나중에 호출 할 때에는 면적을 구하고 싶은 메서드의 객체를 생성하여 AreaCalculator.calculateArea의 파라미터로 넘겨주게 되면 3번과정이 실행되고 도형의 면적을 return해준다.
shape 인터페이스를 만들어 구현하게 되면 만약 다른 도형의 면적을 구하고 싶을 때에는 다른 도형의 클래스만 추가해주고 객체를 넘겨주면 되기 때문에 기존 코드를 변경하지 않아도 된다. 즉 다형성을 활용해서 개방 폐쇄 원칙을 지킬 수 있다.
코드는 아래와 같다.
interface Shape {
public double calculateArea();
}
class Rectangle implements Shape {
public double length;
public double width;
public double calculateArea() {
return this.length * this.width;
}
}
class Circle implements Shape {
public double radius;
public double calculateArea() {
return 3.14 * this.radius * this.radius;
}
}
class Triangle implements Shape {
public double length;
public double width;
public double calculateArea() {
return this.length * this.width / 2;
}
}
// package calculator
class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.calculateArea();
}
}
public class Sample {
// package main
public static void main(String[] args) {
Rectangle r = new Rectangle();
r.length = 1;
r.width = 1;
Circle c = new Circle();
c.radius = 1;
Triangle t = new Triangle();
t.width = 1;
t.length = 1;
AreaCalculator calculator = new AreaCalculator();
System.out.println(calculator.calculateArea(r));
System.out.println(calculator.calculateArea(c));
System.out.println(calculator.calculateArea(t));
}
}
3. 리스코프 치환 원칙 (LSP - Liskov Substitution Principle)
리스코프 치환 원칙의 정의 : "프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다."
정의만 보면 되게 어렵게 느껴진다. 쉽게 이해하자면 자동차에 대한 인터페이스가 있다. 자동차의 기능중에서 앞으로 가는 기능인 엑셀 기능을 구현했다면, 엑셀을 실행하면 무조건 앞으로 가야한다. 후진 기능을 만들고싶어 뒤로가는 엑셀을 구현했다면 이것은 LSP위반이다. 엑셀의 기능인 앞으로 가는 기능을 구현해야한다. 느리더라도 앞으로 가야한다.
4. 인터페이스 분리 원칙 (ISP - Interface Segregation Principle)
리스코프 치환 원칙의 정의 : "특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다."
클라이언트는 자신이 사용하지 않는 메소드에는 의존하지 않아야 한다는 의미를 가지고 있다. 또한 큰 덩어리의 인터페이스보다 구체적인 기능을 가진 인터페이스로 분리를 해줘야 한다 라는 의미 또한 가지고있다.
아래 코드를 먼저 살펴보면,
아래의 코드는 전자기기에 사용 될 인터페이스를 구현한 것이다. 보면 프린터에 사용 할 powerOn(), print() / 스마트폰에 사용 할 powerOn(), call() 이라는 기능이 있다. 상속을 받게 되면 모든 인터페이스를 무조건 오버라이딩 해야하는 의무가 있다. 그래서 프린터 클래스는 사용하지 않는 call() 이라는 기능도 구현하게 된다. 이는 ISP위반이다. 개선한 코드를 다음 코드박스에서 보자
interface Electronics {
void powerOn();
void print();
void call();
}
class Printer implements Electronics {
@Override
public void powerOn() {
System.out.println('Power On!');
}
@Override
public void print() {
System.out.println('프린트 진행');
}
@Override
public void call() {
System.out.println('전화 걸기');
}
}
ISP원칙을 위반한 코드를 개선해보면 아래와 같이 개선할 수 있다. 전자기기는 전원을 켜야하기 때문에 따로 분리했고 나머지도 마찬가지로 사용에 따라 분리했다. 위의 코드와 다르게 프린터는 사용하지 않는 call()메서드를 구현하지 않아도 된다. 만약 스마트폰이 아닌 2g를 만든다고 해도 전화의 기능이 없어지는건 아니기 때문에 다시 callable인터페이스를 상속받아서 구현하면 된다.
interface Electronics {
void powerOn();
}
interface printable {
void print();
}
interface callable {
void call();
}
class Printer implements Electronics, printable {
@Override
public void powerOn() {
System.out.println('Power On!');
}
@Override
public void print() {
System.out.println('프린트 진행');
}
}
class SmartPhone implements Electronics, callable {
@Override
public void powerOn() {
System.out.println('Power On!');
}
@Override
public void callable() {
System.out.println('전화 걸기!');
}
}
ISP원칙은 자신이 사용하는 메서드외에는 의존하지 않아야 한다는 점에서 SOLID원칙 1번인 SRP원칙과도 비슷한 성질을 가지고 있다.
5. 의존관계 역전 원칙 (DIP - Dependency Inversion Principle)
리스코프 치환 원칙의 정의 : "프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다"
쉽게 이해해 보면 상위의 인터페이스 타입의 객체로 통신하라는 의미이다. 보통 자바에서 ArrayList를 인스턴스화 할 때 변수의 타입을 동일한 타입인 ArrayList로 선언하는 것이 아니라 더 높은 인터페이스 타입인 List로 선언하는 코드를 본 적이 있을 것이다. 이게 바로 대표적인 DIP를 따른 코드라고 볼 수 있다. 이와같이 추상클래스를 구현한 저수준 모듈을 추상클래스인 고수준 모듈모다 더욱 변경될 여지가 많고, 그렇기 때문에 고수준 모듈이 저수준 모듈을 의존하게 되면 고수준 모듈이 영향을 받기 쉬워지기 때문에 의존관계를 역전 시켜야한다.
여기까지 생각해보면 이는 앞서 살펴본 단일 책임 원칙과도 큰 연관이 있다.
디자인 패턴을 공부하고 기록하는 개인 공부 블로그입니다.
내용 중 틀린 부분 피드백 혹은 궁금한 점이 있으면 댓글로 남겨주시면 참고 및 답변 달아드리겠습니다🧐
'Design Pattern' 카테고리의 다른 글
[디자인패턴] Singleton Pattern이란? (1) | 2023.12.24 |
---|