하나의 클래스는 단 하나의 책임만 가져야 한다.
클래스가 여러가지 역할을 수행하게 되면 코드를 이해하기 어려워 지고 수정이 필요한 경우 다른기능에도 영향을 주게 된다.
변경을 했을때 파급효과가 적으면 단일책임원칙을 잘 따른것이다.
예시:
주문처리와 결제기능을 담당하는 클래스가 하나의 클래스에 합쳐져 있을때 SRP에 위배된다.
각각 주문처리와 결제기능을 따로 넣어야한다.
// SRP (단일 책임 원칙) 예제
// 주문을 처리하는 클래스
class OrderProcessor {
public void processOrder(Order order) {
// 주문 처리 로직
// ...
}
}
// 결제를 처리하는 클래스
class PaymentProcessor {
public void processPayment(Payment payment) {
// 결제 처리 로직
// ...
}
}
소프트웨어 개체(클래스,모듈,함수등)는 모든확장에 대해 열려있어야하지만 수정에 대해서는 폐쇄적이어야한다.
기존의 코드를 변경하지 않으면서 새로운 기능을 추가할수 있도록 설계해야함을 의미한다.
다형성을 활용해보자
인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현한다.
이렇게 글로만 보면 이해가 안가는데,,, 다시 풀어서 설명하자면
public void MemberService() {
MemberRepository m = new MemoryMemberRepository();
m.save();
}
m.save는 memoryMemberRepository에 구현되어있는 save를 가져온다.
하지만 그 후에 JdbcMemberRepository에 구현되어있는 save를 가지고 오고 싶다면 코드를 변경해야한다.
public void MemberService() {
//JDBCMemberRepository로 변경되었다.
MemberRepository m = new JdbcMemberRepository();
m.save();
}
이런경우에 OCP가 깨진다. 그럼 이문제를 어떻게 해결할까,
이문제를 바로 Spring이 해준다. DI와 IoC컨테이너가 해결해준다.
DI와 IoC컨테이너에 대한 자세한 설명은 여기에 →
예시:
자동차가 테슬라에서 k3로 바뀌어도 운전자는 운전을 할 수 있다
주문을 처리하는 클래스에서 주문의 유형에 따라 다른 로직을 수행해야할 때 기존의 주문 처리 클래스를 수정하지 않고 새로운 클래스를 추가하여 해당주문 유형에대한 로직을 구현한다.
// OCP (개방-폐쇄 원칙) 예제
// 주문 처리 인터페이스
interface OrderProcessor {
void processOrder(Order order);
}
// 주문을 처리하는 구현 클래스
class DefaultOrderProcessor implements OrderProcessor {
public void processOrder(Order order) {
// 주문 처리 로직
// ...
}
}
// 새로운 주문 유형을 처리하는 클래스
class CustomOrderProcessor implements OrderProcessor {
public void processOrder(Order order) {
// 새로운 주문 유형에 대한 처리 로직
// ...
}
}
프로그램의 객체는 부모클래스의 인스턴스를 자식 클래스의 인스턴스로 대체해도 프로그램의 의도가 변하지 않아야 한다는 원칙이다.
예시:
자동차에서 엑셀이라는 기능을 구현할 때,
엑셀이라는 기능은 앞으로 나아가기 위한 기능인데 불구하고
오버라이딩을 한 자식 클래스에서 엑셀기능을 후진기능으로 반대로 만들어서 구현을 한다면 엑셀이라는 기능을 만든 부모클래스의 의도가 변했으므로 리스코프 치환 위반으로 볼 수 있다.
클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 한다는 원칙이다.
하나의 큰 인터페이스보다는 여러개의 작은 인터페이스로 분리함으로서 인터페이스간의 종속성을 줄이고 클라이언트가 필요한 기능에만 의존하도록한다.
예시:
파일인터페이스 → 파일 저장, 파일 로드 인터페이스로 분리하기
파일을 저장하고 불러오는 기능을 제공하는 인터페이스안에 save와 load메서드가 포함되어있을때
사실상 저장과 로드는 다른 기능이므로 인터페이스 분리를 해야한다.
interface FileSaver {
void save(File file);
}
// 파일 불러오기 기능을 제공하는 인터페이스
interface FileLoader {
void load(File file);
}
// 파일 처리 클래스
class FileManager implements FileSaver, FileLoader {
public void save(File file) {
// 파일 저장 로직
// ...
}
public void load(File file) {
// 파일 불러오기 로직
// ...
}
}
고수준 모듈은 저수준 모듈에 의존하면 안되며, 모든 모듈은 추상화에 의존해야한다는 원칙이다.
즉 추상화된 인터페이스나 추상클래스에 의존함으로써 구체적인 구현에 대한 의존성을 줄이고 유연하고 확장가능한 시스템을 만들어야한다.
쉽게 얘기해서 구현클래스에 의존하지 말고 인터페이스에 의존하라는 뜻이다. 역할에 의존해야한다는 것이다.
클라이언트코드가 구현클래스를 바라보지말고 인터페이스를 바라봐야한다.
MemerService가 MemberServiceInterface만 바라보고 memoryServiceIntereface나 JdbcServiceInterface는몰라야한다.
DIP에서는 운전자는 운전하기 위해서 자동차역할에 대해서만 알면되지 K3나 테슬라 모델에 대해서 자세히 알 필요가 없다.
운전자가 테슬라에대해서만 알면 그랜저로 바뀌었을때 대체가 불가능하기때문이다.
OCP에서 설명한
public void MemberService() {
MemberRepository m = new MemoryMemberRepository();
m.save();
}
public void MemberService() {
MemberRepository m = new MemoryMemberRepository();
m.save();
}
위의 코드는 DIP를 위반한다.
MemberService는 memberRepository에도 MemoryMemberRepository에도 의존하고 있기 때문이다.
memberRepository라는 추상화에는 의존해도 되지만, 구체화인 memoryMemberRepository에는 의존해선 안된다 .
예시:
주문처리기능을 담당하는 클래스가 데이터베이스에 직접 의존하는 경우에는 데이터베이스와의 의존성을 인터페이스로 추상화 하고, 주문처리 클래스는 이 인터페이스에 의존하여 데이터베이스와의 결합도를 낮춘다.
// DIP (의존 역전 원칙) 예제
// 데이터베이스 접근 인터페이스
interface DatabaseAccessor {
void connect();
void disconnect();
// ...
}
// 주문 처리 클래스
class OrderProcessor {
private DatabaseAccessor databaseAccessor;
public OrderProcessor(DatabaseAccessor databaseAccessor) {
this.databaseAccessor = databaseAccessor;
}
public void processOrder(Order order) {
databaseAccessor.connect();
// 주문 처리 로직
// ...
databaseAccessor.disconnect();
}
}
정리를 하자면 ,
참조: 김영한 스프링핵심원리 기본편