2023.05.27 - [Engineering WIKI/Docs] - SOLID 5가지 설계 원칙
2021.01.17 - [Engineering WIKI/Docs] - 객체지향 (LID)
Liskov Substitution Principle (리스코프 치환 원칙)
- 자식 클래스는 부모 클래스에서 가능한 행위를 수행할 수 있어야 한다.
- 자식 클래스가 부모 클래스의 기본 의미를 해치지 않는지!
- 부모클래스와 자식클래스 사이의 행위에는 일관성이 있어야 한다.
- (부모 클래스의 인스턴스 대신 자식 클래스의 인스터스를 사용해도 문제가 없어야 한다는 것을 의미한다.)
- 상속 관계에서는 일반화 관계(IS - A)가 성립해야 한다. 일반화 관계에 있다는 것은 일관성이 있다는 것이다. 따라서 리스코프 치환 원칙은 일반화 관계에 대해 묻는 것 이다.
- Ex) 도형 클래스와 사각형 클래스가 있고, 사각형 클래스는 도형 클래스의 상속을 받는다고 가정하자.
- (1) 도형은 둘레를 가지고 있다.
- (2) 도형은 넓이를 가지고 있다.
- (3) 도형은 각을 가지고 있다.
- 일반화 관계(일관성인지 확인하는 방법은 단어를 교체해 보면 알 수 있다. (1) ~ (3)의 도형이란 단어 대신 사각형을 넣어보자.
- (1) 사각형은 둘레를 가지고 있다.
- (2) 사각형은 넓이를 가지고 있다.
- (3) 사각형은 각을 가지고 있다.
- (1) ~ (3) 모두 딱히 이상한 부분이 보이지 않는다. 따라서 도형과 사각형 사이에는 일관성이 있다고 할 수 있다.
- 여기서 원(Circle) 이라는 도형에 대해 생각해보자. 원 클래스 역시 도형 클래스의 상속을 받는다고 가정하자. 앞에서 언급한 (1) ~ (3)의 도형 단어 대신 원을 대입해보자.
- (1) 원은 둘레를 가지고 있다.
- (2) 원은 넓이를 가지고 있다.
- (3) 원은 각을 가지고 있다.
- 문장을 읽어보면 (3)번 문장이 어색하다는 것을 알 수 있다. 따라서 도형 클래스는 LSP을 만족하지 않은 설계라 할 수 있다. 따라서 (3)문장에 대해서는 일반화 관계가 성립하도록 수정되어야 한다.
- Ex) 도형 클래스와 사각형 클래스가 있고, 사각형 클래스는 도형 클래스의 상속을 받는다고 가정하자.
Interface Segregation Principle (인터페이스 분리 원칙)
- 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다. 하나의 일반적인 인터페이스 보다는 여러개의 구체적인 인터페이스가 낫다.
- 이는 다시 말해서, 자신이 사용하지 않는 기능(인터페이스)에는 영향을 받지 말아야 한다.
- 인터페이스 분리 원칙을 통해 시스템의 내부 의존성을 약화시켜 리팩토링, 수정, 재배포를 쉽게 할 수 있다.
- SRP는 객체의 단일 책임을 뜻한다면, ISP는 인터페이스의 단일 책임을 의미한다고 보면 됩니다.
- ISP를 만족하려면 Phone 인터페이스에 call(), sms(), alarm(), calculator() 함수를 모두 정의하는 것보다, Call, Sms, Alarm, Calculator 인터페이스를 각각 정의하여, 3G폰과 스마트폰 클래스에서 4개의 인터페이스를 구현하도록 설계되어야 합니다.
- 이렇게 설계를 하면, 각 인터페이스의 메서드들이 서로 영향을 미치지 않게 됩니다. 즉, 자신이 사용하지 않는 메서드에 대해서 영향력이 줄어들게 됩니다.
- Bad Code
interface SmartPrinter {
print();
fax();
scan();
}
class AllInOnePrinter implements SmartPrinter {
print() {
// ...
}
fax() {
// ...
}
scan() {
// ...
}
}
class EconomicPrinter implements SmartPrinter {
print() {
// ...
}
fax() {
throw new Error('Fax not supported.');
}
scan() {
throw new Error('Scan not supported.');
}
}
- Good Code
interface Printer {
print();
}
interface Fax {
fax();
}
interface Scanner {
scan();
}
class AllInOnePrinter implements Printer, Fax, Scanner {
print() {
// ...
}
fax() {
// ...
}
scan() {
// ...
}
}
class EconomicPrinter implements Printer {
print() {
// ...
}
}
Dependency Inversion Principle (의존 역전 원칙)
- 의존 관계를 맺을 떄, 변화하기 쉬운것 보단 변화하기 어려운 것에 의존해야 한다는 원칙.
- 저수준 모듈이 고수준 모듈에 의존하게 되는 것
- Ex) 계층구조 예시
- 표현 계층 : 사용자의 요청을 받아 응용 영역에 전달함과 동시에 처리 결과를 사용자에게 표시
- 응용 영역 : 사용자에게 제공해야 할 기능 구현
- 도메인 영역 : 도메인의 핵심 로직 & 도메인 모델 구현
- 인프라 계층 : 구현 기술을 다룸 (ex. DB 연동)
- 각 계층은 위와 같은 특징을 가지고 있으며, 상위 계층은 하위 계층에게 의존하지만, 하위 계층은 상위 계층에 의존하지 않습니다. 하지만 그렇게 되면 인프라 계층에게 종속적인 현상이 많이 일어나게 됩니다.
- Ex) 계층구조 예시
- 여기서 말하는 변화하기 쉬운 것 이란 구체적인 것을 말하고, 변화하기 어려운 것이란 추상적인것을 말한다.
- 변화하기 쉽고 구체적인 것 → 클래스
- 변화하기 어려운 것 → 인터페이스, 추상 클래스
- Client 객체는 Cat, Dog, Bird의 crying() 메서드에 직접 접근하지 않고, Animal 인터페이스의 crying() 메서드를 호출함으로써 DIP를 만족할 수 있습니다.
- 기존 코드의 문제
/* A사의 알람 서비스 */
public class A {
public String beep() {
return "beep!";
}
}
/* 서비스 코드*/
public class AlarmService {
private A alarm;
public String beep() {
return alarm.beep();
}
}
- 문제점 : 테스트의 어려움
- 위 코드에서 AlarmService만 온전히 테스트할 수 없습니다. 인프라 계층에 속하는 A가 완벽하게 동작해야만 AlarmService를 테스트할 수 있습니다.
- 문제점 : 확장 및 변경이 어려움
- 만약 알람 서비스에 B사가 추가된다면 어떻게 될까요? 아래와 같이 서비스 코드를 변경해야합니다.
/* B사의 알림 서비스 */
public class B {
public String beep() {
return "beep";
}
}
/* 서비스 코드 */
public class AlarmService {
private A alarmA;
private B alarmB;
public String beep(String company) {
if (company.equals("A")) {
return alarmA.beep();
} else {
return alarmB.beep();
}
}
}
- 해결책 : 방금 설명한 기능을 고수준 모듈과 저수준 모듈로 분리하면 아래와 같습니다.
- 고수준 모듈 : 알림
- 저수준 모듈 : A사의 알림 서비스, B사의 알림 서비스
- 지금까지 사용한 방법은 고수준 모듈이 저수준 모듈에 의존하는 방법이지만, DIP를 적용하게 되면 저수준 모듈이 고수준 모듈에 의존해야 합니다. 그러기 위해 사용하는 것이 추상 타입(ex. 인터페이스, 추상 클래스)입니다.
public interface Alarm {
String beep();
}
- 저수준 모듈이 상속 받을 Alarm 인터페이스를 만들어줍니다.
/* A사의 알람 서비스 */
public class A implements Alarm {
@Override
public String beep() {
return "beep!";
}
}
/* B사의 알림 서비스 */
public class B implements Alarm {
@Override
public String beep() {
return "beep";
}
}
- 그 후에 저수준 모듈들이 Alarm을 구현하게 하면 됩니다. 그렇게 되면 서비스 코드를 아래와 같이 변경할 수 있습니다.
/* 서비스 코드 */
public class AlarmService {
private Alarm alarm;
public AlarmService(Alarm alarm) {
this.alarm = alarm;
}
public String beep() {
return alarm.beep();
}
}
- 더 이상 AlarmService는 알람 서비스가 추가된다고 코드를 변경하거나 추가해야 하는 일이 없어집니다.
- 또한, 알람 관련 객체는 무조건 인터페이스인 Alarm에 의존하기 때문에 Alarm을 Mock 객체로 만들어 다양한 시나리오로 AlarmService 기능을 온전히 테스트할 수 있다는 장점도 가져갈 수 있습니다.
'Engineering WIKI > Docs' 카테고리의 다른 글
orphanRemoval 이란? (0) | 2022.04.02 |
---|---|
Spring JPA CascadeType 종류 (0) | 2022.04.02 |
동시성 vs 병렬성 (헷갈리는 개념 뿌시기) (0) | 2022.02.28 |
API의 개념 뿌수기! (필수 API 개념 기술) (0) | 2021.01.17 |
객체지향 (S: SRP / O : OCP) (0) | 2021.01.17 |
Socket 통신 (Http 통신과의 차이점) (0) | 2021.01.17 |
SSL, SSH, HTTPS vs HTTP (0) | 2020.11.29 |
Tomcat War 파일 배포 (0) | 2020.03.22 |